Go Interfaces Explained
Understand Go interfaces — implicit satisfaction, the empty interface, type assertions, interface composition, and real design patterns.
What you'll learn
- ✓How Go interfaces work via implicit satisfaction
- ✓The empty interface and type assertions
- ✓Interface composition and small interface design
- ✓Common patterns: Stringer, Reader, Writer
Prerequisites
- •Go basics (structs, methods, functions)
Go interfaces are satisfied implicitly — no implements keyword. A type satisfies an interface simply by having the right methods. This design leads to small, composable interfaces that are fundamentally different from Java or C# interfaces.
Defining and satisfying interfaces
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string {
return d.Name + " says woof!"
}
type Cat struct{ Name string }
func (c Cat) Speak() string {
return c.Name + " says meow!"
}
Neither Dog nor Cat declares that it implements Speaker. They just have the Speak() string method, so they satisfy it automatically.
func greet(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
greet(Dog{Name: "Rex"}) // Rex says woof!
greet(Cat{Name: "Luna"}) // Luna says meow!
}
The empty interface
interface{} (or any in Go 1.18+) is satisfied by every type. It is Go’s equivalent of Object in Java.
func printAnything(v any) {
fmt.Println(v)
}
printAnything(42)
printAnything("hello")
printAnything([]int{1, 2, 3})
Use it sparingly — you lose type safety.
Type assertions
Extract the concrete type from an interface value.
var s Speaker = Dog{Name: "Rex"}
dog, ok := s.(Dog)
if ok {
fmt.Println("It's a dog named", dog.Name)
}
Without the comma-ok form, a failed assertion panics:
cat := s.(Cat) // panic: interface conversion
Type switches
func describe(s Speaker) string {
switch v := s.(type) {
case Dog:
return "Dog: " + v.Name
case Cat:
return "Cat: " + v.Name
default:
return "Unknown speaker"
}
}
Interface composition
Build larger interfaces from smaller ones.
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
}
This is how the standard library builds io.ReadWriter, io.ReadCloser, etc.
Design principle: accept interfaces, return structs
func ProcessData(r io.Reader) (*Result, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return &Result{Data: data}, nil
}
The function accepts any io.Reader — a file, a network connection, a buffer, a test stub. It returns a concrete type so the caller gets full access to its fields and methods.
Small interfaces
The Go standard library’s most powerful interfaces have one or two methods:
| Interface | Methods | Package |
|---|---|---|
io.Reader | Read([]byte) (int, error) | io |
io.Writer | Write([]byte) (int, error) | io |
fmt.Stringer | String() string | fmt |
error | Error() string | builtin |
sort.Interface | Len, Less, Swap | sort |
http.Handler | ServeHTTP(ResponseWriter, *Request) | net/http |
Keep your own interfaces small. One method is ideal. Two is fine. Five is a code smell.
The Stringer interface
Implement fmt.Stringer to control how your type prints.
type Point struct{ X, Y int }
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
fmt.Println(Point{3, 4}) // (3, 4)
Interface for testing
Interfaces enable dependency injection without frameworks.
type UserStore interface {
GetUser(id string) (*User, error)
}
type Service struct {
store UserStore
}
func (s *Service) Greet(id string) (string, error) {
user, err := s.store.GetUser(id)
if err != nil {
return "", err
}
return "Hello, " + user.Name, nil
}
In tests, provide a mock:
type mockStore struct {
users map[string]*User
}
func (m *mockStore) GetUser(id string) (*User, error) {
u, ok := m.users[id]
if !ok {
return nil, errors.New("not found")
}
return u, nil
}
func TestGreet(t *testing.T) {
svc := &Service{store: &mockStore{
users: map[string]*User{"1": {Name: "Alice"}},
}}
msg, err := svc.Greet("1")
if err != nil {
t.Fatal(err)
}
if msg != "Hello, Alice" {
t.Errorf("got %q", msg)
}
}
Nil interface values
An interface value is nil only when both its type and value are nil.
var s Speaker // nil — both type and value are nil
var d *Dog // nil pointer
var s2 Speaker = d // NOT nil — type is *Dog, value is nil
This is a common gotcha. Always return nil explicitly for interface return types, not a typed nil pointer.
Summary
Go interfaces are small, implicit, and composable. Accept them as parameters, return concrete types, and keep them to one or two methods. Implicit satisfaction means any type can fulfill any interface without the author even knowing the interface exists — that is what makes Go’s interface system so flexible.
Related articles
- Go The context Package in Go
Master Go's context package — cancellation, timeouts, deadlines, values, and how to propagate them through your application stack.
- Go Error Handling Best Practices in Go
Write idiomatic Go error handling — wrapping, sentinel errors, custom error types, errors.Is, errors.As, and patterns for clean error flows.
- Go Testing Strategies in Go
Write effective Go tests — table-driven tests, subtests, test helpers, mocking with interfaces, benchmarks, and integration testing patterns.
- Go Go Interfaces and Duck Typing
How Go interfaces work in practice: implicit satisfaction, small interfaces, the empty interface, type assertions, and idiomatic interface design patterns.