Golang Interfaces Tutorial with Examples
Golang 14 min read
Interfaces are satisfied implicitly, which is why they belong in the consumer package. Plus the typed-nil trap that makes a non-nil interface hold a nil pointer, and why small interfaces compose.
Go interfaces are satisfied implicitly: a type implements one by having the methods, with no declaration of intent. That single design decision changes where interfaces should live, how large they should be, and who defines them, and it produces one genuinely nasty bug that catches everybody once.
Written against Go 1.22.
Implicit satisfaction
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return d.Name + " says woof" }
var s Speaker = Dog{Name: "Rex"} // no "implements" anywhere
Dog never mentions Speaker. That means an interface can be defined for a type you do not own —
including one in the standard library or a third-party package, and it means adding an interface
breaks nothing.
The consequence worth internalising: interfaces belong in the package that consumes them, not
the one that implements them. A package providing a PostgresStore should export the concrete type;
the package that needs “something I can save an order to” declares its own one-method interface.
That inverts the Java habit of shipping interface Foo next to FooImpl, and it is why Go code has
so few interfaces defined alongside their implementations.
Keep them small
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
The standard library’s most-used interfaces have one method each, and larger ones are built by embedding. “The bigger the interface, the weaker the abstraction” is the proverb, and it is practical advice: a one-method interface is trivially satisfied by any type, easy to fake in a test, and composes.
An interface with eight methods is satisfied by roughly one type, which means it is documenting a class rather than abstracting a capability.
Accept interfaces, return structs. A function taking io.Reader works with a file, a network
connection, a strings.Reader and a test fixture. A function returning an interface hides
information the caller might need and pins you to that surface.
The nil interface trap
This is the one to know, because the symptom makes no sense until you have seen it.
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func doWork() error {
var err *MyError = nil // a nil POINTER
return err // returned as an interface
}
func main() {
if err := doWork(); err != nil {
fmt.Println("failed") // this PRINTS
}
}
An interface value is a pair: a type and a value. It is nil only when both halves are nil.
Returning a nil *MyError produces an interface holding type *MyError and value nil, the type
half is set, so the interface is not nil.
The fix is never to return a typed nil:
func doWork() error {
var err *MyError
if somethingWrong() {
err = &MyError{msg: "..."}
}
if err != nil {
return err
}
return nil // explicit untyped nil
}
Better: declare the variable as error rather than as the concrete type, so there is nothing to
convert.
fmt.Printf("%v %T\n", err, err) // <nil> *main.MyError — the tell
%T printing a concrete type next to a <nil> value is the diagnostic.
Type assertions and type switches
var s Speaker = Dog{Name: "Rex"}
d := s.(Dog) // panics if s is not a Dog
d, ok := s.(Dog) // comma-ok — no panic
Always use the comma-ok form unless a failure genuinely is a programming error.
switch v := value.(type) {
case string:
fmt.Println(len(v)) // v is a string
case Speaker:
fmt.Println(v.Speak()) // v is a Speaker
case nil:
fmt.Println("nil interface")
default:
fmt.Printf("unhandled %T\n", v)
}
A type switch matches in order, so a broader case placed before a narrower one shadows it. case nil
matches a genuinely nil interface: not the typed nil above, which matches its concrete type instead.
The empty interface, and any
func describe(v any) { // any is an alias for interface{}
fmt.Printf("%v %T\n", v, v)
}
any is satisfied by everything, so it carries no information. Every use costs a type assertion at
the other end and moves a compile-time check to runtime.
Since Go 1.18, generics cover most of what any was used for:
func first[T any](items []T) (T, bool) {
var zero T
if len(items) == 0 {
return zero, false
}
return items[0], true
}
The remaining legitimate uses are genuinely heterogeneous data: JSON decoding into
map[string]any, a printf-style API, a cache holding anything.
Verifying satisfaction at compile time
var _ Speaker = (*Dog)(nil)
That line asserts at compile time that *Dog satisfies Speaker, without allocating anything. It is
worth adding in a package that implements an interface from elsewhere, because implicit satisfaction
means a renamed or re-signatured method breaks the implementation silently, the error appears
wherever the type is used as the interface, which can be another package entirely.
Value or pointer receivers, and the method set
type Cat struct{ Name string }
func (c *Cat) Speak() string { return c.Name + " says meow" }
var s Speaker = Cat{} // COMPILE ERROR
var s Speaker = &Cat{} // fine
A pointer receiver means only *Cat satisfies the interface. The method set of Cat does not include
pointer-receiver methods, because taking the address of a value is not always possible, a map
element, for instance, is not addressable.
With a value receiver both Cat and *Cat satisfy it. This is why mixing receiver kinds on one type
is confusing, and why the convention is to pick one.
The error message, “Cat does not implement Speaker (method Speak has pointer receiver)”, is unusually good and says exactly this.
Embedding an interface in a struct
type LoggingStore struct {
Store // embedded interface
log *slog.Logger
}
func (l LoggingStore) Save(o Order) error {
l.log.Info("saving", "id", o.ID)
return l.Store.Save(o) // delegate
}
Embedding the interface promotes every method, so LoggingStore satisfies Store while overriding
only the one it cares about. That is the decorator pattern with no boilerplate, and it is how
middleware and instrumentation wrappers are usually written.
The trap: an embedded interface that is nil panics on any method not overridden, and the panic points
at the delegation rather than at the missing dependency. It also means adding a method to Store
silently changes LoggingStore’s behaviour from “wrapped” to “passed through”, which is convenient
until the new method needed wrapping too.
The same embedding is what makes partial test doubles cheap:
type stubStore struct{ Store } // every method panics except the ones defined
func (s stubStore) Save(Order) error { return nil }
A test then implements only the methods the code under test actually calls, and any other call fails loudly rather than returning a zero value.
Interfaces have a runtime cost
A method call through an interface is an indirect call: the runtime looks the method up in a table rather than jumping to a known address, and it cannot be inlined.
The cost is small and real. It matters in a hot loop and nowhere else, so the guidance is the usual
one: write the interface for the design, and measure before removing it. What is worth avoiding
unconditionally is any in a hot path, where the assertion cost compounds with an allocation for
boxing.
More in the Golang guides, including structs and methods.
Frequently asked questions
How does a type implement an interface in Go?
By having the methods. There is no implements
keyword and no declaration, satisfaction is checked structurally at compile time.
Where should an interface be declared?
In the package that uses it, not the one that implements it. That is what implicit satisfaction enables and it keeps the abstraction with the consumer.
Why is my error non-nil when I returned nil?
A typed nil. An interface is nil only when both its
type and value halves are nil; returning a nil *MyError sets the type half. Return an untyped
nil, or declare the variable as error.
How do I detect a typed nil?
fmt.Printf("%T", err) prints the concrete type next to a <nil>
value. That mismatch is the signature.
Why does my type not satisfy the interface?
Most often a pointer receiver: only *T has those
methods in its method set, so T does not implement the interface. The compiler error says so
explicitly.
Should interfaces be small?
Yes. One or two methods, composed by embedding when more is needed. A large interface is satisfied by one type, which means it is not abstracting anything.
What is the difference between any and interface{}?
None, any is an alias added in Go 1.18. It
is purely a readability improvement.
Should I use any or generics?
Generics for anything with a consistent type, which is most of what
any used to cover. Keep any for genuinely heterogeneous data.
How do I check satisfaction at compile time?
var _ Iface = (*T)(nil) in the implementing
package. It costs nothing and turns a silent break into a compile error.
Do interfaces slow code down?
A little, the call is indirect and cannot be inlined. It matters only in a hot loop; design first, measure before removing.