Interfaces
An interface in Go is a set of method signatures. A type satisfies an interface simply by implementing all those methods — there is no implements keyword. This implicit satisfaction makes Go interfaces lightweight and highly composable.
Defining and Implementing an Interface
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func printShape(s Shape) {
fmt.Printf("Area: %.2f Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
printShape(Circle{Radius: 5})
printShape(Rectangle{Width: 3, Height: 4})
}The io.Writer and io.Reader Pattern
The standard library's most important interfaces are io.Writer and io.Reader. Anything that implements Write([]byte) (int, error) is an io.Writer:
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func writeAll(w io.Writer, messages []string) error {
for _, m := range messages {
if _, err := fmt.Fprintln(w, m); err != nil {
return err
}
}
return nil
}
func main() {
var buf bytes.Buffer
writeAll(&buf, []string{"Hello", "from", "Go"})
fmt.Print(buf.String())
writeAll(strings.NewReader(""), []string{}) // also valid — Reader is Writer? No.
// But os.Stdout implements io.Writer, so:
writeAll(writerOnly{}, []string{"direct to stdout would work too"})
}
type writerOnly struct{}
func (writerOnly) Write(p []byte) (int, error) { return len(p), nil }Idiomatic Go: Accept interfaces, return concrete types. Design functions to accept the smallest interface that satisfies your needs.
The Empty Interface
Before Go 1.18 generics, interface{} (now also written any) was used to represent a value of any type:
package main
import "fmt"
func describe(v any) {
fmt.Printf("value=%v type=%T\n", v, v)
}
func main() {
describe(42)
describe("hello")
describe([]int{1, 2, 3})
describe(nil)
}Use any sparingly — it sacrifices type safety. Prefer generics (Go 1.18+) for new code.
Type Assertions
A type assertion extracts the concrete value from an interface:
package main
import "fmt"
func main() {
var i any = "hello"
// Single-value form — panics if wrong type
s := i.(string)
fmt.Println(s, len(s))
// Two-value form — safe
if n, ok := i.(int); ok {
fmt.Println("int:", n)
} else {
fmt.Println("not an int") // this branch runs
}
}Type Switches
A type switch branches on the dynamic type of an interface value:
package main
import "fmt"
func classify(v any) string {
switch x := v.(type) {
case int:
return fmt.Sprintf("int: %d", x)
case string:
return fmt.Sprintf("string: %q (len %d)", x, len(x))
case bool:
return fmt.Sprintf("bool: %t", x)
case []int:
return fmt.Sprintf("[]int with %d elements", len(x))
default:
return fmt.Sprintf("unknown type: %T", x)
}
}
func main() {
fmt.Println(classify(42))
fmt.Println(classify("gopher"))
fmt.Println(classify(true))
fmt.Println(classify([]int{1, 2, 3}))
}Interface Composition
Interfaces can embed other interfaces to build richer contracts:
package main
import (
"fmt"
"io"
)
type ReadWriter interface {
io.Reader
io.Writer
}
func copy(dst io.Writer, src io.Reader) (int64, error) {
return io.Copy(dst, src)
}
func main() {
// bytes.Buffer satisfies both io.Reader and io.Writer
var buf io.ReadWriter = &struct {
read func([]byte) (int, error)
write func([]byte) (int, error)
}{} // simplified; bytes.Buffer is the real-world example
fmt.Printf("%T\n", buf)
}Key Takeaways
- Interfaces are satisfied implicitly — any type with the right methods satisfies an interface.
- Accept the smallest interface you need; return concrete types.
- Use
any(alias forinterface{}) only when you genuinely need to store arbitrary values. - Type assertions extract the concrete type; use the two-value form to avoid panics.
- Type switches branch on the dynamic type of an interface value.