Interfaces and Generality

When and why to return interface types from Go constructors, with the hash.Hash pattern and trade-offs for designing flexible APIs.

The Default — Return Concrete Types

The broader Go idiom says: accept interfaces, return structs. A function that receives an interface stays decoupled from any single implementation; a function that hands back a concrete type gives the caller full control over what they do with it. Most of the time this is the right call — it keeps your API simple and the caller’s options open.

But “most of the time” is not “always”. The Go standard library contains well-known functions that return an interface type. Those functions are not mistakes. They exist to solve a specific problem: when the whole point of the API is to hide which concrete implementation is in play, so that callers are forced — deliberately — to treat the value as an abstract capability.

This section is about recognising those situations and using the pattern without creating the problems it is meant to avoid.

The hash.Hash Constructor Pattern

The most cited example is hash.Hash. Every hash algorithm in the standard library — SHA‑256, MD5, SHA‑512 — exposes a constructor named New that returns a hash.Hash interface, not a concrete type.

package main
import (
    "crypto/sha256"
    "fmt"
    "hash"
)
func main() {
    var h hash.Hash
    h = sha256.New()   // returns hash.Hash, not *sha256.digest
    h.Write([]byte("hello"))
    fmt.Printf("%x\n", h.Sum(nil))
}

sha256.New() creates a *sha256.digest internally. But the signature is func New() hash.Hash. The caller never sees the concrete type, and they don’t need to. They only need Write, Sum, Reset, Size, and BlockSize — exactly the methods hash.Hash declares.

The same pattern appears in net.Dial (which returns a net.Conn interface), os.Open (which returns an *os.File, but many functions in the io package accept and return io.Reader or io.ReadCloser), and anywhere the standard library wants to swap one concrete implementation for another without the caller’s code changing.

Implicit satisfaction makes this possible:

Because Go interfaces are satisfied implicitly, a constructor can return an interface without the concrete type ever mentioning that interface. The author of the concrete type only implements the methods; the decision to expose an interface is made by the function that returns the value.

How Returning an Interface Enables Generality

A function that returns an interface is saying: “The type I return is not part of the contract. I promise it will do these things, but the caller should not rely on what it is.”

This gives the package author freedom to change the internal type later — even at runtime — without breaking callers. For example, crypto/tls can negotiate different cipher suites and return a net.Conn that wraps the raw connection with the appropriate encryption. The caller never knows whether they are talking to a *tls.Conn, a *tcpip.Conn, or a mock during a test.

Beginners can think of it as a vending machine. You put in your money and press a button, and the machine gives you “a drink”. You don’t care whether the internal mechanism dispensed a can of cola from shelf three or a bottle of water from shelf seven. You only care that what you got satisfies the interface “drink” — you can open it and consume it. The manufacturer can rearrange the shelves, change suppliers, or even swap a faulty dispenser without you ever noticing.

What You Lose When You Return an Interface

Returning an interface is a deliberate restriction. The caller cannot use any method, field, or type-specific behaviour that is not part of the interface. If the concrete type has a helper method that is useful in practice — say, a ResetWithSeed(int64) that does not appear in the interface — the caller would need a type assertion to reach it, and that breaks the abstraction.

This trade‑off is acceptable only when the interface captures all the behaviour that callers genuinely need, and when the freedom to change the implementation is more valuable than the extra methods the concrete type would expose.

When callers keep casting back to the concrete type:

If callers regularly write t := val.(*concreteType) to regain access to methods you hid, the interface you returned is too small or the wrong abstraction entirely. Either broaden the interface or return the concrete type directly.

The Nil Interface Trap

When a function’s return type is an interface, return nil gives a true nil interface — its type and value are both nil. But return (*MyType)(nil) produces a non‑nil interface that holds a nil pointer. That interface is not equal to nil, and calling a method on it will panic with a nil pointer dereference because the concrete value inside is nil.

type Greeter interface {
    Greet() string
}
type Person struct{ Name string }
func (p *Person) Greet() string { return "Hello, " + p.Name }
func NewGreeter() Greeter {
    var p *Person   // p is nil
    return p        // returns a non-nil Greeter holding nil *Person
}
func main() {
    g := NewGreeter()
    if g != nil {
        fmt.Println(g.Greet()) // panic: nil pointer dereference
    }
}

Nil pointer wrapped in an interface is not nil:

Always check whether the concrete value you are returning could be nil. If the intent is to signal “no value”, return a plain nil without a concrete type.

When Not to Return an Interface

The pattern is not a universal good. Return a concrete type when:

  • There is only one plausible implementation and you expect callers to use the full surface area of that type.
  • The interface would need to be large to capture everything callers need — large interfaces violate the “small interfaces” idiom and couple callers to methods they never use.
  • The function is part of an internal package where decoupling offers no real benefit and the indirection just makes the code harder to follow.
  • The concrete type is a simple data structure (like a config struct) that callers will inspect and modify.

The interface you return should be small and focused:

When the interface exposes just a handful of methods that are the essence of the abstraction — io.Reader with a single Read, hash.Hash with five methods — callers are rarely tempted to break out of it. That’s the sweet spot.

Comparing the Two Approaches

The choice between returning a concrete struct and returning an interface hinges on whether you want the caller to depend on the type or only on the behaviour.

// NewClient returns a concrete *http.Client so callers
// can set timeouts, transports, and other fields directly.
func NewClient() *http.Client {
    return &http.Client{
        Timeout: 30 * time.Second,
    }
}

Callers get the full API of *http.Client. They can modify any exported field, pass it to any function that accepts *http.Client, and test with a real client. This is appropriate when the type is the intended point of use and alternative implementations are not expected.

Summary

Returning an interface from a constructor is a way to hide the concrete implementation when the value’s exact type is an internal detail. It gives the package author the freedom to evolve the implementation, and it gives the caller a narrower, more stable contract.

Use it when the interface is small, when multiple implementations are genuinely needed (or anticipated), and when losing access to the concrete type’s extra methods is acceptable. The pattern works best when you treat the interface as the real product and the concrete type as nobody else’s business.

When you do use it, keep the interface tiny, never return a nil pointer dressed as an interface, and verify that callers are not immediately casting back to the concrete type. If they are, the abstraction is not pulling its weight — return the struct instead.