Mode d’emploi
- Copiez la commande et examinez le paquet avant de l’installer dans votre environnement agent.
- Consultez le document original pour les outils, identifiants et environnements requis.
- Décrivez les entrées et le livrable souhaité ; comparez la sortie aux sources.
npx skills add https://github.com/victorzhuk/skills --skill z-go-safetyAvant de commencer
- L’installation n’inclut ni abonnement au modèle, ni quota API, ni accès aux fichiers.
- Étoiles et installations reflètent l’activité, pas un test indépendant de qualité ou de sécurité.
Rendu depuis le dépôt source en conservant titres, exemples, code, tableaux, liens et images.
Go Safety
Quick Reference
| Risk | Fix |
|---|---|
| Typed nil in interface | Return untyped nil, never a typed nil pointer |
| Nil map write | make or lazy-init in the method |
append aliasing | Cap-limit before growing; clone for independent elements; both are shallow |
| Bare type assertion | Always comma-ok: v, ok := x.(T) |
defer in loop | Extract body to a function |
| Integer narrowing | Bounds-check against math.MaxInt32 first |
| Float comparison | choose domain-specific absolute/relative tolerances; handle NaN/infinity explicitly |
| Iota enum | Plain iota from 0 with a real state at zero (default); iota + 1 only if zero-as-invalid genuinely matters |
| Mutable global | Inject func() time.Time |
| Crypto keys | rand.Read() + hex.EncodeToString (or rand.Int(rand.Reader, ...)); rand.Text() (Go 1.24+) is a shorthand alt |
Nil Interface Trap
A typed nil pointer sets the interface type descriptor — the interface is not nil:
// bad: returns non-nil interface wrapping a nil *Handler
func getHandler() http.Handler { var h *Handler; return h }
// good
func getHandler() http.Handler {
if !enabled { return nil }
return &Handler{}
}Lazy-init maps to keep zero value usable:
func (r *Registry) Add(name string, v int) {
if r.items == nil { r.items = make(map[string]int) }
r.items[name] = v
}Append Aliasing
append reuses backing array when cap > len — both slices share memory:
b := append(a, 4) // bad: b[0] = 99 also mutates a[0]
b := append(a[:len(a):len(a)], 4) // good: forces new allocationExported getters return copies: func (c *Config) Hosts() []string { return slices.Clone(c.hosts) }
A full slice expression (or slices.Clip) limits capacity; it does not copy existing elements or release the backing array. Appending at least one element to a slice with len == cap obtains new backing storage. Direct writes before that still affect aliases.
copy, slices.Clone, and maps.Clone are shallow: nested slices, maps, and pointers still share data. At an API boundary, copy the mutable depth the contract promises to isolate, or document immutable sharing/ownership transfer. Sending the value through a channel does not change this; see [[z-go-concurrency]].
Range and map mutation
for _, v := range scopies each element intov;&vis not&s[i], even with Go 1.22 loop semantics. Use indices when mutating elements or retaining their addresses.- Slice range saves the initial slice header and length. Appending does not extend that iteration; writes to shared backing storage can affect later values.
- Map iteration order is unspecified. Deleting an unreached entry skips it; an entry inserted during iteration may be visited or skipped. Sort keys when order matters; use a separate pass when visiting every new entry matters.
- Concurrent map access involving a write requires synchronization. Runtime detection is not a safety guarantee; see [[z-go-concurrency]].
Type Assertions
// bad — panics on mismatch
s := x.(string)
// good
s, ok := x.(string)
if !ok { ... }Defer in Loops
defer runs at function exit, not loop iteration — resources accumulate. Extract body to a function:
// bad
for _, path := range paths {
f, _ := os.Open(path)
defer f.Close() // leaks until function returns
}
// good
for _, path := range paths {
if err := processOne(path); err != nil { return err }
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
return process(f)
}Deferred call arguments and method receivers are evaluated when registering the defer. A closure instead reads captured variables when it runs; choose deliberately for cleanup targets, named results, and elapsed-time measurements. In defer record(time.Since(start)), the duration is computed immediately; defer a closure to measure at return. Defers run in reverse registration order.
Numeric Conversions
// bad — silently wraps: 3_000_000_000 becomes -1294967296
i32 := int32(val)
// good
if val > math.MaxInt32 || val < math.MinInt32 {
return fmt.Errorf("value %d overflows int32", val)
}Float comparisons need scale-appropriate absolute/relative tolerances; no universal epsilon works for every domain. Decide how NaN, infinity and exact representable values should behave. Guard division by zero where the domain forbids it.
API Boundary Checklist
- Establish ownership before storing incoming slices/maps; clone mutable data when callers retain access
- Return copies when promising isolation; shallow clones do not isolate nested mutable values
- Compile-time interface check:
var _ I = (*T)(nil) defercleanup immediately after resource open- iota enums start at 0 with a real state there; reserve
iota + 1for when zero-as-invalid genuinely matters - Inject
time.Nowasfunc() time.Time; never call it directly in logic crypto/randfor keys, nevermath/rand
Zero-Value & Initialization
Design zero values deliberately. Initialize fallible dependencies explicitly and return errors. If an existing API uses sync.Once, store both the value and initialization error; sync.Once will not retry a failed call. Do not discard sql.Open errors or assume opening validates connectivity; use a bounded PingContext when startup readiness requires it.
Enums — plain iota from 0, with a real state at zero (observed default): const ( Add Operation = iota; Subtract; Multiply ). Use iota + 1 only when zero-as-invalid genuinely matters and an unset value must be caught.
Mutable Globals
Inject instead of mutating package-level vars: type signer struct{ now func() time.Time }
Panic / Must
Must helpers are acceptable at package init only. Return errors for expected failures. Do not add blanket recovery at package or server-goroutine boundaries: an unexpected panic may indicate corrupted shared state.
var re = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)Recover only an owned internal panic protocol or at a documented containment boundary with state isolation, cleanup, an explicit failure result, and stack diagnostics. Re-panic unexpected values; see [[z-go-errors]]. Recovery must be called directly by a deferred function in the panicking goroutine. A caller's defer cannot recover a child goroutine's panic.
Crypto
math/rand and math/rand/v2 are deterministic from a seed — never safe for keys, tokens, or anything else that must be unpredictable. crypto/rand is the only safe source; rand.Int(rand.Reader, max) covers a bounded integer, rand.Text() (Go 1.24+) a random string.
See [[z-go-security]] for the token-generation and encryption snippets.
Inbound server hardening
An unbounded inbound http.Server can be held open by a slow client (slowloris). The gosec G112 fix is setting all four timeouts plus a header cap at construction:
srv := &http.Server{
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}The values are defaults; tune per route (long uploads or SSE need larger WriteTimeout or per-route overrides). Drain-on-shutdown wiring lives in [[z-go-entrypoint]].
Verify
golangci-lint run --enable=forcetypeassert,nilerr,errcheck
go vet ./...
