# Go - Correctness Idioms
Several Go traps produce no error message until the program panics or silently drops input. This page collects idioms that remove those traps: guard typed nils behind interfaces, prefer the stdlib calls that make the trap impossible, dispatch parsers on declared names instead of field presence, and keep test doubles in step with production. The MCP Go SDK examples come from a public open-source tracing contrib.
## Key points
- A typed-nil pointer stored in an interface makes the interface non-nil. The type assertion `params.(*mcp.InitializeParams)` returned `ok == true` for `(*mcp.InitializeParams)(nil)`. The panic occurred only at the next dereference of the nested `*Implementation` field. Guard the pointer, not the interface: check `ok && initParams != nil && initParams.ClientInfo != nil`. That panic path shipped in the contrib for several releases with no test that hit it.
- The same trap enters through parameters. A nil `*mcp.CallToolResult` assigned into an `mcp.Result` parameter makes `output != nil` hold up to the `.IsError` method-call panic. Pass the untyped result `res` so a typed nil cannot occur. Keep an explicit `toolResult != nil` guard for handlers that return their own typed nil.
- `slices.Delete(s, i, i+1)` replaces `append(s[:i], s[i+1:]...)`. It allocates a new backing array, so the old slice keeps no alias into the result.
- `t.Cleanup(func() { tp.Shutdown(ctx) })` runs after the test completes and after all subtests. `defer` runs only when the enclosing function returns. `t.Cleanup` is therefore the tighter choice inside tests that use subtests.
- Compare sentinels with `errors.Is(err, redis.Nil)`, not `err == redis.Nil`. The check stays correct when callers wrap the error.
- Count runes with `utf8.RuneCountInString`, not `len`. Length limits on user-facing text, such as a description field, must not punish multi-byte characters.
- A rule parser that dispatches on field presence silently drops mixed specifications. When a container field is set, the parser returns early and ignores the sibling leaf fields on the same definition. No error and no warning occur. Dispatch on the declared modifier name instead. This deletes the inference step and the fragile ordering hacks that it forced.
- Nil-guard each child value before you append it. An unconditional append of a nil child turns a later match into a nil panic. A default branch that returns `(nil, nil)` makes that panic reachable.
- Test doubles must fail in the same direction as production. A test matcher whose default branch returns `true` treats every unknown predicate as a match. Production rejects the same predicate with a hard error. Fixtures then encode golden output that production never produces.
- Blast radius decides struct layout. Dozens of rule literals across AST, instrument, and setup tests, plus non-test files, would break if action payloads were embedded into the flat rule structs. Keep the runtime structs byte-for-byte and copy typed payloads in a separate schema layer.
## How to apply
Guard the pointer after every type assertion on a request parameter:
```go
initParams, ok := params.(*mcp.InitializeParams)
if !ok || initParams == nil || initParams.ClientInfo == nil {
// reject the malformed request
}
```
Pass the untyped result `res` into interface parameters. A typed nil then cannot occur. Guard the handler output with `toolResult != nil` when the handler may return its own typed nil.
Checklist:
1. Replace every `append(s[:i], s[i+1:]...)` with `s = slices.Delete(s, i, i+1)`.
2. Register test cleanup with `t.Cleanup`, not `defer`, when subtests run.
3. Use `errors.Is` for every sentinel comparison.
4. Use `utf8.RuneCountInString` for every user-facing length limit.
5. Dispatch schema parsers on the declared field name. Never infer the type from field presence.
6. Nil-guard each value before you append it.
7. Give every default branch in a test double the same failure as the production builder.
8. Count the call sites that a struct change breaks. Then decide between embed and a separate schema layer.
## Observations
- [pitfall] A typed-nil pointer passes a type assertion, so `ok == true` does not prove the value is safe to dereference. #nil-safety (source: agent sessions, 2026-09)
- [pitfall] A typed nil inside an interface parameter keeps `output != nil` true up to the method-call panic. #nil-safety (source: agent sessions, 2026-10)
- [practice] `slices.Delete` removes an element without aliasing the old backing array. #stdlib (source: agent sessions, 2026-03)
- [practice] `t.Cleanup` covers the whole test, including subtests. #testing (source: agent sessions, 2026-03)
- [practice] `errors.Is` keeps sentinel comparison correct across error wrapping. #error-handling (source: agent sessions, 2026-03)
- [practice] `utf8.RuneCountInString` measures the characters a user sees. #i18n (source: agent sessions, 2026-08)
- [pitfall] Field-presence dispatch drops mixed specifications without an error. #parsing (source: agent sessions, 2026-06)
- [pitfall] An unconditional append of a nil child makes a nil panic reachable. #parsing (source: agent sessions, 2026-06)
- [pitfall] A test double that defaults to match produces golden output that production never produces. #testing (source: agent sessions, 2026-06)
- [design] The number of touched literals decides between embedding payloads and a separate schema layer. #design (source: agent sessions, 2026-07)
## Relations
- relates_to [[resources/documentation/Go/Go - Internals]] (typed nils and slice aliasing depend on interface and slice internals)
- relates_to [[resources/documentation/Go/Go - Testing Patterns and Tests That Certify Nothing]] (test doubles must fail in the same direction as the production builder)
- relates_to [[resources/documentation/Go/Go - Portability and Crash Handling]] (a nil-dereference panic reaches crash handling)
## Sources
Personal agent-session notes, March 2026 to October 2026