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.
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: