Skip to content
Codeloom
Go

Go Interfaces Explained

Understand Go interfaces — implicit satisfaction, the empty interface, type assertions, interface composition, and real design patterns.

·4 min read · By Codeloom
Intermediate 10 min read

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:

InterfaceMethodsPackage
io.ReaderRead([]byte) (int, error)io
io.WriterWrite([]byte) (int, error)io
fmt.StringerString() stringfmt
errorError() stringbuiltin
sort.InterfaceLen, Less, Swapsort
http.HandlerServeHTTP(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.