Question and context
Go 1.26 lifts the restriction that a generic type cannot mention itself in its type-parameter list. Explain the constraint below and provide a compilable call:
type Adder[A Adder[A]] interface {
Add(A) A
}
func SumPair[A Adder[A]](x, y A) A {
return x.Add(y)
}Also explain why this self-reference does not make every recursive type valid.
What the interviewer is testing
- Whether you distinguish a self-referential constraint from an infinitely recursive struct.
- Whether you can explain constraint satisfaction, type-argument substitution, and method-set checking.
- Whether you can instantiate the algorithm with a concrete type and reason about value versus pointer receivers.
- Whether you provide version, compiler, and test-matrix safeguards instead of quoting release notes.
Clarifying questions to ask first
Target Go version
Do production compilers, generators, and CI all support Go 1.26? Must a public library remain compatible with Go 1.25?
Algebraic semantics
Does Add require associativity, commutativity, or no side effects? Is the algorithm a single call or a fold over an arbitrary collection?
Instantiation shape
Is the implementation a value or pointer type? Must Add return exactly the receiver's type?
A 30-second answer framework
Go 1.26 lets a constraint refer to the generic type currently being declared, enabling an F-bounded shape such as “the argument implements operations returning itself.” Adder[A Adder[A]] still requires a concrete argument to provide Add(A) A; instantiation substitutes the argument before checking its method set. The change broadens constraint declarations, not the rules for finite value layout.
Deep-dive answer steps
1. Explain the syntax change
Older versions rejected Adder inside its own parameter constraint. Go 1.26 binds type parameters at the start of the list, so the constraint can refer to the generic type being declared. This adds expressive power without changing static interface-method checks.
2. Define a satisfying type
type IntAdder int
func (x IntAdder) Add(y IntAdder) IntAdder {
return x + y
}
func example() IntAdder {
return SumPair(IntAdder(2), IntAdder(3))
}IntAdder satisfies Adder[IntAdder] because both the parameter and result use the instantiated argument type. A method returning int would fail even when the underlying representation is the same.
3. Describe instantiation checking
For SumPair[IntAdder], the compiler substitutes IntAdder for A, obtains Adder[IntAdder], and checks IntAdder's method set. Type inference may infer A from the arguments, but it does not replace constraint satisfaction.
4. Handle pointer method sets
If Add exists only on *IntAdder, callers must use *IntAdder as the argument and preserve that shape in the result; a value IntAdder does not automatically gain pointer methods. Receiver choice changes the method set and is not a generic conversion.
5. Set the recursive-type boundary
The self-reference is in the constraint graph, such as Adder[A]; it is not a by-value infinite nesting. type Node[T any] struct { Next Node[T] } still has an infinitely large value layout, while *Node[T] has finite layout. Constraint cycles and concrete layout are separate checks.
6. Evaluate algorithmic value
The pattern suits homogeneous generic algorithms such as combining values or accumulating numeric objects. It does not provide associativity or prove overflow safety. If the algorithm needs algebraic laws, document and test them; method presence is not a mathematical proof.
7. Plan migration and verification
Align the minimum version in go.mod, CI, and release images; keep a normal-interface path for older toolchains. Test successful instantiation, rejected signatures, value and pointer receivers, empty input, and numeric boundaries, and include compile-failure examples in the workflow.
A high-quality sample answer
I would describe Adder[A Adder[A]] as requiring an argument whose Add accepts and returns that same instantiated type, not as permission for arbitrary recursion. I would first compile IntAdder with a value receiver, then show the pointer method-set boundary. Callers still supply the algebraic contract; migration locks the toolchain to Go 1.26 and uses positive and negative compile tests to protect the constraint.
Common mistakes
- Treating a self-referential constraint as permission for by-value infinite structs.
- Ignoring that the instantiated method signature must match
Aexactly. - Assuming identical underlying types imply identical interface method sets.
- Treating method presence as proof of associativity, commutativity, or no overflow.
- Publishing Go 1.26 syntax while older compilers still build production artifacts.
- Testing only successful examples and never checking rejected constraints or pointer receivers.
Follow-up questions and answers
Is Adder[A Adder[A]] a recursive interface?
It is a declaration pattern in which a constraint refers to the generic type being declared. After substitution, the compiler checks whether the argument implements the expanded interface; that differs from a field recursively embedding a value forever.
Why does type IntAdder int satisfy the constraint?
It declares Add(IntAdder) IntAdder, so the receiver and result exactly use the instantiated argument type. Underlying-type identity cannot replace method-signature checking.
When does a pointer receiver fail?
It fails when the argument is a value type but the method exists only in the pointer method set. Choose value or pointer instantiation deliberately and keep the result shape consistent.
Does this constraint guarantee associativity?
No. The interface describes callable method signatures. Associativity, overflow policy, and side effects belong in documentation, tests, or a higher-level protocol.
How do you support Go 1.25?
Isolate the self-referential constraint in Go 1.26 files or modules, use a normal interface or specialized implementation for older versions, and compile positive and negative examples with a multi-version CI matrix.