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
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:
- Third-party types can satisfy your interfaces without modification
- You can create interfaces for types you don't own
- 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
| Concept | Description |
|---|---|
| Implicit Implementation | No "implements" keyword needed |
| Interface Composition | Combine small interfaces |
Empty Interface (any) | Accepts any type |
| Type Assertions | Get concrete type from interface |
| Type Switches | Handle multiple types cleanly |
| Pointer Receivers | Use when methods modify state |
Best Practices Checklist
| Practice | Benefit |
|---|---|
| Keep interfaces small | Easy to implement, compose |
| Accept interfaces, return structs | Flexible inputs, concrete outputs |
| Define interfaces at usage site | Decoupled design |
| Use composition over large interfaces | Better abstraction |
| Return nil explicitly | Avoid nil interface gotcha |
| Use pointer receivers for mutations | Correct behavior |
Your Next Steps
- Review your existing interfaces - are they too large?
- Define interfaces where they're used, not where types are defined
- Write tests using mock implementations
- Practice type assertions and switches
- 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.