skills/go-style/SKILL.md
Go style and idioms from official Go Code Review Comments wiki.
npx skillsauth add jcleira/agent-skills go-styleInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Review Go code for style and idioms based on the official Go Code Review Comments wiki.
This skill covers general Go idioms and standard practices. Some projects use different conventions:
go-review-ddd skillgo-repository skillAll Go code must be formatted with gofmt.
// Issue: manual formatting
func main(){
fmt.Println( "hello" )
}
// Correct: gofmt-formatted
func main() {
fmt.Println("hello")
}
Doc comments should be complete sentences.
// Issue: incomplete comment
// returns the user name
func GetName() string { ... }
// Correct: complete sentence starting with function name
// GetName returns the user's full name.
func GetName() string { ... }
Every exported name should have a doc comment.
// Issue: no doc comment on exported function
func ProcessUser(user *User) error { ... }
// Correct: doc comment starting with function name
// ProcessUser validates and stores the user in the database.
// It returns an error if validation fails.
func ProcessUser(user *User) error { ... }
// Issue: doc comment doesn't start with type name
// This represents a user
type User struct { ... }
// Correct: starts with type name
// User represents an authenticated user in the system.
type User struct { ... }
Use MixedCaps or mixedCaps for multi-word names, not underscores.
user_count)// Issue: underscores
var user_count int
func get_user_by_id() { ... }
// Correct: MixedCaps
var userCount int
func GetUserByID() { ... } // Note: ID not Id
Every package should have a package comment.
package clausepackage main comment doesn't describe the binary// Issue: no package comment
package user
// Issue: blank line between comment and package
// Package user provides user management functionality.
package user
// Correct: package comment adjacent to package clause
// Package user provides user management functionality.
package user
For package main, use a binary description:
// Issue: generic comment
// Package main is the entry point.
package main
// Correct: describes what the binary does
// Binary server runs the HTTP API server for user management.
package main
// Also acceptable prefixes: Command, Program
// Command mycli provides a CLI for managing users.
package main
No rigid limit, but avoid uncomfortably long lines.
// Issue: arbitrarily broken
result := someFunction(ctx, userID,
email, name, createdAt, updatedAt)
// Correct: break at semantic boundaries
result := someFunction(
ctx,
userID,
email,
name,
createdAt,
updatedAt,
)
// Also correct: keep short calls on one line
result := someFunction(ctx, userID)
Guidelines:
Error strings should not be capitalized or end with punctuation.
// Issue: capitalized, punctuation
errors.New("Failed to connect to database.")
fmt.Errorf("Invalid user ID!")
// Correct: lowercase, no punctuation
errors.New("failed to connect to database")
fmt.Errorf("invalid user ID")
// OK: proper noun
errors.New("failed to connect to PostgreSQL")
Don't discard errors using _.
_ = someFunc()// Issue: discarding error
_ = file.Close()
// Issue: not handling error
someFunc()
// Correct: handle error
if err := file.Close(); err != nil {
return fmt.Errorf("failed to close file: %w", err)
}
// OK: rare case with comment
_ = file.Close() // closing read-only file, error not relevant
Handle errors first, then normal code.
// Issue: happy path indented
func doSomething() error {
if err := step1(); err == nil {
if err := step2(); err == nil {
return step3()
} else {
return err
}
} else {
return err
}
}
// Correct: error flow handling with early returns
func doSomething() error {
if err := step1(); err != nil {
return err
}
if err := step2(); err != nil {
return err
}
return step3()
}
Don't use panic for normal error handling.
// Issue: panic for expected error
func GetUser(id string) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // Wrong!
}
return user
}
// Correct: return error
func GetUser(id string) (*User, error) {
user, err := db.Find(id)
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
return user, nil
}
Avoid in-band error values (special return values indicating errors).
// Issue: in-band error (-1 means error)
func GetAge() int {
if err := loadUser(); err != nil {
return -1 // Caller must check for -1
}
return user.Age
}
// Correct: explicit error return
func GetAge() (int, error) {
if err := loadUser(); err != nil {
return 0, fmt.Errorf("failed to load user: %w", err)
}
return user.Age, nil
}
Group imports into standard library, third-party, and local imports.
// Issue: not grouped
import (
"fmt"
"github.com/gin-gonic/gin"
"context"
"myapp/internal/domain"
"strings"
)
// Correct: grouped and sorted
import (
"context"
"fmt"
"strings"
"github.com/gin-gonic/gin"
"myapp/internal/domain"
)
Use blank imports (import _ "pkg") only for side effects.
main package or test files// Issue: blank import in library without explanation
import _ "github.com/lib/pq"
// Correct: blank import with comment explaining why
import (
"database/sql"
_ "github.com/lib/pq" // PostgreSQL driver registration
)
// Correct: in main.go for side effects
package main
import (
_ "image/png" // Register PNG format
_ "image/jpeg" // Register JPEG format
)
Avoid dot imports (import . "pkg") in production code.
_test.go files to break circular dependencies// Issue: dot import in production code
import . "github.com/onsi/gomega"
func Process() {
Expect(result).To(BeTrue()) // Where does Expect come from?
}
// Correct: explicit import
import "github.com/onsi/gomega"
func Process() {
gomega.Expect(result).To(gomega.BeTrue())
}
// OK: dot import in test file for readability
// foo_test.go
package foo_test
import (
. "myapp/foo" // Testing package foo from external test
)
Define interfaces in the package that uses them, not in the package that implements them.
io.Reader, io.Writer// Issue: interface defined with implementation
// storage/storage.go
package storage
type Storage interface { // Don't define here!
Get(key string) ([]byte, error)
Set(key string, value []byte) error
}
type RedisStorage struct { ... }
func (r *RedisStorage) Get(key string) ([]byte, error) { ... }
func (r *RedisStorage) Set(key string, value []byte) error { ... }
// Correct: interface defined where it's used
// cache/cache.go
package cache
// Storage is the interface for cache backends.
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
}
type Cache struct {
storage Storage // Accept interface
}
func New(s Storage) *Cache {
return &Cache{storage: s}
}
Prefer small interfaces with 1-3 methods.
// Issue: large interface
type UserService interface {
Create(ctx context.Context, user *User) error
Get(ctx context.Context, id string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context) ([]*User, error)
Search(ctx context.Context, query string) ([]*User, error)
// ... 10 more methods
}
// Correct: small, focused interfaces
type UserReader interface {
Get(ctx context.Context, id string) (*User, error)
}
type UserWriter interface {
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
}
// Compose when needed
type UserReadWriter interface {
UserReader
UserWriter
}
Design APIs that are testable without mocking interfaces.
// Issue: interface just for mocking
type TimeProvider interface {
Now() time.Time
}
type realTime struct{}
func (realTime) Now() time.Time { return time.Now() }
// Correct: accept a function or use dependency injection
type Service struct {
now func() time.Time
}
func New() *Service {
return &Service{now: time.Now}
}
// In tests:
svc := &Service{now: func() time.Time { return fixedTime }}
Use named results sparingly, only when they improve clarity.
// Issue: named result just for naked return
func GetUser(id string) (user *User, err error) {
user, err = db.Find(id)
return // Naked return obscures what's being returned
}
// Correct: explicit return
func GetUser(id string) (*User, error) {
user, err := db.Find(id)
return user, err
}
// OK: named results for documentation in complex function
func Divide(dividend, divisor float64) (quotient, remainder float64, err error) {
if divisor == 0 {
return 0, 0, errors.New("division by zero")
}
quotient = dividend / divisor
remainder = math.Mod(dividend, divisor)
return quotient, remainder, nil
}
Avoid naked returns except in very short functions.
// Issue: naked return in long function
func Process(data string) (result string, err error) {
// ... 50 lines of code ...
return // What values are being returned?
}
// Correct: explicit return
func Process(data string) (string, error) {
// ... processing ...
return result, err
}
Use short, consistent receiver names (1-2 letters).
// Issue: "this" or "self"
func (this User) GetName() string { ... }
func (self User) GetEmail() string { ... }
// Issue: inconsistent names
func (u User) GetName() string { ... }
func (user User) GetEmail() string { ... }
// Correct: short, consistent
func (u *User) GetName() string { ... }
func (u *User) GetEmail() string { ... }
Use pointer receivers unless you have a good reason not to.
Note: Some projects use pointer-first conventions. This guidance is for general Go code.
// Issue: inconsistent receiver types
func (u *User) SetName(name string) { u.Name = name } // Pointer
func (u User) GetName() string { return u.Name } // Value - inconsistent!
// Correct: consistent pointer receivers
func (u *User) SetName(name string) { u.Name = name }
func (u *User) GetName() string { return u.Name }
// OK: small immutable type with value receivers
type Point struct{ X, Y int }
func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
Context should be the first parameter in functions.
// Issue: context not first
func GetUser(id string, ctx context.Context) (*User, error) { ... }
// Correct: context first
func GetUser(ctx context.Context, id string) (*User, error) { ... }
Document goroutine lifetimes, especially in concurrent code.
// Issue: unclear goroutine lifetime
func StartProcessor() {
go process() // When does this stop?
}
// Correct: documented lifetime and cleanup
// StartProcessor launches a background goroutine that processes
// items until ctx is canceled.
func StartProcessor(ctx context.Context) {
go func() {
for {
select {
case <-ctx.Done():
return // Goroutine exits when context canceled
case item := <-queue:
process(item)
}
}
}()
}
Prefer synchronous functions; let caller decide on concurrency.
// Issue: forces async on caller
func ProcessData(data []byte) {
go func() {
// process data
}()
}
// Correct: synchronous, caller controls concurrency
func ProcessData(data []byte) error {
// process data synchronously
return nil
}
// Caller can make it async if needed:
go func() {
if err := ProcessData(data); err != nil {
log.Println(err)
}
}()
For small structs, consider passing by value.
Note: Some projects use pointer-first approaches. This is standard Go guidance for optimal performance.
// Small immutable struct - value is fine
type Point struct {
X, Y int
}
func Distance(p1, p2 Point) float64 {
dx := p1.X - p2.X
dy := p1.Y - p2.Y
return math.Sqrt(float64(dx*dx + dy*dy))
}
// Large struct or mutable - use pointer
type User struct {
ID string
Email string
Name string
CreatedAt time.Time
// ... many more fields
}
func UpdateUser(u *User) error { ... }
Be careful when copying structs to avoid unexpected aliasing.
// Issue: copying struct with pointer receiver methods
type Counter struct {
count int
}
func (c *Counter) Increment() { c.count++ }
func (c *Counter) Value() int { return c.count }
func main() {
c1 := Counter{}
c2 := c1 // Copied! c2 is independent
c1.Increment()
fmt.Println(c2.Value()) // Prints 0, not 1 - may be unexpected
}
// Issue: copying struct with mutex
type SafeMap struct {
mu sync.Mutex
m map[string]int
}
func main() {
sm1 := SafeMap{m: make(map[string]int)}
sm2 := sm1 // BUG: mutex copied, map shared!
}
// Issue: copying struct with pointer field causes aliasing
type User struct {
Name string
Friends []*User // Pointer slice
}
func main() {
u1 := User{Name: "Alice", Friends: []*User{...}}
u2 := u1 // u2.Friends points to same slice!
u2.Friends[0].Name = "Bob" // Modifies u1's friend too!
}
// Correct: if you need to copy, implement Clone()
func (u *User) Clone() *User {
clone := *u
clone.Friends = make([]*User, len(u.Friends))
copy(clone.Friends, u.Friends)
return &clone
}
Rule of thumb: If a type's methods have pointer receivers, don't copy values of that type.
Use var s []T for empty slices, not s := []T{}.
make([]T, 0) or []T{}var s []T (nil slice)// Issue: unnecessary allocation
s := []string{}
s := make([]string, 0)
// Correct: nil slice (preferred)
var s []string
// Also OK when size is known
s := make([]string, 0, 10) // Preallocate capacity
Note: nil slices and empty slices behave identically for append, len, and range.
Use crypto/rand for keys and secrets, not math/rand.
// Issue: insecure for tokens
import "math/rand"
token := rand.Int63()
// Correct: crypto/rand for security
import "crypto/rand"
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return err
}
token := base64.URLEncoding.EncodeToString(b)
Use testable examples in _test.go files.
Example* naming convention// Output: comment// Correct: testable example
func ExampleUser_GetFullName() {
u := &User{FirstName: "John", LastName: "Doe"}
fmt.Println(u.GetFullName())
// Output: John Doe
}
Test failures should clearly indicate what went wrong.
t.Fatal when t.Error would suffice// Issue: uninformative failure
if got != want {
t.Fatal("wrong result")
}
// Correct: clear failure message
if got != want {
t.Errorf("GetName(%q) = %q; want %q", input, got, want)
}
Use correct casing for initialisms (URL, ID, HTTP, etc.).
Url, Id, Http, JsonURL, ID, HTTP, JSON (all caps or all lowercase)// Issue: wrong casing
type User struct {
UserId string
ImageUrl string
HttpCode int
}
// Correct: proper initialisms
type User struct {
UserID string // ID not Id
ImageURL string // URL not Url
HTTPCode int // HTTP not Http
}
// Also correct when not exported
userID := "123"
imageURL := "https://..."
| Wrong | Correct |
|-------|---------|
| Id | ID |
| Url | URL |
| Http | HTTP |
| Json | JSON |
| Xml | XML |
| Html | HTML |
| Api | API |
| Db | DB |
| Sql | SQL |
| Ui | UI |
Package names should be short, lowercase, no underscores or mixedCaps.
// Issue: wrong package names
package user_service
package UserService
package users
// Correct: short, lowercase, singular
package user
package service
package http
Use short names for short scope, descriptive names for longer scope.
index, element)i, j, k in loops// Issue: overly verbose loop vars
for index, element := range items {
fmt.Println(index, element)
}
// Correct: short names for short scope
for i, item := range items {
fmt.Println(i, item)
}
// Issue: single letter for package var
var u *User // Package level - too short
// Correct: descriptive for longer scope
var DefaultUser *User
When reviewing Go code, report findings as:
**Go Style Review**
Issues found:
- `user.go:12` - Function `GetUser` missing doc comment
- `user.go:23` - Error string "Failed to connect" should be lowercase: "failed to connect"
- `user.go:45` - Receiver name "this" should be short (e.g., "u")
- `user.go:67` - Initialism "UserId" should be "UserID"
- `auth.go:34` - Imports not grouped (stdlib, third-party, local)
- `handler.go:89` - Error discarded with `_ = file.Close()` without comment
Recommendations:
- Add doc comments to all exported functions
- Use consistent receiver names across User methods
- Group imports: stdlib, third-party, then local
No issues:
- All code is gofmt-formatted
- Error handling uses early returns
- Context passed as first parameter
This skill auto-invokes when reviewing all Go files (.go extension).
For more focused reviews:
go-review-ddd skillgo-repository skillgin-go skilldevelopment
DDD architecture review for Go projects enforcing layer boundaries, pointer semantics, and domain-driven design patterns.
development
Repository pattern review for Go codebases using Jet ORM, domain models, and adapter patterns.
development
Review Go code using Gin framework for routing patterns, middleware usage, request/response handling, and error conventions.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.