Interfaces in Go From First Principles to Real Use
How Go interfaces work, from implicit satisfaction and method sets to small consumer-side interfaces, fakes in tests, boxing cost and the traps to avoid.

Interfaces are the one feature of Go where the language's design is most unlike the languages it is usually compared to. There is no implements, no inheritance tree and no declaration on the type that says what it can do. A type has methods, and anything that needs those methods can accept it. That sounds minor and it changes how you structure programs: interfaces are discovered by the code that consumes values, not planned by the code that produces them. This guide starts with the mechanism, builds up to the habits that make Go interfaces useful, and ends with what they cost. It sits in the Go basics path, builds on structs and pointer versus value receivers, and has deeper companions on the nil interface trap, accept interfaces, return structs and type assertions and type switches.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64, Apple M1 Max)
What an interface is
An interface type is a list of method signatures. A value of any type that has all those methods can be assigned to a variable of the interface type. The satisfaction is checked by the compiler and never declared:
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }
func describe(s Shape) string {
return fmt.Sprintf("%T area=%.2f perimeter=%.2f", s, s.Area(), s.Perimeter())
}
func main() {
shapes := []Shape{Rect{3, 4}, Circle{1}}
for _, s := range shapes {
fmt.Println(describe(s))
}
}main.Rect area=12.00 perimeter=14.00
main.Circle area=3.14 perimeter=6.28Rect and Circle know nothing about Shape. They each define the two methods, so they qualify, and the slice holds both. The %T verb prints the dynamic type that is stored inside the interface, which is the answer to "what is actually in here". This is called structural typing, and the practical effect is that you can write an interface for a type that someone else wrote, in a package you cannot change, because the only requirement is the method names and signatures.
A mistake shows up as a compile error at the place the conversion happens, not inside the type. If Circle lacked Perimeter, the error would be on the line that put a Circle into a Shape. To make the compiler check a type against an interface where no assignment happens, write a blank assignment at package level: var _ Shape = Circle{} or var _ Shape = (*Circle)(nil). It fails the build when a method is renamed or removed, and I add it for every public type that is meant to satisfy a public interface.
Keep them small, and define them where they are used
The standard library's most successful interfaces have one or two methods. io.Reader has one, Read(p []byte) (n int, err error). io.Writer has one. fmt.Stringer has one. error has one. Because the requirements are tiny, hundreds of types satisfy them, and functions that accept them work with files, network connections, buffers, compressed streams and your own types without any of them being aware:
package main
import (
"bytes"
"fmt"
"io"
"strings"
"unicode"
)
type upperReader struct{ r io.Reader }
func (u upperReader) Read(p []byte) (int, error) {
n, err := u.r.Read(p)
for i := range n {
p[i] = byte(unicode.ToUpper(rune(p[i])))
}
return n, err
}
func countBytes(r io.Reader) (int, error) {
n, err := io.Copy(io.Discard, r)
return int(n), err
}
func main() {
fromString := strings.NewReader("hello, interfaces")
fromBuffer := bytes.NewBufferString("a buffer works too")
fromUpper := upperReader{strings.NewReader("shout")}
for _, r := range []io.Reader{fromString, fromBuffer} {
fmt.Println(countBytes(r))
}
out, _ := io.ReadAll(fromUpper)
fmt.Println(string(out))
}17 <nil>
18 <nil>
SHOUTcountBytes takes an io.Reader and has no idea whether it reads a string, a buffer or a socket. upperReader is a custom type with one method, and it plugs straight into io.ReadAll. This is the best argument for small interfaces: the fewer methods a type must have, the more types qualify. A ten-method interface is a description of one implementation.
The corollary, which the Go community calls "accept interfaces, return structs", is to define the interface in the package that consumes it, with only the methods that consumer calls. A package that needs to save users declares type userSaver interface { Save(User) error } with the single method it uses, and any database layer that has Save fits. The database package does not export an interface at all. That inverts the habit from languages where the provider defines the abstraction, and it means a change to the provider's other methods never forces a change in the consumer. The rule has exceptions, covered in accept interfaces, return structs and when to break it.
What an interface value holds
An interface variable is two words: a pointer to the dynamic type, and the value (or a pointer to it). That explains three behaviors that otherwise look odd.
A nil interface has neither. A non-nil interface can hold a nil pointer, which is how a function returning error can return something that is != nil while appearing empty. The details and the fix are in the nil interface trap, and it is worth reading before you ship your first error return that wraps a pointer type.
Comparison compares both words, so two interface values are equal if they hold the same type and equal values, and comparing interfaces whose dynamic types are not comparable (slices, maps) panics at run time.
Boxing a value into an interface sometimes allocates. I measured it with testing.AllocsPerRun, using values the compiler could not turn into constants:
package main
import (
"fmt"
"os"
"testing"
)
var sinkAny any
type Triple struct{ A, B, C int }
func TestBoxing(t *testing.T) {
small, large := len(os.Args)%2+1, len(os.Args)+100000
fmt.Println("small int:", testing.AllocsPerRun(100, func() { sinkAny = small }))
fmt.Println("large int:", testing.AllocsPerRun(100, func() { sinkAny = large }))
s := fmt.Sprint(large)
fmt.Println("string:", testing.AllocsPerRun(100, func() { sinkAny = s }))
tr := Triple{large, 2, 3}
fmt.Println("struct of 3 ints:", testing.AllocsPerRun(100, func() { sinkAny = tr }))
p := &tr
fmt.Println("pointer:", testing.AllocsPerRun(100, func() { sinkAny = p }))
}=== RUN TestBoxing
small int: 0
large int: 1
string: 1
struct of 3 ints: 1
pointer: 0
--- PASS: TestBoxing (0.00s)
PASS
ok example.test/lab 0.266sSmall integers, under 256, are boxed without allocating because the runtime keeps a table of them. A larger integer, a string and a three-field struct each allocated once, since the interface needs a pointer to a copy of the value. A pointer fits directly in the interface's second word and allocated nothing. That is the reason fmt.Println(x) on a hot path allocates, and why a performance-sensitive function that takes any can sometimes be made cheaper by passing pointers. For almost all code the allocation is the right trade for the flexibility, and it is something to know about before you put an interface in a loop that runs a million times.
Calling a method through an interface is a dynamic call. I benchmarked a direct call against a call through an interface returned from a function the compiler could not see through:
package main
import "testing"
type Adder interface{ Add(int) int }
type impl struct{ n int }
func (i impl) Add(x int) int { return i.n + x }
//go:noinline
func newAdder() Adder { return impl{1} }
var sink int
func BenchmarkDirect(b *testing.B) {
s := impl{1}
for b.Loop() {
sink += s.Add(2)
}
}
func BenchmarkInterface(b *testing.B) {
a := newAdder()
for b.Loop() {
sink += a.Add(2)
}
}goos: darwin
goarch: arm64
pkg: example.test/lab
cpu: Apple M1 Max
BenchmarkDirect-10 536627630 2.174 ns/op 0 B/op 0 allocs/op
BenchmarkDirect-10 555528549 2.145 ns/op 0 B/op 0 allocs/op
BenchmarkDirect-10 563410920 2.146 ns/op 0 B/op 0 allocs/op
BenchmarkInterface-10 562711344 2.153 ns/op 0 B/op 0 allocs/op
BenchmarkInterface-10 559261852 2.196 ns/op 0 B/op 0 allocs/op
BenchmarkInterface-10 564695364 2.137 ns/op 0 B/op 0 allocs/op
PASS
ok example.test/lab 7.458sBoth took about 2 ns on this machine. The interface call is an indirect jump through a table, and on a modern CPU with a predictable target that is nearly free. What an interface can cost is the inlining it prevents: a direct call to a tiny method can be inlined to nothing, while an interface call usually cannot, and the compiler's profile-guided optimization is the tool that devirtualizes hot paths. Do not avoid interfaces for speed unless a profile points at one.
Method sets and the pointer receiver rule
Which methods count depends on whether you hold a value or a pointer. The method set of T has the value-receiver methods, and the method set of *T has both kinds. So if Save has a pointer receiver, *Store satisfies an interface needing Save and Store does not. The compile error spells it out, and pointer or value receiver shows it. The practical consequence is that you usually store &Thing{} in an interface and not Thing{}.
Composing interfaces
An interface can embed other interfaces, which is how the standard library builds larger ones from small pieces. io.ReadWriter is Reader plus Writer, and io.ReadWriteCloser adds Closer:
type ReadWriteCloser interface {
Reader
Writer
Closer
}A function should ask for the smallest of these that covers what it does. A function that only reads takes io.Reader even though the caller has a file that could also write. The more it asks for, the fewer values qualify and the harder it is to test.
Interfaces as the test seam
The most common reason to write your own interface is to substitute a fake in a test. Code that sends email, calls an HTTP API or writes to a database depends on an interface with the one or two methods it needs, and the test supplies a type that records calls:
package notify
import "fmt"
type Sender interface {
Send(to, subject string) error
}
type Service struct {
Sender Sender
}
func (s Service) Welcome(user string) error {
if user == "" {
return fmt.Errorf("welcome: empty user")
}
return s.Sender.Send(user+"@example.test", "Welcome")
}package notify
import "testing"
type fakeSender struct {
calls []string
}
func (f *fakeSender) Send(to, subject string) error {
f.calls = append(f.calls, to+"|"+subject)
return nil
}
func TestWelcomeSends(t *testing.T) {
fake := &fakeSender{}
svc := Service{Sender: fake}
if err := svc.Welcome("ada"); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 || fake.calls[0] != "[email protected]|Welcome" {
t.Fatalf("unexpected calls: %v", fake.calls)
}
}
func TestWelcomeRejectsEmptyUser(t *testing.T) {
fake := &fakeSender{}
if err := (Service{Sender: fake}).Welcome(""); err == nil || len(fake.calls) != 0 {
t.Fatalf("expected an error and no send, got %v and %v", err, fake.calls)
}
}=== RUN TestWelcomeSends
--- PASS: TestWelcomeSends (0.00s)
=== RUN TestWelcomeRejectsEmptyUser
--- PASS: TestWelcomeRejectsEmptyUser (0.00s)
PASS
ok example.test/lab 1.246sThe fake is eight lines and needs no mocking library. The real implementation, an SMTP client or an API wrapper, never appears in the test. This is a good use of an interface because it has two implementations, the real one and the fake. A weaker use is the "interface per struct" habit from other languages, with an IUserService that has one implementation and exists for no reason. It adds a file, an indirection and a name to keep in sync, and buys nothing until a second implementation appears. Write the interface when the seam is needed, and it costs nothing to add later because satisfaction is implicit.
Wrapping behavior: the decorator
Because satisfaction is structural, wrapping a value to add behavior is easy: write a type that holds an interface value and also implements the interface. The standard library's http.Handler middleware works this way, and the same shape applies to any small interface. Here is a decorator that logs and counts calls to anything with a Lookup method:
package main
import (
"fmt"
"strings"
)
type Directory interface {
Lookup(name string) (string, bool)
}
type memory map[string]string
func (m memory) Lookup(name string) (string, bool) {
v, ok := m[name]
return v, ok
}
type logged struct {
next Directory
calls int
log *strings.Builder
}
func (l *logged) Lookup(name string) (string, bool) {
l.calls++
v, ok := l.next.Lookup(name)
fmt.Fprintf(l.log, "lookup %q found=%t\n", name, ok)
return v, ok
}
func greet(d Directory, name string) string {
if email, ok := d.Lookup(name); ok {
return "mail " + email
}
return "no such user"
}
func main() {
var log strings.Builder
d := &logged{next: memory{"ada": "[email protected]"}, log: &log}
fmt.Println(greet(d, "ada"))
fmt.Println(greet(d, "bob"))
fmt.Print(log.String())
fmt.Println(d.calls)
}mail [email protected]
no such user
lookup "ada" found=true
lookup "bob" found=false
2greet takes a Directory and has no idea that its argument is a wrapper around a map. The wrapper counted two calls and recorded both, and the map type itself carries no logging code. Caches, retries, metrics, rate limits and authorization checks are all built this way, one small wrapper per concern, composed at the place where the program is wired together. The interface is what makes each wrapper replaceable, and it stays small because every wrapper has to implement every method. That is one more argument for one or two methods per interface.
Older interfaces and newer helpers
Not every interface in the standard library is a good model. sort.Interface has three methods, Len, Less and Swap, and sorting a slice of structs with it means defining a named slice type to hang them on. Since Go 1.21 the slices package can sort with a comparison function, and the interface is no longer the best way to write new code:
package main
import (
"cmp"
"fmt"
"slices"
"sort"
)
type Person struct {
Name string
Age int
}
type byAge []Person
func (a byAge) Len() int { return len(a) }
func (a byAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a byAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func main() {
people := []Person{{"ada", 36}, {"grace", 45}, {"linus", 28}}
sort.Sort(byAge(people))
fmt.Println(people)
slices.SortFunc(people, func(a, b Person) int {
return cmp.Compare(b.Age, a.Age)
})
fmt.Println(people)
}[{linus 28} {ada 36} {grace 45}]
[{grace 45} {ada 36} {linus 28}]The first form needs a type and three methods, and the second needs one closure. Both work, and sort.Interface remains useful for data structures that are not slices. The lesson for your own code: an interface that forces every implementer to write boilerplate is a candidate for a function value. If the behavior you want to vary is a single operation, a parameter of function type is simpler than an interface with one method, and the call site can pass a literal. Interfaces are for cases where the thing has several related operations, or where you want to attach a name that other code can implement.
Finding out what an interface requires
You rarely need to guess. go doc prints the definition and its documentation from the command line, with the same text that pkg.go.dev shows:
go doc io.Writerpackage io // import "io"
type Writer interface {
Write(p []byte) (n int, err error)
}
Writer is the interface that wraps the basic Write method.
...The documentation part of an interface is where the contract lives, including the rules the compiler cannot check. io.Writer says that Write must return a non-nil error if it writes fewer bytes than it was given, and that it must not modify or retain the slice. A type that satisfies the method signatures and breaks those rules compiles and misbehaves, so read the paragraph before you implement a standard interface. The editor's "find implementations" command, backed by gopls, answers the opposite question and lists every type in your module that satisfies a given interface.
A registry of implementations
When a program has several interchangeable behaviors selected by name, a command line tool with subcommands or a set of exporters, a map from name to interface value is the usual shape:
package main
import (
"errors"
"fmt"
"sort"
"strings"
)
type Command interface {
Name() string
Run(args []string) error
}
type echo struct{}
func (echo) Name() string { return "echo" }
func (echo) Run(args []string) error {
fmt.Println(strings.Join(args, " "))
return nil
}
type fail struct{}
func (fail) Name() string { return "fail" }
func (fail) Run([]string) error { return errors.New("this command always fails") }
func main() {
registry := map[string]Command{}
for _, c := range []Command{echo{}, fail{}} {
registry[c.Name()] = c
}
names := make([]string, 0, len(registry))
for name := range registry {
names = append(names, name)
}
sort.Strings(names)
fmt.Println(names)
for _, line := range []string{"echo hello world", "fail", "missing now"} {
fields := strings.Fields(line)
cmd, ok := registry[fields[0]]
if !ok {
fmt.Println("unknown command:", fields[0])
continue
}
if err := cmd.Run(fields[1:]); err != nil {
fmt.Println("error:", err)
}
}
}[echo fail]
hello world
error: this command always fails
unknown command: missingAdding a command means writing one type and one line in the list. The dispatcher never changes, which is the whole point of the abstraction. Notice that the two implementations have no fields and the methods use value receivers on empty structs, so echo{} costs nothing to create. Tools built this way scale to dozens of commands, and the cobra library is a more complete version of the same idea.
Naming interfaces
The convention for a one-method interface is the method name plus er: Reader, Writer, Closer, Stringer, Formatter. Effective Go suggests naming the method after the interface and keeping the canonical signature, so a type with a String method should have the signature fmt.Stringer expects, and a method called Read should look like io.Reader.Read. Do not prefix with I, do not add Interface to the name and do not reuse a well known name for a different shape, because a reader will assume the standard meaning. For interfaces with several methods, name the role: Store, Directory, Clock. If you cannot find a short noun for what the interface does, it may be doing two things.
Interfaces or type parameters
Since Go 1.18 a function can take a type parameter, and the two features overlap enough to cause a recurring question. They answer different problems. An interface describes behavior that varies at run time: the program picks a storage engine or a notifier from configuration, and the code calling it is the same either way. A type parameter describes an algorithm that is the same over many types and where the type is known at compile time: a Max over any ordered type, a Set[T], a Map function over slices. Using an interface for the second case boxes each element and loses type information, and using a type parameter for the first case gives you no way to choose at run time. The practical test is to ask whether the concrete type is chosen when the program is written or when it runs. The rule I follow is to start with concrete types, add an interface when a second implementation or a test needs it, and add a type parameter only when I would otherwise copy the same function for several types.
Where interfaces cause trouble
Large interfaces. Twenty methods mean every fake has twenty methods to write. Split by caller.
Returning interfaces from constructors. func NewStore() Store hides the concrete type, so callers cannot reach extra methods, and a new method on the struct is invisible until the interface changes. Return the struct and let the caller narrow it, which is why the community rule leans that way.
The empty interface as a design. any or interface{} has no methods, so it carries no information, and every use needs a type assertion. It has honest uses, described in the empty interface and the any alias, and as a general parameter type it moves errors from compile time to run time.
Type switches everywhere. If a function switches over the dynamic type of a value, the abstraction is not abstracting. That is acceptable in a small, closed set, and a signal to add a method to the interface when it spreads. Type assertions and type switches covers when each form is appropriate.
Comparing to nil after a typed nil. Covered above, and in the dedicated article.
When not to use an interface
If a function is called with one concrete type and nothing else will ever call it, take the concrete type. If a value needs to be one of a fixed set of known types, a struct with a kind field or a small set of concrete types in a type switch is often clearer than an interface. If performance is the concern, as in a tight inner loop, take a concrete type or a function value, and keep the interface at the boundary. And if you notice you are designing an interface before writing any of its implementations, stop and write two implementations first. The interface that falls out of real code is smaller and better than the one drawn in advance.
The official references are the specification on interface types, Effective Go on interfaces, and the Go code review comments on interfaces, which is where the advice to define them on the consumer side was written down.


