Back to Blog
Go2025-02-20

Go Interfaces: The Power of Implicit Implementation

Understand Go interfaces, implicit implementation, interface composition, and common patterns like io.Reader.

What Makes Go Interfaces Special?

In Go, interfaces are implemented implicitly. A type satisfies an interface simply by implementing all its methods — no implements keyword needed.

``go type Speaker interface { Speak() string }

type Dog struct{ Name string } type Cat struct{ Name string }

func (d Dog) Speak() string { return d.Name + " says Woof!" } func (c Cat) Speak() string { return c.Name + " says Meow!" }

// Both Dog and Cat satisfy Speaker without declaring it func greet(s Speaker) { fmt.Println(s.Speak()) }

func main() { greet(Dog{Name: "Rex"}) // "Rex says Woof!" greet(Cat{Name: "Luna"}) // "Luna says Meow!" } `

The Empty Interface

interface{} (or any in Go 1.18+) accepts any value:

`go func printAnything(v any) { fmt.Printf("Type: %T, Value: %v\n", v, v) }

printAnything(42) // Type: int, Value: 42 printAnything("hello") // Type: string, Value: hello printAnything(true) // Type: bool, Value: true `

Interface Composition

Build larger interfaces from smaller ones:

`go 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 }

// Any type implementing both Read and Write satisfies ReadWriter `

Type Assertions and Type Switches

`go // Type assertion func processValue(v any) { if s, ok := v.(string); ok { fmt.Println("String:", s) } }

// Type switch func describe(v any) string { switch val := v.(type) { case int: return fmt.Sprintf("Integer: %d", val) case string: return fmt.Sprintf("String: %s", val) case bool: return fmt.Sprintf("Boolean: %t", val) default: return fmt.Sprintf("Unknown: %v", val) } } `

Common Interface Patterns

Stringer (like toString)

`go type Stringer interface { String() string }

type Point struct{ X, Y int }

func (p Point) String() string { return fmt.Sprintf("(%d, %d)", p.X, p.Y) }

fmt.Println(Point{3, 4}) // "(3, 4)" `

Error Interface

`go type error interface { Error() string }

type NotFoundError struct { Resource string ID string }

func (e *NotFoundError) Error() string { return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID) } `

Sort Interface

`go type ByAge []User

func (a ByAge) Len() int { return len(a) } func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age } func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

sort.Sort(ByAge(users)) ``

Best Practices

1. Keep interfaces small — prefer single-method interfaces 2. Define interfaces where they're used, not where they're implemented 3. Accept interfaces, return structs — for maximum flexibility 4. Don't create interfaces prematurely — wait until you need polymorphism 5. Use standard library interfaces (io.Reader, io.Writer, fmt.Stringer) when possible