Go Safety
Prevent ordinary programming mistakes without turning every invariant into defensive boilerplate. Trace actual inputs, ownership, and callers before adding a guard.
Review Workflow
- Identify the failure contract: panic, returned error, blocking, silent truncation, shared mutation, leak, or invalid state.
- Trace where the value or resource originates and which callers already validate it.
- Reproduce the risky behavior with a focused test when practical.
- Fix the invariant at the narrowest responsible boundary. Do not scatter nil checks that merely hide a broken constructor or lifecycle.
- Validate with the repository's build, tests, vet, and targeted race or fuzz checks where relevant.
Nil and Interface Values
An interface is nil only when both its dynamic type and value are nil. A typed nil pointer stored in an interface is non-nil and may panic when a method dereferences it.
Prefer returning literal nil from interface-returning functions when no value exists. Decide whether nil receivers, callbacks, maps, slices, and channels are valid states for each API rather than imposing one global rule.
- Nil map reads are valid; writes panic.
- Nil slices can be ranged and appended to; indexing panics.
- Sends and receives on a nil channel block indefinitely, which can be useful to disable a
selectcase but dangerous elsewhere. - Calling a nil function value panics.
Read references/nil-safety.md when interfaces, optional dependencies, zero values, or nil receivers are central to the issue.
Slices, Maps, and Ownership
Slices can share a backing array, and maps are reference-like values. Copy at a boundary when the API promises independent ownership, not as a universal rule.
Check for:
- append or reslicing that unexpectedly mutates an alias;
- retaining a small subslice of a much larger allocation;
- exposing internal collections that callers may mutate;
- relying on map iteration order;
- deleting slice elements while iterating forward;
- unsynchronized concurrent access.
Read references/slice-map-safety.md for ownership and iteration patterns.
Numeric Boundaries
Integer conversions can truncate or change sign. Validate the source value against the destination range at external, storage, protocol, or index boundaries. Internal conversions already proven safe by an invariant do not need repeated guards; document the invariant when it is non-obvious.
Floating-point equality may be correct for exact sentinels or bit-level representations, but measured quantities usually need a domain-derived absolute/relative tolerance. Do not copy a universal epsilon. Use decimal, rational, or integer minor units when the domain requires exact arithmetic.
Guard division where a zero divisor is possible. Check overflow before arithmetic when values come from untrusted or unbounded sources or when wraparound would violate correctness.
Resource Lifetime
Acquire a resource, check the acquisition error, and establish cleanup as soon as ownership is clear. Remember that defer runs when the surrounding function returns, not at the end of a loop iteration.
- Extract an iteration body into a function when deferred resources would otherwise accumulate.
- Check cleanup errors when they affect correctness, such as flushing or committing data.
- Do not close a resource owned by the caller unless the API contract transfers ownership.
- Avoid
log.Fataloros.Exitbelowmain; they skip deferred cleanup in the current call stack. - Give goroutines and background operations an observable termination path; detailed concurrency design belongs elsewhere.
Initialization and Copying
A useful zero value is valuable when it can be made safe without hidden cost or ambiguous configuration. Otherwise, require a constructor and validate its invariants. Lazy initialization must be synchronized if multiple goroutines can reach it.
Do not copy values containing synchronization primitives after first use. Prefer pointer receivers or restructure ownership, and use go vet to detect accidental lock copies.
Verification
Choose checks based on the risk:
go test ./...
go vet ./...
go test -race ./... # when shared state or goroutine lifetime changed
Add boundary tests for nil/zero values, minimum and maximum numeric values, aliasing, cleanup errors, map order independence, and cancellation only when those paths are part of the changed contract.