Skip to main content
Technology & EngineeringGo185 lines

Interfaces

Interface design principles, implicit satisfaction, and composition patterns in Go

Quick Summary17 lines
You are an expert in Go interface design, helping developers write clean, decoupled, and testable code through effective use of interfaces.

## Key Points

- Keep interfaces small: 1-3 methods is ideal. The bigger the interface, the weaker the abstraction.
- Define interfaces in the consumer package, not the provider package.
- Use standard library interfaces (`io.Reader`, `io.Writer`, `fmt.Stringer`, `sort.Interface`) wherever possible.
- Name single-method interfaces as `<Method>er`: `Reader`, `Writer`, `Closer`, `Formatter`.
- Do not export interfaces solely for mocking — define them where needed.
- Return concrete types from constructors so callers get the full method set.
- **Nil interface vs nil pointer in interface**: `var w io.Writer = (*bytes.Buffer)(nil)` is a non-nil interface containing a nil pointer; `w != nil` is true.
- **Too-large interfaces**: a 10-method interface is hard to mock and tightly couples consumers to a specific implementation.
- **Premature abstraction**: do not introduce an interface until you have at least two implementations or a testing need.
- **Pointer vs value receivers**: a type with pointer receivers only satisfies an interface when used as a pointer. Value receiver methods satisfy it for both pointer and value.
- **Interface pollution**: creating interfaces for every type "just in case" adds complexity without benefit.
skilldb get go-skills/InterfacesFull skill: 185 lines
Paste into your CLAUDE.md or agent config

Interfaces — Go Programming

You are an expert in Go interface design, helping developers write clean, decoupled, and testable code through effective use of interfaces.

Core Philosophy

Go's implicit interface satisfaction is one of its most powerful design decisions. Unlike Java or C# where a type must explicitly declare that it implements an interface, in Go a type satisfies an interface simply by having the right methods. This inverts the dependency: instead of the implementer knowing about the interface, the consumer defines the interface it needs. The result is naturally decoupled code where packages depend on narrow contracts rather than concrete implementations.

The most important interface design principle in Go is to keep interfaces small. The standard library leads by example: io.Reader has one method, io.Writer has one method, fmt.Stringer has one method. Small interfaces are easy to implement, easy to mock, and composable — you can embed Reader and Writer to get ReadWriter. The moment an interface grows beyond three or four methods, it becomes tightly coupled to a specific implementation and loses its power as an abstraction boundary.

Interfaces should be defined by the consumer, not the producer. If package A provides a UserService struct with ten methods, package B should not import package A's ten-method interface. Instead, package B defines its own two-method interface containing only the methods it actually calls. This is the "accept interfaces, return structs" principle: functions accept the narrowest interface that satisfies their needs and return concrete types that give callers full access. This approach keeps import graphs shallow and makes testing straightforward — you only need to mock the two methods your code actually uses.

Anti-Patterns

  • Defining interfaces in the provider package "for mocking": Exporting a UserServiceInterface next to UserService couples every consumer to the full surface area of the implementation. Let consumers define their own narrow interfaces tailored to what they actually call.

  • Creating interfaces with no implementations yet: An interface that has exactly one implementation is premature abstraction. It adds an indirection layer with no benefit. Wait until you have a genuine second implementation or a testing need before extracting an interface.

  • Fat interfaces with many methods: A ten-method interface is essentially a concrete type hiding behind an abstraction. No one wants to implement ten methods just to write a test mock. Break large interfaces into small, composable ones (Reader, Writer, Closer) that consumers can combine as needed.

  • Confusing nil interface with nil concrete value: var w io.Writer = (*bytes.Buffer)(nil) is a non-nil interface holding a nil pointer. Checking w != nil returns true, and calling w.Write() will panic. Be explicit about whether you are checking the interface value or the underlying pointer.

  • Using any (empty interface) as a function parameter: Accepting any tells callers nothing about what the function expects and moves type checking from compile time to runtime. Every use of any is a lost opportunity for the compiler to catch bugs. Use it only when truly necessary (e.g., JSON marshaling, generic containers).

Overview

Go interfaces are satisfied implicitly — a type implements an interface simply by having the right method set, with no implements keyword. This enables loose coupling and makes it easy to define narrow contracts at the point of consumption rather than at the point of definition.

Core Concepts

Implicit Satisfaction

Any type that has the methods declared by an interface automatically satisfies it. This means you can define interfaces in the package that uses them, not the package that implements them.

Interface Values

An interface value holds a (type, value) pair. A nil interface is different from an interface holding a nil pointer — the latter is non-nil.

The Empty Interface

any (alias for interface{}) accepts every type. Use sparingly; prefer typed interfaces.

Type Assertions and Switches

if s, ok := val.(Stringer); ok {
    fmt.Println(s.String())
}

switch v := val.(type) {
case int:
    fmt.Println("int:", v)
case string:
    fmt.Println("string:", v)
}

Implementation Patterns

Accept Interfaces, Return Structs

// Good: function accepts an interface
func Process(r io.Reader) error {
    buf := make([]byte, 1024)
    _, err := r.Read(buf)
    return err
}

// Good: function returns a concrete type
func NewService(db DatabaseReader) *Service {
    return &Service{db: db}
}

Small Interfaces (1-2 methods)

// Define interfaces where they are consumed
type Sender interface {
    Send(ctx context.Context, msg Message) error
}

type Closer interface {
    Close() error
}

Interface Composition

type ReadCloser interface {
    io.Reader
    io.Closer
}

type Repository interface {
    Reader
    Writer
}

type Reader interface {
    Get(ctx context.Context, id string) (*Entity, error)
    List(ctx context.Context, filter Filter) ([]*Entity, error)
}

type Writer interface {
    Create(ctx context.Context, e *Entity) error
    Update(ctx context.Context, e *Entity) error
    Delete(ctx context.Context, id string) error
}

Functional Options via Interfaces

type Option interface {
    apply(*config)
}

type optionFunc func(*config)
func (f optionFunc) apply(c *config) { f(c) }

func WithTimeout(d time.Duration) Option {
    return optionFunc(func(c *config) {
        c.timeout = d
    })
}

func New(opts ...Option) *Client {
    cfg := defaultConfig()
    for _, o := range opts {
        o.apply(&cfg)
    }
    return &Client{cfg: cfg}
}

Testing with Interfaces

// In production code
type Store interface {
    Get(ctx context.Context, key string) (string, error)
}

// In test code
type mockStore struct {
    getFunc func(ctx context.Context, key string) (string, error)
}

func (m *mockStore) Get(ctx context.Context, key string) (string, error) {
    return m.getFunc(ctx, key)
}

func TestHandler(t *testing.T) {
    store := &mockStore{
        getFunc: func(ctx context.Context, key string) (string, error) {
            return "value", nil
        },
    }
    h := NewHandler(store)
    // test h ...
}

Best Practices

  • Keep interfaces small: 1-3 methods is ideal. The bigger the interface, the weaker the abstraction.
  • Define interfaces in the consumer package, not the provider package.
  • Use standard library interfaces (io.Reader, io.Writer, fmt.Stringer, sort.Interface) wherever possible.
  • Name single-method interfaces as <Method>er: Reader, Writer, Closer, Formatter.
  • Do not export interfaces solely for mocking — define them where needed.
  • Return concrete types from constructors so callers get the full method set.

Common Pitfalls

  • Nil interface vs nil pointer in interface: var w io.Writer = (*bytes.Buffer)(nil) is a non-nil interface containing a nil pointer; w != nil is true.
  • Too-large interfaces: a 10-method interface is hard to mock and tightly couples consumers to a specific implementation.
  • Premature abstraction: do not introduce an interface until you have at least two implementations or a testing need.
  • Pointer vs value receivers: a type with pointer receivers only satisfies an interface when used as a pointer. Value receiver methods satisfy it for both pointer and value.
  • Interface pollution: creating interfaces for every type "just in case" adds complexity without benefit.

Install this skill directly: skilldb add go-skills

Get CLI access →