Code of the Day
IntermediateInterfaces & errors

Lab: interfaces, assertions, and errors

Scenario questions that test interface satisfaction, type assertions, and error wrapping patterns in Go.

Lab · optionalGoIntermediate15 min
Recommended first
By the end of this lesson you will be able to:
  • Identify whether a type satisfies a given interface
  • Predict the behaviour of type assertions and type switches
  • Trace an error chain built with %w and Unwrap

Optional lab. These scenario questions reinforce the ideas from the Interfaces and errors module. Work through them in order — each builds on the previous — and check the explanations carefully when you're wrong.

Interface satisfaction scenarios

Knowledge check

  1. 1.
    You define:

    type Printer interface { Print() }
    type Doc struct{ Text string }
    func (d Doc) Print() { fmt.Println(d.Text) }

    Which assignment compiles?
  2. 2.
    You change Print() to use a pointer receiver: func (d *Doc) Print(). Now which assignment compiles?
  3. 3.
    Every Go type satisfies the empty interface interface{}.

Type assertion and type switch scenarios

Knowledge check

  1. 1.
    var i interface{} = "hello"
    n := i.(int) // single-return form
    What happens at runtime?
  2. 2.
    In a type switch, what does the default case match?

Error wrapping scenarios

Knowledge check

  1. 1.
    Consider:

    var ErrTimeout = errors.New("timeout")
    inner := fmt.Errorf("db query: %w", ErrTimeout)
    outer := fmt.Errorf("handler: %w", inner)

    What does errors.Is(outer, ErrTimeout) return?
  2. 2.
    You want to check whether an error chain contains a *ValidationError and retrieve its Field. Which function do you use?
  3. 3.
    Using fmt.Errorf("context: %v", err) preserves the error chain so errors.Is can find the original error.

Putting it together

Here is a pattern you will see in production Go code. Read it and make sure each piece makes sense:

var ErrNotFound = errors.New("not found")

type StoreError struct {
    Op  string
    Err error
}

func (e *StoreError) Error() string {
    return fmt.Sprintf("store.%s: %v", e.Op, e.Err)
}

func (e *StoreError) Unwrap() error {
    return e.Err
}

func getUser(id int) (*User, error) {
    u := db[id]
    if u == nil {
        return nil, &StoreError{Op: "GetUser", Err: ErrNotFound}
    }
    return u, nil
}

// In the caller:
err := getUser(42)
if errors.Is(err, ErrNotFound) {
    // respond with 404
}
var se *StoreError
if errors.As(err, &se) {
    log.Printf("store op failed: %s", se.Op)
}

The StoreError adds structured context (Op) while keeping the sentinel ErrNotFound reachable through the chain. This is idiomatic Go error design.

Where to go next

The next module, Packages & testing, covers how Go organises code at scale — package design, modules, and writing tests that give you confidence to refactor.

Finished reading? Mark it complete to track your progress.

On this page