Community default. A company skill that explicitly supersedes samber/cc-skills-golang@golang-code-style skill takes precedence.
Go Code Style
Style rules that require human judgment — linters handle formatting, this skill handles clarity. For naming see samber/cc-skills-golang@golang-naming skill; for design patterns see samber/cc-skills-golang@golang-design-patterns skill; for struct/interface design see samber/cc-skills-golang@golang-structs-interfaces skill.
"Clear is better than clever." — Go Proverbs
When ignoring a rule, add a comment to the code.
Line Length & Breaking
No rigid line limit, but lines beyond ~120 characters MUST be broken. Break at semantic boundaries, not arbitrary column counts. Function calls with 4+ arguments MUST use one argument per line — even when the prompt asks for single-line code:
go
1// Good — each argument on its own line, closing paren separate
2mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
3 handleUsers(
4 w,
5 r,
6 serviceName,
7 cfg,
8 logger,
9 authMiddleware,
10 )
11})
When a function signature is too long, the real fix is often fewer parameters (use an options struct) rather than better line wrapping. For multi-line signatures, put each parameter on its own line.
Variable Declarations
SHOULD use := for non-zero values, var for zero-value initialization. The form signals intent: var means "this starts at zero."
go
1var count int // zero value, set later
2name := "default" // non-zero, := is appropriate
3var buf bytes.Buffer // zero value is ready to use
Slice & Map Initialization
Slices and maps MUST be initialized explicitly, never nil. Nil maps panic on write; nil slices serialize to null in JSON (vs [] for empty slices), surprising API consumers.
go
1users := []User{} // always initialized
2m := map[string]int{} // always initialized
3users := make([]User, 0, len(ids)) // preallocate when capacity is known
4m := make(map[string]int, len(items)) // preallocate when size is known
Do not preallocate speculatively — make([]T, 0, 1000) wastes memory when the common case is 10 items.
Composite Literals
Composite literals MUST use field names — positional fields break when the type adds or reorders fields:
go
1srv := &http.Server{
2 Addr: ":8080",
3 ReadTimeout: 5 * time.Second,
4 WriteTimeout: 10 * time.Second,
5}
Control Flow
Reduce Nesting
Errors and edge cases MUST be handled first (early return). Keep the happy path at minimal indentation:
go
1func process(data []byte) (*Result, error) {
2 if len(data) == 0 {
3 return nil, errors.New("empty data")
4 }
5
6 parsed, err := parse(data)
7 if err != nil {
8 return nil, fmt.Errorf("parsing: %w", err)
9 }
10
11 return transform(parsed), nil
12}
Eliminate Unnecessary else
When the if body ends with return/break/continue, the else MUST be dropped. Use default-then-override for simple assignments — assign a default, then override with independent conditions or a switch:
go
1// Good — default-then-override with switch (cleanest for mutually exclusive overrides)
2level := slog.LevelInfo
3switch {
4case debug:
5 level = slog.LevelDebug
6case verbose:
7 level = slog.LevelWarn
8}
9
10// Bad — else-if chain hides that there's a default
11if debug {
12 level = slog.LevelDebug
13} else if verbose {
14 level = slog.LevelWarn
15} else {
16 level = slog.LevelInfo
17}
Complex Conditions & Init Scope
When an if condition has 3+ operands, MUST extract into named booleans — a wall of || is unreadable and hides business logic. Keep expensive checks inline for short-circuit benefit. Details
go
1// Good — named booleans make intent clear
2isAdmin := user.Role == RoleAdmin
3isOwner := resource.OwnerID == user.ID
4isPublicVerified := resource.IsPublic && user.IsVerified
5if isAdmin || isOwner || isPublicVerified || permissions.Contains(PermOverride) {
6 allow()
7}
Scope variables to if blocks when only needed for the check:
go
1if err := validate(input); err != nil {
2 return err
3}
Switch Over If-Else Chains
When comparing the same variable multiple times, prefer switch:
go
1switch status {
2case StatusActive:
3 activate()
4case StatusInactive:
5 deactivate()
6default:
7 panic(fmt.Sprintf("unexpected status: %d", status))
8}
Function Design
- Functions SHOULD be short and focused — one function, one job.
- Functions SHOULD have ≤4 parameters. Beyond that, use an options struct (see
samber/cc-skills-golang@golang-design-patterns skill).
- Parameter order:
context.Context first, then inputs, then output destinations.
- Naked returns help in very short functions (1-3 lines) where return values are obvious, but become confusing when readers must scroll to find what's returned — name returns explicitly in longer functions.
go
1func FetchUser(ctx context.Context, id string) (*User, error)
2func SendEmail(ctx context.Context, msg EmailMessage) error // grouped into struct
Prefer range for Iteration
SHOULD use range over index-based loops. Use range n (Go 1.22+) for simple counting.
go
1for _, user := range users {
2 process(user)
3}
Value vs Pointer Arguments
Pass small types (string, int, bool, time.Time) by value. Use pointers when mutating, for large structs (~128+ bytes), or when nil is meaningful. Details
Code Organization Within Files
- Group related declarations: type, constructor, methods together
- Order: package doc, imports, constants, types, constructors, methods, helpers
- One primary type per file when it has significant methods
- Blank imports (
_ "pkg") register side effects (init functions). Restricting them to main and test packages makes side effects visible at the application root, not hidden in library code
- Dot imports pollute the namespace and make it impossible to tell where a name comes from — never use in library code
- Unexport aggressively — you can always export later; unexporting is a breaking change
String Handling
Use strconv for simple conversions (faster), fmt.Sprintf for complex formatting. Use %q in error messages to make string boundaries visible. Use strings.Builder for loops, + for simple concatenation.
Type Conversions
Prefer explicit, narrow conversions. Use generics over any when a concrete type will do:
go
1func Contains[T comparable](slice []T, target T) bool // not []any
Philosophy
- "A little copying is better than a little dependency"
- Use
slices and maps standard packages; for filter/group-by/chunk, use github.com/samber/lo
- "Reflection is never clear" — avoid
reflect unless necessary
- Don't abstract prematurely — extract when the pattern is stable
- Minimize public surface — every exported name is a commitment
Parallelizing Code Style Reviews
When reviewing code style across a large codebase, use up to 5 parallel sub-agents (via the Agent tool), each targeting an independent style concern (e.g. control flow, function design, variable declarations, string handling, code organization).
Enforce with Linters
Many rules are enforced automatically: gofmt, gofumpt, goimports, gocritic, revive, wsl_v5. → See the samber/cc-skills-golang@golang-lint skill.
Cross-References
- → See the
samber/cc-skills-golang@golang-naming skill for identifier naming conventions
- → See the
samber/cc-skills-golang@golang-structs-interfaces skill for pointer vs value receivers, interface design
- → See the
samber/cc-skills-golang@golang-design-patterns skill for functional options, builders, constructors
- → See the
samber/cc-skills-golang@golang-lint skill for automated formatting enforcement
- → See
samber/cc-skills-golang@golang-continuous-integration skill for automated AI-driven code review in CI using these guidelines