Interfaces in Go
Understand Go's implicit interface implementation, the empty interface, type assertions, type switches, and common interface patterns.
Defining and Implementing Interfaces
An interface in Go is a named collection of method signatures. Any type that has all those methods satisfies the interface — no implements keyword, no registration, no explicit declaration. This implicit satisfaction is one of Go’s most powerful design choices: it means you can write an interface after the fact to match types that already exist, and third-party types can satisfy your interfaces without modifying their source code.
type Shape interface {
Area() float64
Perimeter() float64
}
type Rectangle struct {
Width, Height float64
}
// Rectangle satisfies Shape by having both required methods — no declaration needed
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
type Circle struct {
Radius float64
}
// Circle also satisfies Shape — both types can be used wherever Shape is expected
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func printShape(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
printShape(Rectangle{10, 5})
printShape(Circle{7})
}
Interfaces as Behavior Contracts
The real power of interfaces is decoupling: a function that accepts an interface does not know or care what concrete type it receives, only that the type can do what the interface promises. This makes code testable (you can swap in a fake implementation for tests), extensible (new implementations work without changing existing code), and composable (implementations can be mixed and matched freely).
type Logger interface {
Log(level, message string)
}
// ConsoleLogger writes to standard output
type ConsoleLogger struct{}
func (l ConsoleLogger) Log(level, message string) {
fmt.Printf("[%s] %s\n", level, message)
}
// FileLogger writes to a file — same interface, completely different implementation
type FileLogger struct {
file *os.File
}
func (l *FileLogger) Log(level, message string) {
fmt.Fprintf(l.file, "[%s] %s\n", level, message)
}
// process doesn't know which Logger it gets — it just calls Log
// In tests you can pass a mock; in production you pass ConsoleLogger or FileLogger
func process(data string, log Logger) {
log.Log("INFO", "Processing: "+data)
// ...
log.Log("INFO", "Done")
}
The empty interface — any
interface{} (aliased as any since Go 1.18) specifies no methods, so every type in Go satisfies it. It is the mechanism behind functions like fmt.Println that must accept arguments of any type. Before generics were added in Go 1.18, any was also used for generic containers. Prefer typed alternatives when possible — any sacrifices compile-time type checking, and every value stored in an any must be type-asserted before you can use it as its original type.
func describe(i any) {
fmt.Printf("(%v, %T)\n", i, i)
}
describe(42) // (42, int)
describe("hello") // (hello, string)
describe(true) // (true, bool)
describe([]int{1,2}) // ([1 2], []int)
// any was commonly used for generic containers before Go 1.18 generics
type Stack struct {
items []any
}
func (s *Stack) Push(v any) { s.items = append(s.items, v) }
func (s *Stack) Pop() any {
n := len(s.items)
v := s.items[n-1]
s.items = s.items[:n-1]
return v
}
Prefer typed alternatives (generics, concrete types) over any when possible — using any loses compile-time type safety.
Type Assertions
A type assertion extracts the concrete value stored inside an interface variable. The single-value form panics if the assertion is wrong — use it only when you are certain of the type. The two-value form is safe: it returns the zero value and false rather than panicking, which is almost always what you want.
var i any = "hello"
// Single-value form — panics if wrong type; only use when you are certain
s := i.(string)
fmt.Println(s) // hello
// Two-value form — safe, never panics
s, ok := i.(string)
fmt.Println(s, ok) // hello true
n, ok := i.(int)
fmt.Println(n, ok) // 0 false — assertion failed but no panic
// Single-value form with wrong type panics at runtime:
// n := i.(int) // panic: interface conversion: interface {} is string, not int
Type Switches
A type switch dispatches on the dynamic (runtime) type stored in an interface value. It is cleaner than a chain of type assertions and is the standard way to write code that behaves differently depending on what type it receives — common in serialization, protocol handling, and plugin architectures.
func formatValue(v any) string {
switch val := v.(type) {
case int:
return fmt.Sprintf("int(%d)", val)
case float64:
return fmt.Sprintf("float64(%.2f)", val)
case string:
return fmt.Sprintf("string(%q)", val)
case bool:
return fmt.Sprintf("bool(%t)", val)
case []int:
return fmt.Sprintf("[]int(len=%d)", len(val))
case nil:
return "nil"
default:
// val has type any here — use %T to show the actual type
return fmt.Sprintf("unknown(%T)", val)
}
}
fmt.Println(formatValue(42)) // int(42)
fmt.Println(formatValue(3.14)) // float64(3.14)
fmt.Println(formatValue("hello")) // string("hello")
fmt.Println(formatValue([]int{1,2})) // []int(len=2)
Common Standard Library Interfaces
The standard library defines a small set of interfaces that appear throughout Go code. Implementing these interfaces makes your types work with a wide range of standard library functions and third-party packages, which is why knowing them matters even for beginners.
// io.Reader — anything you can read bytes from (files, network, strings, etc.)
type Reader interface {
Read(p []byte) (n int, err error)
}
// io.Writer — anything you can write bytes to
type Writer interface {
Write(p []byte) (n int, err error)
}
// io.Closer — anything that holds a resource and must be released
type Closer interface {
Close() error
}
// io.ReadWriteCloser — composed interface: a type that satisfies Reader, Writer, and Closer
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// fmt.Stringer — implement this to control how your type is printed by fmt
type Stringer interface {
String() string
}
// error — the built-in error interface; implement it to create custom error types
type error interface {
Error() string
}
Interface Composition
Interfaces can embed other interfaces, building larger contracts from smaller ones. This encourages designing minimal interfaces — a function that only needs to read data should accept io.Reader, not io.ReadWriteCloser. Small interfaces are easier to satisfy, easier to mock in tests, and communicate intent more precisely.
type Reader interface {
Read() ([]byte, error)
}
type Writer interface {
Write([]byte) error
}
// ReadWriter is satisfied by any type that has both Read and Write
type ReadWriter interface {
Reader
Writer
}
// Buffer satisfies ReadWriter because it implements both methods
type Buffer struct {
data []byte
pos int
}
func (b *Buffer) Read() ([]byte, error) {
if b.pos >= len(b.data) {
return nil, io.EOF
}
chunk := b.data[b.pos:]
b.pos = len(b.data)
return chunk, nil
}
func (b *Buffer) Write(p []byte) error {
b.data = append(b.data, p...)
return nil
}
Nil Interface vs Nil Concrete Value
This is one of the most subtle gotchas in Go. An interface value is only nil if both its type and its value are nil. If you assign a nil pointer of a concrete type to an interface variable, the interface is not nil — it has a type, just a nil value. This causes confusing bugs where err != nil is true even though the underlying pointer is nil.
var err error // nil interface — type is nil, value is nil
fmt.Println(err == nil) // true
var p *MyError = nil
var err2 error = p // non-nil interface! type is *MyError, value is nil
fmt.Println(err2 == nil) // false — the interface has a type component, even though p is nil
// This is why functions that return error should always return the untyped nil, not a typed nil pointer
func doWork() error {
var err *MyError = nil
if false {
err = &MyError{"something went wrong"}
}
return err // BUG: returns a non-nil interface even when err pointer is nil
}
// Correct: return untyped nil directly
func doWorkCorrect() error {
// ...
return nil // interface type and value are both nil — callers can safely check == nil
}
Practical Example — Plugin Architecture
Interfaces enable a plugin architecture where the core system defines a contract and implementations are added independently. The MultiNotifier here is itself a Notifier — it composes multiple notifiers using the same interface it satisfies, a pattern that scales to any number of implementations.
package main
import "fmt"
// Notifier defines what all notification backends must be able to do
type Notifier interface {
Notify(message string) error
}
type EmailNotifier struct {
To string
}
func (e EmailNotifier) Notify(message string) error {
fmt.Printf("Email to %s: %s\n", e.To, message)
return nil
}
type SlackNotifier struct {
Channel string
}
func (s SlackNotifier) Notify(message string) error {
fmt.Printf("Slack #%s: %s\n", s.Channel, message)
return nil
}
// MultiNotifier satisfies Notifier and fans out to multiple backends
// New backends can be added without changing this struct or any call site
type MultiNotifier struct {
notifiers []Notifier
}
func (m *MultiNotifier) Add(n Notifier) {
m.notifiers = append(m.notifiers, n)
}
func (m MultiNotifier) Notify(message string) error {
for _, n := range m.notifiers {
if err := n.Notify(message); err != nil {
return err
}
}
return nil
}
func main() {
multi := &MultiNotifier{}
multi.Add(EmailNotifier{To: "[email protected]"})
multi.Add(SlackNotifier{Channel: "alerts"})
multi.Notify("Deployment complete!")
// Email to [email protected]: Deployment complete!
// Slack #alerts: Deployment complete!
}