go-interfaces
Go Interfaces and Composition
Go's interfaces enable flexible, decoupled designs through implicit satisfaction and composition. This skill covers interface fundamentals, type inspection, and Go's approach to composition over inheritance.
Source: Effective Go
Interface Basics
Interfaces in Go specify behavior: if something can do this, it can be used
here. Types implement interfaces implicitly—no implements keyword needed.
// io.Writer interface - any type with this method satisfies it
type Writer interface {
Write(p []byte) (n int, err error)
}
A type satisfies an interface by implementing its methods:
type ByteSlice []byte
// ByteSlice now implements io.Writer
func (p *ByteSlice) Write(data []byte) (n int, err error) {
*p = append(*p, data...)
return len(data), nil
}
// Can be used anywhere io.Writer is expected
var w io.Writer = &ByteSlice{}
fmt.Fprintf(w, "Hello, %s", "World")
Multiple Interface Implementation
A type can implement multiple interfaces simultaneously:
type Sequence []int
// Implements sort.Interface
func (s Sequence) Len() int { return len(s) }
func (s Sequence) Less(i, j int) bool { return s[i] < s[j] }
func (s Sequence) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// Implements fmt.Stringer
func (s Sequence) String() string {
sort.Sort(s)
return fmt.Sprint([]int(s))
}
Interface Naming
By convention, one-method interfaces use the method name plus -er suffix:
Reader, Writer, Formatter, Stringer.
Type Assertions
A type assertion extracts the concrete value from an interface.
Basic Syntax
value.(typeName)
The result has the static type typeName. The type must be either:
- The concrete type held by the interface, or
- Another interface type the value can be converted to
var w io.Writer = os.Stdout
f := w.(*os.File) // Extract *os.File from io.Writer
Comma-Ok Idiom
Without checking, a failed assertion causes a runtime panic. Use the comma-ok idiom to test safely:
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
If the assertion fails, str is the zero value (empty string) and ok is
false.
Checking Interface Implementation
To check if a value implements an interface without using the result:
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v implements json.Marshaler\n", val)
}
Type Switch
A type switch discovers the dynamic type of an interface variable using the
.(type) syntax:
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t)
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
Idiomatic Reuse of Variable Name
It's idiomatic to reuse the name in the switch expression. This declares a new variable with the same name but the correct type in each case branch.
Mixing Concrete and Interface Types
Type switches can match both concrete types and interface types:
type Stringer interface {
String() string
}
var value interface{}
switch str := value.(type) {
case string:
return str // str is string
case Stringer:
return str.String() // str is Stringer
}
Generality
If a type exists only to implement an interface with no exported methods beyond that interface, don't export the type—return the interface from constructors.
Hide Implementation, Expose Interface
// Good: Constructor returns interface type
func NewHash() hash.Hash32 {
return &myHash{} // unexported type
}
// The implementation is hidden; callers only see hash.Hash32
Real-World Example: crypto/cipher
The crypto/cipher package demonstrates this pattern:
type Block interface {
BlockSize() int
Encrypt(dst, src []byte)
Decrypt(dst, src []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
// Returns Stream interface, hiding implementation details
func NewCTR(block Block, iv []byte) Stream
Benefits:
- Implementation can change without affecting callers
- Substituting algorithms requires only changing the constructor call
- Documentation lives on the interface, not repeated on each implementation
Interface Embedding
Combine interfaces by embedding them:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter combines Reader and Writer
type ReadWriter interface {
Reader
Writer
}
A ReadWriter can do what a Reader does and what a Writer does—it's a
union of the embedded interfaces.
Rule: Only interfaces can be embedded within interfaces.
Struct Embedding
Go uses embedding for composition instead of inheritance. Embedding promotes methods from the inner type to the outer type.
Basic Struct Embedding
// bufio.ReadWriter embeds Reader and Writer
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
Without embedding, you'd need forwarding methods:
// Without embedding - tedious boilerplate
type ReadWriter struct {
reader *Reader
writer *Writer
}
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
With embedding, methods are promoted automatically. bufio.ReadWriter satisfies
io.Reader, io.Writer, and io.ReadWriter without explicit forwarding.
Embedding for Convenience
Mix embedded and named fields:
type Job struct {
Command string
*log.Logger
}
// Job now has Print, Printf, Println methods
job.Println("starting now...")
Accessing Embedded Fields
The type name (without package qualifier) serves as the field name:
// Access the embedded Logger directly
job.Logger.SetPrefix("Job: ")
Method Overriding
Define a method on the outer type to override the embedded method:
func (job *Job) Printf(format string, args ...interface{}) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
Embedding vs Subclassing
Key difference: When an embedded method is invoked, the receiver is the inner type, not the outer one. The embedded type doesn't know it's embedded.
type ReadWriter struct {
*Reader
*Writer
}
// When rw.Read() is called, the receiver is the Reader, not ReadWriter
Name Conflict Resolution
- Outer fields/methods hide inner ones: A field
Xon the outer type hides anyXin embedded types - Same level conflicts are errors: Embedding two types with the same method name at the same level causes an error (unless never accessed)
Interface Satisfaction Checks
Most interface conversions are checked at compile time. But sometimes you need to verify implementation explicitly.
Compile-Time Interface Check
Use a blank identifier assignment to verify a type implements an interface:
// Verify *RawMessage implements json.Marshaler at compile time
var _ json.Marshaler = (*RawMessage)(nil)
This causes a compile error if *RawMessage doesn't implement json.Marshaler.
When to Use This Pattern
Use compile-time checks when:
- There are no static conversions that would verify the interface automatically
- The type must satisfy an interface for correct behavior (e.g., custom JSON marshaling)
- Interface changes should break compilation, not silently degrade
// In your package
type MyType struct { /* ... */ }
func (m *MyType) MarshalJSON() ([]byte, error) { /* ... */ }
// Compile-time check - fails if MarshalJSON signature is wrong
var _ json.Marshaler = (*MyType)(nil)
Don't add these checks for every interface implementation—only when there's no other static conversion that would catch the error.
Receiver Type
Advisory: Go Wiki CodeReviewComments
Choosing whether to use a value or pointer receiver on methods can be difficult. If in doubt, use a pointer, but there are times when a value receiver makes sense.
When to Use Pointer Receiver
- Method mutates receiver: The receiver must be a pointer
- Receiver contains sync.Mutex or similar: Must be a pointer to avoid copying
- Large struct or array: A pointer receiver is more efficient. If passing all elements as arguments feels too large, it's too large for a value receiver
- Concurrent or called methods might mutate: If changes must be visible in the original receiver, it must be a pointer
- Elements are pointers to something mutating: Prefer pointer receiver to make the intention clearer
When to Use Value Receiver
- Small unchanging structs or basic types: Value receiver for efficiency
- Map, func, or chan: Don't use a pointer to them
- Slice without reslicing/reallocating: Don't use a pointer if the method doesn't reslice or reallocate the slice
- Small value types with no mutable fields: Types like
time.Timewith no mutable fields and no pointers work well as value receivers - Simple basic types:
int,string, etc.
// Value receiver: small, immutable type
type Point struct {
X, Y float64
}
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// Pointer receiver: method mutates receiver
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
// Pointer receiver: contains sync.Mutex
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
Consistency Rule
Don't mix receiver types. Choose either pointers or struct types for all available methods on a type. If any method needs a pointer receiver, use pointer receivers for all methods.
// Good: Consistent pointer receivers
type Buffer struct {
data []byte
}
func (b *Buffer) Write(p []byte) (int, error) { /* ... */ }
func (b *Buffer) Read(p []byte) (int, error) { /* ... */ }
func (b *Buffer) Len() int { return len(b.data) }
// Bad: Mixed receiver types
func (b Buffer) Len() int { return len(b.data) } // inconsistent
Quick Reference
| Concept | Pattern | Notes |
|---|---|---|
| Implicit implementation | Just implement the methods | No implements keyword |
| Type assertion | v := x.(Type) |
Panics if wrong type |
| Safe type assertion | v, ok := x.(Type) |
Returns zero value + false |
| Type switch | switch v := x.(type) |
Variable has correct type per case |
| Interface embedding | type RW interface { Reader; Writer } |
Union of methods |
| Struct embedding | type S struct { *T } |
Promotes T's methods |
| Access embedded field | s.T or s.T.Method() |
Type name is field name |
| Interface check | var _ I = (*T)(nil) |
Compile-time verification |
| Generality | Return interface from constructor | Hide implementation |
See Also
- go-style-core: Core Go style principles and formatting
- go-naming: Interface naming conventions (Reader, Writer, etc.)
- go-error-handling: Error interface and custom error types
- go-functional-options: Using interfaces for flexible APIs
- go-defensive: Defensive programming patterns