Interfaces & Type Assertions
📋 Jump to TakeawaysInterfaces are the secret weapon of Go. They're the reason the standard library is so composable. io.Reader and io.Writer alone power files, HTTP bodies, compression, encryption, and network connections, all with the same interface. And you never have to write implements. If your type has the right methods, it just works.
What Interfaces Are
An interface defines a set of method signatures. Any type that implements all those methods satisfies the interface automatically. No implements keyword needed.
// Defined in the fmt package. Any type that has a String() method
// satisfies this interface, and fmt.Println will use it automatically.
type Stringer interface {
String() string
}Implicit Implementation
A type satisfies an interface just by having the right methods.
type User struct {
Name string
}
func (u User) String() string {
return "User: " + u.Name
}
// User satisfies Stringer — no declaration needed
var s Stringer = User{Name: "Alice"}
fmt.Println(s.String()) // User: Alice
fmt.Println(s) // User: Alice — same thing, fmt calls String() for youEmpty Interface
interface{} (or any in Go 1.18+) accepts any type.
func printVal(v any) {
fmt.Println(v)
}
printVal(42) // 42
printVal("hello") // hello
printVal(true) // trueType Assertions
A type assertion extracts the concrete type from an interface value. Use the comma-ok pattern to avoid panics.
var val any = "hello"
// Direct assertion — panics if wrong type
s := val.(string)
fmt.Println(s) // hello
// Comma-ok pattern — safe
n, ok := val.(int)
fmt.Println(n, ok) // 0 falseType Switches
A type switch checks the concrete type of an interface value.
func describe(val any) string {
switch v := val.(type) {
case string:
return "string: " + v
case int:
return fmt.Sprintf("int: %d", v)
case bool:
return fmt.Sprintf("bool: %t", v)
default:
return "unknown"
}
}
fmt.Println(describe("hi")) // string: hi
fmt.Println(describe(42)) // int: 42
fmt.Println(describe(true)) // bool: trueCommon Interfaces
Go's standard library relies heavily on small interfaces.
// fmt.Stringer — controls how a type prints
type Stringer interface {
String() string
}
// error — the built-in error interface
type error interface {
Error() string
}
// io.Reader and io.Writer — the foundation of I/O
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}Implementing error:
type AppError struct {
Code int
Message string
}
func (e AppError) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message)
}
var err error = AppError{Code: 404, Message: "not found"}
fmt.Println(err) // 404: not foundInterface Values
An interface value holds a (type, value) pair. A nil interface is different from an interface holding a nil pointer.
var s Stringer // nil interface — both type and value are nil
fmt.Println(s == nil) // true
var u *User // nil pointer
s = u // interface holds (*User, nil)
fmt.Println(s == nil) // false — the interface itself is not nilThis is a common gotcha. An interface is only nil when both its type and value are nil.
You can inspect the concrete type inside an interface with %T:
var val any = "hello"
fmt.Printf("%T\n", val) // string
val = 42
fmt.Printf("%T\n", val) // intKey Takeaways
- Interfaces are satisfied implicitly — just implement the methods
any(orinterface{}) accepts any type- Use the comma-ok pattern with type assertions to avoid panics
- Type switches cleanly handle multiple possible types
- Common interfaces like
error,Stringer,io.Readerare small by design - A nil interface ≠ an interface holding a nil value — check both