Skip to content

Map Field Access with Comma‐ok Propagation #2572

@xushiwei

Description

@xushiwei

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.ym["field"]["x"]["y"]
  • ✅ Channel operations: (<-ch).field(<-ch)["field"]
  • ✅ Assignments: m.field = xm["field"] = x
  • ✅ Any valid Go expression with v["field"] has an equivalent with v.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 string keys
  • Keys that are valid Go identifiers (no spaces, special characters, etc.)
  • Both read and write operations
  • Implicit map access on any type: Variables of type any can also use dot notation, which performs an implicit type assertion to map[string]any

Basic Examples

Basic access:

config := map[string]string{
    "host": "localhost",
    "port": "8080",
}
url := config.host + ":" + config.port

Assignment:

data := make(map[string]int)
data.count = 42  // equivalent to data["count"] = 42

Nested 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

  1. Valid identifiers only: m.fieldName where fieldName matches [a-zA-Z_][a-zA-Z0-9_]*
  2. String keys only: Only maps with string keys support dot notation
  3. Compile-time transformation: m.fieldm["field"] at compile time
  4. No shadowing: If a map type has actual methods/fields, those take precedence
  5. Implicit type assertion for any: v.field where v is of type anyv.(map[string]any)["field"]
  6. Complete equivalence: Any context where v["field"] is valid, v.field must 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 = false

This 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 behavior

Overview

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 ok value 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 == nil

Rule 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 == false

Detailed 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 value

Example 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 = false

Example 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 = false

Example 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 succeeded

Comparison 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 exists

Type assertion:

value, ok := x.(T)  // ok indicates if assertion succeeded

Channel receive:

value, ok := <-ch  // ok indicates if channel is open

This proposal:

value, ok := data.user.name  // ok indicates if entire path succeeded

Part 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

  1. Improved Readability: Dot notation is more familiar and easier to read, especially for developers coming from JavaScript, Python, or TypeScript
  2. Reduced Verbosity: Less typing, fewer quotes and brackets
  3. Consistency with Structs: Maps used as data containers have syntax similar to structs
  4. Better for DSLs: Improves ergonomics when using maps for configuration or data description
  5. Complete Go Compatibility: Works in all contexts where bracket notation works

For Comma-ok Propagation

  1. Leverages Existing Go Semantics: Uses Go's native comma-ok chain evaluation - no new runtime behavior
  2. Safety: Eliminates panics when traversing uncertain data structures
  3. Ergonomics: Single line replaces multiple explicit type checks
  4. Clarity: ok boolean clearly indicates overall success/failure
  5. Zero Overhead: Direct transformation to standard Go code
  6. 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 , ok syntax)

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 any types
  • 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 with v.field

Type Checker

  • Verify the map has string keys
  • Ensure no conflicts with actual methods
  • For any type: generate implicit type assertion to map[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 sugar

Comma-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 = true

Field 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 failure

Type 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 here

Channel Operations

// Source (fully supported - same as Go with brackets)
value := (<-ch).field

// Transformed to
value := (<-ch)["field"]

// This works in Go, so it works here

Without 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 code

any 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 code

Short-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 optimization

Compiler 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 type any
  • 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 ok boolean (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 any from 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:

  1. Basic field access syntax sugar
  2. 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 allows v["field"].(T)["other"]
  • Inconsistent with Go behavior
  • Reduces flexibility
  • Users would be confused why m["a"].(T)["b"] works but m.a.(T).b doesn'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.fieldv["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

Sub-issues

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions