Loading...
Loading...

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)
}
0 Interaction
0 Views
Views
0 Likes
×
×
×
🍪 CookieConsent@Ptutorials:~

Welcome to Ptutorials

$ Allow cookies on this site ? (y/n)

top-home