Go Interfaces: The Complete Deep Dive

Why Go Interfaces Are Different (And Better)

If you come from Java or C#, Go interfaces will feel strange at first. In those languages, you explicitly declare that a class implements an interface:
java
// Java - Explicit implementation public class Dog implements Animal { // Must declare "implements" }
In Go, implementation is implicit. If your type has the right methods, it implements the interface. No declaration needed:
go
// Go - Implicit implementation type Dog struct{} func (d Dog) Speak() string { return "Woof!" } // Dog now implements any interface requiring Speak() string // No "implements" keyword needed!
This seemingly small difference has profound implications for how Go code is designed.
Design principles diagram 1

Design principles diagram 1

The USB Port Analogy (Revisited for Go)

Think of a Go interface as a USB port, but with a twist:
Traditional interfaces are like proprietary connectors. The cable manufacturer must explicitly license and implement the connector spec. They declare "this cable is USB-C compatible."
Go interfaces are like a universal scanner. Any device that happens to have the right electrical contacts will work. The device doesn't need to know about the scanner - it just needs to have the right shape.
This means:
  1. Third-party types can satisfy your interfaces without modification
  2. You can create interfaces for types you don't own
  3. Interfaces can be defined where they're used, not where types are defined

Interface Basics: The Foundation

Defining Interfaces

An interface is a collection of method signatures:
go
// Filename: interface_basics.go package main import ( "fmt" "math" ) // ============================================================================= // INTERFACE DEFINITION // ============================================================================= // Shape defines what all shapes must do // Why: An interface is just method signatures // Why: No fields, no implementation, just the contract type Shape interface { Area() float64 Perimeter() float64 } // Namer is a simple single-method interface type Namer interface { Name() string } // ShapeWithName composes two interfaces type ShapeWithName interface { Shape Namer } // ============================================================================= // IMPLEMENTATIONS // ============================================================================= // Rectangle implements Shape type Rectangle struct { Width float64 Height float64 } // Area implements Shape.Area func (r Rectangle) Area() float64 { return r.Width * r.Height } // Perimeter implements Shape.Perimeter func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) } // Name implements Namer func (r Rectangle) Name() string { return "Rectangle" } // Circle implements Shape type Circle struct { Radius float64 } func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius } func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius } func (c Circle) Name() string { return "Circle" } // Triangle implements Shape type Triangle struct { Base float64 Height float64 SideA float64 SideB float64 SideC float64 } func (t Triangle) Area() float64 { return 0.5 * t.Base * t.Height } func (t Triangle) Perimeter() float64 { return t.SideA + t.SideB + t.SideC } func (t Triangle) Name() string { return "Triangle" } // ============================================================================= // FUNCTIONS THAT USE INTERFACES // ============================================================================= // PrintShapeInfo works with ANY Shape // Why: The function doesn't care about the concrete type func PrintShapeInfo(s Shape) { fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter()) } // PrintWithName works with anything that has both Shape and Namer methods func PrintWithName(s ShapeWithName) { fmt.Printf("%s - Area: %.2f, Perimeter: %.2f\n", s.Name(), s.Area(), s.Perimeter()) } // TotalArea calculates total area of any shapes func TotalArea(shapes []Shape) float64 { var total float64 for _, shape := range shapes { total += shape.Area() } return total } func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ GO INTERFACES: BASICS ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") // Create shapes rect := Rectangle{Width: 10, Height: 5} circle := Circle{Radius: 7} triangle := Triangle{ Base: 6, Height: 4, SideA: 5, SideB: 5, SideC: 6, } // All shapes work with the same function fmt.Println("\n📐 Individual shapes:") fmt.Print("Rectangle: ") PrintShapeInfo(rect) fmt.Print("Circle: ") PrintShapeInfo(circle) fmt.Print("Triangle: ") PrintShapeInfo(triangle) // Using composed interface fmt.Println("\n📐 With names (composed interface):") PrintWithName(rect) PrintWithName(circle) PrintWithName(triangle) // Slice of interfaces fmt.Println("\n📐 Total area calculation:") shapes := []Shape{rect, circle, triangle} fmt.Printf("Total area of all shapes: %.2f\n", TotalArea(shapes)) }
Expected Output:
╔══════════════════════════════════════════════════════════╗ ║ GO INTERFACES: BASICS ║ ╚══════════════════════════════════════════════════════════╝ 📐 Individual shapes: Rectangle: Area: 50.00, Perimeter: 30.00 Circle: Area: 153.94, Perimeter: 43.98 Triangle: Area: 12.00, Perimeter: 16.00 📐 With names (composed interface): Rectangle - Area: 50.00, Perimeter: 30.00 Circle - Area: 153.94, Perimeter: 43.98 Triangle - Area: 12.00, Perimeter: 16.00 📐 Total area calculation: Total area of all shapes: 215.94

The Empty Interface: any

The empty interface interface{} (aliased as any in Go 1.18+) has zero methods. Since every type has at least zero methods, every type implements the empty interface.
go
// Filename: empty_interface.go package main import "fmt" // Container can hold anything type Container struct { items []any // any == interface{} } func (c *Container) Add(item any) { c.items = append(c.items, item) } func (c *Container) Get(index int) any { if index < 0 || index >= len(c.items) { return nil } return c.items[index] } func (c *Container) Print() { for i, item := range c.items { fmt.Printf("[%d] Type: %T, Value: %v\n", i, item, item) } } func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ THE EMPTY INTERFACE (any) ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") container := &Container{} // Can add any type! container.Add(42) container.Add("hello") container.Add(3.14) container.Add([]int{1, 2, 3}) container.Add(struct{ Name string }{"Alice"}) fmt.Println("\nContainer contents:") container.Print() // But you lose type safety! fmt.Println("\n⚠️ Warning: You lose type information") item := container.Get(0) // item is 'any' - you need type assertion to use it // num := item + 1 // ERROR: cannot add if num, ok := item.(int); ok { fmt.Printf("Got int: %d, doubled: %d\n", num, num*2) } }
When to use any:
  • JSON parsing with unknown structure
  • Generic containers (before Go 1.18 generics)
  • Plugin systems
  • Reflection-based code
When NOT to use any:
  • When you know the types (use proper types!)
  • When type safety matters (use specific interfaces)
  • When performance is critical (type assertions have overhead)

Type Assertions and Type Switches

When you have an interface value, sometimes you need to get the concrete type back.
go
// Filename: type_assertions.go package main import "fmt" // Animal interface type Animal interface { Speak() string } // Dog implementation type Dog struct { Name string } func (d Dog) Speak() string { return "Woof!" } func (d Dog) Fetch() string { return fmt.Sprintf("%s fetches the ball!", d.Name) } // Cat implementation type Cat struct { Name string } func (c Cat) Speak() string { return "Meow!" } func (c Cat) Scratch() string { return fmt.Sprintf("%s scratches the furniture!", c.Name) } // Bird implementation type Bird struct { Name string } func (b Bird) Speak() string { return "Tweet!" } func (b Bird) Fly() string { return fmt.Sprintf("%s flies away!", b.Name) } // ============================================================================= // TYPE ASSERTIONS // ============================================================================= // ProcessAnimal demonstrates type assertions func ProcessAnimal(a Animal) { fmt.Printf("Animal says: %s\n", a.Speak()) // Type assertion with comma-ok idiom // Why: Safe way to check if animal is a specific type if dog, ok := a.(Dog); ok { fmt.Printf(" It's a dog named %s!\n", dog.Name) fmt.Printf(" %s\n", dog.Fetch()) return } if cat, ok := a.(Cat); ok { fmt.Printf(" It's a cat named %s!\n", cat.Name) fmt.Printf(" %s\n", cat.Scratch()) return } fmt.Println(" It's some other animal") } // ============================================================================= // TYPE SWITCH // ============================================================================= // DescribeAnimal uses type switch for cleaner multiple type handling func DescribeAnimal(a Animal) string { // Type switch: like a switch statement, but on types switch v := a.(type) { case Dog: return fmt.Sprintf("Dog named %s who can fetch", v.Name) case Cat: return fmt.Sprintf("Cat named %s who likes to scratch", v.Name) case Bird: return fmt.Sprintf("Bird named %s who can fly", v.Name) default: return fmt.Sprintf("Unknown animal: %T", v) } } // ProcessAny handles any type func ProcessAny(v any) string { switch x := v.(type) { case string: return fmt.Sprintf("String of length %d: %q", len(x), x) case int: return fmt.Sprintf("Integer: %d (doubled: %d)", x, x*2) case float64: return fmt.Sprintf("Float: %.2f", x) case bool: return fmt.Sprintf("Boolean: %t", x) case []int: return fmt.Sprintf("Int slice with %d elements: %v", len(x), x) case nil: return "Nil value" default: return fmt.Sprintf("Unknown type: %T", x) } } func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ TYPE ASSERTIONS AND TYPE SWITCHES ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") animals := []Animal{ Dog{Name: "Buddy"}, Cat{Name: "Whiskers"}, Bird{Name: "Tweety"}, } fmt.Println("\n📋 Type Assertions (comma-ok idiom):") fmt.Println("─"*50) for _, animal := range animals { ProcessAnimal(animal) fmt.Println() } fmt.Println("📋 Type Switch:") fmt.Println("─"*50) for _, animal := range animals { fmt.Println(DescribeAnimal(animal)) } fmt.Println("\n📋 Processing Any Type:") fmt.Println("─"*50) values := []any{ "hello", 42, 3.14, true, []int{1, 2, 3}, nil, struct{ X int }{42}, } for _, v := range values { fmt.Println(ProcessAny(v)) } }

Interface Composition: Building Bigger Interfaces

Go encourages small, focused interfaces. Combine them when needed.
go
// Filename: interface_composition.go package main import ( "fmt" "io" "strings" ) // ============================================================================= // SMALL, FOCUSED INTERFACES (Like Go's standard library) // ============================================================================= // Reader reads data type Reader interface { Read(p []byte) (n int, err error) } // Writer writes data type Writer interface { Write(p []byte) (n int, err error) } // Closer closes a resource type Closer interface { Close() error } // Seeker seeks to a position type Seeker interface { Seek(offset int64, whence int) (int64, error) } // ============================================================================= // COMPOSED INTERFACES // ============================================================================= // ReadWriter combines Reader and Writer type ReadWriter interface { Reader Writer } // ReadWriteCloser combines Reader, Writer, and Closer type ReadWriteCloser interface { Reader Writer Closer } // ReadWriteSeeker combines Reader, Writer, and Seeker type ReadWriteSeeker interface { Reader Writer Seeker } // ============================================================================= // IMPLEMENTATION // ============================================================================= // Buffer implements multiple interfaces type Buffer struct { data []byte pos int closed bool } func NewBuffer() *Buffer { return &Buffer{data: make([]byte, 0)} } // Read implements Reader func (b *Buffer) Read(p []byte) (int, error) { if b.closed { return 0, fmt.Errorf("buffer is closed") } if b.pos >= len(b.data) { return 0, io.EOF } n := copy(p, b.data[b.pos:]) b.pos += n return n, nil } // Write implements Writer func (b *Buffer) Write(p []byte) (int, error) { if b.closed { return 0, fmt.Errorf("buffer is closed") } b.data = append(b.data, p...) return len(p), nil } // Close implements Closer func (b *Buffer) Close() error { b.closed = true return nil } // Seek implements Seeker func (b *Buffer) Seek(offset int64, whence int) (int64, error) { var newPos int64 switch whence { case io.SeekStart: newPos = offset case io.SeekCurrent: newPos = int64(b.pos) + offset case io.SeekEnd: newPos = int64(len(b.data)) + offset } if newPos < 0 { return 0, fmt.Errorf("negative position") } b.pos = int(newPos) return newPos, nil } // String returns buffer contents as string func (b *Buffer) String() string { return string(b.data) } // ============================================================================= // FUNCTIONS USING COMPOSED INTERFACES // ============================================================================= // Copy reads from Reader and writes to Writer // Why: Works with any Reader and Writer, not specific types func Copy(dst Writer, src Reader) (int64, error) { buf := make([]byte, 32*1024) // 32KB buffer var total int64 for { n, readErr := src.Read(buf) if n > 0 { written, writeErr := dst.Write(buf[:n]) total += int64(written) if writeErr != nil { return total, writeErr } } if readErr == io.EOF { return total, nil } if readErr != nil { return total, readErr } } } // ProcessAndClose reads all data and closes the source func ProcessAndClose(rc io.ReadCloser) (string, error) { defer rc.Close() var builder strings.Builder buf := make([]byte, 1024) for { n, err := rc.Read(buf) if n > 0 { builder.Write(buf[:n]) } if err == io.EOF { break } if err != nil { return "", err } } return builder.String(), nil } func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ INTERFACE COMPOSITION ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") // Create buffer buf := NewBuffer() // Use as Writer fmt.Println("\n✍️ Writing to buffer (as Writer):") buf.Write([]byte("Hello, ")) buf.Write([]byte("Interface ")) buf.Write([]byte("Composition!")) fmt.Printf(" Buffer contents: %q\n", buf.String()) // Use as Seeker fmt.Println("\n🔍 Seeking to start (as Seeker):") buf.Seek(0, io.SeekStart) // Use as Reader fmt.Println("\n📖 Reading from buffer (as Reader):") readBuf := make([]byte, 7) buf.Read(readBuf) fmt.Printf(" Read: %q\n", string(readBuf)) // Copy between buffers fmt.Println("\n📋 Copying between buffers (Reader -> Writer):") src := NewBuffer() src.Write([]byte("This is the source data!")) src.Seek(0, io.SeekStart) dst := NewBuffer() copied, _ := Copy(dst, src) fmt.Printf(" Copied %d bytes\n", copied) fmt.Printf(" Destination: %q\n", dst.String()) // Demonstrate interface satisfaction fmt.Println("\n📊 Interface Satisfaction:") fmt.Println("─"*50) var _ Reader = buf // ✓ Buffer satisfies Reader var _ Writer = buf // ✓ Buffer satisfies Writer var _ Closer = buf // ✓ Buffer satisfies Closer var _ Seeker = buf // ✓ Buffer satisfies Seeker var _ ReadWriter = buf // ✓ Buffer satisfies ReadWriter var _ ReadWriteCloser = buf // ✓ Buffer satisfies ReadWriteCloser var _ ReadWriteSeeker = buf // ✓ Buffer satisfies ReadWriteSeeker fmt.Println(" Buffer satisfies all composed interfaces!") fmt.Println("\n" + "═"*60) fmt.Println("COMPOSITION BENEFITS:") fmt.Println(" ✓ Small interfaces are easy to implement") fmt.Println(" ✓ Functions request only what they need") fmt.Println(" ✓ New compositions without new code") fmt.Println(" ✓ Maximum flexibility, minimum coupling") fmt.Println("═"*60) }

Interface Best Practices

1. Keep Interfaces Small

go
// BAD: Large interface type UserManager interface { Create(user User) error Read(id int) (User, error) Update(user User) error Delete(id int) error List() ([]User, error) Search(query string) ([]User, error) Validate(user User) error Hash(password string) string SendEmail(user User, template string) error GenerateReport(users []User) Report } // GOOD: Small, focused interfaces type UserCreator interface { Create(user User) error } type UserReader interface { Read(id int) (User, error) } type UserUpdater interface { Update(user User) error } type UserDeleter interface { Delete(id int) error } // Compose when needed type UserReadWriter interface { UserReader UserUpdater }

2. Accept Interfaces, Return Structs

go
// BAD: Returning interface func NewLogger() Logger { return &FileLogger{} // Returns interface } // GOOD: Return concrete type func NewFileLogger(path string) *FileLogger { return &FileLogger{path: path} } // BAD: Accepting concrete type func ProcessUser(repo *PostgresUserRepo, id int) error { // Tightly coupled to Postgres } // GOOD: Accept interface func ProcessUser(repo UserReader, id int) error { // Works with any UserReader implementation }

3. Define Interfaces Where They're Used

go
// BAD: Defining interface in the implementation package // file: database/user_repo.go package database type UserRepository interface { Find(id int) (User, error) } type PostgresUserRepo struct{} func (p *PostgresUserRepo) Find(id int) (User, error) { ... } // GOOD: Define interface in the consumer package // file: handlers/user_handler.go package handlers // Interface defined where it's USED, not where it's implemented type UserFinder interface { Find(id int) (User, error) } type UserHandler struct { finder UserFinder // Can use any implementation }

4. Don't Export Interfaces for Types You Own

go
// BAD: Exporting interface when you only have one implementation type Validator interface { Validate(data interface{}) error } type DefaultValidator struct{} // GOOD: Just export the concrete type // Add interface later when you actually need polymorphism type Validator struct{} func (v *Validator) Validate(data interface{}) error { ... }

Real World Pattern: Dependency Injection with Interfaces

go
// Filename: dependency_injection.go package main import ( "fmt" "time" ) // ============================================================================= // INTERFACES: Define contracts // ============================================================================= type UserRepository interface { FindByID(id int) (*User, error) Save(user *User) error } type EmailService interface { Send(to, subject, body string) error } type Logger interface { Info(msg string) Error(msg string) } type Clock interface { Now() time.Time } // ============================================================================= // DOMAIN // ============================================================================= type User struct { ID int Email string Name string LastLogin time.Time } // ============================================================================= // SERVICE: Depends on interfaces, not implementations // ============================================================================= type UserService struct { repo UserRepository email EmailService logger Logger clock Clock } func NewUserService( repo UserRepository, email EmailService, logger Logger, clock Clock, ) *UserService { return &UserService{ repo: repo, email: email, logger: logger, clock: clock, } } func (s *UserService) Login(userID int) error { s.logger.Info(fmt.Sprintf("User %d attempting login", userID)) user, err := s.repo.FindByID(userID) if err != nil { s.logger.Error(fmt.Sprintf("User %d not found: %v", userID, err)) return err } user.LastLogin = s.clock.Now() if err := s.repo.Save(user); err != nil { s.logger.Error(fmt.Sprintf("Failed to update user: %v", err)) return err } s.logger.Info(fmt.Sprintf("User %s logged in successfully", user.Name)) return nil } func (s *UserService) SendWelcome(userID int) error { user, err := s.repo.FindByID(userID) if err != nil { return err } return s.email.Send( user.Email, "Welcome!", fmt.Sprintf("Hello %s, welcome to our platform!", user.Name), ) } // ============================================================================= // PRODUCTION IMPLEMENTATIONS // ============================================================================= type PostgresUserRepo struct{} func (p *PostgresUserRepo) FindByID(id int) (*User, error) { fmt.Printf("[POSTGRES] Finding user %d\n", id) return &User{ID: id, Name: "Alice", Email: "alice@example.com"}, nil } func (p *PostgresUserRepo) Save(user *User) error { fmt.Printf("[POSTGRES] Saving user %d\n", user.ID) return nil } type SMTPEmailService struct{} func (s *SMTPEmailService) Send(to, subject, body string) error { fmt.Printf("[SMTP] Sending to %s: %s\n", to, subject) return nil } type ConsoleLogger struct{} func (c *ConsoleLogger) Info(msg string) { fmt.Printf("[INFO] %s\n", msg) } func (c *ConsoleLogger) Error(msg string) { fmt.Printf("[ERROR] %s\n", msg) } type RealClock struct{} func (r *RealClock) Now() time.Time { return time.Now() } // ============================================================================= // TEST IMPLEMENTATIONS (Mocks) // ============================================================================= type MockUserRepo struct { users map[int]*User SaveCalls int } func NewMockUserRepo() *MockUserRepo { return &MockUserRepo{ users: map[int]*User{ 1: {ID: 1, Name: "Test User", Email: "test@test.com"}, }, } } func (m *MockUserRepo) FindByID(id int) (*User, error) { if user, ok := m.users[id]; ok { return user, nil } return nil, fmt.Errorf("user not found") } func (m *MockUserRepo) Save(user *User) error { m.SaveCalls++ m.users[user.ID] = user return nil } type MockEmailService struct { SentEmails []struct { To, Subject, Body string } } func (m *MockEmailService) Send(to, subject, body string) error { m.SentEmails = append(m.SentEmails, struct { To, Subject, Body string }{to, subject, body}) return nil } type MockLogger struct { Messages []string } func (m *MockLogger) Info(msg string) { m.Messages = append(m.Messages, "[INFO] "+msg) } func (m *MockLogger) Error(msg string) { m.Messages = append(m.Messages, "[ERROR] "+msg) } type MockClock struct { Time time.Time } func (m *MockClock) Now() time.Time { return m.Time } // ============================================================================= // DEMONSTRATION // ============================================================================= func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ DEPENDENCY INJECTION WITH INTERFACES ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") // Production setup fmt.Println("\n🏭 PRODUCTION MODE:") fmt.Println("─"*50) prodService := NewUserService( &PostgresUserRepo{}, &SMTPEmailService{}, &ConsoleLogger{}, &RealClock{}, ) prodService.Login(1) prodService.SendWelcome(1) // Test setup fmt.Println("\n🧪 TEST MODE:") fmt.Println("─"*50) mockRepo := NewMockUserRepo() mockEmail := &MockEmailService{} mockLogger := &MockLogger{} mockClock := &MockClock{Time: time.Date(2024, 2, 13, 10, 0, 0, 0, time.UTC)} testService := NewUserService(mockRepo, mockEmail, mockLogger, mockClock) testService.Login(1) testService.SendWelcome(1) // Verify behavior fmt.Println("\n✅ TEST ASSERTIONS:") fmt.Printf(" Save called: %d times\n", mockRepo.SaveCalls) fmt.Printf(" Emails sent: %d\n", len(mockEmail.SentEmails)) fmt.Printf(" Log messages: %d\n", len(mockLogger.Messages)) fmt.Printf(" User last login: %s\n", mockRepo.users[1].LastLogin.Format(time.RFC3339)) fmt.Println("\n" + "═"*60) fmt.Println("DEPENDENCY INJECTION BENEFITS:") fmt.Println(" ✓ Same service code, different implementations") fmt.Println(" ✓ Easy to test with mocks") fmt.Println(" ✓ Easy to swap implementations (Postgres → MySQL)") fmt.Println(" ✓ Loose coupling, high cohesion") fmt.Println("═"*60) }

Common Interface Mistakes

Mistake 1: Nil Interface vs Nil Concrete Value

go
// Filename: nil_interface_gotcha.go package main import "fmt" type Printer interface { Print() } type ConsolePrinter struct { prefix string } func (c *ConsolePrinter) Print() { fmt.Println(c.prefix + "Hello!") } func GetPrinter(enabled bool) Printer { var printer *ConsolePrinter // nil pointer if enabled { printer = &ConsolePrinter{prefix: "> "} } return printer // DANGER: Returns interface with nil value! } func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ NIL INTERFACE GOTCHA ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") // Get disabled printer printer := GetPrinter(false) // This is surprising! fmt.Printf("printer == nil: %v\n", printer == nil) fmt.Printf("printer type: %T\n", printer) // The interface has: // - Type: *ConsolePrinter // - Value: nil // So interface != nil (it has a type!) // This will panic! // printer.Print() // PANIC: nil pointer dereference fmt.Println("\n⚠️ SOLUTION: Return nil explicitly") printer2 := GetPrinterFixed(false) fmt.Printf("printer2 == nil: %v\n", printer2 == nil) } func GetPrinterFixed(enabled bool) Printer { if !enabled { return nil // Return nil interface, not nil-valued interface } return &ConsolePrinter{prefix: "> "} }

Mistake 2: Pointer vs Value Receivers

go
// Filename: receiver_types.go package main import "fmt" type Counter interface { Increment() Value() int } // ValueCounter uses value receiver type ValueCounter struct { count int } func (v ValueCounter) Increment() { v.count++ // This modifies a COPY! } func (v ValueCounter) Value() int { return v.count } // PointerCounter uses pointer receiver type PointerCounter struct { count int } func (p *PointerCounter) Increment() { p.count++ // This modifies the original } func (p *PointerCounter) Value() int { return p.count } func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ POINTER VS VALUE RECEIVERS ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") // Value receiver - Increment doesn't work! fmt.Println("\n📋 Value Receiver:") var c1 Counter = ValueCounter{count: 0} c1.Increment() c1.Increment() c1.Increment() fmt.Printf("After 3 increments: %d (Expected: 3, Got: 0!)\n", c1.Value()) // Pointer receiver - Works correctly fmt.Println("\n📋 Pointer Receiver:") var c2 Counter = &PointerCounter{count: 0} c2.Increment() c2.Increment() c2.Increment() fmt.Printf("After 3 increments: %d (Correct!)\n", c2.Value()) // Note: Value receiver CAN be assigned to interface // But pointer receiver MUST use pointer // var c3 Counter = PointerCounter{} // ERROR! var c4 Counter = &PointerCounter{} // OK _ = c4 }

Summary: Go Interfaces Mastery

Key Concepts

ConceptDescription
Implicit ImplementationNo "implements" keyword needed
Interface CompositionCombine small interfaces
Empty Interface (any)Accepts any type
Type AssertionsGet concrete type from interface
Type SwitchesHandle multiple types cleanly
Pointer ReceiversUse when methods modify state

Best Practices Checklist

PracticeBenefit
Keep interfaces smallEasy to implement, compose
Accept interfaces, return structsFlexible inputs, concrete outputs
Define interfaces at usage siteDecoupled design
Use composition over large interfacesBetter abstraction
Return nil explicitlyAvoid nil interface gotcha
Use pointer receivers for mutationsCorrect behavior

Your Next Steps

  1. Review your existing interfaces - are they too large?
  2. Define interfaces where they're used, not where types are defined
  3. Write tests using mock implementations
  4. Practice type assertions and switches
  5. Study the standard library interfaces (io.Reader, io.Writer, etc.)

"The bigger the interface, the weaker the abstraction." - Rob Pike
Go interfaces are your most powerful tool for building flexible, maintainable, and testable software. Master them, and you'll write Go code that stands the test of time.
All Blogs
Tags:golanginterfacesdesign-patternspolymorphismclean-code