Go Interfaces
Introduction
Interfaces are one of the most powerful features in Go. They provide a way to specify the behavior of an object: if something can do this, then it can be used here. Interfaces are types that define a set of method signatures without implementing them. They enable polymorphism and help write flexible, testable, and maintainable code.
What are Interfaces in Go?
An interface in Go is a type that specifies a set of method signatures. A type implements an interface by implementing all the methods in the interface. Unlike many other languages, Go interfaces are satisfied implicitly — there's no explicit declaration of intent.
Key Characteristics:
- Interfaces define behavior, not data
- A type implements an interface by implementing its methods
- Interface implementation is implicit (no "implements" keyword)
- Interfaces enable duck typing: "If it walks like a duck and quacks like a duck, it's a duck"
Interface Declaration and Implementation
Basic Interface Declaration
package main
import "fmt"
// Shape is an interface with two methods
type Shape interface {
Area() float64
Perimeter() float64
}
// Rectangle type
type Rectangle struct {
Width float64
Height float64
}
// Rectangle implements Shape interface
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
}
// Circle implements Shape interface
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 accepts any Shape
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}
PrintShapeInfo(rect) // Area: 50.00, Perimeter: 30.00
PrintShapeInfo(circle) // Area: 153.94, Perimeter: 43.98
}
Multiple Interface Implementation
A type can implement multiple interfaces:
package main
import "fmt"
type Writer interface {
Write([]byte) (int, error)
}
type Closer interface {
Close() error
}
type File struct {
name string
}
func (f *File) Write(data []byte) (int, error) {
fmt.Printf("Writing %d bytes to %s\n", len(data), f.name)
return len(data), nil
}
func (f *File) Close() error {
fmt.Printf("Closing file %s\n", f.name)
return nil
}
func main() {
f := &File{name: "test.txt"}
// File implements both Writer and Closer
var w Writer = f
var c Closer = f
w.Write([]byte("Hello"))
c.Close()
}
Empty Interface (interface{})
The empty interface interface{} specifies zero methods and is satisfied by any type. It's Go's way of representing "any type".
package main
import "fmt"
func PrintAnything(v interface{}) {
fmt.Printf("Type: %T, Value: %v\n", v, v)
}
func main() {
PrintAnything(42) // Type: int, Value: 42
PrintAnything("hello") // Type: string, Value: hello
PrintAnything(3.14) // Type: float64, Value: 3.14
PrintAnything([]int{1, 2}) // Type: []int, Value: [1 2]
// Storing different types in a slice
var things []interface{}
things = append(things, 42)
things = append(things, "hello")
things = append(things, true)
for _, thing := range things {
fmt.Println(thing)
}
}
Note on Go 1.18+
With Go 1.18 and later, any is an alias for interface{}:
func PrintAnything(v any) { // any is equivalent to interface{}
fmt.Printf("Type: %T, Value: %v\n", v, v)
}
Type Assertions and Type Switches
Type Assertions
Type assertions provide access to an interface value's underlying concrete value:
package main
import "fmt"
func main() {
var i interface{} = "hello"
// Type assertion
s := i.(string)
fmt.Println(s) // hello
// Safe type assertion with comma-ok idiom
s, ok := i.(string)
if ok {
fmt.Println("String value:", s)
}
// This would panic without comma-ok
// f := i.(float64) // panic: interface conversion
// Safe way
f, ok := i.(float64)
if !ok {
fmt.Println("Value is not a float64")
}
}
Type Switches
Type switches allow you to handle multiple types:
package main
import "fmt"
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s (length: %d)\n", v, len(v))
case bool:
fmt.Printf("Boolean: %t\n", v)
case float64:
fmt.Printf("Float: %f\n", v)
case []int:
fmt.Printf("Slice of ints: %v (length: %d)\n", v, len(v))
case nil:
fmt.Println("nil value")
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
func main() {
describe(42) // Integer: 42
describe("hello") // String: hello (length: 5)
describe(3.14) // Float: 3.140000
describe(true) // Boolean: true
describe([]int{1, 2, 3}) // Slice of ints: [1 2 3] (length: 3)
describe(struct{}{}) // Unknown type: struct {}
}
Common Interfaces
Stringer Interface
The fmt.Stringer interface is used to define custom string representations:
package main
import "fmt"
type Person struct {
Name string
Age int
}
// Implement the Stringer interface
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) // Alice (30 years old)
// Works with any fmt function
fmt.Printf("Person: %s\n", p) // Person: Alice (30 years old)
}
Error Interface
The error interface is fundamental in Go error handling:
package main
import (
"fmt"
)
// Custom error type
type ValidationError struct {
Field string
Message string
}
// Implement the error interface
func (e ValidationError) Error() string {
return fmt.Sprintf("validation error on field '%s': %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return ValidationError{
Field: "age",
Message: "age cannot be negative",
}
}
if age > 150 {
return ValidationError{
Field: "age",
Message: "age seems unrealistic",
}
}
return nil
}
func main() {
if err := validateAge(-5); err != nil {
fmt.Println(err) // validation error on field 'age': age cannot be negative
}
// Type assertion to get more details
if err := validateAge(200); err != nil {
if valErr, ok := err.(ValidationError); ok {
fmt.Printf("Field: %s, Message: %s\n", valErr.Field, valErr.Message)
}
}
}
io.Reader and io.Writer Interfaces
These are among the most important interfaces in Go's standard library:
package main
import (
"fmt"
"io"
"strings"
"bytes"
)
// Custom type implementing io.Reader
type RotReader struct {
reader io.Reader
}
func (r RotReader) Read(p []byte) (n int, err error) {
n, err = r.reader.Read(p)
// ROT13 transformation
for i := 0; i < n; i++ {
if p[i] >= 'A' && p[i] <= 'Z' {
p[i] = 'A' + (p[i]-'A'+13)%26
} else if p[i] >= 'a' && p[i] <= 'z' {
p[i] = 'a' + (p[i]-'a'+13)%26
}
}
return
}
// Function that works with any io.Reader
func ReadAndPrint(r io.Reader) error {
buf := make([]byte, 1024)
n, err := r.Read(buf)
if err != nil && err != io.EOF {
return err
}
fmt.Printf("Read %d bytes: %s\n", n, buf[:n])
return nil
}
func main() {
// strings.Reader implements io.Reader
sr := strings.NewReader("Hello, World!")
ReadAndPrint(sr) // Read 13 bytes: Hello, World!
// bytes.Buffer implements io.Reader and io.Writer
var buf bytes.Buffer
buf.WriteString("Buffer content")
ReadAndPrint(&buf) // Read 14 bytes: Buffer content
// Custom reader with ROT13
rot := RotReader{reader: strings.NewReader("Hello, World!")}
ReadAndPrint(rot) // Read 13 bytes: Uryyb, Jbeyq!
}
Example: Copy Function Using io.Reader and io.Writer
package main
import (
"io"
"os"
"strings"
)
// Copy data from reader to writer
func Copy(dst io.Writer, src io.Reader) (int64, error) {
return io.Copy(dst, src)
}
func main() {
// Copy from string to stdout
reader := strings.NewReader("Hello from reader!\n")
Copy(os.Stdout, reader) // Prints: Hello from reader!
// Works with any types implementing the interfaces
var buf bytes.Buffer
reader2 := strings.NewReader("Copying to buffer")
Copy(&buf, reader2)
fmt.Println(buf.String()) // Copying to buffer
}
Interface Composition
Interfaces can be composed from other interfaces:
package main
import (
"fmt"
"io"
)
// Individual interfaces
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
type Closer interface {
Close() error
}
// Composed interfaces
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// This is equivalent to io.ReadWriteCloser
type ReadWriteCloser2 interface {
io.Reader
io.Writer
io.Closer
}
// Example implementation
type File struct {
name string
}
func (f *File) Read(p []byte) (int, error) {
fmt.Printf("Reading from %s\n", f.name)
return 0, io.EOF
}
func (f *File) Write(p []byte) (int, error) {
fmt.Printf("Writing %d bytes to %s\n", len(p), f.name)
return len(p), nil
}
func (f *File) Close() error {
fmt.Printf("Closing %s\n", f.name)
return nil
}
func ProcessFile(rwc ReadWriteCloser) {
defer rwc.Close()
rwc.Write([]byte("some data"))
buf := make([]byte, 1024)
rwc.Read(buf)
}
func main() {
f := &File{name: "example.txt"}
ProcessFile(f)
// Output:
// Writing 9 bytes to example.txt
// Reading from example.txt
// Closing example.txt
}
Named Interface Composition
package main
import "fmt"
type Animal interface {
Name() string
Sound() string
}
type Mover interface {
Move() string
}
// Composed interface with additional method
type Pet interface {
Animal
Mover
Play() string
}
type Dog struct {
name string
}
func (d Dog) Name() string { return d.name }
func (d Dog) Sound() string { return "Woof!" }
func (d Dog) Move() string { return "Running" }
func (d Dog) Play() string { return "Fetching ball" }
func DescribePet(p Pet) {
fmt.Printf("%s says %s\n", p.Name(), p.Sound())
fmt.Printf("%s is %s\n", p.Name(), p.Move())
fmt.Printf("%s is %s\n", p.Name(), p.Play())
}
func main() {
dog := Dog{name: "Buddy"}
DescribePet(dog)
// Output:
// Buddy says Woof!
// Buddy is Running
// Buddy is Fetching ball
}
Best Practices
1. Keep Interfaces Small
Prefer many small interfaces over few large ones:
// Good - Small, focused interfaces
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
// Less ideal - Large interface
type FileHandler interface {
Read([]byte) (int, error)
Write([]byte) (int, error)
Seek(int64, int) (int64, error)
Close() error
Stat() (FileInfo, error)
Chmod(FileMode) error
// ... many more methods
}
2. Accept Interfaces, Return Concrete Types
// Good - Accept interface
func SaveData(w io.Writer, data []byte) error {
_, err := w.Write(data)
return err
}
// Good - Return concrete type
func OpenFile(name string) (*os.File, error) {
return os.Open(name)
}
// Less ideal - Returning interface when concrete type would be better
func OpenFile2(name string) (io.ReadWriteCloser, error) {
return os.Open(name)
}
3. Design Interfaces at the Point of Use
Don't define interfaces before you need them:
// Define interface where it's used, not where types are defined
package consumer
type DataStore interface {
Get(key string) (string, error)
Set(key string, value string) error
}
func ProcessData(ds DataStore, key string) error {
value, err := ds.Get(key)
if err != nil {
return err
}
// Process value...
return ds.Set(key, processedValue)
}
4. Use Interface{} Sparingly
Prefer specific types when possible:
// Less ideal - loses type safety
func Add(a, b interface{}) interface{} {
// Need type assertions...
}
// Better - type safe
func AddInts(a, b int) int {
return a + b
}
// Or use generics (Go 1.18+)
func Add[T ~int | ~float64](a, b T) T {
return a + b
}
5. Pointer vs Value Receivers
Be consistent with receiver types:
type Counter struct {
count int
}
// If one method has pointer receiver, usually all should
func (c *Counter) Increment() {
c.count++
}
func (c *Counter) Value() int {
return c.count
}
// Interface is satisfied by *Counter, not Counter
type Incrementer interface {
Increment()
Value() int
}
6. Document Interface Contracts
// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
type Writer interface {
Write(p []byte) (n int, error)
}
7. Testing with Interfaces
Interfaces make testing easier:
package main
import (
"fmt"
"errors"
)
type Database interface {
Get(id string) (string, error)
Set(id, value string) error
}
type Service struct {
db Database
}
func (s *Service) GetUser(id string) (string, error) {
return s.db.Get(id)
}
// Mock for testing
type MockDB struct {
data map[string]string
}
func (m *MockDB) Get(id string) (string, error) {
if val, ok := m.data[id]; ok {
return val, nil
}
return "", errors.New("not found")
}
func (m *MockDB) Set(id, value string) error {
m.data[id] = value
return nil
}
func main() {
// In tests, use mock
mock := &MockDB{data: map[string]string{"1": "Alice"}}
service := Service{db: mock}
user, _ := service.GetUser("1")
fmt.Println(user) // Alice
}
Advanced Example: Plugin System
Here's a practical example showing how interfaces enable extensible design:
package main
import (
"fmt"
"strings"
)
// Plugin interface
type TextProcessor interface {
Process(text string) string
Name() string
}
// Plugin implementations
type UppercaseProcessor struct{}
func (u UppercaseProcessor) Process(text string) string {
return strings.ToUpper(text)
}
func (u UppercaseProcessor) Name() string {
return "Uppercase"
}
type ReverseProcessor struct{}
func (r ReverseProcessor) Process(text string) string {
runes := []rune(text)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
func (r ReverseProcessor) Name() string {
return "Reverse"
}
type TrimProcessor struct{}
func (t TrimProcessor) Process(text string) string {
return strings.TrimSpace(text)
}
func (t TrimProcessor) Name() string {
return "Trim"
}
// Pipeline that uses multiple processors
type Pipeline struct {
processors []TextProcessor
}
func (p *Pipeline) AddProcessor(proc TextProcessor) {
p.processors = append(p.processors, proc)
}
func (p *Pipeline) Process(text string) string {
result := text
for _, proc := range p.processors {
fmt.Printf("Applying %s processor\n", proc.Name())
result = proc.Process(result)
}
return result
}
func main() {
pipeline := &Pipeline{}
// Add processors dynamically
pipeline.AddProcessor(TrimProcessor{})
pipeline.AddProcessor(UppercaseProcessor{})
pipeline.AddProcessor(ReverseProcessor{})
input := " Hello, World! "
output := pipeline.Process(input)
fmt.Printf("Input: '%s'\n", input)
fmt.Printf("Output: '%s'\n", output)
// Output:
// Applying Trim processor
// Applying Uppercase processor
// Applying Reverse processor
// Input: ' Hello, World! '
// Output: '!DLROW ,OLLEH'
}
Summary
Interfaces are a cornerstone of Go's type system, enabling:
- Polymorphism: Different types can be used interchangeably if they satisfy the same interface
- Decoupling: Code can depend on behavior (interfaces) rather than concrete types
- Testability: Easy to create mock implementations for testing
- Composition: Build complex behaviors from simple interfaces
- Flexibility: Add new types without changing existing code
Remember:
- Keep interfaces small and focused
- Define interfaces where they're used
- Accept interfaces, return concrete types
- Use empty interface sparingly
- Document interface contracts clearly
- Leverage interfaces for better testing
Interfaces are implicitly satisfied in Go, which means types automatically implement an interface if they have the required methods. This makes Go's interface system both powerful and easy to use, encouraging good design practices and clean, modular code.