-
Notifications
You must be signed in to change notification settings - Fork 562
Description
Abstract
This proposal suggests allowing direct field access syntax (e.g., v.field) for map types in XGo, which would be equivalent to v["field"]. This syntax sugar would improve code readability and reduce verbosity when working with maps. Additionally, this proposal defines comma-ok propagation semantics for chained field access operations, enabling safe and ergonomic handling of uncertain data structures.
Core Principle: v.field and v["field"] must have identical behavior in all contexts. This is pure syntax sugar with zero semantic differences.
Background
Currently in XGo (and Go), accessing map elements requires bracket notation:
m := map[string]int{"count": 42, "total": 100}
value := m["count"]While this syntax is clear and unambiguous, it can become verbose and less readable, especially when:
- Chaining multiple map accesses
- Working with nested map structures
- Using maps as configuration or data objects with known keys
- Processing dynamic data like JSON without verbose type assertions
Many modern languages (JavaScript, Python, Lua) support both bracket and dot notation for accessing map/dictionary elements, improving developer ergonomics.
Part 1: Map Field Access Syntax Sugar
Proposal
Allow dot notation for map element access where the key is a valid identifier:
m := map[string]int{"count": 42, "total": 100}
value := m.count // equivalent to m["count"]Core Equivalence Principle
CRITICAL: v.field is pure syntax sugar for v["field"]. They must behave identically in ALL contexts:
- ✅ Function calls:
m.field()≡m["field"]() - ✅ Type assertions:
m.field.(T)≡m["field"].(T) - ✅ Chaining:
m.field.x.y≡m["field"]["x"]["y"] - ✅ Channel operations:
(<-ch).field≡(<-ch)["field"] - ✅ Assignments:
m.field = x≡m["field"] = x - ✅ Any valid Go expression with
v["field"]has an equivalent withv.field
If v["field"] compiles and works in Go, then v.field must compile and work identically.
Scope
This feature would apply to:
- Maps with
stringkeys - Keys that are valid Go identifiers (no spaces, special characters, etc.)
- Both read and write operations
- Implicit map access on
anytype: Variables of typeanycan also use dot notation, which performs an implicit type assertion tomap[string]any
Basic Examples
Basic access:
config := map[string]string{
"host": "localhost",
"port": "8080",
}
url := config.host + ":" + config.portAssignment:
data := make(map[string]int)
data.count = 42 // equivalent to data["count"] = 42Nested maps:
nested := map[string]map[string]int{
"metrics": {"count": 10, "total": 100},
}
count := nested.metrics.count // equivalent to nested["metrics"]["count"]Implicit map access on any type:
var v any = map[string]any{
"name": "Alice",
"age": 30,
}
name := v.name // equivalent to v.(map[string]any)["name"]Syntax Rules
- Valid identifiers only:
m.fieldNamewherefieldNamematches[a-zA-Z_][a-zA-Z0-9_]* - String keys only: Only maps with
stringkeys support dot notation - Compile-time transformation:
m.field→m["field"]at compile time - No shadowing: If a map type has actual methods/fields, those take precedence
- Implicit type assertion for
any:v.fieldwherevis of typeany→v.(map[string]any)["field"] - Complete equivalence: Any context where
v["field"]is valid,v.fieldmust also be valid with identical behavior
Type Constraints
The feature applies only when:
- The map key type is
string - The field name is a valid Go identifier
- There's no ambiguity with actual type methods
Compilation
The compiler would transform dot notation to bracket notation during parsing:
// Source code
value := myMap.fieldName
// Transformed to
value := myMap["fieldName"]For any type variables:
// Source code
var v any = map[string]any{"key": "value"}
result := v.key
// Transformed to
result := v.(map[string]any)["key"]Part 2: Comma-ok Propagation Semantics
Problem Statement
When working with uncertain data structures (especially any types from JSON or dynamic sources), developers need a safe way to traverse potentially invalid paths. The comma-ok idiom is Go's standard pattern for safe operations, but using it with verbose map access chains is cumbersome.
Important Discovery: Go already supports comma-ok evaluation for chained expressions:
// This is valid Go and works as expected
value, ok := data.(map[string]any)["user"].(map[string]any)["name"].(string)
// If any step fails (type assertion or map access), ok = falseThis proposal makes this existing capability more ergonomic by introducing field access syntax sugar.
Consider these scenarios:
var data any = unmarshalJSON(input)
// Scenario 1: Verbose but safe (current Go)
name, ok := data.(map[string]any)["user"].(map[string]any)["name"].(string)
// Scenario 2: What we want (this proposal)
name, ok := data.user.name.(string)
// Both compile to the same code and have identical behaviorOverview
When using comma-ok form with chained field access, the ok value reflects whether the entire chain succeeded. This leverages Go's existing comma-ok semantics for chained expressions - any failure in the chain (whether a type assertion or map key access) results in ok == false and the zero value of the target type.
This design provides:
- Safety: No panics when traversing uncertain structures
- Ergonomics: Simple syntax for complex data validation
- Clarity: Single
okvalue indicates overall success/failure - Native Go Semantics: Uses Go's existing comma-ok chain evaluation
Propagation Rules
Rule 1: Pure Field Access Chain
All intermediate field access steps must succeed for ok to be true.
var data any = map[string]any{
"user": map[string]any{
"profile": map[string]any{
"name": "Alice",
},
},
}
// Success case
name, ok := data.user.profile.name
// ok == true, name == "Alice"
// Failure case: intermediate key missing
city, ok := data.user.address.city
// ok == false (data.user succeeds, but address key doesn't exist)
// Evaluation short-circuits, city == nilRule 2: Field Access with Type Assertion
When a type assertion appears at the end of a chain, both the field access chain AND the type assertion must succeed.
var data any = map[string]any{
"user": map[string]any{
"age": 30,
},
}
// Success case
age, ok := data.user.age.(int)
// ok == true, age == 30
// Failure case: field exists but wrong type
age, ok := data.user.age.(string)
// ok == false (field access succeeds, type assertion fails)
// Failure case: field access fails
age, ok := data.user.height.(int)
// ok == false (height key doesn't exist, type assertion never attempted)Rule 3: Short-circuit Evaluation (Go Native Behavior)
Go's comma-ok idiom already supports short-circuit evaluation for chained expressions. The first failure in a chain stops evaluation and returns (zero-value, false). This proposal simply transforms field access syntax to leverage this existing behavior.
var data any = map[string]any{
"user": "not a map", // Wrong type
}
// Short-circuit example
name, ok := data.user.profile.name.(string)
// Transformed to:
// name, ok := data.(map[string]any)["user"].(map[string]any)["profile"].(map[string]any)["name"].(string)
// Go's evaluation:
// Step 1: data.(map[string]any) ✓ succeeds
// Step 2: ["user"] ✓ returns "not a map"
// Step 3: "not a map".(map[string]any) ✗ FAILS
// Go short-circuits here: remaining steps not evaluated
// Result: ok == false, name == ""Key Insight: This proposal doesn't introduce new short-circuit semantics - it simply makes Go's existing comma-ok chain behavior accessible through cleaner syntax.
Rule 4: Zero-value on Failure
When any step fails, the result is the zero value of the target type and ok == false.
// Target type is string
name, ok := data.missing.field.(string)
// Result: name == "", ok == false
// Target type is int
count, ok := data.missing.field.(int)
// Result: count == 0, ok == false
// Target type is any (no type assertion)
value, ok := data.missing.field
// Result: value == nil, ok == falseDetailed Transformation Examples
Go already supports chained comma-ok expressions natively. The compiler simply transforms field access notation to bracket notation, and Go's existing comma-ok semantics handle the short-circuit behavior automatically.
Example 1: Two-level Field Access
// Source code
name, ok := data.user.name
// Transformed to:
name, ok := data.(map[string]any)["user"].(map[string]any)["name"]
// Go's native comma-ok behavior:
// - If any type assertion fails, ok = false and name = nil
// - If any map access returns a missing key, ok = false and name = nil
// - Only if all steps succeed, ok = true and name has the valueExample 2: Field Access with Type Assertion
// Source code
age, ok := data.user.age.(int)
// Transformed to:
age, ok := data.(map[string]any)["user"].(map[string]any)["age"].(int)
// Go's native comma-ok behavior handles the entire chain:
// - data.(map[string]any) must succeed
// - ["user"] must return a value
// - .(map[string]any) type assertion must succeed
// - ["age"] must return a value
// - .(int) final type assertion must succeed
// If any step fails: age = 0, ok = falseExample 3: Complex Nested Access
// Source code
count, ok := data.metrics.current.count.(int)
// Transformed to:
count, ok := data.(map[string]any)["metrics"].(map[string]any)["current"].(map[string]any)["count"].(int)
// This leverages Go's existing chain evaluation:
// 1. data.(map[string]any) - type assert to map
// 2. ["metrics"] - access key
// 3. .(map[string]any) - type assert result to map
// 4. ["current"] - access key
// 5. .(map[string]any) - type assert result to map
// 6. ["count"] - access key
// 7. .(int) - final type assertion
//
// If ALL checks pass: count gets the value, ok == true
// If ANY check fails: count = 0, ok = falseExample 4: Without Type Assertion
// Source code
value, ok := data.user.name
// Transformed to:
value, ok := data.(map[string]any)["user"].(map[string]any)["name"]
// Note: No final type assertion, value remains type `any`
// ok indicates if the path exists and all intermediate steps succeededComparison with Non-Comma-ok Form
Without comma-ok (panic on failure):
// This will panic if any step fails
name := data.user.name
// Equivalent to:
name := data.(map[string]any)["user"].(map[string]any)["name"]With comma-ok (safe, no panic):
// This never panics, returns false on failure
name, ok := data.user.name
if !ok {
// Handle missing/invalid data
}Interaction with Type Assertions
Since v.field is pure syntax sugar for v["field"], type assertions work identically to how they work with bracket notation.
Type Assertion at Any Position
Type assertions can appear at any position in the chain, just like with bracket notation:
// End of chain (most common)
value, ok := data.user.age.(int)
// Equivalent to: data["user"]["age"].(int)
// Middle of chain (fully supported, same as Go)
value := data.user.(UserMap).profile.name
// Equivalent to: data["user"].(UserMap)["profile"]["name"]
// Multiple type assertions
value := data.config.(Config).settings.(Settings).theme.(string)
// Equivalent to: data["config"].(Config)["settings"].(Settings)["theme"].(string)
// With comma-ok on middle assertion
user, ok := data.user.(UserType)
if ok {
name := user.profile.name
}Complete Equivalence Examples
// All these pairs are equivalent:
// 1. Basic type assertion
x := m.field.(string)
x := m["field"].(string)
// 2. Type assertion in middle
x := m.a.(T).b.c
x := m["a"].(T)["b"]["c"]
// 3. Channel receive + field access
x := (<-ch).field
x := (<-ch)["field"]
// 4. Function call result
x := getMap().field
x := getMap()["field"]
// 5. Complex chaining
x := (<-ch).field.(T).other.value.(int)
x := (<-ch)["field"].(T)["other"]["value"].(int)Rule: If you can write it with v["field"] in Go, you can write it with v.field in XGo with identical semantics.
Rationale
Why This Design Works
This proposal leverages Go's existing comma-ok semantics for chained expressions. Go already supports expressions like:
// Go currently supports this
value, ok := someInterface.(Type1).field.(Type2)Our proposal simply adds syntactic sugar to make map access chains more readable:
// Before (verbose but valid Go)
name, ok := data.(map[string]any)["user"].(map[string]any)["name"].(string)
// After (this proposal)
name, ok := data.user.name.(string)No new semantics - just cleaner syntax that compiles to existing Go patterns.
Why Go's Native Short-circuit Works Perfectly
Safety: Go's comma-ok already prevents panics in chains
// If any step fails in Go's existing comma-ok chains, ok = false
x, ok := value.(Type1)["key"].(Type2)Performance: Go compiler already optimizes comma-ok chains
Clarity: Developers already understand comma-ok behavior
// Familiar pattern
value, ok := m["key"]
if !ok {
// Handle missing key
}
// Same pattern, just longer chain
value, ok := data.user.name
if !ok {
// Handle missing path
}Why Single Boolean?
Simplicity: Users don't need to track which step failed
// Simple two-value return
value, ok := data.a.b.c
if !ok {
// Handle error - don't care which specific step failed
}Consistency: Matches existing Go patterns (map access, type assertion, channel receive)
Alternative Considered: Three-value return (value, ok, error) was rejected as too complex for most use cases.
When Users Need Detailed Errors
For cases requiring precise error information, use explicit steps:
// Precise error handling
user, ok := data.user
if !ok {
return fmt.Errorf("data.user failed: not a map or missing")
}
userMap, ok := user.(map[string]any)
if !ok {
return fmt.Errorf("data.user is not a map[string]any")
}
name, ok := userMap["name"]
if !ok {
return fmt.Errorf("data.user.name missing")
}Design Consistency
This design aligns with existing Go patterns:
Map access:
value, ok := m["key"] // ok indicates if key existsType assertion:
value, ok := x.(T) // ok indicates if assertion succeededChannel receive:
value, ok := <-ch // ok indicates if channel is openThis proposal:
value, ok := data.user.name // ok indicates if entire path succeededPart 3: Combined Examples
Use Case 1: JSON Processing
// Parse JSON from external API
var response any = json.Unmarshal(apiResponse)
// Safe traversal without verbose type assertions
if name, ok := response.data.user.name.(string); ok {
fmt.Println("User name:", name)
}
// Chain multiple checks
if age, ok := response.data.user.age.(int); ok {
if age >= 18 {
fmt.Println("Adult user")
}
}
// Handle missing optional fields
city, ok := response.data.user.address.city.(string)
if !ok {
city = "Unknown"
}Use Case 2: Configuration Objects
config := map[string]any{
"database": map[string]any{
"host": "localhost",
"port": 5432,
"credentials": map[string]any{
"user": "admin",
"pass": "secret",
},
},
}
// Clean, readable configuration access
host := config.database.host.(string)
port := config.database.port.(int)
user := config.database.credentials.user.(string)
// With safety
if host, ok := config.database.host.(string); !ok {
log.Fatal("database.host is required")
}Use Case 3: Dynamic Data Validation
func validateUser(data any) error {
// Check required fields with clear error messages
if _, ok := data.user.id.(string); !ok {
return errors.New("user.id is required and must be string")
}
if _, ok := data.user.email.(string); !ok {
return errors.New("user.email is required and must be string")
}
// Check optional fields
if age, ok := data.user.age.(int); ok {
if age < 0 {
return errors.New("user.age must be positive")
}
}
return nil
}Use Case 4: Safe Nested Access with Fallbacks
// Get user's city with multiple fallback levels
var data any = loadUserData()
city := "Unknown"
if c, ok := data.user.profile.address.city.(string); ok {
city = c
} else if c, ok := data.user.location.city.(string); ok {
city = c
} else if c, ok := data.defaults.city.(string); ok {
city = c
}
fmt.Println("City:", city)Use Case 5: Chaining with Standard Map Access
// Mix field access and bracket notation
var data any = map[string]any{
"users": map[string]any{
"user-123": map[string]any{
"name": "Alice",
},
},
}
// Field access for known structure
users, ok := data.users.(map[string]any)
if !ok {
log.Fatal("invalid data structure")
}
// Bracket notation for dynamic keys
userID := getUserID()
if user, ok := users[userID].(map[string]any); ok {
if name, ok := user["name"].(string); ok {
fmt.Println("User:", name)
}
}Use Case 6: Building Response Objects
func getUserProfile(userID string) (map[string]any, error) {
var data any = fetchFromDatabase(userID)
// Extract and validate fields
name, ok1 := data.user.name.(string)
email, ok2 := data.user.email.(string)
if !ok1 || !ok2 {
return nil, errors.New("invalid user data")
}
// Optional fields with defaults
age := 0
if a, ok := data.user.age.(int); ok {
age = a
}
bio := ""
if b, ok := data.user.profile.bio.(string); ok {
bio = b
}
return map[string]any{
"name": name,
"email": email,
"age": age,
"bio": bio,
}, nil
}Use Case 7: Type Assertion in Middle of Chain
// Define custom types for structured data
type UserData struct {
Profile map[string]any
Settings map[string]any
}
type Config struct {
Users map[string]UserData
}
var data any = loadConfig()
// Type assert to Config in the middle, then continue with field access
// This is equivalent to: data["config"].(Config)["users"]["alice"]["profile"]["name"]
name := data.config.(Config).users.alice.profile.name
// With comma-ok for safety
if name, ok := data.config.(Config).users.alice.profile.name.(string); ok {
fmt.Println("User name:", name)
}Use Case 8: Channel Operations
// Channel of maps
ch := make(chan map[string]any)
go func() {
ch <- map[string]any{
"event": "user_login",
"data": map[string]any{
"user_id": "123",
"timestamp": time.Now(),
},
}
}()
// Receive and access in one expression
// Equivalent to: (<-ch)["data"]["user_id"]
userID := (<-ch).data.user_id
// With comma-ok
if userID, ok := (<-ch).data.user_id.(string); ok {
fmt.Println("User logged in:", userID)
}Benefits
For Basic Field Access
- Improved Readability: Dot notation is more familiar and easier to read, especially for developers coming from JavaScript, Python, or TypeScript
- Reduced Verbosity: Less typing, fewer quotes and brackets
- Consistency with Structs: Maps used as data containers have syntax similar to structs
- Better for DSLs: Improves ergonomics when using maps for configuration or data description
- Complete Go Compatibility: Works in all contexts where bracket notation works
For Comma-ok Propagation
- Leverages Existing Go Semantics: Uses Go's native comma-ok chain evaluation - no new runtime behavior
- Safety: Eliminates panics when traversing uncertain data structures
- Ergonomics: Single line replaces multiple explicit type checks
- Clarity:
okboolean clearly indicates overall success/failure - Zero Overhead: Direct transformation to standard Go code
- Real-world Ready: Perfect for JSON, APIs, and dynamic data processing
Combined Benefits
The combination of field access syntax and Go's existing comma-ok semantics creates a powerful pattern:
Before (verbose but valid):
name, ok := data.(map[string]any)["user"].(map[string]any)["name"].(string)After (clean and equivalent):
name, ok := data.user.name.(string)Same semantics, same performance, much better readability.
Compatibility
Backward Compatibility
This proposal is fully backward compatible:
- Existing code continues to work unchanged
- Bracket notation remains valid and preferred in many cases
- No existing valid code would break
- Comma-ok behavior is opt-in (requires explicit
, oksyntax)
Migration
No migration needed. Developers can:
- Continue using bracket notation
- Adopt dot notation gradually
- Use comma-ok form only where safety is needed
- Mix both styles as appropriate
When to Use Each Style
Use dot notation when:
- Keys are known at development time
- Keys are valid identifiers
- Readability is prioritized
Use bracket notation when:
- Keys are computed at runtime
- Keys contain special characters or spaces
- Compatibility with existing code is needed
- Explicit type assertions are preferred
Implementation Considerations
Parser Changes
- Extend the parser to recognize dot notation on map types and
anytypes - Transform to bracket notation in the AST
- Validate that field names are valid identifiers
- Recognize comma-ok form and generate appropriate short-circuit logic
- Ensure all contexts that work with
v["field"]also work withv.field
Type Checker
- Verify the map has
stringkeys - Ensure no conflicts with actual methods
- For
anytype: generate implicit type assertion tomap[string]any - For comma-ok form: validate complete expression chain
- Track intermediate types through chain for proper type assertions
- Provide clear error messages for invalid usage
- Ensure type assertion can appear at any position in the chain
Code Generation
The transformation is straightforward - field access notation is converted to bracket notation, and Go's existing comma-ok chain semantics handle the rest.
Simple Field Access
// Source
value := m.field
// AST transformation
value := m["field"]
// No runtime overhead - pure syntax sugarComma-ok Field Access
// Source
value, ok := data.user.name
// Direct transformation (no closures needed)
value, ok := data.(map[string]any)["user"].(map[string]any)["name"]
// Go's existing comma-ok chain semantics apply:
// - Any type assertion failure → ok = false
// - Any missing map key → ok = false
// - All steps succeed → ok = trueField Access with Type Assertion
// Source
age, ok := data.user.age.(int)
// Direct transformation
age, ok := data.(map[string]any)["user"].(map[string]any)["age"].(int)
// Go evaluates left-to-right with short-circuit on failureType Assertion in Middle
// Source (fully supported - same as Go with brackets)
name := data.config.(Config).users.alice.name
// Transformed to
name := data["config"].(Config)["users"]["alice"]["name"]
// This works in Go, so it works hereChannel Operations
// Source (fully supported - same as Go with brackets)
value := (<-ch).field
// Transformed to
value := (<-ch)["field"]
// This works in Go, so it works hereWithout Comma-ok (May Panic)
// Source
name := data.user.name
// Transformed to
name := data.(map[string]any)["user"].(map[string]any)["name"]
// Will panic if any step fails (standard Go behavior)Key Implementation Points:
- Pure AST transformation during parsing
- No runtime code generation needed
- No closures or helper functions
- Zero performance overhead
- Leverages existing Go type checker and code generator
- Must support all contexts that work with
v["field"]
Error Messages
m := map[int]string{1: "one"}
_ = m.field
// Error: dot notation requires string keys, got int
m2 := map[string]int{"key": 1}
_ = m2.invalid-name
// Error: "invalid-name" is not a valid identifier
var v any = "not a map"
_ = v.field
// Runtime panic: interface conversion: any is string, not map[string]any
// Safe alternative using comma-ok:
value, ok := v.field
// No panic, ok == false
// Type assertion in middle (fully supported)
type UserData struct {
Name string
}
var data any = map[string]any{"user": "not UserData"}
_ = data.user.(UserData).Name
// Runtime panic: interface conversion (same as with brackets)
// Safe version:
user, ok := data.user.(UserData)
if ok {
fmt.Println(user.Name)
}Performance Considerations
Zero Overhead for All Cases
Since this proposal is pure syntax sugar that transforms to standard Go code, there is no runtime overhead:
Typed Maps:
m := map[string]int{"count": 42}
value := m.count
// Identical to: value := m["count"]
// Zero overhead - same compiled codeany Type with Comma-ok:
var data any = map[string]any{"key": "value"}
value, ok := data.user.name
// Transforms to: value, ok := data.(map[string]any)["user"].(map[string]any)["name"]
// Uses Go's native comma-ok chain evaluation
// Same performance as hand-written codeShort-circuit Benefits:
name, ok := data.user.profile.settings.theme.(string)
// If data.user fails, remaining steps are not evaluated
// Go's existing short-circuit behavior provides this optimizationCompiler Optimizations
The compiler can apply its existing optimizations:
- Type assertion elimination when types are known
- Inline map access operations
- Dead code elimination after failed assertions
- Register allocation for intermediate values
No special optimization passes needed - this is standard Go code after transformation.
Open Questions
1. Should any type access support other map types?
Question: Currently v.field on any assumes map[string]any. Should it support map[string]int, map[string]string, etc?
var v any = map[string]int{"count": 42}
value := v.count // Should this work? What type is returned?Options:
- A: Only support
map[string]any(current proposal) - B: Support any
map[string]T, return typeany - C: Require explicit type parameter somehow
Recommendation: Option A - Only map[string]any for consistency and type safety. Other map types require explicit type assertion.
Rationale:
- Prevents type confusion
- Aligns with common JSON use cases
- Keeps implementation simple
- Users needing specific types can be explicit
2. Tooling preferences?
Question: Should gofmt, linters have opinions on when to use dot vs bracket?
Recommendation: Leave as style preference with configurable linter rules.
Possible linter rules:
- Prefer dot notation for literal keys that are identifiers
- Prefer bracket notation for computed keys
- Flag mixed styles in same scope
3. Distinguishing failure types in comma-ok?
Question: Should users be able to distinguish between "key doesn't exist" and "type assertion failed"?
value, ok := data.missing.field
// ok == false, but why?
// - data is not a map?
// - data.missing key doesn't exist?
// - data.missing is not a map?
// - data.missing.field key doesn't exist?Options:
- A: Single
okboolean (current proposal) - B: Three-value return:
(value, ok, error) - C: Different functions for different checks
Recommendation: Option A - Keep simple two-value return.
Rationale:
- Matches Go idioms
- Simple to use
- Users needing details can use explicit steps
- Adding error would break pattern consistency
Alternatives Considered
1. Do Nothing
Keep the current bracket-only syntax.
Pros:
- No changes needed
- Simple and unambiguous
Cons:
- Verbose for common cases
- Less ergonomic than competitors
- Missed opportunity for improvement
Decision: Rejected - Benefits outweigh minimal implementation cost
2. Allow All String Keys
Support any string key with dot notation, using quotes when needed (e.g., m."field-name").
Pros:
- Maximum flexibility
Cons:
- Syntax is awkward
- Less intuitive
- Adds parsing complexity
Decision: Rejected - Limited to valid identifiers is clearer
3. New Map-like Type
Introduce a new Object or Dict type with dot notation.
Pros:
- Clear separation
- No ambiguity
Cons:
- Fragments type system
- Interoperability issues
- Migration burden
Decision: Rejected - Enhance existing maps instead
4. No any Type Support
Only allow dot notation for explicit map[string]T types.
Pros:
- Stricter typing
- No implicit conversions
Cons:
- Reduces ergonomics for JSON/dynamic data
- Main use case is precisely
anyfrom unmarshal - Users would need verbose manual type assertions
Decision: Rejected - any support is crucial for proposal value
5. Three-value Return for Comma-ok
Use (value, ok, error) to provide detailed failure information.
Pros:
- More information available
- Can distinguish error types
Cons:
- Breaks Go idiom patterns
- More complex API
- Most users don't need details
- Can use explicit steps when needed
Decision: Rejected - Two-value return maintains simplicity and consistency
6. Separate Proposal for Comma-ok Propagation
Split this into two proposals:
- Basic field access syntax sugar
- General comma-ok chain propagation
Pros:
- Clearer separation of concerns
- Can implement incrementally
- Focused discussions
Cons:
- Features are tightly coupled semantically
- Users would be confused by incomplete behavior
- Implementation overlap is significant
- Would create version fragmentation
Decision: Rejected - Keep unified but well-structured
7. Restrict Type Assertions to End Only
Only allow type assertions at the end of chains, disallow in the middle.
Pros:
- Simpler to implement
- Simpler to explain
Cons:
- Breaks equivalence with
v["field"]- Go allowsv["field"].(T)["other"] - Inconsistent with Go behavior
- Reduces flexibility
- Users would be confused why
m["a"].(T)["b"]works butm.a.(T).bdoesn't
Decision: Rejected - Violates core principle of equivalence with bracket notation
Conclusion
Adding dot notation for map access with comma-ok support is a high-value, low-risk enhancement that:
✅ Improves Ergonomics: Makes common patterns dramatically simpler
✅ Enhances Safety: Leverages Go's existing comma-ok chain semantics
✅ Maintains Compatibility: Fully backward compatible, opt-in features
✅ Minimal Complexity: Pure syntax sugar - transforms to standard Go code
✅ Zero Runtime Overhead: Direct AST transformation, no closures or helpers
✅ Aligns with Modern Languages: Matches developer expectations
✅ Solves Real Problems: Perfect for JSON, APIs, and dynamic data
✅ Complete Go Equivalence: v.field ≡ v["field"] in ALL contexts
The features work together synergistically:
- Field access syntax provides clean notation
- Go's native comma-ok chains provide safe traversal
- Combined, they eliminate boilerplate for common use cases
- Perfect equivalence with bracket notation eliminates surprises
The proposal is carefully designed to:
- Leverage existing Go semantics (no new runtime behavior)
- Maintain Go's type safety and clarity
- Avoid surprising behavior
- Enable gradual adoption
- Support future extensions
- Preserve complete behavioral equivalence with
v["field"]
Core Principle Satisfied: Any expression that works with v["field"] in Go will work identically with v.field in XGo. This is pure syntax sugar with zero semantic differences.
Implementation impact: Extremely low risk, high reward
- Simple AST transformation during parsing
- No new runtime semantics
- No performance overhead
- Leverages existing Go type checker and compiler
- Clear error messages
- Minimal compiler changes
- No special cases or restrictions beyond those that apply to
v["field"]
This proposal positions XGo as a more productive language for modern development workflows while maintaining its core values of simplicity, clarity, and compatibility with Go's proven design patterns.
Draft: https://github.com/goplus/xgo/wiki/XGo-Proposal:-Map-Field-Access-with-Comma%E2%80%90ok-Propagation