Interfaces & Type Assertions

📋 Jump to Takeaways

Interfaces 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 you

Empty 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)    // true

Type 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 false

Type 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: true

Common 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 found

Interface 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 nil

This 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) // int

Key Takeaways

  • Interfaces are satisfied implicitly — just implement the methods
  • any (or interface{}) 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.Reader are small by design
  • A nil interface ≠ an interface holding a nil value — check both

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

© 2026 ByteLearn.dev. Free courses for developers. · Privacy