Interfaces

Lesson 9: Interfaces

Real-World Analogy

Imagine you go to a restaurant to order food. You don't need to know who the chef is, what pan they use, or how they cook — you just look at the menu and order "Kung Pao Chicken." The menu is an interface: it defines "what can be done" without caring about "who does it" or "how it's done."

In Go, interfaces work the same way. They define a set of method signatures, and any type that implements these methods automatically satisfies the interface — no explicit declaration needed.


Core Concepts

Concept Description
Interface A collection of method signatures that defines a behavioral contract
Implicit Implementation A type automatically satisfies an interface if it implements all the interface's methods
Duck Typing "If it walks like a duck and quacks like a duck, then it's a duck"
Empty Interface interface{} Contains no methods; any type satisfies it
Type Assertion Extracts a concrete type from an interface value
Interface Composition Build larger interfaces by embedding multiple interfaces

Basic Syntax and Usage

Defining an Interface

GO
// Define a Speaker interface
type Speaker interface {
    Speak() string
}

Implicit Implementation

GO
// Dog type implements the Speaker interface (no declaration needed)
type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return "Woof! I'm " + d.Name
}

// Cat type also implements the Speaker interface
type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "Meow! I'm " + c.Name
}
💡 Tip: Go has no implements keyword. As long as a type has all the methods required by an interface, it automatically implements that interface.

Using Interfaces

GO
func makeItSpeak(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    dog := Dog{Name: "Rex"}
    cat := Cat{Name: "Whiskers"}

    makeItSpeak(dog) // Output: Woof! I'm Rex
    makeItSpeak(cat) // Output: Meow! I'm Whiskers
}

Empty Interface interface{}

GO
// Empty interface can hold values of any type
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(3.14)      // Value: 3.14, Type: float64
}
💡 Tip: In Go 1.18+, interface{} can be shortened to any. They are equivalent.

Type Assertions and Type Switches

GO
func describe(v interface{}) {
    // Type assertion: try to convert the interface value to a concrete type
    str, ok := v.(string)
    if ok {
        fmt.Println("This is a string:", str)
        return
    }

    // Type switch: elegantly handle multiple types
    switch val := v.(type) {
    case int:
        fmt.Println("This is an integer:", val)
    case float64:
        fmt.Println("This is a float:", val)
    case bool:
        fmt.Println("This is a boolean:", val)
    default:
        fmt.Printf("Unknown type: %T\n", val)
    }
}
💡 Tip: Using the "comma ok" pattern with type assertions avoids panic. v.(Type) panics if the assertion fails, while v, ok := v.(Type) safely returns a zero value and false.

Interface Composition

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

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

// Composed interface: embeds multiple interfaces
type ReadWriter interface {
    Reader
    Writer
}

// ReadWriter requires both Read and Write methods to be implemented
💡 Tip: Interface composition follows the "small interface" principle. Many interfaces in Go's standard library have only 1-2 methods, such as io.Reader, io.Writer, fmt.Stringer, etc.


Examples

Example: Shape Area Calculation (Difficulty ⭐)

GO
package main

import (
    "fmt"
    "math"
)

// Shape interface defines the behavior of "shapes"
type Shape interface {
    Area() float64
    Perimeter() float64
}

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

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

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

// Circle
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
}

// printShapeInfo accepts any implementation of the Shape interface
func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

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

    fmt.Print("Rectangle -> ")
    printShapeInfo(rect)

    fmt.Print("Circle -> ")
    printShapeInfo(circle)
}
▶ Try it Yourself
Rectangle -> Area: 50.00, Perimeter: 30.00
Circle -> Area: 153.94, Perimeter: 43.98

Example: Interface Slices and Sorting (Difficulty ⭐⭐)

GO
package main

import (
    "fmt"
    "sort"
)

// Employee interface
type Employee interface {
    Name() string
    Salary() float64
}

// FullTime full-time employee
type FullTime struct {
    name   string
    annual float64 // Annual salary
}

func (f FullTime) Name() string    { return f.name }
func (f FullTime) Salary() float64 { return f.annual }

// Contractor contract worker
type Contractor struct {
    name    string
    hourly  float64 // Hourly rate
    hours   float64 // Hours worked
}

func (c Contractor) Name() string    { return c.name }
func (c Contractor) Salary() float64 { return c.hourly * c.hours }

// BySalary implements sort.Interface, sorts by salary
type BySalary []Employee

func (s BySalary) Len() int           { return len(s) }
func (s BySalary) Less(i, j int) bool { return s[i].Salary() < s[j].Salary() }
func (s BySalary) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

// totalCost calculates total labor cost
func totalCost(employees []Employee) float64 {
    total := 0.0
    for _, e := range employees {
        total += e.Salary()
    }
    return total
}

func main() {
    team := []Employee{
        FullTime{name: "Zhang San", annual: 120000},
        Contractor{name: "Li Si", hourly: 200, hours: 1000},
        FullTime{name: "Wang Wu", annual: 150000},
        Contractor{name: "Zhao Liu", hourly: 180, hours: 800},
    }

    fmt.Println("=== Before Salary Sort ===")
    for _, e := range team {
        fmt.Printf("  %s: $%.0f\n", e.Name(), e.Salary())
    }

    sort.Sort(BySalary(team))

    fmt.Println("\n=== After Salary Sort ===")
    for _, e := range team {
        fmt.Printf("  %s: $%.0f\n", e.Name(), e.Salary())
    }

    fmt.Printf("\nTotal labor cost: $%.0f\n", totalCost(team))
}
▶ Try it Yourself
=== Before Salary Sort ===
  Zhang San: $120000
  Li Si: $200000
  Wang Wu: $150000
  Zhao Liu: $144000

=== After Salary Sort ===
  Zhang San: $120000
  Zhao Liu: $144000
  Wang Wu: $150000
  Li Si: $200000

Total labor cost: $614000

Example: Implementing io.Reader/Writer Interfaces (Difficulty ⭐⭐⭐)

GO
package main

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

// UpperReader converts read content to uppercase
type UpperReader struct {
    source io.Reader
}

// Implements io.Reader interface
func (u *UpperReader) Read(p []byte) (n int, err error) {
    n, err = u.source.Read(p)
    // Convert all read bytes to uppercase
    for i := 0; i < n; i++ {
        if p[i] >= 'a' && p[i] <= 'z' {
            p[i] = p[i] - 32 // ASCII: lowercase to uppercase
        }
    }
    return
}

// UpperReader constructor
func NewUpperReader(r io.Reader) *UpperReader {
    return &UpperReader{source: r}
}

// PrefixWriter adds a prefix before each write
type PrefixWriter struct {
    prefix string
    target io.Writer
}

// Implements io.Writer interface
func (p *PrefixWriter) Write(data []byte) (n int, err error) {
    // Write the prefix first
    _, err = p.target.Write([]byte(p.prefix))
    if err != nil {
        return 0, err
    }
    // Then write the actual data
    return p.target.Write(data)
}

// PrefixWriter constructor
func NewPrefixWriter(prefix string, w io.Writer) *PrefixWriter {
    return &PrefixWriter{prefix: prefix, target: w}
}

// TeeReader reads and writes simultaneously (similar to the tee command)
func TeeReader(r io.Reader, w io.Writer) io.Reader {
    return &teeReader{r: r, w: w}
}

type teeReader struct {
    r io.Reader
    w io.Writer
}

func (t *teeReader) Read(p []byte) (n int, err error) {
    n, err = t.r.Read(p)
    if n > 0 {
        // Write to w while reading
        t.w.Write(p[:n])
    }
    return
}

func main() {
    fmt.Println("=== UpperReader Example ===")
    // Create a Reader from a string
    source := strings.NewReader("hello, go interfaces!")
    upper := NewUpperReader(source)

    // Use io.ReadAll to read all content
    buf := make([]byte, 64)
    n, _ := upper.Read(buf)
    fmt.Printf("Uppercase result: %s\n", string(buf[:n]))

    fmt.Println("\n=== PrefixWriter Example ===")
    // Write to stdout with prefix
    writer := NewPrefixWriter("[LOG] ", &strings.Builder{})
    writer.Write([]byte("System started\n"))
    // Use strings.Builder to capture output
    var builder strings.Builder
    pw := NewPrefixWriter("[DEBUG] ", &builder)
    pw.Write([]byte("Interface initialized"))
    fmt.Println(builder.String())

    fmt.Println("\n=== TeeReader Example ===")
    // Read and simultaneously write to another Writer
    input := strings.NewReader("Go is powerful")
    var capture strings.Builder
    tee := TeeReader(input, &capture)

    buf2 := make([]byte, 1024)
    n2, _ := tee.Read(buf2)
    fmt.Printf("Read: %s\n", string(buf2[:n2]))
    fmt.Printf("Also captured: %s\n", capture.String())
}
▶ Try it Yourself
=== UpperReader Example ===
Uppercase result: HELLO, GO INTERFACES!

=== PrefixWriter Example ===
[DEBUG] Interface initialized

=== TeeReader Example ===
Read: Go is powerful
Also captured: Go is powerful

Real-World Application Scenarios

Scenario 1: Logging System (Strategy Pattern)

GO
package main

import (
    "fmt"
    "os"
    "time"
)

// Logger logging interface
type Logger interface {
    Log(message string)
}

// ConsoleLogger console logger
type ConsoleLogger struct{}

func (c ConsoleLogger) Log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    fmt.Printf("[%s] %s\n", timestamp, message)
}

// FileLogger file logger
type FileLogger struct {
    file *os.File
}

func NewFileLogger(filename string) (*FileLogger, error) {
    f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return nil, err
    }
    return &FileLogger{file: f}, nil
}

func (f *FileLogger) Log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    fmt.Fprintf(f.file, "[%s] %s\n", timestamp, message)
}

// MultiLogger outputs to multiple loggers simultaneously
type MultiLogger struct {
    loggers []Logger
}

func (m *MultiLogger) Add(l Logger) {
    m.loggers = append(m.loggers, l)
}

func (m *MultiLogger) Log(message string) {
    for _, l := range m.loggers {
        l.Log(message)
    }
}

// App uses the Logger interface, doesn't care about the specific implementation
type App struct {
    logger Logger
}

func (a *App) Run() {
    a.logger.Log("Application started")
    a.logger.Log("Processing request...")
    a.logger.Log("Request processing complete")
}

func main() {
    // Combine multiple log outputs
    multi := &MultiLogger{}
    multi.Add(ConsoleLogger{})

    // Can easily switch or add log output methods
    app := &App{logger: multi}
    app.Run()
}
[2026-06-26 10:30:00] Application started
[2026-06-26 10:30:00] Processing request...
[2026-06-26 10:30:00] Request processing complete

Scenario 2: Data Storage Abstraction Layer

GO
package main

import "fmt"

// Store storage interface
type Store interface {
    Get(key string) (string, bool)
    Set(key string, value string)
    Delete(key string)
    Keys() []string
}

// MemoryStore in-memory storage implementation
type MemoryStore struct {
    data map[string]string
}

func NewMemoryStore() *MemoryStore {
    return &MemoryStore{data: make(map[string]string)}
}

func (m *MemoryStore) Get(key string) (string, bool) {
    val, ok := m.data[key]
    return val, ok
}

func (m *MemoryStore) Set(key string, value string) {
    m.data[key] = value
}

func (m *MemoryStore) Delete(key string) {
    delete(m.data, key)
}

func (m *MemoryStore) Keys() []string {
    keys := make([]string, 0, len(m.data))
    for k := range m.data {
        keys = append(keys, k)
    }
    return keys
}

// CacheService uses the Store interface, decoupled from specific storage
type CacheService struct {
    store Store
}

func (c *CacheService) GetOrSet(key, defaultValue string) string {
    if val, ok := c.store.Get(key); ok {
        return val
    }
    c.store.Set(key, defaultValue)
    return defaultValue
}

func (c *CacheService) GetAll() map[string]string {
    result := make(map[string]string)
    for _, key := range c.store.Keys() {
        if val, ok := c.store.Get(key); ok {
            result[key] = val
        }
    }
    return result
}

func main() {
    // Use in-memory storage
    store := NewMemoryStore()
    cache := &CacheService{store: store}

    // Write data
    cache.GetOrSet("user:1", "Alice")
    cache.GetOrSet("user:2", "Bob")
    cache.GetOrSet("config:theme", "dark")

    // Read data
    fmt.Println("All cached data:")
    for k, v := range cache.GetAll() {
        fmt.Printf("  %s = %s\n", k, v)
    }

    // Test GetOrSet: existing key returns old value
    result := cache.GetOrSet("user:1", "Charlie")
    fmt.Printf("\nuser:1 value: %s\n", result)
}
All cached data:
  user:1 = Alice
  user:2 = Bob
  config:theme = dark

user:1 value: Alice

📖 Summary

Key Point Description
Interfaces define behavior Only care about "what it can do," not "what it is"
Implicit implementation No declaration needed; implementing the methods satisfies the interface
Empty interface any Can hold values of any type
Type assertion Extract concrete types from interface values; use comma ok pattern for safety
Interface composition Build large interfaces by embedding small ones
Program to interfaces Depend on interfaces rather than concrete implementations for flexibility
💡 Best Practice: Go favors small interfaces. The most commonly used interfaces in the standard library usually have only 1-2 methods. When defining interfaces, start from the consumer's (caller's) needs, not the implementer's.


❓ FAQ

Q1: What's the difference between an interface and a struct?

A struct is a concrete data type that defines "what it is"; an interface is a behavioral contract that defines "what it can do." Structs can be instantiated; interfaces cannot be directly instantiated, but can hold values of any type that implements the interface.

Q2: Why doesn't Go need an implements keyword?

Go uses duck typing design. The compiler automatically checks at compile time whether a type satisfies an interface. This design completely decouples interfaces from implementations — you can define new interfaces for third-party library types without modifying existing code.

Q3: When should I define an interface?

💡 Rule of thumb: Write concrete implementations first, then define interfaces when you discover the need for abstraction. Don't over-design.

Q4: What's the difference between interface{} and `any?

None. any is a type alias for interface{} introduced in Go 1.18. They are completely equivalent. any is recommended because it's more concise.


📝 Exercises

Exercise 1: Basics — Implement the Stringer Interface

The fmt.Stringer interface has only one method: String() string. When using fmt.Println or %v formatting, Go automatically calls this method.

GO
// Implement the fmt.Stringer interface for the following types
type Temperature struct {
    Celsius float64
}

type Money struct {
    Amount   float64
    Currency string
}

// Expected behavior:
// fmt.Println(Temperature{36.5})  -> "36.5°C"
// fmt.Println(Money{99.9, "USD"}) -> "$99.90"

Exercise 2: Intermediate — Design a Notification System

Design a notification system that supports multiple notification methods:

GO
// 1. Define a Notifier interface
// 2. Implement EmailNotifier, SMSNotifier, WechatNotifier
// 3. Implement a function that can send to multiple Notifiers simultaneously
// 4. Use an interface slice to store different Notifiers

Exercise 3: Challenge — Implement a Simple Plugin System

GO
// Define a Plugin interface with Name(), Version(), Execute() methods
// Implement at least 3 different Plugins
// Create a PluginManager that can register, find, and execute plugins
// Hint: Use map[string]Plugin to store plugins

Next Lesson

Interfaces are the cornerstone of polymorphism in Go. With interfaces mastered, you can write flexible, testable, and extensible code. Next, we'll learn about Go's error handling mechanism — one of the most important design philosophies in the Go language.

👉 Next Lesson: Error Handling

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏