8 Interfaces
You have been writing Go code for several chapters now, and you have already brushed against interfaces — fmt.Stringer, io.Reader, error. This chapter is where they become first-class citizens. Go interfaces are the primary tool for abstraction, and they work very differently from Java’s. Java uses nominal interfaces: a class is a Comparable only if its source says implements Comparable, so retrofitting a type you do not own means editing (or wrapping) its source. Go uses structural typing: a type satisfies an interface the moment it has the right methods, no declaration required, which lets you define a tiny interface in your own package and have third-party types satisfy it for free. That decoupling is what makes Go code so easy to mock in tests and extend without inheritance. Understanding interfaces unlocks idiomatic Go: clean, testable, composable code that does not require a class hierarchy.
8.1 Implicit Interface Satisfaction
In Java, a class declares that it implements an interface:
class Song implements Stringer { ... }If you forget implements, the class is not a Stringer, even if it has every required method. The compiler checks the declaration, not the methods.
Go flips this completely. There is no implements keyword. Any type that has the required methods automatically satisfies the interface. No declaration needed.
Here is the fmt.Stringer interface from the standard library:
type Stringer interface {
String() string // returns a human-readable representation of the value
}Any type with a String() string method is automatically a fmt.Stringer.
type Track struct {
Title string
Artist string
BPM int
}
func (t Track) String() string {
return fmt.Sprintf("%s by %s (%d BPM)", t.Title, t.Artist, t.BPM)
}Track is now a fmt.Stringer. No annotation, no registration. fmt.Println calls String() automatically when it sees a Stringer:
t := Track{Title: "Sounds of Slashdot", Artist: "San Mehat", BPM: 144}
fmt.Println(t) // Sounds of Slashdot by San Mehat (144 BPM)
Tip: This is called structural typing or duck typing with compile-time checking. If it has the right methods, it satisfies the interface — the compiler verifies this at the point of use, not at the point of definition. Java’s approach is nominal typing: the name of the interface in the declaration is what matters, not the shape of the type.
8.1.1 Checking Satisfaction Explicitly
You will sometimes want to assert at compile time that a type satisfies an interface, without actually using the interface in a function call. The idiom is:
var _ fmt.Stringer = Track{} // compile error if Track does not satisfy fmt.StringerThe blank identifier discards the value; the assignment only exists to force a compile-time check. This is useful at the top of a file as documentation and a safety net.
8.1.2 Pointer Receivers and the Method Set
There is a wrinkle that trips up Java programmers, who are used to every method being dispatched on a reference. If a method has a pointer receiver (Chapter 6), only *T satisfies the interface, not T. The value type T is missing that method from its method set, so it does not satisfy the interface.
type Counter struct{ n int }
func (c *Counter) String() string { return fmt.Sprintf("count=%d", c.n) }
var _ fmt.Stringer = &Counter{} // OK: *Counter has String()
var _ fmt.Stringer = Counter{} // compile error: Counter does not implement fmt.Stringer
// (method String has pointer receiver)The fix is to pass the address (&Counter{}) or to give the method a value receiver if it does not need to mutate. See Chapter 6 for the full rules on value versus pointer receivers and method sets.
8.2 Interface Composition
Go interfaces can embed other interfaces, combining their method sets. The standard library uses this pervasively.
// io.Reader requires one method
type Reader interface {
Read(p []byte) (n int, err error) // reads up to len(p) bytes into p
}
// io.Writer requires one method
type Writer interface {
Write(p []byte) (n int, err error) // writes len(p) bytes from p
}
// io.ReadWriter is both
type ReadWriter interface {
Reader // embeds io.Reader
Writer // embeds io.Writer
}A type that implements both Read and Write automatically satisfies io.ReadWriter.
You can define your own composed interfaces the same way:
type ReadWriteCloser interface {
io.Reader // Read(p []byte) (n int, err error)
io.Writer // Write(p []byte) (n int, err error)
io.Closer // Close() error
}In Java, you would write interface ReadWriteCloser extends Reader, Writer, Closeable. Go’s embedding syntax is a bit cleaner — you list the interfaces you want to include, and the compiler assembles the combined method set.
Tip: Prefer small, single-method interfaces over large ones. The io package is the gold standard: Reader, Writer, Closer, Seeker are each one method. Larger interfaces emerge from composing small ones. Do not define an interface until you have a concrete use case that requires it — premature abstraction adds indirection without benefit. [no-premature-interface]
8.3 any — The Top Type
Sometimes you want a variable that can hold any type. In Java, you do this with Object — Object v means any type of object can be assigned to v. You can do the same in Go with var v interface{}, but that is a rather verbose way to express that v can be assigned anything. So, Go 1.18 introduced any as an alias for interface{}. They are identical; any is just friendlier to read.
var x any = 42 // x holds an int
x = "sabor a mí" // now x holds a string
x = []int{1, 2, 3} // now x holds a sliceany is Go’s counterpart to Java’s Object — every type satisfies the empty interface because there are no methods to implement.
Wut: any is not a magic box that avoids copies. Assigning a value to an any variable wraps it in an interface value, which holds the concrete type plus a pointer to the data. Despite folklore to the contrary, modern Go does not store small values like int or bool inline in the interface; that inline optimization was removed back in Go 1.4, so a non-pointer value is boxed. The runtime may avoid the allocation in some cases (for example, small integers it keeps cached), but it never stores the value itself inline in the interface word.
Older Go code uses interface{} everywhere. When you read code that predates Go 1.18, interface{} and any mean exactly the same thing. New code should use any.
Tip: Use any sparingly. Code that traffics in any values gives up compile-time type safety and often requires type assertions (see below) to get the value back out. Generics (Chapter 18) are usually the better choice when you want a function that works with multiple types.
8.4 Type Assertions
An interface value holds two things: the concrete type and the concrete value. A type assertion extracts the concrete value.
8.4.1 The Panicking Form
var i any = "Saltwater"
s := i.(string) // assert that i holds a string; assign it to s
fmt.Println(s) // SaltwaterIf the assertion is wrong, the program panics immediately:
n := i.(int) // panic: interface conversion: interface {} is string, not intUse this form when you are certain of the type — for example, immediately after a type switch case.
8.4.2 The Safe Form
s, ok := i.(string) // ok is true if i holds a string
if ok {
fmt.Println("got string:", s)
} else {
fmt.Println("not a string")
}The safe form never panics. If the type does not match, ok is false and s is the zero value of the asserted type.
Trap: Always use the two-value form (v, ok := i.(T)) when you are not certain of the type. The single-value form panics on a wrong guess, which is a runtime crash, not a compile error.
8.5 Type Switches
A type switch is a switch statement that dispatches on the dynamic type of an interface value. Chapter 4 showed a preview; here is the full picture.
func describe(i any) string {
switch v := i.(type) {
case int:
return fmt.Sprintf("int: %d", v) // v is int here
case string:
return fmt.Sprintf("string: %q", v) // v is string here
case bool:
return fmt.Sprintf("bool: %t", v) // v is bool here
case []int:
return fmt.Sprintf("[]int of length %d", len(v))
case nil:
return "nil"
default:
return fmt.Sprintf("unknown type: %T", v)
}
}The expression i.(type) is only valid inside a type switch — you cannot write it anywhere else. In each case, v is automatically converted to the matched concrete type. In the default case, v retains the type of i (i.e., any).
You can match multiple types in one case:
switch v := i.(type) {
case int, int64:
fmt.Println("some integer:", v) // v is any here because the types differ
case string:
fmt.Println("string:", v)
}When a case lists more than one type, v takes the type of the switch expression (here any) because the compiler cannot assign a single concrete type to v.
Tip: Type switches are the idiomatic Go replacement for Java’s instanceof chains. In Java you write:
if (obj instanceof String s) { ... }
else if (obj instanceof Integer n) { ... }In Go you write a type switch. It is cleaner and exhaustive — the default case catches everything else.
8.6 Key Standard Library Interfaces
Go’s standard library defines a small set of interfaces that appear everywhere. Knowing them lets you understand most Go code at a glance.
8.6.1 io.Reader
type Reader interface {
Read(p []byte) (n int, err error) // reads into p; returns bytes read and error
}Read fills the slice p with up to len(p) bytes. It returns the number of bytes actually read and any error. When the underlying data is exhausted, it returns 0, io.EOF. A reader may also return the final n > 0 bytes together with io.EOF in the same call, so always process the n bytes you got before acting on the error.
Here is a concrete type that implements io.Reader:
// CountReader wraps an io.Reader and counts bytes as they are read.
type CountReader struct {
r io.Reader
count int
}
func (cr *CountReader) Read(p []byte) (int, error) {
n, err := cr.r.Read(p) // delegate to the underlying reader
cr.count += n // tally the bytes
return n, err
}Any function that accepts an io.Reader will accept *CountReader without modification.
8.6.2 io.Writer
type Writer interface {
Write(p []byte) (n int, err error) // writes all of p; returns count and error
}Write must write exactly len(p) bytes or return an error. os.Stdout, *os.File, *bytes.Buffer, and *strings.Builder all satisfy io.Writer.
A minimal implementation:
// UpperWriter wraps an io.Writer and converts all bytes to upper case.
type UpperWriter struct {
w io.Writer
}
func (uw *UpperWriter) Write(p []byte) (int, error) {
upper := bytes.ToUpper(p) // convert to upper case
return uw.w.Write(upper) // delegate to the underlying writer
} 8.6.3 fmt.Stringer
type Stringer interface {
String() string // returns a human-readable string representation
}fmt.Println, fmt.Sprintf with %v or %s, and most other fmt functions check whether a value satisfies Stringer and call String() if so. You saw this at the start of the chapter with Track.
8.6.4 error
type error interface {
Error() string // returns the error message
}error is a predeclared interface, not a type in any package — it is part of the language itself. Any type with an Error() string method is an error. Error handling is covered in full in Chapter 9; for now, just note the shape.
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}*ValidationError is now an error and can be returned anywhere an error is expected.
8.6.5 sort.Interface
type Interface interface {
Len() int // returns the number of elements in the collection
Less(i, j int) bool // reports whether element i should sort before element j
Swap(i, j int) // swaps the elements at positions i and j
}Any type that implements all three methods can be sorted by sort.Sort. The standard library does not know or care what you are sorting — it only calls these three methods.
Here is a concrete type that sorts a slice of songs by title:
type Song struct {
Title string
Artist string
}
// ByTitle wraps a []Song and sorts it alphabetically by title.
type ByTitle []Song
func (b ByTitle) Len() int { return len(b) }
func (b ByTitle) Less(i, j int) bool { return b[i].Title < b[j].Title }
func (b ByTitle) Swap(i, j int) { b[i], b[j] = b[j], b[i] }Use it with sort.Sort:
songs := []Song{
{Title: "The Sound of Silence", Artist: "Disturbed"},
{Title: "Sandstorm", Artist: "Darude"},
{Title: "Better Off Alone", Artist: "DJ Cobra"},
}
sort.Sort(ByTitle(songs))
for _, s := range songs {
fmt.Printf("%s --- %s\n", s.Title, s.Artist)
}
// Better Off Alone --- DJ Cobra
// Sandstorm --- Darude
// The Sound of Silence --- DisturbedByTitle(songs) is a type conversion that reinterprets the []Song slice as a ByTitle — no copying occurs.
Tip: sort.Interface is best suited to types that need to be sortable as a first-class capability — for example, a domain type that your package exposes and that users will sort repeatedly or pass to generic sort utilities. For a one-off sort inside a single function, Go 1.21’s slices.SortFunc is simpler:
slices.SortFunc(songs, func(a, b Song) int {
return cmp.Compare(a.Title, b.Title) // cmp package, Chapter 7
})You saw slices.SortFunc and cmp.Compare in Chapter 7 (remember to import "cmp"); use sort.Interface when the sortable behavior belongs to the type itself.
8.7 Accept Interfaces, Return Structs
One of Go’s most important design idioms is: accept interfaces, return concrete types.
When a function’s parameter is an interface, the caller can pass any type that satisfies it — including a mock in a test. When a function returns a concrete type, callers get access to all the methods of that type, not just the interface subset. Define the interface in the package that consumes it, not in the package that provides the implementation. [interface-in-consumer]
// Good: accepts io.Writer so any writer will do --- os.Stdout, a file, a buffer
func WriteJSON(w io.Writer, v any) error {
enc := json.NewEncoder(w)
return enc.Encode(v)
}
// Less flexible: only accepts *os.File
func WriteJSONToFile(f *os.File, v any) error {
enc := json.NewEncoder(f)
return enc.Encode(v)
}Returning interfaces makes things harder:
// Avoid: callers cannot use *bytes.Buffer methods --- only io.Writer methods
func NewBuffer() io.Writer {
return &bytes.Buffer{}
}
// Better: callers get the full *bytes.Buffer API
func NewBuffer() *bytes.Buffer {
return &bytes.Buffer{}
}
Tip: Accept the narrowest interface that meets your needs. Return the most specific concrete type you can. This maximizes caller flexibility and testability without hiding useful API. [return-concrete-types]
The rule has one well-known exception: the error interface. Functions return error (an interface) rather than a concrete error type so that callers are not coupled to the specific error implementation. Chapter 9 explains why.
8.8 The Interface Nil Trap
This is one of Go’s most notorious gotchas. Read this section carefully.
An interface value has two components: a type and a value. An interface is nil only when both the type and the value are nil. A typed nil — a nil pointer stored in an interface — is not nil.
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func mayFail(fail bool) error {
var e *MyError // e is a typed nil: type=*MyError, value=nil
if fail {
e = &MyError{msg: "something went wrong"}
}
return e // BUG: always returns a non-nil interface!
}
func main() {
err := mayFail(false)
if err != nil {
fmt.Println("got error:", err) // this line executes even when fail=false!
}
}When fail is false, e is nil (a nil *MyError), but return e wraps it in an error interface value with type *MyError and value nil. That interface value is not nil because the type field is populated. err != nil is true.
Trap: Never return a typed nil pointer in an interface. If you want to return “no error,” return the untyped nil literal directly:
func mayFail(fail bool) error {
if fail {
return &MyError{msg: "something went wrong"}
}
return nil // untyped nil: type=nil, value=nil --- this is a nil error
}The fix is to return nil (untyped) rather than a variable of the concrete error type.
You can inspect the components of an interface value using reflection, but in practice the fix is always the same: return nil directly, not a typed nil pointer.
This trap generalizes beyond pointers. Any typed nil boxed in an interface is non-nil — a nil map, a nil slice, a nil function value, or a nil channel all carry a populated type field once stored in an interface, so the interface compares unequal to nil.
var s []int // s is a nil slice
var i any = s // but i is NOT nil: type=[]int, value=nil
fmt.Println(i == nil) // falseThe lesson is the same regardless of the underlying kind: to mean “no value,” assign or return the untyped nil literal, never a typed nil that has been wrapped in an interface.
Trap: Comparing two interface values with == compares their dynamic types and then their dynamic values, and it panics at run time if the dynamic type is not comparable (a slice, map, or function). This bites hardest when you use interface values as map keys, since inserting a key with a non-comparable dynamic type panics on the spot.
var a, b any = []int{1}, []int{1}
fmt.Println(a == b) // panic: runtime error: comparing uncomparable type []intIf you need to compare interface values whose dynamic type might be uncomparable, reach for reflect.DeepEqual instead.
8.9 Try It
Type this in and run it to watch structural typing, interface composition, a type switch, and sort.Interface all working together. Notice that Track never says it implements Playable — it just has the right methods.
package main
import (
"fmt"
"sort"
)
// Playable is a tiny interface: anything that can describe itself and report a
// duration satisfies it --- no "implements" needed.
type Playable interface {
fmt.Stringer
Seconds() int
}
type Track struct {
Title string
Artist string
Length int // seconds
}
func (t Track) String() string { return fmt.Sprintf("%s --- %s", t.Title, t.Artist) }
func (t Track) Seconds() int { return t.Length }
type byLength []Track
func (b byLength) Len() int { return len(b) }
func (b byLength) Less(i, j int) bool { return b[i].Length < b[j].Length }
func (b byLength) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func describe(p Playable) {
fmt.Printf("%s (%d s)\n", p, p.Seconds())
}
func main() {
tracks := []Track{
{"Espresso", "Sabrina Carpenter", 175},
{"Good Luck, Babe!", "Chappell Roan", 218},
{"Birds of a Feather", "Billie Eilish", 210},
}
sort.Sort(byLength(tracks))
for _, t := range tracks {
describe(t) // Track satisfies Playable structurally
}
var v any = tracks[0]
switch p := v.(type) {
case Playable:
fmt.Println("playable:", p)
default:
fmt.Println("not playable")
}
}Try these modifications:
- Add a
Podcasttype withString()andSeconds()and pass it todescribe— it satisfiesPlayablefor free. - Replace
sort.Sort(byLength(tracks))withslices.SortFuncandcmp.Compare(Chapter 7) to sort by title instead. - Add a
var _ Playable = Track{}line at the top of the file, then delete theSeconds()method and watch the compile-time check fire.
8.10 Key Points
- Go uses structural typing: a type satisfies an interface by having the right methods, with no
implementsdeclaration required. - Interface composition embeds interfaces inside interfaces;
io.ReadWriterisio.Reader+io.Writer. any(alias forinterface{}) is the top type; use it sparingly and prefer generics when possible.- Type assertions come in two forms:
v := i.(T)panics on failure;v, ok := i.(T)is safe. - Type switches dispatch on dynamic type:
switch v := i.(type) { case int: ... }. - The five interfaces to know first:
io.Reader,io.Writer,fmt.Stringer,error, andsort.Interface. sort.Interface(Len,Less,Swap) lets any type be sorted bysort.Sort; preferslices.SortFuncfor one-off sorts.- Accept interfaces, return concrete types — this maximizes flexibility and testability.
- A typed nil pointer stored in an interface is not nil; always return the untyped
nilto signal “no error.”
8.11 Exercises
Think about it: Go’s structural typing means any package can retroactively make its types satisfy an interface defined in any other package. In Java, if you want your
Songclass to satisfy a new interfacePlayabledefined in a library you do not control, you must modifySong’s source. Explain how Go’s approach changes the relationship between library authors and library users. What does this mean for extending types from packages you cannot modify?What does this print?
package main import "fmt" type Celsius float64 type Fahrenheit float64 func (c Celsius) String() string { return fmt.Sprintf("%.1f°C", float64(c)) } func printTemp(v fmt.Stringer) { fmt.Println(v.String()) } func main() { c := Celsius(37.5) f := Fahrenheit(99.5) printTemp(c) fmt.Println(f) }Calculation: An interface value in Go stores two fields: a pointer to type information and a pointer to (or copy of) the data. Given a variable declared as
var r io.Reader = &bytes.Buffer{}, how many distinct type/value components doesrhold? Ifris then assignednil, describe the type and value components of the resulting interface value.Where is the bug?
package main import "fmt" type DBError struct{ code int } func (e *DBError) Error() string { return fmt.Sprintf("db error %d", e.code) } func connect(bad bool) error { var err *DBError if bad { err = &DBError{code: 500} } return err } func main() { e := connect(false) if e == nil { fmt.Println("connected OK") } else { fmt.Println("failed:", e) } }Write a program: Define an interface
Shapewith two methods:Area() float64andPerimeter() float64. ImplementShapefor two concrete types:Rectangle(with fieldsWidthandHeight float64) andCircle(with fieldRadius float64; usemath.Pi). Write a functionprintShapeInfo(s Shape)that prints the area and perimeter. Inmain, create oneRectangleand oneCircleand callprintShapeInfoon each.