-
Notifications
You must be signed in to change notification settings - Fork 566
Map Type
XGo provides a concise syntax for working with maps. Maps are key-value data structures that allow you to store and retrieve values using keys.
XGo provides two ways to create maps: using map literals for quick initialization with data, and using the make function for more control over map types and capacity.
In XGo, you can create maps using curly braces {}:
a := {"Hello": 1, "xsw": 3} // map[string]int
b := {"Hello": 1, "xsw": 3.4} // map[string]float64
c := {"Hello": 1, "xsw": "XGo"} // map[string]any
e := {1: "one", 2: "two"} // map[int]string
d := {} // map[string]anyYou can also explicitly specify the map type to override automatic type inference:
var a map[string]float64 = {"Hello": 1, "xsw": 3} // Values converted to float64
var c map[string]any = {"x": 1, "y": "text"} // Explicit any typeWhen a type is explicitly declared, the literal values are converted to match the declared type.
When using the := syntax without explicit type declaration, XGo automatically infers the complete map type map[KeyType]ValueType based on the literal syntax and values provided.
Type Inference Rules
Both KeyType and ValueType follow the same inference rules:
-
All elements have the same type: The type is that common type. Note that untyped literals (such as untyped integers and untyped floats) follow Go's conversion rules - untyped values can be implicitly converted to a common type when compatible (e.g., untyped integer literals like
1can be converted tofloat64when mixed with untyped float literals like3.4) -
Not all elements have the same type: The type is
any(which can hold any value)
Empty Map Special Case
The empty map literal {} is a special case:
-
KeyTypeis inferred asstring -
ValueTypeis inferred asany
This provides the most flexible type for an empty map, allowing you to add any string-keyed values dynamically.
While map literals are convenient for initializing maps with data, XGo also supports the standard make function for creating maps, particularly when you need more control over the map type or capacity.
// Create an empty map with specific types
m := make(map[string]int)
m["count"] = 42
echo m // Output: map[count:42]
// Create a map with non-string keys
ages := make(map[int]string)
ages[1] = "Alice"
ages[2] = "Bob"
echo ages // Output: map[1:Alice 2:Bob]
// Create a map with complex key types
type Point (x, y int)
positions := make(map[Point]string)
positions[(0, 0)] = "origin"For performance optimization, you can specify an initial capacity hint:
// Create a map with initial capacity for ~100 elements
largeMap := make(map[string]int, 100)
// This doesn't limit the map size, but helps reduce allocations
for i := 0; i < 150; i++ {
largeMap["key${i}"] = i
}The capacity hint doesn't limit the map's size but helps the runtime allocate memory more efficiently when you know approximately how many elements you'll add.
Use map literals ({}) when:
- You have initial data to populate
- You want automatic type inference for convenience
- You prefer concise, readable code
Use map literals with explicit type (var m map[K]V = {}) when:
- You have initial data with a specific type requirement
- You need type safety while keeping syntax concise
- You want to ensure value types are converted correctly
Use make when:
- You're creating an empty map and plan to add elements later
- You want to pre-allocate capacity for performance
- You prefer the traditional Go style
- Working with codebases that consistently use
make
// Literal with auto inference - quick and convenient
config := {"host": "localhost", "port": 8080}
// Literal with explicit type - type safety with initial data
var settings map[string]int = {"timeout": 30, "retries": 3}
var userIDs map[int]string = {1001: "Alice", 1002: "Bob"}
// make - empty map with planned additions
options := make(map[string]int)
options["timeout"] = 30
options["retries"] = 3
// make - performance optimization with capacity
largeCache := make(map[string][]byte, 10000)You can add new elements or update existing ones using either notation:
a := {"a": 1, "b": 0}
// Using bracket notation
a["c"] = 100
// Using field notation
a.d = 200
echo a // Output: map[a:1 b:0 c:100 d:200]
// Works with maps created by make too
m := make(map[string]int)
m["x"] = 10
m.y = 20
echo m // Output: map[x:10 y:20]Use the delete function to remove elements from a map:
a := {"a": 1, "b": 0, "c": 100}
delete(a, "b")
echo a // Output: map[a:1 c:100]
// Works with any key type
m := make(map[int]string)
m[1] = "one"
m[2] = "two"
delete(m, 1)
echo m // Output: map[2:two]You can get the number of elements in a map using the len function:
a := {"a": 1, "b": 2, "c": 3}
echo len(a) // Output: 3
b := make(map[string]int)
b["x"] = 10
echo len(b) // Output: 1XGo provides two ways to access map elements: bracket notation and field access notation.
The traditional way to access map elements is using the [] operator with a key:
a := {"name": "Alice", "age": 25}
echo a["name"] // Output: Alice
// Works with any key type
m := make(map[int]string)
m[42] = "answer"
echo m[42] // Output: answerFor string-keyed maps, XGo allows you to use dot notation when the key is a valid identifier:
config := {"host": "localhost", "port": 8080}
echo config.host // Output: localhost
echo config.port // Output: 8080
// Equivalent to:
echo config["host"]
echo config["port"]Field access is pure syntax sugar - m.field and m["field"] behave identically in all contexts.
Use field notation (m.field) when:
- Keys are known at development time
- Keys are valid identifiers (letters, digits, underscores only)
- You want more readable code
Use bracket notation (m["key"]) when:
- Keys are computed at runtime
- Keys contain special characters, spaces, or start with digits
- You need explicit compatibility with standard Go
- Working with non-string key types
// Field notation - clean and readable
user := {"name": "Alice", "age": 30}
echo user.name
echo user.age
// Bracket notation - necessary for dynamic or special keys
keyName := "name"
echo user[keyName] // Dynamic key
echo user["first-name"] // Key with hyphen
echo user["2024-score"] // Key starting with digit
// Bracket notation - required for non-string keys
scores := make(map[int]float64)
scores[1] = 95.5
echo scores[1] // Must use bracket notationField notation works seamlessly with nested maps:
data := {
"user": {
"profile": {
"name": "Alice",
"age": 30,
},
},
}
// Clean nested access
echo data.user.profile.name // Output: Alice
// Equivalent to:
echo data["user"]["profile"]["name"]Field notation also works with variables of type any, automatically treating them as map[string]any:
var response any = {"status": "ok", "code": 200}
echo response.status // Output: ok
echo response.code // Output: 200When accessing uncertain data (such as from JSON or external APIs), use the comma-ok form to safely check if a path exists. The comma-ok form returns two values:
- The value itself (or zero value if path doesn't exist)
- A boolean indicating whether the access succeeded
a := {"a": 1, "b": 0}
// Check if key exists
v, ok := a["c"]
echo v, ok // Output: 0 false (key doesn't exist)
v, ok = a["b"]
echo v, ok // Output: 0 true (key exists with value 0)
// Works with field notation too
v, ok = a.c
echo v, ok // Output: 0 false
// Direct conditional check
if v, ok := a["c"]; ok {
echo "Found:", v
} else {
echo "Not found" // Output: Not found
}With comma-ok, accessing non-existent paths never panics - it simply returns false:
data := {"user": {"name": "Alice"}}
// Safe single-level access
name, ok := data.user
if ok {
echo "User found:", name
}
// Safe nested access
profile, ok := data.user.profile
if !ok {
echo "Profile not found" // This will print
}
// Safe access with type assertion
var response any = {"status": "ok", "code": 200}
code, ok := response.code.(int)
if ok {
echo "Status code:", code
}This is especially useful when working with dynamic data:
var data any = {"user": "Alice"}
// Without comma-ok - may panic if structure is wrong
// name := data.user.profile.name // Would panic!
// With comma-ok - safe, never panics
name, ok := data.user.profile.name
if !ok {
echo "Path does not exist" // Output: Path does not exist
name = "Unknown"
}
// Processing API response
var apiResponse any = fetchFromAPI()
// Safely extract nested values
if userID, ok := apiResponse.data.user.id.(string); ok {
processUser(userID)
} else {
echo "Invalid response structure"
}
// With fallback values
city := "Unknown"
if c, ok := apiResponse.user.address.city.(string); ok {
city = c
}
echo "City:", cityXGo provides two forms of for in loop for iterating over maps:
m := {"x": 10, "y": 20, "z": 30}
for key, value in m {
echo key, value
}
// Works with any map type
ages := make(map[string]int)
ages["Alice"] = 30
ages["Bob"] = 25
for name, age in ages {
echo name, "is", age, "years old"
}To iterate over just the keys, you can use the blank identifier _ for the value part.
m := {"x": 10, "y": 20, "z": 30}
for key, _ in m {
echo key
}m := {"x": 10, "y": 20, "z": 30}
for value in m {
echo value
}Map comprehensions provide a concise and expressive way to create new maps by transforming or filtering existing sequences. They follow a syntax similar to Python's dictionary comprehensions.
The general form of a map comprehension is:
{keyExpr: valueExpr for vars in iterable}This creates a new map where each element from the iterable is transformed into a key-value pair.
// Map slice values to their indices
numbers := [10, 20, 30, 40, 50]
valueToIndex := {v: i for i, v in numbers}
echo valueToIndex // Output: map[10:0 20:1 30:2 40:3 50:4]// Character positions in a string
word := "hello"
charPositions := {char: i for i, char in word}
echo charPositions // Output: map[h:0 e:1 l:3 o:4]
// Note: 'l' appears twice, so the last occurrence (index 3) is keptAdd an if clause to filter elements:
{keyExpr: valueExpr for vars in iterable if condition}numbers := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Only even numbers
evenSquares := {v: v * v for v in numbers if v%2 == 0}
echo evenSquares // Output: map[2:4 4:16 6:36 8:64 10:100]
// Only odd indices
oddIndexValues := {i: v for i, v in numbers if i%2 == 1}
echo oddIndexValues // Output: map[1:2 3:4 5:6 7:8 9:10]- Use comprehensions for simple transformations: They're most readable when the logic is straightforward
- Consider traditional loops for complex logic: If you need multiple statements or complex conditions, a regular loop may be clearer
- Watch for duplicate keys: In comprehensions, later values overwrite earlier ones for the same key
- Keep conditions simple: Complex filtering logic is often better in a traditional loop
// Using literals for initial configuration
config := {
"host": "localhost",
"port": 8080,
"debug": true,
}
// Access with field notation
echo "Connecting to", config.host, "on port", config.port
// Using explicit type for type safety
var settings map[string]int = {
"maxConnections": 100,
"timeout": 30,
}
// Using make for type-safe configuration
options := make(map[string]int)
options["maxConnections"] = 100
options["timeout"] = 30var response any = parseJSON(apiData)
// Safe extraction with defaults
userID, ok := response.user.id.(string)
if !ok {
userID = "guest"
}
userName, ok := response.user.name.(string)
if !ok {
userName = "Anonymous"
}
echo "User:", userName, "(", userID, ")"// Using make with pre-allocated capacity
wordCounts := make(map[string]int, 1000)
for word in words {
wordCounts[word]++
}
// Using comprehension to initialize
words := ["apple", "banana", "apple", "orange", "banana", "apple"]
uniqueWords := {w: 0 for w in words} // Initialize all to 0
for word in words {
uniqueWords[word]++
}// Simple lookup table with literals
statusCodes := {
"ok": 200,
"not_found": 404,
"error": 500,
}
echo statusCodes.ok // Output: 200
// Using comprehension to reverse the mapping
codeToStatus := {code: status for status, code in statusCodes}
echo codeToStatus[200] // Output: ok// Cache with pre-allocated capacity for performance
cache := make(map[string][]byte, 10000)
func getCachedData(key string) []byte {
if data, ok := cache[key]; ok {
return data
}
data := fetchData(key)
cache[key] = data
return data
}// Group items by category
groups := make(map[string][]string)
for item in items {
category := getCategory(item)
groups[category] = append(groups[category], item)
}
// Access grouped data
for category, items in groups {
echo "Category:", category
for item in items {
echo " -", item
}
}- Use field notation for readability when keys are known and are valid identifiers
- Use bracket notation when keys are dynamic, contain special characters, or are non-string types
- Use comma-ok form when working with uncertain data structures (APIs, JSON, dynamic data)
- Use map literals for quick initialization with known data
- Use explicit type declaration with literals when you need type safety or specific conversions
-
Use
makewhen you need specific types, non-string keys, or want to pre-allocate capacity - Use map comprehensions for simple transformations and filtering of sequences
- Check for key existence before accessing values when the key might not exist
- Pre-allocate capacity with
makefor large maps when the approximate size is known - Use consistent value types when possible for type safety
- Consider using
makewith explicit types for better code documentation and type safety in larger projects
-
Pre-allocate capacity: When you know the approximate size, use
make(map[K]V, size)to reduce allocations - Avoid frequent reallocations: Maps grow dynamically, but pre-allocation prevents repeated internal resizing
- Use appropriate key types: Simple types (int, string) as keys are more efficient than complex structs
- Consider zero values: Accessing non-existent keys returns zero values, which can be useful for counters
- Comprehensions vs loops: For large datasets or complex transformations, traditional loops with pre-allocation may be more efficient than comprehensions