Skip to content
CalliCoder

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.