GO

Go Fundamentals

18 lessons

Progress0%
1. Introduction to Go
1What is Go?
2. Variables and Data Types
1Data Types in Go
3. Control Flow
If, For, and SwitchDefer, Panic, Recover
4. Functions
Function BasicsError Handling
5. Structs and Methods
StructsMethods and Interfaces
6. Concurrency
Goroutines and ChannelsSelect and Sync
7. Maps & Slices Advanced
Slices Deep DiveMaps Operations & Patterns
8. Interfaces Deep Dive
Interface Composition & anyCommon Interfaces & Patterns
9. Packages & Modules
Package SystemGo Modules & Workspace
10. Testing & Standard Library
Testing in GoStandard Library Essentials
All Tutorials
GoInterfaces Deep Dive
Lesson 13 of 18 min
Chapter 8 · Lesson 1

Interface Composition & any

Interface Composition & any

Go interfaces are satisfied implicitly — a type implements an interface simply by having the required methods. This makes interfaces extraordinarily flexible and promotes loose coupling.

Embedding Interfaces Just as structs can embed other structs, interfaces can embed other interfaces. The embedding composes the method sets:

go
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type ReadWriter interface {
    Reader
    Writer
}

A type that implements both Read and Write automatically satisfies ReadWriter. The standard library builds rich hierarchies this way (e.g., io.ReadWriteCloser).

The Empty Interface — any any (an alias for interface{} since Go 1.18) is satisfied by every type because it has no method requirements:

go
func printAnything(v any) {
    fmt.Println(v)
}

Use any sparingly; prefer concrete types or specific interfaces for type safety.

Type Assertions Extract the concrete value from an interface with a type assertion:

go
var i any = "hello"
s := i.(string)          // panics if i is not a string
s, ok := i.(string)      // safe; ok is false if type mismatch

Always use the comma-ok form unless you are certain of the underlying type.

Type Switches A type switch handles multiple possible concrete types cleanly:

go
switch v := i.(type) {
case string:  fmt.Println("string:", v)
case int:     fmt.Println("int:", v)
default:      fmt.Println("unknown type")
}

fmt.Stringer Any type implementing the Stringer interface controls its own string representation:

go
type Stringer interface { String() string }

fmt.Println calls String() automatically when available.

The error Interface error is just a built-in interface:

go
type error interface { Error() string }

Any type with an Error() string method satisfies error.

The Nil Interface Pitfall An interface value has two components: a concrete type and a concrete value. An interface is nil only when both are nil. Storing a nil pointer of a concrete type inside an interface creates a non-nil interface — a common trap:

go
var p *MyError = nil     // typed nil pointer
var err error = p        // err is NOT nil (type is set)
fmt.Println(err == nil)  // false!

Return the interface type directly (return nil) rather than returning a typed nil pointer.

Code Examples

Interface embedding and compositiongo
package main

import (
    "fmt"
    "strings"
)

// Small interface building blocks
type Reader interface {
    Read() string
}

type Writer interface {
    Write(s string)
}

// Composed interface via embedding
type ReadWriter interface {
    Reader
    Writer
}

// Buffer satisfies ReadWriter
type Buffer struct {
    data strings.Builder
}

func (b *Buffer) Write(s string) { b.data.WriteString(s) }
func (b *Buffer) Read() string   { return b.data.String() }

// Stringer implementation
func (b *Buffer) String() string {
    return fmt.Sprintf("Buffer(%q)", b.Read())
}

func copyContent(rw ReadWriter, src Reader) {
    rw.Write(src.Read())
}

func main() {
    src := &Buffer{}
    src.Write("Hello, interfaces!")

    dst := &Buffer{}
    copyContent(dst, src)

    fmt.Println(dst) // calls String() automatically
    fmt.Printf("src still has data: %q\n", src.Read())
}

Buffer satisfies the composed ReadWriter because it has both Read and Write methods. fmt.Println calls String() via the Stringer interface automatically.

Type assertions and type switches over anygo
package main

import "fmt"

func describe(i any) string {
    switch v := i.(type) {
    case nil:
        return "nil"
    case int:
        return fmt.Sprintf("int: %d", v)
    case float64:
        return fmt.Sprintf("float64: %.2f", v)
    case string:
        return fmt.Sprintf("string: %q (len=%d)", v, len(v))
    case bool:
        return fmt.Sprintf("bool: %t", v)
    case []int:
        return fmt.Sprintf("[]int with %d elements", len(v))
    default:
        return fmt.Sprintf("unknown type: %T", v)
    }
}

func main() {
    values := []any{42, 3.14, "hello", true, nil, []int{1, 2, 3}, struct{}{}}
    for _, v := range values {
        fmt.Println(describe(v))
    }

    // Safe type assertion with comma-ok
    var i any = "world"
    if s, ok := i.(string); ok {
        fmt.Println("\nExtracted string:", s)
    }

    // Unsafe assertion would panic — demonstrate safe form
    if n, ok := i.(int); ok {
        fmt.Println("int:", n)
    } else {
        fmt.Println("Not an int, ok =", ok)
    }
}

The type switch variable v is already typed in each case branch — no cast needed. The comma-ok type assertion avoids panics when the dynamic type might not match.

The nil interface trapgo
package main

import "fmt"

type AppError struct{ Msg string }

func (e *AppError) Error() string { return e.Msg }

// BAD: returns a typed nil — caller's err != nil check passes
func riskyOp(fail bool) error {
    var e *AppError // typed nil pointer
    if fail {
        e = &AppError{"something failed"}
    }
    return e // always wraps *AppError in error interface
}

// GOOD: return untyped nil when there is no error
func safeOp(fail bool) error {
    if fail {
        return &AppError{"something failed"}
    }
    return nil // untyped nil — interface value is truly nil
}

func main() {
    err := riskyOp(false)
    fmt.Println("riskyOp(false) err == nil:", err == nil)  // false!
    fmt.Printf("riskyOp(false) err value: %v\n", err)

    err2 := safeOp(false)
    fmt.Println("safeOp(false) err == nil: ", err2 == nil) // true

    err3 := safeOp(true)
    fmt.Println("safeOp(true)  err == nil: ", err3 == nil) // false
    fmt.Println("safeOp(true)  error:      ", err3)
}

When a typed nil *AppError is stored in the error interface, the interface itself is non-nil because the type field is set. Always return a bare nil from error-returning functions to signal success.

Quick Quiz

1. How does a type satisfy an interface in Go?

2. When is an interface value considered nil in Go?

3. What does the comma-ok idiom `s, ok := i.(string)` protect against?

4. What is `any` in Go 1.18+?

Was this lesson helpful?

PreviousNext