Jak używać
- Skopiuj polecenie instalacji i sprawdź źródła przed instalacją we własnym środowisku agenta.
- W oryginale sprawdź wymagane narzędzia, dane dostępowe i obsługiwane środowiska.
- Opisz dane wejściowe i oczekiwany rezultat; porównaj wynik ze źródłami.
npx skills add https://github.com/victorzhuk/skills --skill z-go-safetyPrzed rozpoczęciem
- Instalacja nie obejmuje subskrypcji modelu, limitu API ani uprawnień do plików.
- Gwiazdki i instalacje pokazują aktywność, a nie niezależny test jakości lub bezpieczeństwa.
Treść z repozytorium z zachowaniem nagłówków, przykładów, kodu, tabel, linków i obrazów.
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 ./...
