From frostyard-dev
WHEN: Writing, reviewing, or refactoring Go code. WHEN NOT: Non-Go languages, general questions unrelated to Go programming.
How this skill is triggered — by the user, by Claude, or both
Slash command
/frostyard-dev:uber-go-styleThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **Priority:** This is the baseline authority for Go style in frostyard repos. Modern Go features (`use-modern-go` skill) override this when newer syntax is available. See `go-best-practices` for patterns where Uber is silent.
Priority: This is the baseline authority for Go style in frostyard repos. Modern Go features (
use-modern-goskill) override this when newer syntax is available. Seego-best-practicesfor patterns where Uber is silent.
Distilled from the Uber Go Style Guide. Focus: patterns that are non-obvious or commonly violated.
| Caller needs to match? | Message | Use |
|---|---|---|
| No | Static | errors.New() |
| No | Dynamic | fmt.Errorf() |
| Yes | Static | var ErrFoo = errors.New() |
| Yes | Dynamic | Custom error type |
%w when callers should unwrap; %v to hide the underlying error."new store: %w" not "failed to create new store: %w".Choose ONE: wrap-and-return OR log-and-degrade. Never log AND return — upstream callers will double-log.
// Good: wrap and return
return fmt.Errorf("get user %q: %w", id, err)
// Good: log and degrade
if err != nil {
log.Warn("user timezone lookup failed", zap.Error(err))
tz = time.UTC
}
// Bad: log AND return
log.Error("failed", zap.Error(err))
return err
ErrFoo prefixerrFoo prefix (no underscore — exception to global naming)FooError suffixChannels should be unbuffered (make(chan T)) or size 1 (make(chan T, 1)). Any other size needs rigorous justification for why it won't block or saturate.
Every goroutine must have a predictable stop time or a stop signal. Use sync.WaitGroup or chan struct{} to wait. Test with go.uber.org/goleak.
done := make(chan struct{})
go func() {
defer close(done)
// work
}()
<-done
Expose objects with Close()/Shutdown() methods instead. Goroutine lifetime must be controllable by the caller.
An unrecovered panic in a goroutine crashes the entire process. Always defer a recovery handler in goroutines that could panic.
go func() {
defer func() {
if r := recover(); r != nil {
log.Error("goroutine panicked", zap.Any("panic", r))
}
}()
// work that might panic
}()
Use var mu sync.Mutex, not new(sync.Mutex). In structs, use named fields — never embed mutexes (leaks to public API).
type SMap struct {
mu sync.Mutex // named field, not embedded
data map[string]string
}
Always pass context.Context as the first parameter, named ctx. Never store it in a struct.
// Good
func (s *Store) Get(ctx context.Context, id string) (*Item, error)
// Bad — context in struct
type Store struct { ctx context.Context }
Copy when receiving (prevents caller mutation of your state) AND when returning (prevents caller mutation of returned state).
// Receiving
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
// Returning
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
}
var _ http.Handler = (*Handler)(nil)
Place after type declaration. Catches missing methods at compile time, not runtime.
Inject dependencies instead of using package-level var or function pointers.
// Bad
var timeNow = time.Now
// Good
type signer struct { now func() time.Time }
func newSigner() *signer { return &signer{now: time.Now} }
_const (
_defaultPort = 8080
_defaultUser = "user"
)
Exception: error sentinels use err prefix without underscore.
Move complex init to helper functions called from main(). When init() is unavoidable: no side effects, no I/O, no goroutines, deterministic.
Embedding leaks implementation details and inhibits API evolution. Write delegation methods.
// Bad: exposes AbstractList methods
type ConcreteList struct { *AbstractList }
// Good: controls surface area
type ConcreteList struct { list *AbstractList }
func (l *ConcreteList) Add(e Entity) { l.list.Add(e) }
User{FirstName: "John"} not User{"John", "Doe"}var user User for fully zero-valued structs (not User{})&T{} not new(T) for struct referencesmake(map[K]V) for programmatically populated mapsmap[K]V{k1: v1} for fixed elementsmake(map[K]V, len(items))type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
Zero value means "unset" — distinguishable from valid values. Exception: when zero IS the desired default.
Always tag fields in JSON/YAML structs. Protects against refactoring breaking the serialization contract.
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
}
Only main() calls os.Exit or log.Fatal. All other functions return errors. Wrap business logic in run() error.
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
| Instead of | Use | Why |
|---|---|---|
fmt.Sprint(n) | strconv.Itoa(n) | No reflection, fewer allocs |
Repeated []byte(s) | Convert once, reuse | Avoid allocation per conversion |
make([]T, 0) | make([]T, 0, cap) | Pre-allocate when size known |
make(map[K]V) | make(map[K]V, len) | Pre-allocate when size known |
// Good: handle error first, continue with happy path
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
}
If a variable is set in both branches, initialize before the if:
a := 10
if b {
a = 100
}
Use C-style comments when parameter meaning is unclear:
printInfo("foo", true /* isLocal */, true /* done */)
Or better: use custom types instead of bare booleans.
Soft limit of 99 characters. Wrap before hitting it.
Two groups separated by blank line: standard library first, everything else second.
Rough call order. Exported functions first, then NewXYZ(), then methods by receiver, then utilities.
Use for constructors with 3+ optional parameters:
type options struct {
cache bool
logger *zap.Logger
}
type Option interface { apply(*options) }
type cacheOption bool
func (c cacheOption) apply(o *options) { o.cache = bool(c) }
func WithCache(c bool) Option { return cacheOption(c) }
func Open(addr string, opts ...Option) (*Connection, error) {
o := options{cache: defaultCache, logger: zap.NewNop()}
for _, opt := range opts {
opt.apply(&o)
}
// ...
}
time.Time for instants, time.Duration for periods — never bare inttime.Duration, include unit in field name: IntervalMillisUse tests for the slice, tt for each case, give/want prefixes for fields.
If a table test needs conditional assertions, mock setup functions, or branching beyond a single shouldErr field — split into separate test functions.
With t.Parallel(), loop variables are properly scoped per iteration in Go 1.22+. For older versions, re-assign loop vars.
npx claudepluginhub frostyard/frostyard-ai --plugin frostyard-devGo language conventions, idioms, and toolchain. Invoke when task involves any interaction with Go code — writing, reviewing, refactoring, debugging, or understanding Go projects.
Provides idiomatic Go patterns and best practices for error handling, concurrency like worker pools, simplicity, zero values, and interfaces. Activates for writing, reviewing, refactoring, or designing Go code.
Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications.