Interfaces
Structural typing, the empty interface, and io.Reader.
Interfaces
Go's interfaces are structurally typed — a type satisfies an interface just by having the right methods, with no implements keyword. The compiler checks at compile time; the type doesn't have to know about the interface at all.
Defining one
An interface is a named set of method signatures:
type Stringer interface {
String() string
}
Anything with a String() string method satisfies Stringer. No declaration needed.
type Money struct {
Cents int
}
// Money satisfies Stringer just by having this method.
func (m Money) String() string {
return fmt.Sprintf("$%.2f", float64(m.Cents)/100)
}
fmt.Println(Money{1234}) prints $12.34 because fmt checks if its argument is a Stringer.
Why structural typing matters
You can write an interface AFTER a type already exists. You can write an interface for a stdlib type. You can write an interface that captures a behaviour shared across types from different libraries that don't know about each other:
type Closer interface {
Close() error
}
func cleanup(thing Closer) {
thing.Close()
}
cleanup(file) // *os.File has Close() error
cleanup(httpResp.Body) // io.ReadCloser has Close() error
cleanup(dbConn) // *sql.DB has Close() error
In Java/C# you'd need every type to declare implements Closer. In Go they just have to have the method.
The empty interface
type any = interface{}
any (or interface{}) is the empty set of methods — every type satisfies it. Use sparingly; you've turned the compiler off:
func cache(key string, value any) { ... } // anything goes
Most uses are for serialisation libraries (json.Unmarshal into interface{}) or generic-ish containers before Go's actual generics landed.
Type assertions and type switches
When you have an any (or some interface) and need to use the concrete type, assert:
var i any = "hello"
s := i.(string) // panics if i isn't a string
s, ok := i.(string) // safe — ok is false if i isn't a string
if !ok {
return errors.New("expected string")
}
For more than one possible type, use a type switch:
func describe(i any) string {
switch v := i.(type) {
case int:
return fmt.Sprintf("int %d", v)
case string:
return fmt.Sprintf("string %q", v)
case nil:
return "nil"
default:
return fmt.Sprintf("unknown %T", v)
}
}
Common stdlib interfaces
Go's standard library is built around small, focused interfaces:
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 }
type ReadWriter interface { Reader; Writer } // composed
type ReadCloser interface { Reader; Closer }
io.Reader is everywhere: files, HTTP request bodies, byte buffers, network sockets, gzip streams. Functions written against io.Reader work on every one of them. This is the single most-imitated design in Go's standard library.
Accept interfaces, return concrete types
A widely-quoted Go convention:
Accept interfaces, return structs.
Functions take the smallest interface they need (so callers can pass anything compatible). They return concrete types (so callers see all the methods, not just one interface's worth):
// Good: takes any reader, returns a fully-typed *Stats.
func Analyse(r io.Reader) (*Stats, error) { ... }
// Bad: returning an interface limits the caller.
func NewClient() ClientInterface { ... }
interface satisfaction is implicit — and that's powerful
You can write a "mock" for testing by implementing the interface in your test file. No mocking framework required:
// In your tests:
type fakeReader struct{}
func (fakeReader) Read(p []byte) (int, error) { return 0, io.EOF }
result, _ := Analyse(fakeReader{})
The function under test doesn't know or care whether it got a real *os.File or your fake. It just calls Read.