Go Developer (Modern Go)
Target the latest stable Go release unless the project's go.mod pins an older one.
Confirm the toolchain in use with go version and check the release notes before relying on a recent language or standard-library feature.
Prefer Generics Over interface{}
- Use type parameters instead of
interface{} (or any as a type) when the same logic applies to multiple concrete types.
- Reserve
any for true heterogeneous data (e.g. JSON unmarshaling, plugin payloads).
Do not use it to avoid defining a proper type or constraint.
Preferred Generic Form
func First[T any](s []T) (T, bool) {
if len(s) == 0 {
var zero T
return zero, false
}
return s[0], true
}
Avoided Interface Form
func First(s []interface{}) (interface{}, bool) { ... }
- Use constraints for bounded generics:
comparable (==, !=), cmp.Ordered (ordering), or custom interfaces.
Use ~T for underlying-type constraints (e.g. ~int64 for type-safe IDs).
- Prefer generic containers (e.g.
Stack[T], Set[T]) over []interface{} or reflection.
Type Parameters and Constraints
- Define small, purpose-built constraints; use
any only when no constraint applies.
- Use the stdlib
slices, maps, and cmp packages instead of hand-rolled loops where they improve clarity.
- Use a generic type alias (declared with
=) to name an existing generic type without creating a new identity:
type Set[T comparable] = map[T]struct{} // generic alias
type StringSet = Set[string] // instantiated alias
- Use a defined type when you want a distinct identity, such as a type-safe identifier:
type UserID string // distinct from string; use ~string in a constraint to accept it
Interfaces
- Define interfaces at the consumer, not at the implementer.
Keep them small (one to three methods).
- Prefer accept interfaces, return structs: callers depend on behavior; return concrete types for clarity and evolution.
- Do not introduce interfaces "for testing" before there is a second implementation or a clear boundary (e.g. I/O).
Use small interfaces when a dependency is injected.
- For heterogeneous collections where behavior varies by type, prefer generics or a small interface with a clear contract over
interface{}.
Values and Pointers
- Use values by default.
Use pointers when the value is large and copied often, the type must be mutable and shared, or the type has a
nil meaning.
- Prefer value receivers unless the method must mutate the receiver or the struct is large.
Consistency within a type matters more than a single large copy.
- Do not take a pointer to pass a slice, map, or string to avoid copying; they are reference-like.
Copy the slice header only when needed (e.g. append and reassign).
Errors
- Wrap with
fmt.Errorf("context: %w", err) so callers can use errors.Is / errors.As.
- Prefer typed errors: define error types (structs implementing
error, with Unwrap() error when wrapping) so callers can use errors.As to extract and handle specific cases with context.
Use sentinel errors only when a single global identity check with no extra data is sufficient.
- Return errors; do not log and return a new error unless the log is a side effect the caller cannot perform.
Let the caller decide logging.
Slices and Maps
- Prefer
len(s) == 0 over comparing to nil when checking for "no elements"; treat nil and empty as equivalent for "no elements" in APIs.
- Preallocate when the final size is known:
make([]T, 0, n).
- Prefer
slices.Clone, slices.Concat, and maps.Clone over manual copying when they match the intent.
- For iteration, consider range-over-func iterators and the
iter package helpers such as slices.All where they simplify code; do not force iterator style where a simple for is clearer.
Secret Handling With secret.Do
runtime/secret is not generally available.
Verify its status in the release notes for the toolchain in use before relying on it: it has shipped behind the goexperiment.runtimesecret build tag and only on some operating systems and architectures.
Check with go doc runtime/secret and treat a build-constraint error as "unsupported on this build".
- Where the toolchain and platform do support it, code that handles credentials, tokens, keys, decrypted plaintext, or other secret-bearing data must run the complete secret-bearing call tree inside
secret.Do.
- Import
runtime/secret as secret; the Go API is secret.Do (not secrets.Do).
- Put the experiment check in one shared, build-tag-gated wrapper package in the module (for example
internal/secretutil) instead of scattering experiment checks or direct runtime/secret imports throughout the codebase.
- Call the wrapper unconditionally from secret-bearing code so supported builds use
secret.Do automatically and unsupported builds retain the same control flow.
- Keep secret creation, transformation, comparison, serialization, and consumption inside the callback.
Do not return secret-bearing buffers or closures from it, because that defeats the protected lifetime.
- The unsupported-build fallback may invoke the callback directly, but it does not replace best-effort secure erasure.
Prefer mutable byte buffers over strings for secrets and explicitly zero secret-bearing buffers before releasing them.
- Never log secrets, include them in errors, or retain unnecessary copies, regardless of
secret.Do availability.
- Test that the wrapper invokes its callback in every build mode; when CI can enable the experiment, test the
runtime/secret build-tag path too.
Supported Build Wrapper
//go:build goexperiment.runtimesecret
package secretutil
import "runtime/secret"
func RunWithSecret(f func()) {
secret.Do(f)
}
Fallback Build Wrapper
//go:build !goexperiment.runtimesecret
package secretutil
// RunWithSecret preserves the call shape when runtime/secret is unavailable.
// Callers must still zero secret-bearing mutable buffers.
func RunWithSecret(f func()) {
f()
}
Recommended Wrapper Usage
Use the project wrapper around the full secret-bearing operation:
secretutil.RunWithSecret(func() {
plaintext := decrypt(ciphertext)
defer clear(plaintext)
consume(plaintext)
})
Context and Concurrency
- Pass
context.Context as the first parameter; do not store it in structs.
Use it for cancellation and timeouts.
- Prefer structured concurrency: start goroutines with a clear lifecycle and ensure they exit when context is cancelled or a done channel closes.
Avoid unbounded goroutine spawning.
- Prefer channels or sync primitives for communication; avoid shared mutable state.
When sharing data, document who owns it and when it is safe to read or write.
Standard Library and Modules
- Use module-aware builds only; set the
go directive in go.mod to the oldest release that provides every language and standard-library feature the module uses, and verify that claim against the release notes rather than from memory.
- Prefer
slices, maps, cmp, and (where helpful) iter over hand-written loops and third-party collections for simple cases.
- Do not use deprecated stdlib APIs; check release notes when upgrading.
Anti-Patterns to Avoid
- Bare
interface{} / any when a type parameter or a small interface would express the contract.
- Reflection for polymorphism that generics or interfaces can express.
- Pointer to slice/map just to pass them; pass by value unless you need to reassign the slice/map itself in the callee.
- Init-side effects: avoid
init() that mutates global state or registers handlers; use explicit setup in main or tests.
- Over-genericizing: do not add type parameters "for flexibility" when there is only one concrete type today; add them when you have two or more concrete types or a clear constraint.
- Unprotected secret handling: do not bypass the shared
secret.Do compatibility wrapper on systems that support runtime/secret, and do not omit best-effort buffer erasure on fallback systems.
Quick Reference
- Reusable container/algorithm over multiple types: generics with a constraint
- "Any type" with no operations:
any only when truly heterogeneous
- Ordering or comparison:
cmp.Ordered, cmp.Less, or custom constraint
- Slice utilities:
slices (Contains, Clone, Sort, etc.)
- Map utilities:
maps (Clone, Keys, Values, etc.)
- Error handling: typed errors +
errors.As; wrap with %w; use sentinels only when identity-without-context is enough
- Secret-bearing code: shared build-tagged wrapper that calls
secret.Do when supported; explicit buffer erasure in every build
- Optional value:
T + bool, or a small Result[T] type; avoid *T for "optional" unless nil is meaningful
1---2name: go-developer3description: Applies modern Go semantics, type safety, and secure secret handling when writing or refactoring Go. Use this skill when writing or changing Go code.4---5# Go Developer (Modern Go)67Target the latest stable Go release unless the project's `go.mod` pins an older one.8Confirm the toolchain in use with `go version` and check the release notes before relying on a recent language or standard-library feature.910## Prefer Generics Over `interface{}`1112- Use type parameters instead of `interface{}` (or `any` as a type) when the same logic applies to multiple concrete types.13- Reserve `any` for true heterogeneous data (e.g. JSON unmarshaling, plugin payloads).14 Do not use it to avoid defining a proper type or constraint.1516### Preferred Generic Form1718```go19func First[T any](s []T) (T, bool) {20 if len(s) == 0 {21 var zero T22 return zero, false23 }24 return s[0], true25}26```2728### Avoided Interface Form2930```go31func First(s []interface{}) (interface{}, bool) { ... }32```3334- Use **constraints** for bounded generics: `comparable` (==, !=), `cmp.Ordered` (ordering), or custom interfaces.35 Use `~T` for underlying-type constraints (e.g. `~int64` for type-safe IDs).36- Prefer generic containers (e.g. `Stack[T]`, `Set[T]`) over `[]interface{}` or reflection.3738## Type Parameters and Constraints3940- Define small, purpose-built constraints; use `any` only when no constraint applies.41- Use the stdlib `slices`, `maps`, and `cmp` packages instead of hand-rolled loops where they improve clarity.42- Use a **generic type alias** (declared with `=`) to name an existing generic type without creating a new identity:4344```go45type Set[T comparable] = map[T]struct{} // generic alias46type StringSet = Set[string] // instantiated alias47```4849- Use a **defined type** when you want a distinct identity, such as a type-safe identifier:5051```go52type UserID string // distinct from string; use ~string in a constraint to accept it53```5455## Interfaces5657- **Define interfaces at the consumer**, not at the implementer.58 Keep them small (one to three methods).59- Prefer **accept interfaces, return structs**: callers depend on behavior; return concrete types for clarity and evolution.60- Do not introduce interfaces "for testing" before there is a second implementation or a clear boundary (e.g. I/O).61 Use small interfaces when a dependency is injected.62- For **heterogeneous collections** where behavior varies by type, prefer generics or a small interface with a clear contract over `interface{}`.6364## Values and Pointers6566- Use **values** by default.67 Use pointers when the value is large and copied often, the type must be mutable and shared, or the type has a `nil` meaning.68- Prefer **value receivers** unless the method must mutate the receiver or the struct is large.69 Consistency within a type matters more than a single large copy.70- Do not take a pointer to pass a slice, map, or string to avoid copying; they are reference-like.71 Copy the slice header only when needed (e.g. append and reassign).7273## Errors7475- **Wrap** with `fmt.Errorf("context: %w", err)` so callers can use `errors.Is` / `errors.As`.76- Prefer **typed errors**: define error types (structs implementing `error`, with `Unwrap() error` when wrapping) so callers can use `errors.As` to extract and handle specific cases with context.77 Use sentinel errors only when a single global identity check with no extra data is sufficient.78- Return errors; do not log and return a new error unless the log is a side effect the caller cannot perform.79 Let the caller decide logging.8081## Slices and Maps8283- Prefer `len(s) == 0` over comparing to `nil` when checking for "no elements"; treat nil and empty as equivalent for "no elements" in APIs.84- **Preallocate** when the final size is known: `make([]T, 0, n)`.85- Prefer `slices.Clone`, `slices.Concat`, and `maps.Clone` over manual copying when they match the intent.86- For iteration, consider range-over-func iterators and the `iter` package helpers such as `slices.All` where they simplify code; do not force iterator style where a simple `for` is clearer.8788## Secret Handling With `secret.Do`8990- `runtime/secret` is not generally available.91 Verify its status in the release notes for the toolchain in use before relying on it: it has shipped behind the `goexperiment.runtimesecret` build tag and only on some operating systems and architectures.92 Check with `go doc runtime/secret` and treat a build-constraint error as "unsupported on this build".93- Where the toolchain and platform do support it, code that handles credentials, tokens, keys, decrypted plaintext, or other secret-bearing data **must** run the complete secret-bearing call tree inside `secret.Do`.94- Import `runtime/secret` as `secret`; the Go API is `secret.Do` (not `secrets.Do`).95- Put the experiment check in one shared, build-tag-gated wrapper package in the module (for example `internal/secretutil`) instead of scattering experiment checks or direct `runtime/secret` imports throughout the codebase.96- Call the wrapper unconditionally from secret-bearing code so supported builds use `secret.Do` automatically and unsupported builds retain the same control flow.97- Keep secret creation, transformation, comparison, serialization, and consumption inside the callback.98 Do not return secret-bearing buffers or closures from it, because that defeats the protected lifetime.99- The unsupported-build fallback may invoke the callback directly, but it does **not** replace best-effort secure erasure.100 Prefer mutable byte buffers over strings for secrets and explicitly zero secret-bearing buffers before releasing them.101- Never log secrets, include them in errors, or retain unnecessary copies, regardless of `secret.Do` availability.102- Test that the wrapper invokes its callback in every build mode; when CI can enable the experiment, test the `runtime/secret` build-tag path too.103104### Supported Build Wrapper105106```go107//go:build goexperiment.runtimesecret108109package secretutil110111import "runtime/secret"112113func RunWithSecret(f func()) {114 secret.Do(f)115}116```117118### Fallback Build Wrapper119120```go121//go:build !goexperiment.runtimesecret122123package secretutil124125// RunWithSecret preserves the call shape when runtime/secret is unavailable.126// Callers must still zero secret-bearing mutable buffers.127func RunWithSecret(f func()) {128 f()129}130```131132### Recommended Wrapper Usage133134Use the project wrapper around the full secret-bearing operation:135136```go137secretutil.RunWithSecret(func() {138 plaintext := decrypt(ciphertext)139 defer clear(plaintext)140 consume(plaintext)141})142```143144## Context and Concurrency145146- Pass `context.Context` as the **first parameter**; do not store it in structs.147 Use it for cancellation and timeouts.148- Prefer **structured concurrency**: start goroutines with a clear lifecycle and ensure they exit when context is cancelled or a done channel closes.149 Avoid unbounded goroutine spawning.150- Prefer channels or sync primitives for communication; avoid shared mutable state.151 When sharing data, document who owns it and when it is safe to read or write.152153## Standard Library and Modules154155- Use **module-aware** builds only; set the `go` directive in `go.mod` to the oldest release that provides every language and standard-library feature the module uses, and verify that claim against the release notes rather than from memory.156- Prefer `slices`, `maps`, `cmp`, and (where helpful) `iter` over hand-written loops and third-party collections for simple cases.157- Do not use deprecated stdlib APIs; check release notes when upgrading.158159## Anti-Patterns to Avoid160161- **Bare `interface{}` / `any`** when a type parameter or a small interface would express the contract.162- **Reflection** for polymorphism that generics or interfaces can express.163- **Pointer to slice/map** just to pass them; pass by value unless you need to reassign the slice/map itself in the callee.164- **Init-side effects**: avoid `init()` that mutates global state or registers handlers; use explicit setup in `main` or tests.165- **Over-genericizing**: do not add type parameters "for flexibility" when there is only one concrete type today; add them when you have two or more concrete types or a clear constraint.166- **Unprotected secret handling**: do not bypass the shared `secret.Do` compatibility wrapper on systems that support `runtime/secret`, and do not omit best-effort buffer erasure on fallback systems.167168## Quick Reference169170- **Reusable container/algorithm over multiple types:** generics with a constraint171- **"Any type" with no operations:** `any` only when truly heterogeneous172- **Ordering or comparison:** `cmp.Ordered`, `cmp.Less`, or custom constraint173- **Slice utilities:** `slices` (Contains, Clone, Sort, etc.)174- **Map utilities:** `maps` (Clone, Keys, Values, etc.)175- **Error handling:** typed errors + `errors.As`; wrap with `%w`; use sentinels only when identity-without-context is enough176- **Secret-bearing code:** shared build-tagged wrapper that calls `secret.Do` when supported; explicit buffer erasure in every build177- **Optional value:** `T` + `bool`, or a small `Result[T]` type; avoid `*T` for "optional" unless nil is meaningful