Go · lesson 12 of 25
Interfaces in Go
Go interfaces run for real: implicit satisfaction, pointer-receiver method sets, type assertions and switches, Stringer, io.Reader, and the nil trap.
By SkillByExample · Updated
An interface type lists methods. Any type with those methods satisfies it — no declaration needed. That one rule is what lets fmt, io and sort work with types they have never heard of, and it is behind one well-known bug, the non-nil nil error.
Implicit satisfaction
Circle and Rect never mention Shape, yet both can be used as one.
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ R float64 }
type Rect struct{ W, H float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }
func (r Rect) Area() float64 { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }
var _ Shape = Rect{} // compile-time check that Rect satisfies Shape
func main() {
shapes := []Shape{Circle{1}, Rect{3, 4}}
total := 0.0
for _, s := range shapes {
fmt.Printf("%-14T area %6.2f perimeter %6.2f\n", s, s.Area(), s.Perimeter())
total += s.Area()
}
fmt.Printf("total area %.2f\n", total)
}
$ go run shapes.go
main.Circle area 3.14 perimeter 6.28
main.Rect area 12.00 perimeter 14.00
total area 15.14
The loop calls methods through the interface; Go picks Circle.Area or Rect.Area based on the concrete type stored in each element, which %T reveals. The var _ Shape = Rect{} line costs nothing at run time but makes the compiler prove Rect satisfies Shape, so a typo in a method name fails here rather than at some distant call site.
Pointer receivers and method sets
A method declared on *T is not part of T’s method set, so only the pointer satisfies the interface.
package main
import "fmt"
type Resetter interface{ Reset() }
type Counter struct{ n int }
func (c *Counter) Reset() { c.n = 0 }
func resetAll(rs ...Resetter) {
for _, r := range rs {
r.Reset()
}
}
func main() {
c := Counter{n: 5}
resetAll(c)
fmt.Println(c.n)
}
$ go run methodset.go
# command-line-arguments
./methodset.go:19:11: cannot use c (variable of struct type Counter) as Resetter value in argument to resetAll: Counter does not implement Resetter (method Reset has pointer receiver)
The compiler’s error names the cause: Reset has a pointer receiver, so only *Counter has it. The rule exists because a Counter copied into an interface couldn’t be modified by Reset in any meaningful way — the caller’s variable would never change. With resetAll(&c), the program prints 0.
Stringer: implementing a standard interface
fmt checks whether a value has a String() string method — the fmt.Stringer interface — and uses it.
package main
import "fmt"
type Celsius float64
func (c Celsius) String() string { return fmt.Sprintf("%.1f°C", float64(c)) }
type Status int
const (
Active Status = iota
Suspended
)
func (s Status) String() string {
switch s {
case Active:
return "active"
case Suspended:
return "suspended"
}
return fmt.Sprintf("Status(%d)", int(s))
}
func main() {
fmt.Println(Celsius(21.456), Suspended, Status(7))
fmt.Printf("%v %d\n", Active, Active)
}
$ go run stringer.go
21.5°C suspended Status(7)
active 0
Neither type mentions fmt.Stringer, but Println and %v found the method. %d bypasses it and prints the number. Note the float64(c) conversion inside String: formatting c itself with %f is fine, but %v would call String again and recurse forever.
any, type assertions and type switches
any (an alias for interface{}) holds a value of any type. A type assertion gets the concrete value back.
package main
import "fmt"
func main() {
var v any = "hello"
s, ok := v.(string)
fmt.Println(s, ok)
n, ok := v.(int)
fmt.Println(n, ok)
fmt.Println(v.(int))
}
$ go run assert.go
hello true
0 false
panic: interface conversion: interface {} is string, not int
goroutine 1 [running]:
main.main()
/home/user/gobyex/interfaces/assert/assert.go:14 +0xf0
exit status 2
The two-value form reports failure through ok and returns the zero value. The single-value form panics, and the message names both the type it found and the type you asked for. Use the panicking form only when a different type would be a programming error.
A type switch tests several types at once, and inside each case the variable has that case’s type:
package main
import "fmt"
func describe(v any) string {
switch x := v.(type) {
case nil:
return "nil"
case int, int64:
return fmt.Sprintf("integer %v", x) // x is still any here
case string:
return fmt.Sprintf("string of %d bytes", len(x))
case error:
return "error: " + x.Error()
case fmt.Stringer:
return "stringer: " + x.String()
default:
return fmt.Sprintf("other %T", x)
}
}
func main() {
for _, v := range []any{nil, 42, int64(7), "héllo", fmt.Errorf("boom"), 3.5} {
fmt.Println(describe(v))
}
}
$ go run typeswitch.go
nil
integer 42
integer 7
string of 6 bytes
error: boom
other float64
Cases can name interfaces (error, fmt.Stringer) as well as concrete types, and the first match wins. In a case listing two types, x keeps the type any, because it could be either.
Small interfaces: io.Reader
The standard library’s most important interfaces have one method. Anything that implements Read works with every function that consumes an io.Reader.
package main
import (
"fmt"
"io"
"strings"
)
// upper wraps another Reader and upper-cases ASCII letters as they pass through.
type upper struct{ r io.Reader }
func (u upper) Read(p []byte) (int, error) {
n, err := u.r.Read(p)
for i := range n {
if 'a' <= p[i] && p[i] <= 'z' {
p[i] -= 'a' - 'A'
}
}
return n, err
}
func main() {
src := strings.NewReader("interfaces compose\n")
data, err := io.ReadAll(upper{src})
fmt.Print(string(data))
fmt.Println(err)
}
$ go run reader.go
INTERFACES COMPOSE
<nil>
io.ReadAll knows nothing about upper; it only calls Read until it gets io.EOF, which ReadAll treats as success and turns into a nil error. The same wrapper would work on a file, an HTTP body or a gzip stream. Interfaces compose the same way: io.ReadWriter is just Reader and Writer embedded in one interface.
The nil interface trap
An interface value holds a (type, value) pair, and it equals nil only when both are nil.
package main
import "fmt"
type MyErr struct{}
func (*MyErr) Error() string { return "my error" }
func validate(ok bool) error {
var e *MyErr // nil pointer
if !ok {
e = &MyErr{}
}
return e // BUG: wraps a typed nil
}
func main() {
err := validate(true)
fmt.Println(err == nil)
fmt.Printf("%T %v\n", err, err)
}
$ go run niltrap.go
false
*main.MyErr my error
Validation passed, yet err == nil is false. The function returned a nil *MyErr, and converting it to error produced an interface whose type is *MyErr — not empty, so not nil. Worse, %v called Error() on the nil pointer — which works, because the method never dereferences its receiver — and printed my error, so a log line would report a failure that never happened. The fix is to return a literal nil on success:
package main
import "fmt"
type MyErr struct{}
func (*MyErr) Error() string { return "my error" }
func validate(ok bool) error {
if !ok {
return &MyErr{}
}
return nil
}
func main() {
fmt.Println(validate(true) == nil, validate(false))
}
$ go run nilfix.go
true my error
Practice
1. Add a Triangle with sides A, B, C to shapes.go (use Heron’s formula for the area) and include Triangle{3, 4, 5} in the slice.
Solution
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Triangle struct{ A, B, C float64 }
func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }
func (t Triangle) Area() float64 {
s := t.Perimeter() / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
func main() {
var s Shape = Triangle{3, 4, 5}
fmt.Printf("%.2f %.2f\n", s.Area(), s.Perimeter())
}
$ go run p1.go
6.00 12.00
Once both methods exist, Triangle is a Shape; nothing else had to change.
2. Give Rect a String() method so that fmt.Println(Rect{3, 4}) prints Rect 3x4.
Solution
package main
import "fmt"
type Rect struct{ W, H float64 }
func (r Rect) String() string { return fmt.Sprintf("Rect %gx%g", r.W, r.H) }
func main() {
fmt.Println(Rect{3, 4}, []Rect{{1, 2}, {5, 6}})
}
$ go run p2.go
Rect 3x4 [Rect 1x2 Rect 5x6]
fmt uses the method for elements inside a slice too. %g drops the trailing zeros that %f would print.
3. Write sumNumbers(vals []any) float64 that adds every int and float64 in the slice and ignores everything else.
Solution
package main
import "fmt"
func sumNumbers(vals []any) float64 {
total := 0.0
for _, v := range vals {
switch x := v.(type) {
case int:
total += float64(x)
case float64:
total += x
}
}
return total
}
func main() {
fmt.Println(sumNumbers([]any{1, 2.5, "3", nil, 4}))
}
$ go run p3.go
7.5
Inside each single-type case x has that concrete type, so float64(x) and total += x compile. The string "3" is skipped, not parsed.
4. Write a countingWriter that implements io.Writer, counts bytes written, and use it with fmt.Fprintf.
Solution
package main
import (
"fmt"
"os"
)
type countingWriter struct{ n int }
func (c *countingWriter) Write(p []byte) (int, error) {
c.n += len(p)
return os.Stdout.Write(p)
}
func main() {
cw := &countingWriter{}
fmt.Fprintf(cw, "hello, %s\n", "interfaces")
fmt.Println(cw.n, "bytes")
}
$ go run p4.go
hello, interfaces
18 bytes
Write has a pointer receiver because it updates n, so &countingWriter{} is the value that satisfies io.Writer.