Interface Satisfaction Checks
How to verify at compile time that a concrete type satisfies a Go interface and why this idiom matters for maintainable code
Interface satisfaction in Go is implicit — a type automatically satisfies an interface if it possesses all the methods the interface declares. The compiler enforces this whenever you assign a value to an interface variable. But what if you never actually assign a value of your type to that interface in your code? The check never happens, and a future change to the interface or the type can silently break the contract, surfacing only at runtime when someone finally tries to use it.
The compile‑time interface satisfaction check is a one‑line idiom that forces the compiler to verify the relationship, even when no direct assignment exists. It acts as both a safety net and a form of executable documentation.
The var _ Interface = (*Type)(nil) Pattern
The standard way to assert satisfaction at compile time looks like this:
var _ io.Writer = (*MyWriter)(nil)
Let’s break that down piece by piece.
var _declares a variable with the blank identifier. The variable has no name you can reference, so it cannot be used after the declaration. It exists only to trigger the type check.io.Writeris the interface you want to enforce.(*MyWriter)(nil)creates a nil pointer of the concrete typeMyWriterand casts it to*MyWriter. Casting a nil value is perfectly legal; the resulting expression is simply a typed nil pointer.
The assignment forces the compiler to answer the question: “Can a *MyWriter be stored in a variable of type io.Writer?” If *MyWriter has all the methods required by io.Writer, the answer is yes, and compilation succeeds. If any method is missing or has a mismatched signature, the compiler refuses the assignment and the build fails.
Check Passed:
If your code compiles, the type satisfies the interface. No runtime cost — the compiler removes the blank‑identifier variable after the check.
A Complete Example
package main
import (
"fmt"
"io"
)
type ConsoleLogger struct{}
func (c *ConsoleLogger) Write(p []byte) (int, error) {
fmt.Print(string(p))
return len(p), nil
}
// Compile-time check: *ConsoleLogger satisfies io.Writer
var _ io.Writer = (*ConsoleLogger)(nil)
func main() {
var w io.Writer = &ConsoleLogger{}
w.Write([]byte("hello\n"))
}
Here, the compiler verifies that *ConsoleLogger has a Write([]byte) (int, error) method. The blank‑identifier declaration sits at package level, outside any function. If you later rename the Write method or change its signature, the assignment fails, and you catch the breakage immediately — not when a user of your package calls a function that expects an io.Writer.
Now suppose you remove the Write method and try to compile. The output is a direct, actionable error:
./prog.go:15:5: cannot use (*ConsoleLogger)(nil) (type *ConsoleLogger) as type io.Writer in assignment:
*ConsoleLogger does not implement io.Writer (missing Write method)
That message tells you exactly which method is absent. There’s no ambiguity and no need to trace runtime panics.
Why This Pattern Exists
Implicit satisfaction is one of Go’s most loved features — it decouples interface definitions from implementations. But that same decoupling creates a gap: without an assignment somewhere in the program, the compiler never checks the relationship. In a large codebase, it’s easy for a type to drift away from an interface it is supposed to implement, especially when:
- The interface lives in another package that evolves independently.
- The concrete type is only ever passed around inside a struct or through reflection.
- The type is part of a library and the interface contract is a promise to callers, not something used internally.
The var _ line closes that gap. It forces a proof, at compile time, that the type still respects the contract. It also serves as a signal to anyone reading the code: “This type is intended to satisfy this interface.” It is a declaration of intent that the compiler itself can enforce.
Documentation, Not Just Guarding:
The check doubles as living documentation. Scanning a file and seeing var _ http.Handler = (*MyHandler)(nil) tells you immediately that MyHandler is meant to be an HTTP handler, even before you look at its methods.
Value Receivers vs. Pointer Receivers
The pattern changes slightly depending on whether the methods are defined on the value type or the pointer type. The rule is straightforward: the type used in the assignment must be the type that actually satisfies the interface.
A value receiver method works on both Type and *Type. A pointer receiver method works only on *Type. The compile‑time check should reflect the receiver type your type actually uses.
If all required methods use value receivers, you can check with a plain zero value:
type Rect struct {
Width, Height float64
}
func (r Rect) Area() float64 {
return r.Width * r.Height
}
func (r Rect) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
type Shape interface {
Area() float64
Perimeter() float64
}
// Rect satisfies Shape
var _ Shape = Rect{}
Wrong Receiver Can Break the Check:
A common mistake is using a value assignment like var _ Interface = MyType{} when the required methods are defined with pointer receivers. The compiler will reject it with a “does not implement” error. If the type only ever needs to be used as a pointer, make sure the check reflects that — use (*MyType)(nil) or &MyType{}.
Checking Multiple Interfaces
A single type often satisfies several interfaces. You can stack multiple blank‑identifier lines, one per interface:
var _ io.Reader = (*FileHandle)(nil)
var _ io.Writer = (*FileHandle)(nil)
var _ io.Closer = (*FileHandle)(nil)
Each line independently verifies that *FileHandle has the required method set. If the team later decides that FileHandle should no longer be a Closer and removes the Close method, the third line fails to compile, alerting everyone to the change. This is far safer than discovering the missing method when a deferred Close() panics.
Placing these lines directly after the type definition — or in the same file — keeps the contract visible and close to the implementation.
A Mental Model for Beginners
Think of the var _ Interface = (*Type)(nil) line as a compiler‑enforced handshake. You are telling the compiler: “I intend for this type to act as that interface. Please confirm that it actually can.” The compiler looks at the type’s methods, compares them to the interface’s method set, and either gives a nod of approval or refuses with an error message that names the missing method.
If you’ve ever written a unit test to confirm that a function returns the correct value, the compile‑time satisfaction check is the same idea — except it runs every time you build, costs nothing, and never drifts out of sync with the code.
The Embedding Trick for Tests (Not for Production)
A separate pattern appears in testing code when you need a fake implementation that only stubs out one or two methods. You embed the interface inside a struct and override only the methods you care about:
type fakeWriter struct {
io.Writer
}
func (f *fakeWriter) Write(p []byte) (int, error) {
// custom behavior for test
return len(p), nil
}
The embedded io.Writer ensures the struct satisfies the interface at compile time, but any method that isn’t overridden will panic if called. This is acceptable in a narrow test where you control the calls, but dangerous in production because a panicking interface method is a crash waiting to happen.
The var _ pattern is the production‑grade counterpart — it guarantees all methods are present, with no panicking placeholders.
Don't Use Embedding for Production Satisfaction:
Embedding an interface in a struct to “satisfy” it without implementing every method is a testing convenience, not a production pattern. Use the var _ check and implement every method explicitly in production code.
Runtime Checks Are Not a Substitute
Go also allows you to check interface satisfaction at runtime using a type assertion:
var w interface{} = someValue
if _, ok := w.(io.Writer); ok {
// someValue satisfies io.Writer
}
This works, but the failure surfaces only when that line executes — potentially deep in a running application or, worse, never covered by tests. The compile‑time check catches the problem the moment you save the file and try to build, long before any code runs.
A compile‑time check is always preferable for static contracts because it shifts the failure from a possible runtime panic or silent fallback to a guaranteed build error. Reserve runtime checks for situations where the type truly cannot be known until runtime, such as values coming from plugins or dynamic reflection.
Common Mistakes and How to Avoid Them
- Checking the wrong receiver type. If you write
var _ Interface = MyType{}but the methods are on*MyType, the check fails. Match the receiver: use(*MyType)(nil)for pointer receivers, orMyType{}for value receivers. - Forgetting the check after refactoring. Renaming a method or changing its parameters may break a contract you assumed was safe. Placing the check right next to the type definition makes it visible during reviews and refactors.
- Using an interface that creates a circular dependency. If the interface lives in a package that already imports the package of your concrete type, adding the
var _line in the type’s package would create a cycle. In that case, you can place the check in a separate_test.gofile (using the external test package pattern) or restructure your package dependencies. - Leaving the check in dead code paths. The blank‑identifier line is a top‑level declaration and is always evaluated by the compiler. It cannot be accidentally skipped, which is exactly what you want.
Where This Pattern Appears in Practice
Walking through the Go standard library, you’ll find hundreds of these checks. For instance, the net/http package uses them extensively to guarantee that handler types satisfy http.Handler. The os package checks that *File satisfies io.Reader, io.Writer, io.Closer, and many others. In large open‑source projects, these lines serve as the backbone of interface‑contract enforcement.
Consider a realistic scenario: a custom metrics exporter that must satisfy http.Handler so it can be mounted on a server. Without the check, a rename of the ServeHTTP method would go undetected until a production deployment where /metrics suddenly returns a 404 or a panic. With the check, the build fails instantly.
package metrics
import "net/http"
type Exporter struct {
// fields
}
func (e *Exporter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// serve metrics
}
// Guard: *Exporter must satisfy http.Handler
var _ http.Handler = (*Exporter)(nil)
The line costs nothing and prevents an entire category of mistakes.
Summary
Compile‑time interface satisfaction checks turn Go’s implicit interface system into an explicit, enforced contract when you need it. The var _ Interface = (*Type)(nil) idiom is a zero‑cost declaration of intent that catches method‑set breakages before they ever reach a running process.
Use it wherever a type is meant to satisfy an important interface — especially public API surfaces, library code, and any type where a missing method would cause a production incident. The check is documentation, regression guard, and contract enforcer, all in one line.
The Go community builds on these checks by designing small interfaces — often one to three methods — that maximize the number of types that can satisfy them. That design philosophy makes the var _ pattern even more powerful because a single type can satisfy many small, focused contracts.