How to Define and Implement an Interface in Go

Define a Go interface by listing method signatures and implement it by adding those methods to a type, which Go recognizes automatically.

How to Define and Implement an Interface in Go

You're building a game engine. You have a Sword, a Staff, and a Fist. Each one calculates damage differently. You write a function useWeapon(w) that calls w.attack(). You don't care what the weapon is made of. You only care that it has an attack method. In Go, you don't declare that Sword is a "Weapon". You just give Sword an attack method, and the compiler figures out the rest. This is implicit interface satisfaction. It's the feature that makes Go code feel loose but safe.

Interfaces are contracts without signatures

An interface in Go is a list of method signatures. That's it. No fields. No implementation. A type implements an interface if it has all the methods listed. You don't write implements. You don't write extends. You just write the methods. This is called structural typing. The structure of the type matches the structure of the interface.

This is not duck typing. Duck typing happens at runtime and fails loudly when a method is missing. Go checks the structure at compile time. If the structure matches, it works. If it doesn't, the program won't build. You get safety without ceremony.

Think of a USB-C port. The port doesn't care if the device is a phone, a laptop, or a charger. It only cares that the device has the right pins and speaks the right protocol. If the device has the pins, it plugs in. Go interfaces work the same way. The function defines the port. The struct provides the pins.

Implicit satisfaction has a superpower. You can define an interface in your package, and a third-party type can implement it without you touching their code. If you define an interface with a Read method, anyone can implement it by adding that method to their type. You don't need to ask the author of MyDatabase to add implements Reader. They just add the method. Your code works.

Interfaces are contracts without signatures.

Minimal example

Here's the simplest interface: one method, one struct.

// Speaker defines the ability to speak.
type Speaker interface {
	Speak() string
}

// Dog represents a dog with a name.
type Dog struct {
	name string
}

// Speak returns the dog's sound.
func (d Dog) Speak() string {
	return "woof"
}

func main() {
	// Dog satisfies Speaker because it has Speak() string.
	var s Speaker = Dog{name: "Rex"}
	fmt.Println(s.Speak())
}

The compiler checks Dog against Speaker. Dog has Speak() string. Speaker requires Speak() string. Match. The assignment is valid. No extra syntax needed.

Convention aside: receiver naming. The receiver is d for Dog. Go convention uses one or two letters matching the type. Never use this or self. Use (d Dog) or (d *Dog).

The compiler is the enforcer.

What happens at compile and runtime

When you assign a Dog to a Speaker, the compiler builds a method set for Dog. It lists all methods with a Dog receiver. It finds Speak() string. It compares this list against Speaker. Every method in Speaker exists in Dog with the exact same signature. The assignment passes.

At runtime, an interface value is a pair. The first part is the type. The second part is the value. When you call s.Speak(), Go looks at the type inside the interface, finds the function address, and calls it. This is dynamic dispatch.

The compiler generates an itab (interface table) for each type-interface combination. The itab maps interface methods to concrete methods. When you call a method through an interface, Go uses the itab to find the function. This lookup is fast. It's a single pointer indirection. The cost is tiny, but it's there. Direct calls are faster. Interface calls add a small overhead.

If you forget a method, the compiler rejects the program with cannot use Dog{} as Speaker value: Dog does not implement Speaker (missing method Speak). If the signature is slightly off, like returning int instead of string, you get wrong type for method Speak. The errors are precise. Fix the signature and move on.

Realistic example: decoupling with Cache

Interfaces shine when you need to swap implementations. You want a function that works with any cache, whether it's in memory, Redis, or a file. You define a Cache interface and write your logic against that.

// Cache stores key-value pairs.
type Cache interface {
	Get(key string) (string, bool)
	Set(key string, value string)
}

// MemoryCache implements Cache using a map.
type MemoryCache struct {
	data map[string]string
}

// Get retrieves a value from the map.
func (c *MemoryCache) Get(key string) (string, bool) {
	val, ok := c.data[key]
	return val, ok
}

// Set adds a value to the map.
func (c *MemoryCache) Set(key string, value string) {
	c.data[key] = value
}

// Process uses any Cache implementation.
func Process(c Cache, key string) {
	if val, ok := c.Get(key); ok {
		fmt.Println("Found:", val)
	} else {
		c.Set(key, "default")
	}
}

Process accepts Cache. It doesn't know about MemoryCache. You can pass &MemoryCache{} or a RedisCache that implements the same methods. The function stays the same. This makes testing easy. You can pass a mock cache in tests without touching Process.

Convention aside: "Accept interfaces, return structs." Process accepts Cache. If Process returned a cache, it should return *MemoryCache, not Cache. Functions should accept interfaces to be flexible. They should return structs so the caller knows exactly what they got. This keeps the API clear.

Also notice the pointer receiver *MemoryCache. The methods modify the map, so they need a pointer. If you used a value receiver, Set would modify a copy, and the change would be lost. Pointer receivers are required when the method mutates the struct or when the struct is large.

Decouple early. Test later.

Pitfalls and compiler errors

Interfaces are simple, but a few traps exist.

Method signature mismatch

The signature must match exactly. Return types, parameter types, and names don't matter for parameters, but types do. If the interface says Sum() uint32 and your type says Sum() int, the compiler stops you.

cannot use x as Interface value: x does not implement Interface (wrong type for method Sum).

Fix the type. Use uint32.

Pointer vs value receiver

This is the most common confusion. If you define a method with a pointer receiver, only the pointer type implements the interface.

type Speaker interface {
	Speak() string
}

type Dog struct{}

// Speak has a pointer receiver.
func (d *Dog) Speak() string {
	return "woof"
}

func main() {
	// *Dog implements Speaker. Dog does not.
	var s Speaker = &Dog{} // OK
	var s2 Speaker = Dog{} // Error
}

If you try to assign Dog{} to Speaker, the compiler complains with cannot use Dog{} as Speaker value: Dog does not implement Speaker (method Speak has pointer receiver).

The rule: a value type implements an interface if all methods have value receivers. A pointer type implements an interface if all methods have value or pointer receivers. If any method has a pointer receiver, only the pointer type implements the interface.

The nil trap

An interface value is nil only if both the type and the value are nil. If you assign a nil pointer to an interface, the interface is not nil.

var s Speaker = (*Dog)(nil)

// s is not nil. s holds type *Dog and value nil.
if s == nil {
	fmt.Println("nil") // Never prints
}

// s.Speak() panics: nil pointer dereference.

s == nil is false because s has a type. Calling s.Speak() panics because the value is nil. To check for nil, you need to inspect the concrete value. Use reflection or design your code to handle nil pointers safely. The best fix is to avoid nil pointers in interfaces. Return a zero value or an error instead.

A nil interface is not the same as a nil value inside an interface.

Decision: when to use interfaces

Use an interface when you need to decouple a function from a specific implementation. Use a concrete struct when the behavior is fixed and you don't expect to swap implementations. Use a generic constraint when you need to enforce type relationships or access type parameters at compile time. Use a pointer receiver when the method modifies the struct or the struct is large. Use a value receiver when the method only reads data and the struct is small. Use a small interface with one or two methods when you want to keep dependencies loose. Use a large interface when you are defining a complete protocol, but prefer to split it into smaller interfaces for flexibility.

Small interfaces are the gold standard.

Where to go next