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
// Define a Speaker interface
type Speaker interface {
Speak() string
}
Implicit Implementation
// 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
}
implements keyword. As long as a type has all the methods required by an interface, it automatically implements that interface.
Using Interfaces
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{}
// 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
}
interface{} can be shortened to any. They are equivalent.
Type Assertions and Type Switches
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)
}
}
v.(Type) panics if the assertion fails, while v, ok := v.(Type) safely returns a zero value and false.
Interface Composition
// 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
io.Reader, io.Writer, fmt.Stringer, etc.
Examples
Example: Shape Area Calculation (Difficulty ⭐)
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)
}
Rectangle -> Area: 50.00, Perimeter: 30.00
Circle -> Area: 153.94, Perimeter: 43.98
Example: Interface Slices and Sorting (Difficulty ⭐⭐)
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))
}
=== 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 ⭐⭐⭐)
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())
}
=== 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)
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
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 |
❓ 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?
- When you need polymorphism (same function handles different types)
- When you need decoupling (depend on abstractions, not concrete implementations)
- When you need mock testing (use interfaces to replace real dependencies)
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.
// 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:
// 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
// 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.



