Go method sets, interface satisfaction, and receiver choice
A small receiver choice in Go changes interface satisfaction in ways engineers often misread. This tool shows the method sets for `T` and `*T` through `go/types`, along with the compiler error and the rule behind it.
If you work in Go long enough, you hit the same assignment error again and again. A value of type T does not satisfy an interface, but *T does. The compiler is correct. The problem is that the rule lives in method sets, and method sets are easy to misremember when you are moving fast.
Go Method Sets — why *T implements it and T does not focuses on this narrow failure mode. You paste a type and an interface. It computes the method sets for T and *T through go/types, then shows the compiler error and the language rule behind it. For an engineer, this matters because interface satisfaction in Go is structural. A small receiver choice changes assignability, API ergonomics, and what breaks at a call site.
This kind of tool is useful because the common explanations are incomplete. People remember “pointer receivers need pointers,” but the full behavior depends on method sets, addressability, embedding, and where the interface check happens. If you review Go APIs, maintain shared packages, or debug generic code, you need a way to inspect the exact rule the compiler applies, not a paraphrase.
Inspect the receiver boundary
The core signal is simple. Methods with a value receiver belong to the method set of T and *T. Methods with a pointer receiver belong to the method set of *T only. Interface satisfaction checks the method set of the concrete type on the left side of the assignment or conversion.
A compact example:
type Runner interface {
Run()
}
type Job struct{}
func (Job) Run() {}
Here, both Job and *Job satisfy Runner, because Run has a value receiver.
Now change one line:
func (*Job) Run() {}
Now *Job satisfies Runner, but Job does not. The method exists, but not in the method set of Job.
This is where engineers often mix up two separate facts. First, the compiler lets you call a pointer-receiver method on an addressable value in some cases:
var j Job
j.Run()
That call can work because the compiler rewrites it as (&j).Run() when j is addressable. Second, this rewrite does not change interface satisfaction. If you assign j to a Runner, the compiler checks the method set of Job, not the set of methods reachable through an implicit address-taking rewrite.
That distinction explains a large share of production confusion. Code compiles at the call site. Then it fails when a value crosses an interface boundary.
Verify against go/types, not memory
The strongest design choice in this tool is the use of go/types. That matters because method-set behavior has edge cases, and hand-rolled logic drifts from the compiler model.
When you test interface satisfaction, there are a few things you want to see in one place:
- The method set of
T - The method set of
*T - The exact error for the failing assignment or assertion
- The language rule that explains the mismatch
This gives you a concrete verification path. Paste a minimal snippet. Compare the listed methods. Then inspect the compiler error. If T lacks a method required by the interface, you know the fix is structural. You either change the receiver, pass a pointer, or change the interface boundary.
A good example is constructor return types. Suppose your constructor returns T, but your interface requires a method implemented with a pointer receiver:
type Closer interface {
Close() error
}
type FileLike struct{}
func (*FileLike) Close() error { return nil }
func NewFileLike() FileLike {
return FileLike{}
}
NewFileLike() returns a value, so code like this fails:
var c Closer = NewFileLike()
If your mental model stops at “I can call x.Close() on a value,” you lose time. A method-set view shows the problem fast. FileLike does not implement Closer. *FileLike does.
Read the signals around embedding and promotion
Where this topic gets harder is embedding. Promoted methods make APIs look wider than they are. The type appears to expose methods, but interface satisfaction still follows precise method-set rules.
Consider an embedded field with pointer-receiver methods:
type Base struct{}
func (*Base) Reset() {}
type Wrapper struct {
Base
}
type Resetter interface {
Reset()
}
At a glance, Wrapper seems to have Reset. In practice, whether Wrapper or *Wrapper satisfies Resetter depends on promotion into each method set, not on surface appearance. This is a common review bug. A refactor adds embedding. Method calls still compile in some contexts. Interface assignments elsewhere break.
This class of issue shows up in three places:
- Library APIs that expose structs with mixed receiver choices
- Dependency injection setups where concrete types are stored behind interfaces
- Generic helper code where type arguments must satisfy constraints through method sets
The right signal to inspect is never “does the method seem callable.” It is “which method set contains it.” If a tool prints both sets side by side, you get the right level of detail.
Another edge is unexported methods. Interfaces with unexported methods are often used to restrict implementation to a package. If a type appears to match except for one hidden method, the failure is structural, not accidental. Again, you want the compiler’s own account of the missing method, not a broad explanation.
Where this commonly goes wrong in code review
Method-set bugs are often introduced by good-faith cleanup. A receiver changes from value to pointer to avoid copying or to mutate state. The local code keeps working. Interface satisfaction breaks far from the edit.
Watch for these patterns:
- A constructor returns
Twhile interfaces require pointer-receiver methods - A public interface is satisfied by values in tests, but only because test code uses addressable locals and direct calls
- A struct is embedded to “inherit” behavior, but assignability through interfaces changes
- A generic constraint names methods that only
*Tprovides, while callers passT - A method moves to a pointer receiver after a performance change, with no scan for interface boundaries
A practical review step is to reduce the case to the smallest possible snippet. One concrete type. One interface. One assignment. Then inspect T and *T separately. If a tool derives both sets from go/types, you remove guesswork.
This also improves API design. Receiver choice is not only about mutation and copy cost. It shapes how easy your type is to pass around. If you expect users to store your type in interfaces, pointer-only method sets add friction. Sometimes that is the right trade. Sometimes it is an accidental footgun.
How to use the output well
The useful output here is not the final yes or no. It is the chain of evidence.
Start with the method sets. Check whether the required interface methods appear in T, *T, or both. Then read the compiler error. It usually tells you which method is missing and whether the mismatch is between a value and a pointer receiver. Then read the rule cited by the tool and map it back to your code shape.
You should leave with one of a small number of fixes:
- Pass
&vinstead ofv - Change the constructor to return
*T - Change receiver types to value receivers where mutation and copy semantics allow it
- Narrow or reshape the interface boundary
- Split a type so pointer-only stateful behavior is separate from value-friendly read behavior
Those are engineering choices, not syntax tricks. The tool helps because it exposes the exact boundary the compiler enforces.
What to watch next
Go keeps the rules here stable, but your codebase grows more indirect over time. Interfaces hide concrete types. Embedding widens surfaces. Generics add new places for method sets to matter. When a type “looks like” it should satisfy an interface, inspect the method sets first. That habit saves review time and avoids fixes based on folklore.