Home
Articles

Go interfaces

Go interfaces

By Ibi Hasanli

·

·

4 min read

Go interfaces are how you describe behaviour without naming a concrete type. You write the methods you need; any type that has those methods satisfies the interface with no implements keyword and no ceremony. That design choice shapes almost every good Go API you will touch.

Interfaces are satisfied implicitly

In Go, a type satisfies an interface by having the right method set. There is no declaration on the type itself. The compiler checks compatibility at the call site or when you assign to an interface value.

type Greeter interface {
    Greet() string
}

type Person struct{ Name string }

func (p Person) Greet() string {
    return "hello, " + p.Name
}

var g Greeter = Person{Name: "Ada"}

Person never mentions Greeter. The assignment works because the method sets match. That keeps packages loosely coupled: you can introduce an interface in the consumer without changing the producer, which is the opposite of many OOP languages where the producer must know the contract in advance.

Prefer defining interfaces where they are used, not where types are declared. A package that returns a concrete *os.File is often easier to consume than one that invents a fat interface nobody asked for.

Small interfaces and io.Reader

The standard library’s best interfaces are tiny. io.Reader has one method:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Anything that can fill a byte slice that way is a reader: files, network connections, buffers, HTTP bodies. Functions accept io.Reader and remain useful far beyond the original author’s imagination. I reach for io.Reader-shaped contracts before inventing domain-specific ones.

Compose small interfaces rather than growing one large surface. io.ReadCloser is just Reader plus Closer. Callers who only need to read do not pay for close semantics they ignore. When you design your own contracts, start with one or two methods and stop. If a third method only serves a single call site, that call site probably wants a concrete type or a separate interface.

Type assertions and type switches

An interface value holds a type and a value. When you need the concrete type back, assert or switch.

func describe(v any) string {
    if s, ok := v.(string); ok {
        return "string: " + s
    }
    switch x := v.(type) {
    case error:
        return "error: " + x.Error()
    case int:
        return fmt.Sprintf("int: %d", x)
    default:
        return "unknown"
    }
}

Use a type assertion with the two-value form (val, ok := x.(T)) when you expect one type and want to handle failure without a panic. Use a type switch when several concrete types need different handling, especially errors or values stored as any (historically interface{}). Prefer asserting to a small interface over asserting to a concrete struct when you only need a method.

Common pitfalls with Go interfaces

The classic trap is a nil concrete pointer stored in a non-nil interface. An interface is nil only when both its type and value are unset. A typed nil pointer still makes the interface non-nil, so if err != nil can surprise you after a helper returns a nil pointer converted to error. Return a bare nil from functions whose result type is the interface, not a nil pointer of a concrete error type.

Another habit that bites: interfaces with many methods, or interfaces defined for mocking in the same package as the implementation. You end up with brittle stubs and APIs that are hard to extend. Keep production interfaces small and let tests invent narrower fakes at the call site.

Avoid empty interfaces as a substitute for generics. Since Go 1.18, prefer type parameters when you need compile-time type safety across similar algorithms. Keep any for truly heterogeneous bags of values, such as formatting or reflection-style helpers.

Quick Comparison

  • Interface value. When to use: accept behaviour you do not own. Watch out for: nil interface vs nil pointer.
  • Concrete type. When to use: own the type; need fields or one caller. Watch out for: premature interface wrappers.
  • Type assertion. When to use: expect one concrete type. Watch out for: panic if you skip the ok form.
  • Type switch. When to use: branch on several types. Watch out for: forgetting default and new cases.

Interfaces and concrete types work together: return concrete types from constructors when you can, and accept interfaces at the boundaries where you need substitutability. Assertions and switches are tools for peeling an interface open, not for designing everyday APIs.

When to use Go interfaces

Use Go interfaces when callers need to plug in behaviour you do not control. Small, stable method sets help, as do contracts the standard library already defined (io.Reader, fmt.Stringer, error). Stay with a concrete type when there is one implementation or you need exported fields. Skip an interface that would exist only to satisfy a test double.

Design for the call site: if two methods would do, do not invent five.

If you are shaping Go services or platform APIs and want a second pair of eyes on interface boundaries, get in touch.

Programming

WebRTC Explained

A practical guide to real-time audio, video, data channels, infrastructure choices, security, and production-ready implementation.

·

7 min read

Programming

Event-Driven Systems

A practical guide to designing resilient event-driven platforms that scale cleanly, recover safely, and support modern digital services.

·

7 min read

Programming

Deno JS Guide

A practical guide to Deno, its secure runtime, TypeScript support, tooling, deployment patterns, and when teams should adopt it.

·

7 min read

Programming

Bun.js Guide

A practical guide to Bun.js performance, tooling, migration strategy, and how Eight Mile can help teams adopt it safely.

·

8 min read

Programming

Flutter Basics

A practical beginner guide to Flutter widgets, Dart syntax, state, layouts, and building reliable cross-platform apps.

·

2 min read

Programming

MVC Explained

Learn how Model View Controller separates data, interface, and application logic to make software easier to build and maintain.

·

2 min read

Programming

Testing That Works

A practical guide to choosing the right tests, avoiding brittle suites, and shipping software with confidence.

·

3 min read

Programming

Go for CLI Tools

How to build command line programs in Go that read from pipes, fail properly, handle signals and ship as one binary anyone can run.

·

6 min read

Programming

JavaScript Fundamentals: The Core Concepts

A practical tour of the core JavaScript ideas — types, scope, functions, objects, and asynchrony — that make every framework you learn afterwards feel obvious.

·

6 min read

Programming

What is OOP?

What object-oriented programming is, its four core principles, when to use it, and worked examples in Python, Java and JavaScript.

·

4 min read