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.
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.
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())
}
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.
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)
}
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.