Interface Composition & any
Interface Composition & any
Go interfaces are satisfied implicitly — a type implements an interface simply by having the required methods. This makes interfaces extraordinarily flexible and promotes loose coupling.
Embedding Interfaces Just as structs can embed other structs, interfaces can embed other interfaces. The embedding composes the method sets:
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type ReadWriter interface {
Reader
Writer
}A type that implements both Read and Write automatically satisfies ReadWriter. The standard library builds rich hierarchies this way (e.g., io.ReadWriteCloser).
The Empty Interface — any
any (an alias for interface{} since Go 1.18) is satisfied by every type because it has no method requirements:
func printAnything(v any) {
fmt.Println(v)
}Use any sparingly; prefer concrete types or specific interfaces for type safety.
Type Assertions Extract the concrete value from an interface with a type assertion:
var i any = "hello"
s := i.(string) // panics if i is not a string
s, ok := i.(string) // safe; ok is false if type mismatchAlways use the comma-ok form unless you are certain of the underlying type.
Type Switches A type switch handles multiple possible concrete types cleanly:
switch v := i.(type) {
case string: fmt.Println("string:", v)
case int: fmt.Println("int:", v)
default: fmt.Println("unknown type")
}fmt.Stringer
Any type implementing the Stringer interface controls its own string representation:
type Stringer interface { String() string }fmt.Println calls String() automatically when available.
The error Interface
error is just a built-in interface:
type error interface { Error() string }Any type with an Error() string method satisfies error.
The Nil Interface Pitfall An interface value has two components: a concrete type and a concrete value. An interface is nil only when both are nil. Storing a nil pointer of a concrete type inside an interface creates a non-nil interface — a common trap:
var p *MyError = nil // typed nil pointer
var err error = p // err is NOT nil (type is set)
fmt.Println(err == nil) // false!Return the interface type directly (return nil) rather than returning a typed nil pointer.
Code Examples
package main
import (
"fmt"
"strings"
)
// Small interface building blocks
type Reader interface {
Read() string
}
type Writer interface {
Write(s string)
}
// Composed interface via embedding
type ReadWriter interface {
Reader
Writer
}
// Buffer satisfies ReadWriter
type Buffer struct {
data strings.Builder
}
func (b *Buffer) Write(s string) { b.data.WriteString(s) }
func (b *Buffer) Read() string { return b.data.String() }
// Stringer implementation
func (b *Buffer) String() string {
return fmt.Sprintf("Buffer(%q)", b.Read())
}
func copyContent(rw ReadWriter, src Reader) {
rw.Write(src.Read())
}
func main() {
src := &Buffer{}
src.Write("Hello, interfaces!")
dst := &Buffer{}
copyContent(dst, src)
fmt.Println(dst) // calls String() automatically
fmt.Printf("src still has data: %q\n", src.Read())
}Buffer satisfies the composed ReadWriter because it has both Read and Write methods. fmt.Println calls String() via the Stringer interface automatically.
package main
import "fmt"
func describe(i any) string {
switch v := i.(type) {
case nil:
return "nil"
case int:
return fmt.Sprintf("int: %d", v)
case float64:
return fmt.Sprintf("float64: %.2f", v)
case string:
return fmt.Sprintf("string: %q (len=%d)", v, len(v))
case bool:
return fmt.Sprintf("bool: %t", v)
case []int:
return fmt.Sprintf("[]int with %d elements", len(v))
default:
return fmt.Sprintf("unknown type: %T", v)
}
}
func main() {
values := []any{42, 3.14, "hello", true, nil, []int{1, 2, 3}, struct{}{}}
for _, v := range values {
fmt.Println(describe(v))
}
// Safe type assertion with comma-ok
var i any = "world"
if s, ok := i.(string); ok {
fmt.Println("\nExtracted string:", s)
}
// Unsafe assertion would panic — demonstrate safe form
if n, ok := i.(int); ok {
fmt.Println("int:", n)
} else {
fmt.Println("Not an int, ok =", ok)
}
}The type switch variable v is already typed in each case branch — no cast needed. The comma-ok type assertion avoids panics when the dynamic type might not match.
package main
import "fmt"
type AppError struct{ Msg string }
func (e *AppError) Error() string { return e.Msg }
// BAD: returns a typed nil — caller's err != nil check passes
func riskyOp(fail bool) error {
var e *AppError // typed nil pointer
if fail {
e = &AppError{"something failed"}
}
return e // always wraps *AppError in error interface
}
// GOOD: return untyped nil when there is no error
func safeOp(fail bool) error {
if fail {
return &AppError{"something failed"}
}
return nil // untyped nil — interface value is truly nil
}
func main() {
err := riskyOp(false)
fmt.Println("riskyOp(false) err == nil:", err == nil) // false!
fmt.Printf("riskyOp(false) err value: %v\n", err)
err2 := safeOp(false)
fmt.Println("safeOp(false) err == nil: ", err2 == nil) // true
err3 := safeOp(true)
fmt.Println("safeOp(true) err == nil: ", err3 == nil) // false
fmt.Println("safeOp(true) error: ", err3)
}When a typed nil *AppError is stored in the error interface, the interface itself is non-nil because the type field is set. Always return a bare nil from error-returning functions to signal success.
Quick Quiz
1. How does a type satisfy an interface in Go?
2. When is an interface value considered nil in Go?
3. What does the comma-ok idiom `s, ok := i.(string)` protect against?
4. What is `any` in Go 1.18+?
Was this lesson helpful?