Go Interfaces
Implicit Satisfaction
type Writer interface {
Write(p []byte) (n int, err error)
}
// Any type with this method satisfies Writer
type LogSink struct {
entries []string
}
func (l *LogSink) Write(p []byte) (int, error) {
l.entries = append(l.entries, string(p))
return len(p), nil
}
// LogSink is now a Writer — no registration needed
var w Writer = &LogSink{}
This is structural typing: if a type has all the methods an interface requires, it satisfies the interface. The implementing type does not need to import the package that defines the interface.
Key Standard Library Interfaces
// io.Reader — reads bytes from a source
type Reader interface {
Read(p []byte) (n int, err error)
}
// io.Writer — writes bytes to a destination
type Writer interface {
Write(p []byte) (n int, err error)
}
// fmt.Stringer — custom string representation
type Stringer interface {
String() string
}
// error — yes, it is an interface
type error interface {
Error() string
}
Go’s power comes from composing small interfaces. io.Reader connects files, network sockets, compression, encryption, and HTTP bodies — any type with Read works with all of them.
Interface Composition
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
io.Closer
}
Compose larger interfaces from smaller ones. Prefer accepting the smallest interface your function actually needs.
Type Assertions & Switches
var r io.Reader = os.Stdin
// Safe assertion — check ok
f, ok := r.(*os.File)
if !ok {
log.Fatal("not a file")
}
// Type switch — branch on concrete type
switch v := r.(type) {
case *os.File:
fmt.Println("file:", v.Name())
case *bytes.Buffer:
fmt.Printf("buffer: %d bytes\n", v.Len())
default:
fmt.Println("unknown reader")
}
The two-value form v, ok := r.(Type) never panics — use this in production code. Type switches are the clean way to handle multiple possible concrete types.
The Empty Interface
// Go 1.18+ — any is an alias for interface{}
func printAnything(v any) {
fmt.Printf("%T: %v\n", v, v)
}
any accepts every type but you lose compile-time type safety. Prefer generics (Go 1.18+) or concrete interfaces when possible.
Design Principle
// Good — accepts the narrowest interface needed
func CountLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
// Callers pass anything that implements io.Reader
n, _ := CountLines(os.Stdin)
n, _ := CountLines(resp.Body)
n, _ := CountLines(bytes.NewReader(data))
Accept the smallest interface your function needs, return a concrete type. The caller decides how to supply the dependency.