Back to Blog

The Go interface Keyword: Implicit Interfaces and nil Traps

How Go's interface keyword works: implicit satisfaction, pointer receivers, small interfaces, any, type assertions, the nil error trap and constraints.

The Go interface Keyword: Implicit Interfaces and nil Traps

The interface keyword declares a set of methods. Any type that has those methods satisfies the interface automatically. There is no implements keyword and no list of interfaces on the type, so a struct written years before an interface existed can still satisfy it. Since Go 1.18, an interface can also list types as well as methods, which turns it into a constraint for generic code (Go spec).

TL;DR

  • type Notifier interface { Notify(...) error } declares an interface. Any type with a matching Notify method is a Notifier.
  • Satisfaction is implicit. var _ Notifier = (*SlackNotifier)(nil) makes the compiler check it at build time.
  • A method with a pointer receiver belongs to *T, not T. Assigning a T fails with method Notify has pointer receiver.
  • Keep interfaces small and declare them in the package that uses them. Accept interfaces, return concrete types.
  • any is an alias for interface{} and holds a value of any type. Get the value back with v, ok := x.(T) or a type switch.
  • An interface is only nil when both its type and its value are unset. Returning a nil *ValidationError as an error gives you an err != nil that is true.
  • Interfaces with type lists like ~int64 | ~float64 are constraints. They work in type parameter lists and nowhere else.

How do you declare an interface in Go?

Write type, a name and interface followed by the method signatures. Here is an alerting system that sends the same message to Slack and to email:

example.gogo
package main

import (
	"context"
	"fmt"
)

type Notifier interface {
	Notify(ctx context.Context, msg string) error
}

type SlackNotifier struct {
	Channel string
}

func (s SlackNotifier) Notify(ctx context.Context, msg string) error {
	fmt.Printf("slack #%s: %s\n", s.Channel, msg)
	return nil
}

type EmailNotifier struct {
	To string
}

func (e EmailNotifier) Notify(ctx context.Context, msg string) error {
	fmt.Printf("email %s: %s\n", e.To, msg)
	return nil
}

func alertAll(ctx context.Context, notifiers []Notifier, msg string) {
	for _, n := range notifiers {
		if err := n.Notify(ctx, msg); err != nil {
			fmt.Println("notify failed:", err)
		}
	}
}

func main() {
	notifiers := []Notifier{
		SlackNotifier{Channel: "oncall"},
		EmailNotifier{To: "[email protected]"},
	}
	alertAll(context.Background(), notifiers, "payment API p99 above 2s")
}
example.texttext
slack #oncall: payment API p99 above 2s
email [email protected]: payment API p99 above 2s

alertAll only knows about the Notify method. It doesn't care whether it is talking to Slack, email or a PagerDuty client someone adds next month. Each value in the slice carries its concrete type with it, and the call goes to that type's method at run time.

An interface can embed other interfaces. The embedded method sets are merged, which is how the standard library builds io.ReadWriter out of io.Reader and io.Writer:

example.gogo
type ReadWriter interface {
	Reader
	Writer
}

A type satisfies io.ReadWriter when it has both Read and Write. *os.File and net.Conn do, and *strings.Reader doesn't, because it has no Write method.

How does a type implement an interface in Go?

By having every method of the interface in its method set, with the exact same signature. Go calls this implicit satisfaction, and it is the main difference from Java or C#, where a class has to name each interface it implements.

This keeps packages decoupled. SlackNotifier doesn't import the package that declares Notifier, and it doesn't have to know Notifier exists. You can write an interface today that types from the standard library or a third-party package already satisfy.

When a method is missing, the compiler tells you at the point where the value is used as the interface:

example.gogo
type PagerDutyNotifier struct {
	RoutingKey string
}

func (p PagerDutyNotifier) Send(ctx context.Context, msg string) error {
	return nil
}

var _ Notifier = PagerDutyNotifier{}
example.texttext
./main.go:20:18: cannot use PagerDutyNotifier{} (value of struct type PagerDutyNotifier) as Notifier value in variable declaration: PagerDutyNotifier does not implement Notifier (missing method Notify)

If the method exists but the signature is different, the error shows both versions side by side:

example.texttext
./main.go:16:18: cannot use PagerDutyNotifier{} (value of struct type PagerDutyNotifier) as Notifier value in variable declaration: PagerDutyNotifier does not implement Notifier (wrong type for method Notify)
		have Notify(string) error
		want Notify(context.Context, string) error

How do you check that a type implements an interface at compile time?

Declare a package-level variable with the blank identifier:

example.gogo
var _ Notifier = (*SlackNotifier)(nil)

The line allocates nothing and never runs any code. It only asks the compiler whether a *SlackNotifier can be assigned to a Notifier, so the build breaks the moment someone renames or changes Notify. Without it, you only find out where the type is first used as a Notifier, which might be in a different package. The Go var keyword covers the same trick with http.Handler.

Why does a pointer receiver break interface satisfaction?

Because the method belongs to the pointer type. Every type has a method set, and the rules are (Go spec):

  • The method set of T contains the methods declared with a value receiver (t T).
  • The method set of *T contains those plus the methods declared with a pointer receiver (t *T).

A notifier that batches messages has to modify itself, so it uses a pointer receiver:

example.gogo
type BatchNotifier struct {
	mu      sync.Mutex
	pending []string
}

func (b *BatchNotifier) Notify(ctx context.Context, msg string) error {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.pending = append(b.pending, msg)
	return nil
}

func main() {
	var n Notifier = BatchNotifier{}
	fmt.Println(n)
}
example.texttext
./main.go:26:19: cannot use BatchNotifier{} (value of struct type BatchNotifier) as Notifier value in variable declaration: BatchNotifier does not implement Notifier (method Notify has pointer receiver)

The fix is var n Notifier = &BatchNotifier{}. Go refuses the value version on purpose. An interface holding a BatchNotifier would hold a copy, and Notify would append to the copy's pending slice while the original never changed. Switching Notify to a value receiver doesn't help either. It would copy the sync.Mutex on every call, and go vet reports that as Notify passes lock by value.

A type with pointer-receiver methods has to be checked through its pointer, as in var _ Notifier = (*BatchNotifier)(nil). The pointer form also works for value-receiver types like SlackNotifier, because a *T has every method of T. That makes (*T)(nil) the usual way to write the check.

How should you design interfaces in Go?

The Go proverb is "the bigger the interface, the weaker the abstraction" (Go Proverbs). The most-used interfaces in the standard library have one or two methods:

InterfaceMethodImplemented by
io.ReaderReadfiles, network connections, HTTP bodies, gzip readers, bytes.Buffer
io.WriterWritefiles, http.ResponseWriter, hashes, bytes.Buffer
errorError*fs.PathError, *json.SyntaxError, your own error types
fmt.StringerStringanything that controls how fmt prints it
http.HandlerServeHTTProuters, middleware, http.HandlerFunc

A one-method interface is easy to satisfy, so a lot of types satisfy it. io.Copy works with any source and destination because it asks for so little. A twelve-method Storage interface usually has one real implementation, plus a mock that has to change every time the interface does. http.Handler works the same way. A middleware takes one and returns one, so Go middleware from any package stacks around any router.

fmt.Stringer shows how little a type needs to do to plug into the standard library. Give an enum a String method and every fmt verb uses it:

example.gogo
type OrderStatus int

const (
	Pending OrderStatus = iota
	Paid
	Refunded
)

func (s OrderStatus) String() string {
	switch s {
	case Pending:
		return "pending"
	case Paid:
		return "paid"
	case Refunded:
		return "refunded"
	}
	return fmt.Sprintf("OrderStatus(%d)", int(s))
}

func main() {
	fmt.Println(Paid)
	fmt.Printf("order 1001 is %v\n", Refunded)
}
example.texttext
paid
order 1001 is refunded

Where should a Go interface be declared?

In the package that uses it, not in the package that implements it. Because satisfaction is implicit, the consumer can list only the methods it calls.

A refund service only needs to load an order. It declares a one-method interface for that and takes it in its constructor:

example.gogo
type orderGetter interface {
	GetOrder(ctx context.Context, id int64) (Order, error)
}

type RefundService struct {
	orders orderGetter
}

func NewRefundService(orders orderGetter) *RefundService {
	return &RefundService{orders: orders}
}

func (s *RefundService) CanRefund(ctx context.Context, id int64) (bool, error) {
	o, err := s.orders.GetOrder(ctx, id)
	if err != nil {
		return false, fmt.Errorf("load order %d: %w", id, err)
	}
	return o.Status == "paid", nil
}

In production you pass the Postgres store, which has twenty other methods the refund service never sees. In a test, a map is enough:

example.gogo
type fakeOrders map[int64]Order

func (f fakeOrders) GetOrder(ctx context.Context, id int64) (Order, error) {
	o, ok := f[id]
	if !ok {
		return Order{}, ErrNotFound
	}
	return o, nil
}

func main() {
	svc := NewRefundService(fakeOrders{1001: {ID: 1001, Status: "paid"}})
	fmt.Println(svc.CanRefund(context.Background(), 1001))
	fmt.Println(svc.CanRefund(context.Background(), 1002))
}
example.texttext
true <nil>
false load order 1002: order not found

fakeOrders is a defined map type with a method (see the Go type keyword), so the test doesn't need a mocking library.

What does "accept interfaces, return structs" mean?

It is the other half of the same idea. Functions take interfaces as parameters, so callers can pass anything that fits. Constructors return concrete types like *RefundService or *PostgresStore, so callers get every method and every exported field, and they can decide for themselves which small interface they need.

Returning an interface from a constructor hides the methods the interface doesn't list, and it forces every caller to use the one abstraction you picked. The main exception is error. Functions return error rather than *ValidationError, and the nil section below explains why.

What is any in Go?

any is an alias for interface{}, the interface with no methods. Every type has at least zero methods, so a variable of type any can hold any value. any was added in Go 1.18, and the two spellings are interchangeable (Go spec).

any shows up where the type isn't known until run time. fmt.Println(a ...any) takes arguments of any type, and json.Unmarshal into a map[string]any produces values whose types depend on the JSON.

The cost is that the compiler stops checking. Every read from an any needs a type assertion, and a wrong guess becomes a runtime error instead of a compile error. When a function works on several types but the same operations apply to all of them, use a type parameter instead. func Sum[T Amount](values []T) T, covered in the constraints section below, keeps full type checking, while func Sum(values []any) any pushes every check to run time.

How do type assertions and type switches work?

A type assertion gets the concrete value back out of an interface. The two-value form never panics:

example.gogo
var payload map[string]any
body := `{"user_id": 42, "email": "[email protected]", "roles": ["admin", "billing"], "manager": null}`
if err := json.Unmarshal([]byte(body), &payload); err != nil {
	log.Fatal(err)
}

email, ok := payload["email"].(string)
fmt.Println(email, ok)

id, ok := payload["user_id"].(int)
fmt.Println(id, ok)
fmt.Printf("%T\n", payload["user_id"])
example.texttext
[email protected] true
0 false
float64

The second assertion fails because encoding/json decodes every JSON number into a float64 when the target is any. With ok the failure is just false and the zero value. The one-value form, payload["user_id"].(int), panics instead:

example.texttext
panic: interface conversion: interface {} is float64, not int

Assertions also work for interfaces. r.(io.Writer) asks whether the value inside r has a Write method, and the standard library uses this to look for optional behavior. io.Copy checks whether the source is an io.WriterTo and uses its faster path when it is.

A type switch runs several assertions in one statement. It fits the same JSON payload, where a value can be one of a handful of types:

example.gogo
func describe(v any) string {
	switch v := v.(type) {
	case nil:
		return "null"
	case float64:
		return fmt.Sprintf("number %g", v)
	case string:
		return fmt.Sprintf("string of %d bytes", len(v))
	case []any:
		return fmt.Sprintf("array of %d items", len(v))
	case map[string]any:
		return fmt.Sprintf("object with %d keys", len(v))
	default:
		return fmt.Sprintf("other %T", v)
	}
}

func main() {
	// payload decoded as above
	for _, key := range []string{"user_id", "email", "roles", "manager"} {
		fmt.Println(key+":", describe(payload[key]))
	}
}
example.texttext
user_id: number 42
email: string of 15 bytes
roles: array of 2 items
manager: null

Inside each case, v has that case's type, so len(v) compiles for the string, the slice and the map. Type switches over errors work the same way, but they only see the outermost error. The Go type keyword shows that case and why errors.As is usually the better tool for wrapped errors.

Why is my nil error not nil in Go?

An interface value has two parts, a dynamic type and a dynamic value. It equals nil only when both are unset. If you store a nil pointer in an interface, the type part is set, so the interface is not nil (Go FAQ).

This bites most often with custom error types. Here the function keeps a *ValidationError variable and returns it, whether or not it was ever assigned:

example.gogo
package main

import (
	"fmt"
	"strings"
)

type ValidationError struct {
	Field string
}

func (e *ValidationError) Error() string {
	return "invalid " + e.Field
}

func validate(email string) error {
	var verr *ValidationError
	if !strings.Contains(email, "@") {
		verr = &ValidationError{Field: "email"}
	}
	return verr
}

func main() {
	err := validate("[email protected]")
	if err != nil {
		fmt.Printf("validation failed: %v (%T)\n", err, err)
		return
	}
	fmt.Println("ok")
}
example.texttext
validation failed: <nil> (*main.ValidationError)

The email is valid and verr is nil, but the returned error holds the type *main.ValidationError with a nil pointer inside. err != nil compares the whole interface against an interface with no type, so it is true. The caller rejects a valid signup, and the log line says <nil>, which makes the bug hard to spot.

The fix is to return a literal nil on the success path and never route a typed pointer through an error return:

example.gogo
func validate(email string) error {
	if !strings.Contains(email, "@") {
		return &ValidationError{Field: "email"}
	}
	return nil
}

Declare the return type as error, not *ValidationError, and return nil directly. Callers that need the field still get it with errors.As, which also finds the error after it has been wrapped:

example.gogo
err := fmt.Errorf("signup: %w", validate("ana.example.com"))
var verr *ValidationError
if errors.As(err, &verr) {
	fmt.Println("bad field:", verr.Field)
}

This prints bad field: email. The same trap exists for any interface. A var store *PostgresStore that is never set, passed where an orderGetter is expected, produces a non-nil interface too.

The opposite case is a truly nil interface, such as a struct field of type Notifier that nobody set. Calling a method on it panics with invalid memory address or nil pointer dereference, because there is no type to find the method on. A constructor that requires the dependency, like NewRefundService(orders), prevents that.

How are interfaces used as generic constraints?

Since Go 1.18, an interface can list types as well as methods. The list is called a type set, and ~T means "any type whose underlying type is T":

example.gogo
type Cents int64

type Amount interface {
	~int64 | ~float64
}

func Sum[T Amount](values []T) T {
	var total T
	for _, v := range values {
		total += v
	}
	return total
}

func Dedupe[T comparable](ids []T) []T {
	seen := make(map[T]struct{}, len(ids))
	out := make([]T, 0, len(ids))
	for _, id := range ids {
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	return out
}

func main() {
	fmt.Println(Sum([]Cents{4999, 1250, 300}))
	fmt.Println(Dedupe([]string{"u_42", "u_7", "u_42"}))
}
example.texttext
6549
[u_42 u_7]

Amount allows + because every type in its set supports it. comparable is a predeclared constraint for types that work with ==, which is what a map key needs. The compiler rejects a type outside the set and names the problem:

example.texttext
./main.go:27:17: string does not satisfy Amount (string missing in ~int64 | ~float64)

An interface with a type list can only be used as a constraint. Declaring a variable of that type fails:

example.texttext
./main.go:10:8: cannot use type Amount outside a type constraint: interface contains type constraints

So Go has two kinds of interfaces that share one keyword. Method-only interfaces work everywhere, as variable types, parameters and constraints. Interfaces with type lists only appear in square brackets. The Go blog's introduction to generics describes how type sets unify the two.

Where LevelUpGo fits

LevelUpGo teaches Go through exercises that run real Go code in the browser. Interfaces & Polymorphism covers implicit satisfaction, method sets, any, type assertions and interface composition. Interface Design covers small interfaces, declaring them where they are used and avoiding interfaces that exist only for mocking. Go Generics Masterclass goes further into constraints and type sets. The Training Ground has short standalone exercises on interfaces for practicing outside a course. For the other 24 reserved words, see Go keywords: all 25 explained.

FAQ

Is interface a keyword in Go?

Yes. interface is one of Go's 25 reserved keywords, so you can't use it as a variable or function name. It appears in interface type declarations like type Notifier interface { ... }, in the literal interface{}, and in inline constraints like [T interface{ ~int | ~string }].

Does Go have an implements keyword?

No. A type satisfies an interface just by having all of its methods with matching signatures. To make the compiler check that a type satisfies an interface, add var _ Notifier = (*SlackNotifier)(nil) next to the type.

What is the difference between any and interface{} in Go?

There is none. any is a predeclared alias for interface{}, added in Go 1.18, so the two are the same type. Newer code uses any because it is shorter, and gofmt -r 'interface{} -> any' can rewrite older code.

Why does Go say "method has pointer receiver"?

The method is declared on *T, so only a *T has it. You tried to use a T value as the interface. Pass a pointer instead, as in &BatchNotifier{}, or change the method to a value receiver if it doesn't modify the receiver.

Why is my error not nil when I returned a nil pointer?

An interface is only nil when both its type and its value are unset. Returning a nil *ValidationError from a function whose return type is error sets the type to *ValidationError, so err != nil is true. Return a literal nil on the success path instead of a typed pointer variable.

Can Go interfaces have fields?

No. Interfaces only describe methods, or type sets when used as constraints. If callers need a value, add a getter method such as ID() string, or accept the concrete struct instead of an interface.

Sources

Write Go like a senior engineer

Interactive lessons in your browser. The first ones are free.

Try a free lessonOr create a free account