Skip to content
C Codeloom
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.

·6 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • How implicit interface satisfaction works
  • Why Go favors small interfaces
  • Type assertions and type switches
  • The any/interface{} type
  • Interface design patterns and pitfalls

Prerequisites

  • Basic familiarity with the language

Go’s interface system looks unusual at first. There is no implements keyword. Types do not announce which interfaces they fit. The compiler just checks at use sites. This implicit, structural style takes a few rounds of code review to grow on you, but once it does, it changes how you design APIs.

Implicit satisfaction

A Go interface lists method signatures. Any type that has those methods satisfies the interface. There is no declaration linking the two.

type Stringer interface {
    String() string
}

type Point struct{ X, Y int }

func (p Point) String() string {
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

var s Stringer = Point{1, 2}

Point does not say it implements Stringer. The compiler observes that it has a String() string method and lets the assignment compile. This is duck typing with static checking.

The practical consequence is that you can define interfaces around types you do not own. The standard library’s io.Reader works for files, network connections, byte buffers, gzip streams, and any future type that grows a Read method.

Small interfaces are idiomatic

Look at the Go standard library. The most-used interfaces have one or two methods.

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

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

Bigger interfaces are usually compositions of smaller ones.

type ReadCloser interface {
    Reader
    Closer
}

The Go proverb is “the bigger the interface, the weaker the abstraction.” A Reader is satisfied by hundreds of types. A DatabaseClient with twenty methods is satisfied by one.

The mental model

type Foo interface { A(); B() }

struct X has methods A(), B()    -> X satisfies Foo
struct Y has methods A(), B(), C() -> Y satisfies Foo
struct Z has method  A()         -> Z does not satisfy Foo

assignment to Foo: checked at compile time, no runtime tagging
Interface satisfaction in Go

Implicit satisfaction means interface relationships are inferred from method sets, not declarations. Code organization stops needing to anticipate every future interface.

Define interfaces at the use site

In Go, the consumer of an interface usually declares it, not the producer. A package that uses some behavior defines the interface it needs.

package logger

type Storage interface {
    Append(line string) error
}

func WriteLog(s Storage, line string) error {
    return s.Append(line)
}

The Storage interface lives next to the function that needs it. Implementations (file storage, S3 storage, in-memory storage) live elsewhere and do not import the logger package to declare conformance. Testing is trivial: define a mock with Append and pass it.

This is the opposite of Java, where interfaces are usually defined alongside implementations and consumers import them.

Type assertions and type switches

When you have an interface value and want to recover the concrete type, use a type assertion.

var w io.Writer = os.Stdout

if f, ok := w.(*os.File); ok {
    fmt.Println("it is a file:", f.Name())
}

The two-value form is safe: ok is false if the assertion fails. The single-value form panics on failure.

For multiple cases, a type switch.

func describe(v interface{}) string {
    switch x := v.(type) {
    case int:
        return fmt.Sprintf("int: %d", x)
    case string:
        return fmt.Sprintf("string: %q", x)
    case fmt.Stringer:
        return fmt.Sprintf("stringer: %s", x.String())
    default:
        return "unknown"
    }
}

interface and any

interface{} is the empty interface, satisfied by every type. Since Go 1.18, the alias any means the same thing.

It is the closest Go gets to dynamic typing. JSON decoding, container types in older code, and printf-style APIs all use any. Generics arrived in 1.18 to reduce the need for any in container code.

Use any sparingly. It defers type checking to runtime and tends to spread through codebases.

A hands-on example

A pluggable storage interface.

package store

type KV interface {
    Get(key string) (string, bool)
    Set(key, value string)
}

func Migrate(src, dst KV, keys []string) {
    for _, k := range keys {
        if v, ok := src.Get(k); ok {
            dst.Set(k, v)
        }
    }
}

Implementations: a MapKV for in-memory testing, a RedisKV for production, a FileKV for cold storage. None of them imports store to declare conformance. The compiler checks at the call site.

Common pitfalls

Pointer vs value receivers. A method with a pointer receiver belongs to the pointer’s method set, not the value’s. Assigning a value to an interface that requires pointer-receiver methods fails.

type Saver interface{ Save() }

func (p *Point) Save() {}

var s Saver
s = Point{}       // ERROR: Save has pointer receiver
s = &Point{}      // OK

Nil interface vs nil concrete in interface. An interface value contains both a type and a value. A non-nil type with a nil value is itself non-nil.

var p *MyError = nil
var err error = p
fmt.Println(err == nil) // false, because err has a non-nil type

This bites everyone once. Return concrete nil directly when you mean “no error.”

Overusing the empty interface to avoid generics. Since 1.18, prefer type parameters when possible. They keep checks at compile time.

Practical tips

Define interfaces near consumers. Keep them small. Compose them when you need more. Resist the urge to predeclare every possible interface; let them emerge from real use.

Accept interfaces, return concrete types. A function should ask for the minimum behavior it needs and return something the caller can use specifically.

When generics fit, use them. When you genuinely need runtime polymorphism, interfaces are still the answer.

Wrap-up

Go interfaces flip the usual dependency direction: consumers declare what they need, producers just happen to fit. Combined with the preference for small interfaces, that style yields highly decoupled code with very little ceremony. Learn the conventions about receivers, nil values, and where to declare interfaces, and the rest is just methods and structs.