Course outline

Go / Basic: Values and Program Contracts / Lesson 6

Methods, Interfaces, and Errors

Use small behavioral contracts and explicit errors while keeping dynamic type, dynamic value, and nil distinct.

About 5 min readContent reviewed
On this page

Lesson goals

  • Explain method sets and implicit interface satisfaction.
  • Distinguish a nil interface from an interface holding a typed nil.
  • Wrap and inspect errors without matching messages.

An interface describes behavior

A method attaches behavior to a receiver type. A small interface lists the methods a consumer needs, and a type satisfies it implicitly when its method set matches. This supports substitution without declaring an inheritance relationship. Keep interfaces near consumers when that makes the required behavior clear instead of exporting a large speculative interface.

A value receiver operates on a copy of the receiver value; reference-bearing fields may still share storage. Pointer receivers can mutate the designated receiver and affect method sets: *T has methods with T or *T receivers, while T’s method set does not automatically include *T receiver methods. Some convenient method-call syntax does not change interface satisfaction rules.

Nil has both a type and a context

An interface value can be understood as a dynamic type paired with a dynamic value. It equals nil only when it has neither. Assigning a nil *Label to a Namer interface gives the interface a dynamic type, so the interface is non-nil. Calling a method may be valid if that method deliberately handles a nil receiver without dereferencing it.

The example safely demonstrates that distinction. It does not predict a panic from every method on a typed nil; the method body determines whether it accesses an absent object. A nil-interface check alone cannot establish that every dynamic receiver is non-nil. Prefer avoiding ambiguous optional interface values when an explicit result or concrete pointer is clearer.

go An interface holding a typed nil
package main

import "fmt"

type Namer interface{ Name() string }
type Label struct{ Text string }

func (p *Label) Name() string {
	if p == nil {
		return "absent"
	}
	return p.Text
}

func main() {
	var p *Label
	var n Namer = p
	fmt.Println(p == nil, n == nil, n.Name())
}
Expected output
true false absent

The concrete pointer is nil, the interface is not, and the nil-aware method returns absent.

Errors are inspectable results

The built-in error interface requires Error() string. Return a genuinely nil error interface for success; returning a typed nil error pointer can produce a non-nil interface and a false failure signal. Reserve panic for exceptional situations where that contract is appropriate, rather than routine invalid input or expected I/O failures.

Callers should inspect an error before treating the accompanying result as usable. Error text communicates context to people but should not normally be parsed as a stable program protocol. Use documented sentinels with errors.Is or documented types with errors.As when callers need machine-readable decisions.

Preserve an intentional error relationship

fmt.Errorf with %w adds context while preserving an unwrap relationship. The next program can recognize ErrMissing through the outer message. Wrapping is an API decision: exposing an underlying error makes its identity observable to callers and can constrain future implementation changes. Use %v instead when only text should cross a boundary.

Do not compare a newly constructed errors.New("missing") with the sentinel and expect identity based on text. Distinct created errors are distinct values for that purpose. A package should document which errors callers may inspect and whether a result remains partially usable on failure.

go Context with a stable error identity
package main

import (
	"errors"
	"fmt"
)

var ErrMissing = errors.New("missing")

func load() error { return fmt.Errorf("load: %w", ErrMissing) }

func main() {
	err := load()
	fmt.Println(errors.Is(err, ErrMissing), err)
}
Expected output
true load: missing

errors.Is follows the wrap relationship and identifies the original sentinel.

Use type assertions with a failure plan

A type assertion retrieves a dynamic value under a claimed type. The comma-ok form reports a mismatch without a panic; a type switch can handle several documented alternatives. Avoid using any as an excuse to postpone every type decision until runtime. A small concrete contract often gives clearer errors and fewer invalid states.

Interfaces can hide shared mutable state behind method calls, so behavioral substitution also requires semantic promises about mutation, blocking, and concurrency. A method name matching an interface is only the structural part of compatibility. Document whether implementations may block, retain inputs, or be called concurrently.

Common errors and fixes

A success return is treated as an error.

Why it happens
A typed nil pointer was converted to error.
How to fix it
Return a plain nil interface on the success path.
Prevent it next time
Test both failure and success through the declared interface type.

A type assertion panics on valid alternative input.

Why it happens
The code assumed one dynamic type without checking.
How to fix it
Use comma-ok or a type switch under a documented contract.
Prevent it next time
Test each supported dynamic type and an unsupported one.

Error handling breaks after a message edit.

Why it happens
Program decisions compare human-readable strings.
How to fix it
Use errors.Is or errors.As with documented categories.
Prevent it next time
Treat message wording separately from stable error identity.

Check your understanding

1. A function declares an error result and has var problem *MyError with no assigned object. What should its successful return be?

Show worked solution

Return nil explicitly when no error occurred. Returning problem converts a nil pointer with dynamic type *MyError into the error interface, making the interface non-nil if that type implements error. A success-path test should assign the result to error and require err == nil. Do not repair this by parsing a printed <nil> representation.

2. Why may wrapping a database-specific sentinel in a public package be a compatibility commitment?

Show worked solution

Callers using errors.Is can observe that sentinel through the wrapper and branch on it. Replacing the database later could change their behavior even if the human message is identical. Decide whether to expose that identity or translate it to a package-owned error category. Document the chosen category and test the public behavior rather than private storage details.

Key takeaways

  • Interface nilness depends on both dynamic type and value.
  • Method-set compatibility is only part of a behavioral contract.
  • Error identity and wrapping should be deliberate public decisions.

Official references