Skip to main content
โšก Calmops

Implicit Interfaces and Duck Typing

Implicit Interfaces and Duck Typing

Go’s interface system is unique: interfaces are satisfied implicitly. Any type that implements all methods of an interface automatically satisfies that interface, without explicit declaration. This enables powerful duck typing patterns. This guide covers implicit interfaces and duck typing in Go.

Understanding Implicit Interfaces

Basic Implicit Interface

package main

import "fmt"

// Define an interface
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Type A implements Writer
type FileWriter struct {
    filename string
}

func (fw *FileWriter) Write(p []byte) (n int, err error) {
    fmt.Printf("Writing to file %s: %s\n", fw.filename, string(p))
    return len(p), nil
}

// Type B implements Writer
type ConsoleWriter struct{}

func (cw *ConsoleWriter) Write(p []byte) (n int, err error) {
    fmt.Printf("Writing to console: %s\n", string(p))
    return len(p), nil
}

// Function accepts any Writer
func WriteData(w Writer, data []byte) {
    w.Write(data)
}

func main() {
    fw := &FileWriter{filename: "output.txt"}
    cw := &ConsoleWriter{}
    
    // Both types satisfy Writer interface
    WriteData(fw, []byte("Hello"))
    WriteData(cw, []byte("World"))
}

No Explicit Declaration

package main

import "fmt"

type Reader interface {
    Read(p []byte) (n int, err error)
}

// This type implements Reader without declaring it
type StringReader struct {
    data string
    pos  int
}

func (sr *StringReader) Read(p []byte) (n int, err error) {
    if sr.pos >= len(sr.data) {
        return 0, fmt.Errorf("EOF")
    }
    n = copy(p, sr.data[sr.pos:])
    sr.pos += n
    return n, nil
}

func main() {
    sr := &StringReader{data: "Hello, World!"}
    
    // StringReader is automatically a Reader
    var r Reader = sr
    
    p := make([]byte, 5)
    n, _ := r.Read(p)
    fmt.Printf("Read %d bytes: %s\n", n, string(p[:n]))
}

Duck Typing Patterns

Polymorphism Through Interfaces

package main

import "fmt"

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * 3.14159 * c.Radius
}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Works with any Shape
func PrintShape(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    circle := Circle{Radius: 5}
    rect := Rectangle{Width: 10, Height: 5}
    
    PrintShape(circle)
    PrintShape(rect)
}

Flexible Function Arguments

package main

import (
    "fmt"
    "io"
    "strings"
)

// Function accepts any Reader
func ReadAll(r io.Reader) (string, error) {
    buf := make([]byte, 1024)
    n, err := r.Read(buf)
    if err != nil {
        return "", err
    }
    return string(buf[:n]), nil
}

func main() {
    // Works with strings.Reader
    sr := strings.NewReader("Hello from string")
    data, _ := ReadAll(sr)
    fmt.Println(data)
    
    // Works with any other Reader implementation
}

Common Interface Patterns

Reader and Writer Interfaces

package main

import (
    "fmt"
    "io"
)

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
}

// Custom implementation
type Buffer struct {
    data []byte
    pos  int
}

func (b *Buffer) Read(p []byte) (n int, err error) {
    if b.pos >= len(b.data) {
        return 0, io.EOF
    }
    n = copy(p, b.data[b.pos:])
    b.pos += n
    return n, nil
}

func (b *Buffer) Write(p []byte) (n int, err error) {
    b.data = append(b.data, p...)
    return len(p), nil
}

func main() {
    buf := &Buffer{}
    
    // Can be used as Reader
    var r io.Reader = buf
    fmt.Println(r)
    
    // Can be used as Writer
    var w io.Writer = buf
    fmt.Println(w)
}

Stringer Interface

package main

import "fmt"

type Stringer interface {
    String() string
}

type Person struct {
    Name string
    Age  int
}

// Implements Stringer
func (p Person) String() string {
    return fmt.Sprintf("Person{Name: %s, Age: %d}", p.Name, p.Age)
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    
    // Automatically uses String() method
    fmt.Println(p)
    fmt.Printf("%v\n", p)
}

Error Interface

package main

import (
    "fmt"
)

type error interface {
    Error() string
}

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}

func ValidateEmail(email string) error {
    if email == "" {
        return &ValidationError{
            Field:   "email",
            Message: "email is required",
        }
    }
    return nil
}

func main() {
    err := ValidateEmail("")
    if err != nil {
        fmt.Println(err)
    }
}

Practical Examples

Logger Interface

package main

import (
    "fmt"
    "log"
    "os"
)

type Logger interface {
    Info(msg string)
    Error(msg string)
    Debug(msg string)
}

type ConsoleLogger struct{}

func (cl *ConsoleLogger) Info(msg string) {
    fmt.Printf("[INFO] %s\n", msg)
}

func (cl *ConsoleLogger) Error(msg string) {
    fmt.Printf("[ERROR] %s\n", msg)
}

func (cl *ConsoleLogger) Debug(msg string) {
    fmt.Printf("[DEBUG] %s\n", msg)
}

type FileLogger struct {
    file *os.File
}

func (fl *FileLogger) Info(msg string) {
    log.Printf("[INFO] %s\n", msg)
}

func (fl *FileLogger) Error(msg string) {
    log.Printf("[ERROR] %s\n", msg)
}

func (fl *FileLogger) Debug(msg string) {
    log.Printf("[DEBUG] %s\n", msg)
}

// Works with any Logger
func ProcessData(logger Logger, data string) {
    logger.Info("Processing data")
    logger.Debug("Data: " + data)
    logger.Info("Processing complete")
}

func main() {
    cl := &ConsoleLogger{}
    ProcessData(cl, "test data")
}

Storage Interface

package main

import "fmt"

type Storage interface {
    Get(key string) (interface{}, error)
    Set(key string, value interface{}) error
    Delete(key string) error
}

type MemoryStorage struct {
    data map[string]interface{}
}

func (ms *MemoryStorage) Get(key string) (interface{}, error) {
    if val, ok := ms.data[key]; ok {
        return val, nil
    }
    return nil, fmt.Errorf("key not found")
}

func (ms *MemoryStorage) Set(key string, value interface{}) error {
    ms.data[key] = value
    return nil
}

func (ms *MemoryStorage) Delete(key string) error {
    delete(ms.data, key)
    return nil
}

type DatabaseStorage struct {
    // Database connection
}

func (ds *DatabaseStorage) Get(key string) (interface{}, error) {
    // Query database
    return nil, nil
}

func (ds *DatabaseStorage) Set(key string, value interface{}) error {
    // Insert into database
    return nil
}

func (ds *DatabaseStorage) Delete(key string) error {
    // Delete from database
    return nil
}

// Works with any Storage
func CacheData(storage Storage, key string, value interface{}) {
    storage.Set(key, value)
}

func main() {
    ms := &MemoryStorage{data: make(map[string]interface{})}
    CacheData(ms, "user:1", "Alice")
    
    val, _ := ms.Get("user:1")
    fmt.Println(val)
}

Benefits of Implicit Interfaces

Loose Coupling

package main

// No dependency on concrete types
type DataProcessor interface {
    Process(data []byte) error
}

// Can work with any implementation
func ProcessFile(processor DataProcessor, filename string) error {
    // Read file
    data := []byte("file content")
    return processor.Process(data)
}

Easy Testing

package main

import "testing"

type MockLogger struct {
    messages []string
}

func (ml *MockLogger) Log(msg string) {
    ml.messages = append(ml.messages, msg)
}

type Logger interface {
    Log(msg string)
}

func TestWithMockLogger(t *testing.T) {
    logger := &MockLogger{}
    // Use logger in tests
    logger.Log("test message")
    
    if len(logger.messages) != 1 {
        t.Error("Expected 1 message")
    }
}

Best Practices

โœ… Good Practices

  1. Define small interfaces - Single responsibility
  2. Use implicit interfaces - No explicit implementation needed
  3. Accept interfaces, return concrete types - Flexible inputs
  4. Design for composition - Combine small interfaces
  5. Use standard library interfaces - io.Reader, io.Writer, etc.
  6. Document interface contracts - Explain expected behavior
  7. Avoid interface pollution - Only define needed interfaces
  8. Test with mock implementations - Easy to test

โŒ Anti-Patterns

// โŒ Bad: Large interface with many methods
type Everything interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
    Close() error
    Seek(offset int64, whence int) (int64, error)
    Stat() (os.FileInfo, error)
    // ... many more methods
}

// โœ… Good: Small, focused interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// โŒ Bad: Returning interfaces
func GetUser() UserInterface {
    return &User{}
}

// โœ… Good: Return concrete types
func GetUser() *User {
    return &User{}
}

Summary

Implicit interfaces enable powerful patterns:

  • Interfaces are satisfied implicitly
  • No explicit implementation declarations
  • Enables duck typing and polymorphism
  • Loose coupling between components
  • Easy to test with mock implementations
  • Design small, focused interfaces
  • Accept interfaces, return concrete types

Master implicit interfaces for flexible Go design.

Comments