Interfaces in Go

Interfaces in Go provide a way to specify the behavior of an object. They define a set of method signatures that a type must implement to satisfy the interface.

What Are Interfaces?

An interface is a collection of method signatures that a type can implement. In Go, interfaces are implemented implicitly - you don’t need to explicitly declare that a type implements an interface.

Basic Interface Definition
type Writer interface {
    Write([]byte) (int, error)
}

Basic Interface Example

Let’s create a simple interface and implement it:

package main

import (
    "fmt"
    "strings"
)

// Define an interface
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Rectangle type
type Rectangle struct {
    Width  float64
    Height float64
}

// Implement Shape interface for Rectangle
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

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

// Circle type
type Circle struct {
    Radius float64
}

// Implement Shape interface for Circle
func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

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

// Function that works with any Shape
func printShapeDetails(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
    fmt.Printf("Perimeter: %.2f\n", s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 5, Height: 3}
    circle := Circle{Radius: 4}
    
    fmt.Println("Rectangle:")
    printShapeDetails(rect)
    
    fmt.Println("\nCircle:")
    printShapeDetails(circle)
}

Interface Composition

You can combine multiple interfaces into one:

package main

import "fmt"

// Reader interface
type Reader interface {
    Read() string
}

// Writer interface
type Writer interface {
    Write(data string) error
}

// ReadWriter combines both interfaces
type ReadWriter interface {
    Reader
    Writer
}

// File type that implements ReadWriter
type File struct {
    content string
}

func (f *File) Read() string {
    return f.content
}

func (f *File) Write(data string) error {
    f.content = data
    return nil
}

func main() {
    file := &File{}
    
    var rw ReadWriter = file
    rw.Write("Hello, Go interfaces!")
    fmt.Println(rw.Read()) // Output: Hello, Go interfaces!
}

Empty Interface

The empty interface interface{} can hold values of any type:

package main

import "fmt"

func printValue(value interface{}) {
    fmt.Printf("Value: %v, Type: %T\n", value, value)
}

func main() {
    printValue(42)           // int
    printValue("hello")       // string
    printValue(3.14)          // float64
    printValue([]int{1, 2, 3}) // slice
}

Type Assertions

Type assertions allow you to extract the underlying concrete value from an interface:

package main

import "fmt"

func main() {
    var i interface{} = "hello"
    
    // Type assertion
    s := i.(string)
    fmt.Println(s) // Output: hello
    
    // Type assertion with check
    if s, ok := i.(string); ok {
        fmt.Println("i is a string:", s)
    }
    
    // This will panic
    // num := i.(int) // panic: interface conversion: interface {} is string, not int
    
    // Safe type assertion
    if num, ok := i.(int); ok {
        fmt.Println("i is an int:", num)
    } else {
        fmt.Println("i is not an int")
    }
}

Type Switches

Type switches let you handle different types stored in interfaces:

package main

import "fmt"

func processValue(value interface{}) {
    switch v := value.(type) {
    case int:
        fmt.Printf("Integer: %d (doubled: %d)\n", v, v*2)
    case string:
        fmt.Printf("String: %s (length: %d)\n", v, len(v))
    case []int:
        fmt.Printf("Integer slice: %v (sum: %d)\n", v, sum(v))
    default:
        fmt.Printf("Unknown type: %T, value: %v\n", v, v)
    }
}

func sum(numbers []int) int {
    total := 0
    for _, num := range numbers {
        total += num
    }
    return total
}

func main() {
    processValue(42)
    processValue("Hello, Go!")
    processValue([]int{1, 2, 3, 4, 5})
    processValue(3.14)
}

Practical Example: Database Driver Interface

Here’s a real-world example of how interfaces enable flexibility:

package main

import (
    "fmt"
    "time"
)

// Database interface defines the contract
type Database interface {
    Connect() error
    Disconnect() error
    Query(query string) ([]map[string]interface{}, error)
    Execute(query string) error
}

// MySQL implementation
type MySQL struct {
    host     string
    port     int
    database string
    connected bool
}

func (m *MySQL) Connect() error {
    fmt.Printf("Connecting to MySQL at %s:%d\n", m.host, m.port)
    m.connected = true
    return nil
}

func (m *MySQL) Disconnect() error {
    fmt.Println("Disconnecting from MySQL")
    m.connected = false
    return nil
}

func (m *MySQL) Query(query string) ([]map[string]interface{}, error) {
    if !m.connected {
        return nil, fmt.Errorf("not connected to MySQL")
    }
    fmt.Printf("MySQL executing query: %s\n", query)
    return []map[string]interface{}{
        {"id": 1, "name": "John"},
        {"id": 2, "name": "Jane"},
    }, nil
}

func (m *MySQL) Execute(query string) error {
    if !m.connected {
        return fmt.Errorf("not connected to MySQL")
    }
    fmt.Printf("MySQL executing: %s\n", query)
    return nil
}

// PostgreSQL implementation
type PostgreSQL struct {
    host     string
    port     int
    database string
    connected bool
}

func (p *PostgreSQL) Connect() error {
    fmt.Printf("Connecting to PostgreSQL at %s:%d\n", p.host, p.port)
    p.connected = true
    return nil
}

func (p *PostgreSQL) Disconnect() error {
    fmt.Println("Disconnecting from PostgreSQL")
    p.connected = false
    return nil
}

func (p *PostgreSQL) Query(query string) ([]map[string]interface{}, error) {
    if !p.connected {
        return nil, fmt.Errorf("not connected to PostgreSQL")
    }
    fmt.Printf("PostgreSQL executing query: %s\n", query)
    return []map[string]interface{}{
        {"id": 1, "username": "alice"},
        {"id": 2, "username": "bob"},
    }, nil
}

func (p *PostgreSQL) Execute(query string) error {
    if !p.connected {
        return fmt.Errorf("not connected to PostgreSQL")
    }
    fmt.Printf("PostgreSQL executing: %s\n", query)
    return nil
}

// Service that works with any database
type UserService struct {
    db Database
}

func NewUserService(db Database) *UserService {
    return &UserService{db: db}
}

func (s *UserService) GetUsers() ([]map[string]interface{}, error) {
    return s.db.Query("SELECT * FROM users")
}

func (s *UserService) CreateUser(name string) error {
    query := fmt.Sprintf("INSERT INTO users (name) VALUES ('%s')", name)
    return s.db.Execute(query)
}

func main() {
    // Use MySQL
    mysql := &MySQL{host: "localhost", port: 3306, database: "myapp"}
    userService1 := NewUserService(mysql)
    
    userService1.db.Connect()
    users, _ := userService1.GetUsers()
    fmt.Println("MySQL users:", users)
    userService1.db.Disconnect()
    
    fmt.Println()
    
    // Use PostgreSQL - same service, different implementation
    postgres := &PostgreSQL{host: "localhost", port: 5432, database: "myapp"}
    userService2 := NewUserService(postgres)
    
    userService2.db.Connect()
    users, _ = userService2.GetUsers()
    fmt.Println("PostgreSQL users:", users)
    userService2.db.Disconnect()
}

Best Practices

1. Keep Interfaces Small

Define interfaces with the minimum number of methods needed:

// Good: Small, focused interface
type Reader interface {
    Read([]byte) (int, error)
}

// Avoid: Large interface with many methods
type FileSystem interface {
    Read([]byte) (int, error)
    Write([]byte) (int, error)
    Seek(int64, int) (int64, error)
    Stat() (FileInfo, error)
    Truncate(int64) error
    Sync() error
    // ... many more methods
}

2. Accept Interfaces, Return Concrete Types

When designing functions, accept interfaces as parameters but return concrete types:

// Good: Accept interface, return concrete type
func ProcessData(r Reader) []byte {
    // ... process data
    return result
}

// Avoid: Return interface unless necessary
func GetProcessor() Processor {
    return &MyProcessor{}
}

3. Name Interfaces by Their Behavior

Name interfaces based on what they do, often ending in “-er”:

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

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

type Stringer interface {
    String() string
}

Standard Library Interfaces

Go’s standard library provides many useful interfaces:

io.Reader and io.Writer

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
    "strings"
)

func copyData(src io.Reader, dst io.Writer) {
    data, err := io.ReadAll(src)
    if err != nil {
        fmt.Printf("Error reading: %v\n", err)
        return
    }
    
    _, err = dst.Write(data)
    if err != nil {
        fmt.Printf("Error writing: %v\n", err)
        return
    }
}

func main() {
    // Copy string to bytes buffer
    strReader := strings.NewReader("Hello, World!")
    var buf bytes.Buffer
    copyData(strReader, &buf)
    fmt.Println("Buffer content:", buf.String())
    
    // Copy string to stdout
    strReader2 := strings.NewReader("This goes to stdout!")
    copyData(strReader2, os.Stdout)
}

fmt.Stringer

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    fmt.Println(p) // Automatically calls String() method
}

External Resources:

Related Tutorials:

Last updated on