Go Interfaces Tutorial
Interfaces in Go provide a powerful way to define behavior without specifying implementation. They enable polymorphism and flexible design by focusing on what types can do rather than what they are. This tutorial covers everything from basic interface usage to advanced patterns.
1. Interface Basics
1.1 Defining Interfaces
An interface type is defined as a set of method signatures. Types implicitly implement interfaces by implementing all required methods.
// Basic interface definition
type Speaker interface {
Speak() string
}
// Concrete type implementing Speaker
type Dog struct{ Name string }
func (d Dog) Speak() string {
return fmt.Sprintf("%s says woof!", d.Name)
}
// Another implementation
type Human struct{ Name string }
func (h Human) Speak() string {
return fmt.Sprintf("%s says hello!", h.Name)
}
1.2 Interface Values
Interface values hold a concrete value and its dynamic type. They can be treated polymorphically.
func makeSpeak(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
dog := Dog{"Rex"}
human := Human{"Alice"}
makeSpeak(dog) // Rex says woof!
makeSpeak(human) // Alice says hello!
}
1.3 The Empty Interface
The empty interface interface{} can hold values of any type, similar to Object in other languages.
func printAnything(v interface{}) {
fmt.Printf("Value: %v, Type: %T\n", v, v)
}
func main() {
printAnything(42) // Value: 42, Type: int
printAnything("hello") // Value: hello, Type: string
printAnything(Dog{"Rex"}) // Value: {Rex}, Type: main.Dog
}
2. Working with Interfaces
2.1 Type Assertions
Type assertions provide access to an interface value's underlying concrete value.
var s Speaker = Human{"Alice"}
// Basic type assertion
h := s.(Human)
fmt.Println(h.Name) // Alice
// Safe type assertion with comma-ok
if h, ok := s.(Human); ok {
fmt.Println(h.Name)
} else {
fmt.Println("Not a Human")
}
// Type switch
switch v := s.(type) {
case Human:
fmt.Println("Human:", v.Name)
case Dog:
fmt.Println("Dog:", v.Name)
default:
fmt.Println("Unknown type")
}
2.2 Interface Composition
Interfaces can embed other interfaces to create new interfaces.
type Walker interface {
Walk()
}
type Runner interface {
Run()
}
type Athlete interface {
Walker
Runner
}
type Sprinter struct{}
func (s Sprinter) Walk() { fmt.Println("Walking") }
func (s Sprinter) Run() { fmt.Println("Running") }
func train(a Athlete) {
a.Walk()
a.Run()
}
2.3 Interface Satisfaction
A type satisfies an interface by implementing all its methods, either with value or pointer receivers.
type Mover interface {
Move()
}
// Value receiver implementation
type Car struct{}
func (c Car) Move() { fmt.Println("Car moving") }
// Pointer receiver implementation
type Bike struct{}
func (b *Bike) Move() { fmt.Println("Bike moving") }
func main() {
var m Mover
m = Car{} // Works - value receiver
m = &Car{} // Also works
// m = Bike{} // Doesn't work - needs pointer
m = &Bike{} // Works - pointer receiver
}
3. Common Interfaces
3.1 Stringer Interface
The Stringer interface from the fmt package defines string representation.
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}
func main() {
p := Person{"Alice", 30}
fmt.Println(p) // Alice (30 years)
}
3.2 Error Interface
The built-in error interface is used for error handling.
type DivError struct {
a, b int
}
func (d DivError) Error() string {
return fmt.Sprintf("cannot divide %d by %d", d.a, d.b)
}
func divide(a, b int) (int, error) {
if b == 0 {
return 0, DivError{a, b}
}
return a / b, nil
}
3.3 Reader and Writer
The io.Reader and io.Writer interfaces are fundamental for I/O operations.
type ByteStore struct {
data []byte
}
func (bs *ByteStore) Write(p []byte) (n int, err error) {
bs.data = append(bs.data, p...)
return len(p), nil
}
func (bs ByteStore) Read(p []byte) (n int, err error) {
copy(p, bs.data)
return len(bs.data), io.EOF
}
4. Advanced Interface Patterns
4.1 Interface Wrapping
Interfaces can be wrapped to add functionality while preserving the original interface.
type LoggingReader struct {
Reader io.Reader
Name string
}
func (lr LoggingReader) Read(p []byte) (n int, err error) {
fmt.Printf("Reading from %s\n", lr.Name)
return lr.Reader.Read(p)
}
func main() {
var r io.Reader = strings.NewReader("hello")
lr := LoggingReader{r, "string reader"}
data, _ := io.ReadAll(lr)
fmt.Println(string(data))
}
4.2 Dependency Injection
Interfaces enable clean dependency injection for testing and flexibility.
type Database interface {
GetUser(id int) (string, error)
}
type RealDB struct{}
func (db RealDB) GetUser(id int) (string, error) {
// Actual database implementation
return "real user", nil
}
type MockDB struct{}
func (db MockDB) GetUser(id int) (string, error) {
return "mock user", nil
}
func GetUserName(db Database, id int) string {
name, _ := db.GetUser(id)
return name
}
4.3 Interface Guards
Compile-time checks to ensure types implement interfaces.
var _ Database = (*RealDB)(nil) // Compile-time check
var _ Database = (*MockDB)(nil) // Will fail if not implemented
5. Performance Considerations
5.1 Interface Costs
Interface method calls have a small runtime overhead compared to direct calls.
// Direct call (fastest)
d := Dog{"Rex"}
d.Speak()
// Interface call (slightly slower)
var s Speaker = d
s.Speak()
5.2 Reducing Allocations
Interfaces can cause allocations when storing non-pointer values.
// May allocate (depending on size)
var s Speaker = Dog{"Rex"}
// No allocation (stores pointer)
var s Speaker = &Dog{"Rex"}
5.3 Interface Optimization
Strategies for optimizing interface-heavy code:
- Use pointer receivers for large types
- Consider concrete types in performance-critical paths
- Minimize type assertions/switches in hot loops
6. Common Pitfalls
6.1 Nil Interface Values
An interface value is nil only if both its value and type are nil.
var s Speaker // nil interface
fmt.Println(s == nil) // true
var d *Dog // nil pointer
s = d // interface holds (nil, *Dog)
fmt.Println(s == nil) // false
6.2 Accidental Shadowing
Be careful when redeclaring interface variables in short assignments.
var s Speaker = Dog{"Rex"}
// Wrong - creates new s variable
if s, ok := s.(Dog); ok {
fmt.Println(s.Name)
}
// Original s unchanged
// Correct approach
if d, ok := s.(Dog); ok {
fmt.Println(d.Name)
}
6.3 Overusing Empty Interfaces
interface{} should be used sparingly - prefer specific interfaces.
// Bad - loses type safety
func process(val interface{}) {
// Need type assertions for everything
}
// Better - constrained interface
func processStringer(val fmt.Stringer) {
// Can call String() safely
}
7. Real-world Examples
7.1 HTTP Middleware
Interfaces enable flexible HTTP middleware patterns.
type Middleware func(http.Handler) http.Handler
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func ApplyMiddleware(h http.Handler, middlewares ...Middleware) http.Handler {
for _, m := range middlewares {
h = m(h)
}
return h
}
7.2 Plugin Architecture
Interfaces support extensible plugin systems.
type Processor interface {
Process(data []byte) ([]byte, error)
Name() string
}
var processors = make(map[string]Processor)
func RegisterProcessor(p Processor) {
processors[p.Name()] = p
}
func ProcessWith(name string, data []byte) ([]byte, error) {
if p, ok := processors[name]; ok {
return p.Process(data)
}
return nil, fmt.Errorf("processor %s not found", name)
}
7.3 Strategy Pattern
Interfaces naturally implement the strategy pattern.
type SortStrategy interface {
Sort([]int) []int
}
type BubbleSort struct{}
func (bs BubbleSort) Sort(data []int) []int { /* ... */ }
type QuickSort struct{}
func (qs QuickSort) Sort(data []int) []int { /* ... */ }
func SortData(data []int, strategy SortStrategy) []int {
return strategy.Sort(data)
}