Method Sets Deep — Specification¶
Official Specification Reference Source: Go Language Specification — §Method_sets, §Address_operators, §Selectors, §Calls, §Index_expressions, §For_statements
Table of Contents¶
- Spec Reference
- Formal Grammar (EBNF)
- Method Set Definition
- Addressability Rules
- Map Element Non-Addressability
- Selector and Auto-Address Rules
- Embedding and Promoted Method Sets
- Interface Satisfaction Rules
- For-Range Loop Variable Semantics (Go 1.22)
- Edge Cases from Spec
- Spec Compliance Checklist
1. Spec Reference¶
Method Sets — Official Text¶
The method set of a type determines the interfaces that the type implements. The method set of a defined type T consists of all methods declared with receiver type T. The method set of a pointer to a defined type T (where T is neither a pointer nor an interface) is the set of all methods declared with receiver T or T. Further rules apply to structs containing embedded fields*, as described in the section on struct types. The method set of any other type is empty.
Source: https://go.dev/ref/spec#Method_sets
Address Operators — Official Text¶
The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array. As an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal.
Source: https://go.dev/ref/spec#Address_operators
Selectors — Official Text¶
A selector f may denote a field or method f of type T, or it may refer to a field or method f of a nested embedded field of T. The number of embedded fields traversed to reach f is called its depth in T. [...] For a value x of type T or *T where T is not a pointer or interface type, x.f denotes the field or method at the shallowest depth in T where there is such an f. If there is not exactly one f with shallowest depth, the selector expression is illegal.
Source: https://go.dev/ref/spec#Selectors
Method Call Auto-Address — Official Text¶
A method call x.m() is valid if the method set of (the type of) x contains m and the argument list can be assigned to the parameter list of m. If x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m().
Source: https://go.dev/ref/spec#Calls
Map Index Expressions — Official Text¶
An index expression on a map a of type map[K]V used in an assignment statement or initialization of the special form
v, ok = a[x]... [Map index expressions are not addressable in the spec's definition of addressable expressions.]
Source: https://go.dev/ref/spec#Index_expressions
For Range — Official Text (Go 1.22)¶
Each iteration has its own separate declared variable (or variables). [Behaviour change effective when go.mod specifies
go 1.22or later.]
Source: https://go.dev/ref/spec#For_range
2. Formal Grammar (EBNF)¶
Address Operator¶
UnaryExpr = PrimaryExpr | unary_op UnaryExpr .
unary_op = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .
The grammar accepts & on any UnaryExpr; the type system then rejects non-addressable operands.
Selector¶
Combined with PrimaryExpr to give x.f. The lookup rule (above, in Selectors) governs which f is selected.
Method Set (informal — the spec defines this in prose, not grammar)¶
Pseudo-rules:
methodSet(T) = { M | M declared with receiver of type T }
methodSet(*T) = { M | M declared with receiver of type T or *T }
methodSet(I) = methods declared in I + methods of embedded interfaces
methodSet(other) = ∅
For-Range Statement¶
ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
The := form declares fresh per-iteration variables in Go 1.22+ when go.mod is at version 1.22+.
3. Method Set Definition¶
Defined types¶
| Type | Method set |
|---|---|
T | {V} |
*T | {V, P} |
Interface types¶
The method set of an interface is the set of methods declared in it (plus methods of embedded interfaces).
Other types¶
The method set of any other type is empty.
This includes: - Predeclared types (int, string, bool, ...) - Type aliases (type X = Y — methodSet of X equals methodSet of Y) - Channel/map/slice types not declared as named types
4. Addressability Rules¶
Addressable expressions, per the spec:
| Form | Addressable? |
|---|---|
| Identifier referring to a variable | yes |
*p | yes |
s[i] (s is a slice) | yes |
a[i] (a is an addressable array) | yes |
x.f (x is addressable struct value) | yes |
Composite literal &T{...} (only with explicit &) | yes — special exception |
| Function return value | no |
Map index m[k] | no |
Type assertion result i.(T) | no |
| Constant | no |
| Method call result | no |
| String index | no (immutable bytes) |
Channel receive <-c | no |
The exception: &T{...} is legal as a primary expression (the spec carves this out in Address operators), but the value T{...} outside & is not addressable for selector or method-call purposes.
&Point{X: 1}.X // ❌ — Point{}.X is not addressable; & does not save it
(&Point{X: 1}).X // ✅ — explicit & creates a *Point, then .X via deref
p := &Point{X: 1}
p.X // ✅ — *p is addressable
5. Map Element Non-Addressability¶
The spec explicitly excludes m[k] from addressable expressions. The implementation reason:
The map's underlying hash table may be reallocated when entries are added, rendering any saved address stale.
Consequences for method calls:
type T struct{ n int }
func (t *T) Inc() { t.n++ }
m := map[string]T{"k": {}}
m["k"].Inc() // compile error: cannot take address of m["k"]
The compiler's "auto-address" rewrite (x.m() → (&x).m()) requires x addressable. Since m["k"] is not addressable, the rewrite fails — and the compile error is the intended outcome.
Workarounds:
// 1. Read-modify-write
v := m["k"]; v.Inc(); m["k"] = v
// 2. Store pointers
m2 := map[string]*T{"k": new(T)}
m2["k"].Inc() // m2["k"] has type *T, which directly carries Inc
The second approach works because the map element itself is the pointer value (*T), and that pointer value is what gets addressed (or rather, dereferenced) by Inc's body.
6. Selector and Auto-Address Rules¶
The selector rule for method calls is:
Given x.m() where m has receiver type T or *T:
1. Compute the method set of (type of x).
- If type is *T or T (where T is not pointer/interface), method set follows
the rules above.
- If type is interface I, method set is I's declared methods.
2. If m ∈ methodSet(type of x), call directly.
3. Else if x is addressable AND m ∈ methodSet(*x's type), implicitly take &x:
x.m() ≡ (&x).m()
4. Else if x is *T and m ∈ methodSet(T), implicitly dereference:
x.m() ≡ (*x).m() [always allowed — no addressability needed]
5. Else: compile error.
Auto-dereference is unconditional¶
Always works, no addressability check.
Auto-address requires addressability¶
type T struct{}
func (t *T) M() {}
t := T{} // addressable variable
t.M() // OK — t.M() ≡ (&t).M()
m := map[string]T{"k": {}}
m["k"].M() // compile error — m["k"] not addressable
7. Embedding and Promoted Method Sets¶
Spec text from §Struct types:
A field declared with a type but no explicit field name is called an embedded field. An embedded field must be specified as a type name T or as a pointer to a non-interface type name *T, and T itself may not be a pointer type.
Promotion rules¶
For a struct S with embedded field of type T:
- Methods of
T(i.e., methodSet(T)) are promoted to S — i.e.,s.M()is shorthand fors.T.M(). - If
*Tis embedded (orShas an embeddedTandsis addressable), methods of*Tare also promoted. - Method set of
S(value) = methodSet(T) (always) + methodSet(*T) (only ifSis itself addressable, computed at the call site) - Method set of
*S= methodSet(T) + methodSet(*T) — always
Pseudo-formulation from §Method sets:
For S = struct{ T }:
methodSet(S) ⊇ methodSet(T)
methodSet(*S) = methodSet(T) ∪ methodSet(*T)
For S = struct{ *T }:
methodSet(S) = methodSet(T) ∪ methodSet(*T) // *T embedded, fully addressable
methodSet(*S) = methodSet(T) ∪ methodSet(*T)
Selector ambiguity¶
A selector expression x.f refers to the field or method at the shallowest depth in T where there is such an f. If there is not exactly one f with shallowest depth, the selector expression is illegal.
type A struct{}; func (A) M() {}
type B struct{}; func (B) M() {}
type C struct{ A; B }
var c C
// c.M() // illegal — both A.M and B.M at depth 1
Shadowing¶
A method declared on the outer struct shadows any promoted method:
type Inner struct{}
func (Inner) M() string { return "inner" }
type Outer struct{ Inner }
func (Outer) M() string { return "outer" }
var o Outer
o.M() // "outer" — outer's M shadows Inner.M
8. Interface Satisfaction Rules¶
A type T satisfies interface I iff:
For a value-vs-pointer assignment:
var i I = v // requires methodSet(I) ⊆ methodSet(typeof(v))
// typeof(v) is T if v is a T variable
// is *T if v is &someT or returned as *T
Common consequences:
type Cat struct{}
func (c *Cat) Rename(string) {}
type R interface { Rename(string) }
var _ R = &Cat{} // OK — methodSet(*Cat) ⊇ {Rename}
var _ R = Cat{} // ❌ — methodSet(Cat) ⊉ {Rename}
Interface-to-interface assignment¶
The runtime checks this at the assignment point. If it fails, the assignment panics (not a compile error, because i's dynamic type isn't known at compile time).
For the compile-time form (i.(J)), the check occurs at the assertion site.
9. For-Range Loop Variable Semantics (Go 1.22)¶
Spec change in Go 1.22:
In a
for ... := rangestatement, each iteration has its own separate declared variable (or variables). The variables created in one iteration do not coexist with those of another iteration.
Effective only if go.mod declares go 1.22 or later.
Pre-1.22 behaviour¶
for _, x := range xs {
fns = append(fns, func() { use(x) }) // closes over shared x
}
// All closures see the final value of x
Post-1.22 behaviour¶
for _, x := range xs {
fns = append(fns, func() { use(x) }) // closes over per-iteration x
}
// Each closure sees its own x
Method-value implication¶
A method value x.M evaluates the receiver expression at expression time. With value receivers, the receiver value is captured. With pointer receivers, the address of x is captured.
In Go 1.21 and earlier: &x is the same address every iteration (the shared x). All method values bind the same pointer.
In Go 1.22+: each iteration has its own x, hence its own &x. Each method value binds a distinct pointer.
The go.mod line go 1.22 is the toggle. Modules at older versions retain old semantics even when built by a newer toolchain.
10. Edge Cases from Spec¶
Edge Case 1 — Composite literal with explicit &¶
Without &, the composite literal is not addressable for method calls.
Edge Case 2 — Slice literal element¶
type T struct{ n int }
func (t *T) M() {}
[]T{{n: 1}}[0].M() // legal — slice index is addressable
// even when the slice itself is anonymous
The slice literal is not addressable, but []T{...}[0] (slice indexing) is. Subtle distinction.
Edge Case 3 — Function returning T¶
The fix: store in a variable, or change newT to return *T.
Edge Case 4 — Type assertion is non-addressable¶
Edge Case 5 — nil map element of pointer type¶
Distinct from compile-time error: here the map element is addressable (it's a *T value), but the pointer itself is nil. Method call dereferences nil and panics.
Edge Case 6 — Promoted method through nil embedded pointer¶
type Inner struct{}
func (i *Inner) M() {}
type Outer struct{ *Inner }
var o Outer
o.M() // panic — o.Inner is nil, M dereferences
Edge Case 7 — Embedded interface field¶
type Reader interface{ Read([]byte) (int, error) }
type LoggingReader struct{ Reader } // embedding an interface value
func (lr LoggingReader) Read(p []byte) (int, error) { /* ... */ }
The methodSet of LoggingReader includes Reader's methods (here overridden by Read).
11. Spec Compliance Checklist¶
- Concrete type method set follows: T = value methods only; *T = both
- Pointer-receiver methods are NOT in methodSet(T)
- Auto-address
(&x).m()only happens when x is addressable - Map elements
m[k]are not addressable — pointer-method calls forbidden - Type assertion results are not addressable
- Function return values are not addressable
- Composite literal addressability requires explicit
& - Embedded field promotion respects depth-first selector rule
- Ambiguous selector at the same depth is a compile error
- Interface satisfaction requires methodSet(I) ⊆ methodSet(value-or-pointer)
-
go.modgo 1.22enables per-iteration loop variables - Method values bind the receiver at expression-evaluation time
Related Spec Sections¶
| Section | URL | Relevance |
|---|---|---|
| Method sets | https://go.dev/ref/spec#Method_sets | Core method set rules |
| Address operators | https://go.dev/ref/spec#Address_operators | Addressability list |
| Selectors | https://go.dev/ref/spec#Selectors | Field/method selection, depth, ambiguity |
| Calls | https://go.dev/ref/spec#Calls | Auto-address in method calls |
| Index expressions | https://go.dev/ref/spec#Index_expressions | Map element non-addressability |
| Struct types | https://go.dev/ref/spec#Struct_types | Embedded fields, promotion |
| Interface types | https://go.dev/ref/spec#Interface_types | Interface satisfaction |
| For statements | https://go.dev/ref/spec#For_range | Loop variable semantics (Go 1.22) |
| Composite literals | https://go.dev/ref/spec#Composite_literals | The &T{...} exception |
| Type identity | https://go.dev/ref/spec#Type_identity | Defined vs alias and method-set inheritance |