Interfaces
Interfaces are Go's way of achieving polymorphism. They define behavior, not data.
The Key Insight
Go interfaces are implicit: a type implements an interface by implementing its methods, without declaring it. This is sometimes called "duck typing" (if it walks like a duck and quacks like a duck, it's a duck).
Defining Interfaces
// Interface with one method
type Reader interface {
Read(p []byte) (n int, err error)
}
// Interface with multiple methods
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
// Embedding interfaces
type ReadWriteCloser interface {
Reader // Embeds Reader interface
Writer // Embeds Writer interface
Close() error
}
Implementing Interfaces
No explicit declaration needed. Just implement the methods:
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
// Person now implements Stringer
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
// Can be used anywhere Stringer is expected
func PrintAnything(s Stringer) {
fmt.Println(s.String())
}
p := Person{Name: "Alice", Age: 30}
PrintAnything(p) // "Alice (30 years old)"
The Empty Interface
interface{} (or any in Go 1.18+) matches any type:
func PrintAny(v interface{}) {
fmt.Println(v)
}
// Or with the any alias (Go 1.18+)
func PrintAny(v any) {
fmt.Println(v)
}
PrintAny(42)
PrintAny("hello")
PrintAny([]int{1, 2, 3})
PrintAny(struct{ Name string }{"Alice"})
Type Assertions
Extract the concrete type from an interface:
var i interface{} = "hello"
// Type assertion (panics if wrong type)
s := i.(string)
fmt.Println(s) // "hello"
// Safe type assertion
s, ok := i.(string)
if ok {
fmt.Println("It's a string:", s)
}
// Wrong type with comma-ok
n, ok := i.(int)
if !ok {
fmt.Println("Not an int")
}
Type Switches
Check multiple types:
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Integer %d\n", v)
case string:
fmt.Printf("String %q\n", v)
case bool:
fmt.Printf("Boolean %t\n", v)
case nil:
fmt.Println("Nil value")
default:
fmt.Printf("Unknown type %T\n", v)
}
}
describe(42) // Integer 42
describe("hello") // String "hello"
describe(nil) // Nil value
Common Standard Library Interfaces
Stringer
type Stringer interface {
String() string
}
// Used by fmt.Print, fmt.Printf with %v and %s
type IPAddr [4]byte
func (ip IPAddr) String() string {
return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}
Error
type error interface {
Error() string
}
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
io.Reader and io.Writer
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Many types implement these: files, network connections, buffers...
func Copy(dst io.Writer, src io.Reader) (written int64, err error)
sort.Interface
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
// Custom sorting
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
people := []Person{
{"Bob", 30},
{"Alice", 25},
{"Charlie", 35},
}
sort.Sort(ByAge(people))
Interface Best Practices
Accept Interfaces, Return Structs
// Good: accepts interface (flexible)
func Process(r io.Reader) error {
// Works with files, network, buffers, etc.
}
// Good: returns concrete type
func NewBuffer() *bytes.Buffer {
return &bytes.Buffer{}
}
Keep Interfaces Small
// Good: focused interface
type Reader interface {
Read(p []byte) (n int, err error)
}
// Avoid: bloated interfaces
type FileHandler interface {
Open() error
Read([]byte) (int, error)
Write([]byte) (int, error)
Close() error
Seek(int64, int) (int64, error)
Stat() (os.FileInfo, error)
// ...too many methods
}
Define Interfaces Where They're Used
// Package database
type Store struct { /* ... */ }
func (s *Store) GetUser(id int) (*User, error) { /* ... */ }
// Package handler - defines its own interface
type UserGetter interface {
GetUser(id int) (*User, error)
}
type Handler struct {
users UserGetter // Depends on interface, not concrete type
}
Interface Values
An interface value has two components: a type and a value.
var w io.Writer // nil interface
fmt.Printf("%T %v\n", w, w) // <nil> <nil>
w = os.Stdout // *os.File
fmt.Printf("%T %v\n", w, w) // *os.File &{...}
w = new(bytes.Buffer) // *bytes.Buffer
fmt.Printf("%T %v\n", w, w) // *bytes.Buffer &{}
Nil Interface vs Nil Concrete Value
var w io.Writer // nil interface
var buf *bytes.Buffer // nil pointer
w = buf // interface is NOT nil!
if w == nil {
fmt.Println("nil") // Won't print!
}
// w has type *bytes.Buffer with nil value
fmt.Printf("Type: %T, Value: %v, IsNil: %t\n", w, w, w == nil)
// Type: *bytes.Buffer, Value: <nil>, IsNil: false
This is a common gotcha. Solution:
func returnsWriter() io.Writer {
var buf *bytes.Buffer
// ...
if buf == nil {
return nil // Return nil interface, not nil concrete
}
return buf
}
Interface Composition
Build larger interfaces from smaller ones:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composed interfaces
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// Or inline
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}
Interfaces with Generics (Go 1.18+)
Type Constraints
// Interface as type constraint
type Number interface {
int | int32 | int64 | float32 | float64
}
func Sum[T Number](nums []T) T {
var sum T
for _, n := range nums {
sum += n
}
return sum
}
// Using comparable
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
Constraint with Methods
type Stringer interface {
String() string
}
func PrintAll[T Stringer](items []T) {
for _, item := range items {
fmt.Println(item.String())
}
}
Testing with Interfaces
Interfaces enable easy mocking:
// In production code
type EmailSender interface {
Send(to, subject, body string) error
}
type UserService struct {
emailer EmailSender
}
func (s *UserService) Register(email string) error {
// ... create user
return s.emailer.Send(email, "Welcome!", "Thanks for joining")
}
// In test code
type MockEmailer struct {
Sent []struct{ To, Subject, Body string }
}
func (m *MockEmailer) Send(to, subject, body string) error {
m.Sent = append(m.Sent, struct{ To, Subject, Body string }{to, subject, body})
return nil
}
func TestRegister(t *testing.T) {
mock := &MockEmailer{}
service := &UserService{emailer: mock}
service.Register("test@example.com")
if len(mock.Sent) != 1 {
t.Error("Expected one email sent")
}
}
Verifying Interface Implementation
Compile-time check that a type implements an interface:
// Ensure *MyWriter implements io.Writer
var _ io.Writer = (*MyWriter)(nil)
// Ensure MyStruct implements fmt.Stringer
var _ fmt.Stringer = MyStruct{}
Next Steps
Continue to 08-error-handling.md to learn about Go's error handling philosophy.