Go Interfaces โ Implicit Satisfaction Type Assertions and Empty Interface Explained
In this tutorial, you will learn about Go Interfaces. We cover key concepts, practical examples, and best practices to help you master this topic.
Go interfaces define behavior through method sets with implicit satisfactionโtypes automatically satisfy interfaces by implementing their methods, enabling polymorphism without explicit declarations.
What You'll Learn
- Defining and implementing interfaces
- Type assertions and type switches
- The empty interface
- Interface composition and embedding
Why It Matters
Interfaces are Go's primary abstraction mechanism. Docker uses interfaces for storage drivers, network drivers, and filesystems. Kubernetes uses interfaces for controllers, informers, and clients. DodaZIP uses interfaces for archive format backends.
Real-World Use
io.Reader and io.Writer are the most famous Go interfaces. Every HTTP handler implements http.Handler. Database drivers satisfy sql/driver interfaces.
flowchart LR
A["Interfaces"] --> B["Definition"]
B --> C["Satisfaction"]
C --> D["Type Assertions"]
D --> E["Composition"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Defining Interfaces
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct {
Radius float64
}
// Circle implements Shape implicitly
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
func printShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
c := Circle{Radius: 5}
printShapeInfo(c) // Area: 78.54, Perimeter: 31.42
}
Implicit Interface Satisfaction
A type satisfies an interface automatically by implementing its methods:
type Writer interface {
Write([]byte) (int, error)
}
// bytes.Buffer satisfies Writer without declaring it
// os.File satisfies Writer
// net.Conn satisfies Writer
// Custom type satisfies Writer
type Logger struct{}
func (l Logger) Write(data []byte) (int, error) {
fmt.Println(string(data))
return len(data), nil
}
func writeHello(w Writer) {
w.Write([]byte("Hello, interface!"))
}
Multiple Interfaces
A type can satisfy multiple interfaces:
type Reader interface {
Read([]byte) (int, error)
}
type Closer interface {
Close() error
}
// ReadWriter combines two interfaces
type ReadWriter interface {
Reader
Writer
}
// File satisfies Reader, Writer, Closer, and ReadWriter
type File struct{}
func (f File) Read(p []byte) (int, error) { return 0, nil }
func (f File) Write(p []byte) (int, error) { return len(p), nil }
func (f File) Close() error { return nil }
func main() {
var r Reader = File{}
var w Writer = File{}
var rw ReadWriter = File{}
_ = r
_ = w
_ = rw
}
Type Assertions
Extract the concrete type from an interface:
func process(i interface{}) {
// Type assertion with check
s, ok := i.(string)
if ok {
fmt.Println("Got string:", s)
return
}
n, ok := i.(int)
if ok {
fmt.Println("Got int:", n)
return
}
fmt.Println("Unknown type")
}
func main() {
process("hello") // Got string: hello
process(42) // Got int: 42
process(3.14) // Unknown type
}
Type Switches
Handle multiple types with a switch:
func describe(i interface{}) {
switch v := i.(type) {
case string:
fmt.Printf("String (%d chars): %s\n", len(v), v)
case int:
fmt.Printf("Int: %d\n", v)
case float64:
fmt.Printf("Float: %.2f\n", v)
case bool:
fmt.Printf("Bool: %v\n", v)
case nil:
fmt.Println("Nil")
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
func main() {
describe("hello") // String (5 chars): hello
describe(42) // Int: 42
describe(3.14) // Float: 3.14
describe(true) // Bool: true
describe(nil) // Nil
describe([]int{1, 2, 3}) // Unknown type: []int
}
The Empty Interface
interface{} accepts any type:
func printAny(v interface{}) {
fmt.Printf("Value: %v, Type: %T\n", v, v)
}
func main() {
printAny(42) // Value: 42, Type: int
printAny("hello") // Value: hello, Type: string
printAny([]int{1, 2}) // Value: [1 2], Type: []int
printAny(nil) // Value: nil, Type: <nil>
// Using any (Go 1.18+ alias for interface{})
var data any
data = 42
data = "hello"
data = struct{ Name string }{"Alice"}
_ = data
}
Interface as Contract
Interfaces define behavioral contracts:
type Sortable interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
type Person struct {
Name string
Age int
}
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] }
func main() {
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sort.Sort(ByAge(people))
for _, p := range people {
fmt.Printf("%s: %d\n", p.Name, p.Age)
}
// Bob: 25
// Alice: 30
// Charlie: 35
}
Interface Composition
Interfaces can be composed from smaller interfaces:
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 ReadCloser interface {
Reader
Closer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Common Mistakes
1. Interface Pollution
// Bad โ defining interfaces for everything
type UserService interface {
GetUser(id int) (*User, error)
CreateUser(u *User) error
UpdateUser(u *User) error
DeleteUser(id int) error
}
// Good โ define interfaces where they're used, not where they're implemented
// io.Reader is defined in the io package, not in every package that needs to read
2. Nil Interface vs Nil Concrete Value
func returnsNil() *int {
return nil
}
func main() {
var i interface{} = returnsNil()
fmt.Println(i == nil) // false! Interface has type *int but value nil
// Check both type and value
if i == nil {
fmt.Println("is nil")
} else {
fmt.Println("is not nil") // Prints this!
}
}
3. Not Checking Type Assertions
func getString(i interface{}) string {
// Panics if i is not a string
return i.(string)
}
// Safe version
func getStringSafe(i interface{}) (string, bool) {
s, ok := i.(string)
return s, ok
}
4. Large Interface Definitions
// Bad โ too many methods
type MegaInterface interface {
Read(p []byte) (int, error)
Write(p []byte) (int, error)
Close() error
Flush() error
Seek(offset int64, whence int) (int64, error)
Truncate(size int64) error
Sync() error
}
// Good โ small, focused interfaces
type Reader interface { Read(p []byte) (int, error) }
type Writer interface { Write(p []byte) (int, error) }
type Closer interface { Close() error }
5. Accepting interface{} Without Need
// Bad โ too permissive
func process(v interface{}) {
switch v.(type) {
case string: // ...
case int: // ...
}
}
// Good โ specific where possible
func process(s string) { /* ... */ }
func process(n int) { /* ... */ }
Practice Questions
1. How does a type satisfy an interface in Go?
Implicitly โ by implementing all of the interface's methods. No explicit implements keyword. If a type has the required methods, it satisfies the interface automatically.
2. What is a type assertion?
An operation that extracts the concrete value from an interface: value := i.(Type). Use the two-value form value, ok := i.(Type) to avoid panics.
3. What is the empty interface?
interface{} (or any in Go 1.18+) accepts any value. It has zero methods. Use it for unknown types, heterogeneous collections, or when deferring type handling.
4. What does "accept interfaces, return structs" mean?
Function parameters should use interfaces (flexible), return values should be concrete types (ease of use, no unnecessary abstraction).
Challenge: Implement a Cache interface with Get and Set methods, then provide both an in-memory and a file-backed implementation.
Solution
package main
import (
"encoding/json"
"fmt"
"os"
"sync"
"time"
)
type CacheItem struct {
Value interface{}
ExpiresAt time.Time
}
type Cache interface {
Get(key string) (interface{}, bool)
Set(key string, value interface{}, ttl time.Duration)
Delete(key string)
}
type InMemoryCache struct {
mu sync.RWMutex
items map[string]CacheItem
}
func NewInMemoryCache() *InMemoryCache {
return &InMemoryCache{items: make(map[string]CacheItem)}
}
func (c *InMemoryCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, ok := c.items[key]
if !ok || time.Now().After(item.ExpiresAt) {
return nil, false
}
return item.Value, true
}
func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = CacheItem{
Value: value,
ExpiresAt: time.Now().Add(ttl),
}
}
func (c *InMemoryCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
type FileCache struct {
path string
mu sync.RWMutex
}
func NewFileCache(path string) *FileCache {
return &FileCache{path: path}
}
func (c *FileCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
data, err := os.ReadFile(c.path + "/" + key + ".cache")
if err != nil {
return nil, false
}
var item CacheItem
json.Unmarshal(data, &item)
if time.Now().After(item.ExpiresAt) {
return nil, false
}
return item.Value, true
}
func (c *FileCache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
item := CacheItem{Value: value, ExpiresAt: time.Now().Add(ttl)}
data, _ := json.Marshal(item)
os.WriteFile(c.path+"/"+key+".cache", data, 0644)
}
func (c *FileCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
os.Remove(c.path + "/" + key + ".cache")
}
func main() {
var cache Cache
cache = NewInMemoryCache()
cache.Set("user:1", "Alice", time.Minute)
val, ok := cache.Get("user:1")
fmt.Println(val, ok) // Alice true
cache = NewFileCache("/tmp/cache")
cache.Set("config", map[string]string{"theme": "dark"}, time.Hour)
val, ok = cache.Get("config")
fmt.Println(val, ok) // map[theme:dark] true
}
FAQ
{{< faq question="Are interfaces in Go reference types?" >}} Interface values are two words: a pointer to the type information and a pointer to the data. They behave like references but comparing interface values compares both type and value. {{< /faq >}}
{{< faq question="What is the difference between interface{} and any?" >}}
None. any is an alias introduced in Go 1.18. It's equivalent to interface{}. Prefer any for new code.
{{< /faq >}}
{{< faq question="Can a struct implement multiple interfaces?" >}}
Yes. A struct can satisfy any number of interfaces. Just implement all required methods. This is how os.File satisfies Reader, Writer, Closer, and more.
{{< /faq >}}
{{< faq question="When should I define an interface?" >}
Define interfaces where they're consumed, not where they're produced. If a function needs a Reader, accept an io.Reader. Let callers decide if their type satisfies it.
{{< /faq >}}
{{< faq question="Can I add methods to an interface later?" >} Adding methods breaks existing implementations. Instead, define new interfaces that extend the old one and use type assertions to check for the new methods. {{< /faq >}}
Try It Yourself
package main
import "fmt"
type Notifier interface {
Notify(message string) error
}
type EmailNotifier struct {
Address string
}
func (e EmailNotifier) Notify(message string) error {
fmt.Printf("Email to %s: %s\n", e.Address, message)
return nil
}
type SMSNotifier struct {
Phone string
}
func (s SMSNotifier) Notify(message string) error {
fmt.Printf("SMS to %s: %s\n", s.Phone, message)
return nil
}
func SendAlert(notifier Notifier, message string) {
if err := notifier.Notify(message); err != nil {
fmt.Println("Failed:", err)
}
}
func main() {
email := EmailNotifier{Address: "admin@example.com"}
sms := SMSNotifier{Phone: "+1234567890"}
SendAlert(email, "Server down!")
SendAlert(sms, "Database full!")
// Type switch to handle different notifiers
notifiers := []Notifier{email, sms}
for _, n := range notifiers {
switch v := n.(type) {
case EmailNotifier:
fmt.Printf("Email notifier configured for %s\n", v.Address)
case SMSNotifier:
fmt.Printf("SMS notifier configured for %s\n", v.Phone)
}
}
}
Expected output:
Email to admin@example.com: Server down!
SMS to +1234567890: Database full!
Email notifier configured for admin@example.com
SMS notifier configured for +1234567890
What's Next
Now that you understand interfaces, learn about struct embedding and composition in Go.
| Topic | Description | Link |
|---|---|---|
| Go Embedding | Struct embedding, method promotion | {{< ref "12-embedding" >}} |
| Go Error Handling | Error interface, wrapping, sentinel errors | {{< ref "13-error-handling" >}} |
| Rust Traits | Compare Go interfaces with Rust Traits | Rust |