Skip to content

Go Interfaces โ€” Implicit Satisfaction Type Assertions and Empty Interface Explained

DodaTech Updated 2026-06-28 9 min read

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

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go