Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications.
.claude/skills/golang-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-17 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-01 | ✓→✗ | ▼ Worse | 177% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 180% | 0% |
| case-12 | ✓→✓ | = Same ✓ | 185% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 280% | 0% |
Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications.
Go favors simplicity over cleverness. Code should be obvious and easy to read.
go// Good: Clear and direct func GetUser(id string) (*User, error) { user, err := db.FindUser(id) if err != nil { return nil, fmt.Errorf("get user %s: %w", id, err) } return user, nil } // Bad: Overly clever func GetUser(id string) (*User, error) { return func() (*User, error) { if u, e := db.FindUser(id); e == nil { return u, nil } else { return nil, e } }() }
Design types so their zero value is immediately usable without initialization.
go// Good: Zero value is useful type Counter struct { mu sync.Mutex count int // zero value is 0, ready to use } func (c *Counter) Inc() { c.mu.Lock() c.count++ c.mu.Unlock() } // Good: bytes.Buffer works with zero value var buf bytes.Buffer buf.WriteString("hello") // Bad: Requires initialization type BadCounter struct { counts map[string]int // nil map will panic }
Functions should accept interface parameters and return concrete types.
go// Good: Accepts interface, returns concrete type func ProcessData(r io.Reader) (*Result, error) { data, err := io.ReadAll(r) if err != nil { return nil, err } return &Result{Data: data}, nil } // Bad: Returns interface (hides implementation details unnecessarily) func ProcessData(r io.Reader) (io.Reader, error) { // ... }
go// Good: Wrap errors with context func LoadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("load config %s: %w", path, err) } var cfg Config if err := json.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parse config %s: %w", path, err) } return &cfg, nil }
go// Define domain-specific errors type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) } // Sentinel errors for common cases var ( ErrNotFound = errors.New("resource not found") ErrUnauthorized = errors.New("unauthorized") ErrInvalidInput = errors.New("invalid input") )
gofunc HandleError(err error) { // Check for specific error if errors.Is(err, sql.ErrNoRows) { log.Println("No records found") return } // Check for error type var validationErr *ValidationError if errors.As(err, &validationErr) { log.Printf("Validation error on field %s: %s", validationErr.Field, validationErr.Message) return } // Unknown error log.Printf("Unexpected error: %v", err) }
go// Bad: Ignoring error with blank identifier result, _ := doSomething() // Good: Handle or explicitly document why it's safe to ignore result, err := doSomething() if err != nil { return err } // Acceptable: When error truly doesn't matter (rare) _ = writer.Close() // Best-effort cleanup, error logged elsewhere
gofunc WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) { var wg sync.WaitGroup for i := 0; i < numWorkers; i++ { wg.Add(1) go func() { defer wg.Done() for job := range jobs { results <- process(job) } }() } wg.Wait() close(results) }
gofunc FetchWithTimeout(ctx context.Context, url string) ([]byte, error) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("fetch %s: %w", url, err) } defer resp.Body.Close() return io.ReadAll(resp.Body) }
gofunc GracefulShutdown(server *http.Server) { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Fatalf("Server forced to shutdown: %v", err) } log.Println("Server exited") }
goimport "golang.org/x/sync/errgroup" func FetchAll(ctx context.Context, urls []string) ([][]byte, error) { g, ctx := errgroup.WithContext(ctx) results := make([][]byte, len(urls)) for i, url := range urls { i, url := i, url // Capture loop variables g.Go(func() error { data, err := FetchWithTimeout(ctx, url) if err != nil { return err } results[i] = data return nil }) } if err := g.Wait(); err != nil { return nil, err } return results, nil }
go// Bad: Goroutine leak if context is cancelled func leakyFetch(ctx context.Context, url string) <-chan []byte { ch := make(chan []byte) go func() { data, _ := fetch(url) ch <- data // Blocks forever if no receiver }() return ch } // Good: Properly handles cancellation func safeFetch(ctx context.Context, url string) <-chan []byte { ch := make(chan []byte, 1) // Buffered channel go func() { data, err := fetch(url) if err != nil { return } select { case ch <- data: case <-ctx.Done(): } }() return ch }
go// Good: Single-method interfaces type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type Closer interface { Close() error } // Compose interfaces as needed type ReadWriteCloser interface { Reader Writer Closer }
go// In the consumer package, not the provider package service // UserStore defines what this service needs type UserStore interface { GetUser(id string) (*User, error) SaveUser(user *User) error } type Service struct { store UserStore } // Concrete implementation can be in another package // It doesn't need to know about this interface
gotype Flusher interface { Flush() error } func WriteAndFlush(w io.Writer, data []byte) error { if _, err := w.Write(data); err != nil { return err } // Flush if supported if f, ok := w.(Flusher); ok { return f.Flush() } return nil }
textmyproject/ ├── cmd/ │ └── myapp/ │ └── main.go # Entry point ├── internal/ │ ├── handler/ # HTTP handlers │ ├── service/ # Business logic │ ├── repository/ # Data access │ └── config/ # Configuration ├── pkg/ │ └── client/ # Public API client ├── api/ │ └── v1/ # API definitions (proto, OpenAPI) ├── testdata/ # Test fixtures ├── go.mod ├── go.sum └── Makefile
go// Good: Short, lowercase, no underscores package http package json package user // Bad: Verbose, mixed case, or redundant package httpHandler package json_parser package userService // Redundant 'Service' suffix
go// Bad: Global mutable state var db *sql.DB func init() { db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL")) } // Good: Dependency injection type Server struct { db *sql.DB } func NewServer(db *sql.DB) *Server { return &Server{db: db} }
gotype Server struct { addr string timeout time.Duration logger *log.Logger } type Option func(*Server) func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } } func WithLogger(l *log.Logger) Option { return func(s *Server) { s.logger = l } } func NewServer(addr string, opts ...Option) *Server { s := &Server{ addr: addr, timeout: 30 * time.Second, // default logger: log.Default(), // default } for _, opt := range opts { opt(s) } return s } // Usage server := NewServer(":8080", WithTimeout(60*time.Second), WithLogger(customLogger), )
gotype Logger struct { prefix string } func (l *Logger) Log(msg string) { fmt.Printf("[%s] %s\n", l.prefix, msg) } type Server struct { *Logger // Embedding - Server gets Log method addr string } func NewServer(addr string) *Server { return &Server{ Logger: &Logger{prefix: "SERVER"}, addr: addr, } } // Usage s := NewServer(":8080") s.Log("Starting...") // Calls embedded Logger.Log
go// Bad: Grows slice multiple times func processItems(items []Item) []Result { var results []Result for _, item := range items { results = append(results, process(item)) } return results } // Good: Single allocation func processItems(items []Item) []Result { results := make([]Result, 0, len(items)) for _, item := range items { results = append(results, process(item)) } return results }
govar bufferPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, } func ProcessRequest(data []byte) []byte { buf := bufferPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufferPool.Put(buf) }() buf.Write(data) // Process... return buf.Bytes() }
go// Bad: Creates many string allocations func join(parts []string) string { var result string for _, p := range parts { result += p + "," } return result } // Good: Single allocation with strings.Builder func join(parts []string) string { var sb strings.Builder for i, p := range parts { if i > 0 { sb.WriteString(",") } sb.WriteString(p) } return sb.String() } // Best: Use standard library func join(parts []string) string { return strings.Join(parts, ",") }
bash# Build and run go build ./... go run ./cmd/myapp # Testing go test ./... go test -race ./... go test -cover ./... # Static analysis go vet ./... staticcheck ./... golangci-lint run # Module management go mod tidy go mod verify # Formatting gofmt -w . goimports -w .
yamllinters: enable: - errcheck - gosimple - govet - ineffassign - staticcheck - unused - gofmt - goimports - misspell - unconvert - unparam linters-settings: errcheck: check-type-assertions: true govet: enable: - shadow issues: exclude-use-default: false
| Idiom | Description | |-------|-------------| | Accept interfaces, return structs | Functions accept interface params, return concrete types | | Errors are values | Treat errors as first-class values, not exceptions | | Don't communicate by sharing memory | Use channels for coordination between goroutines | | Make the zero value useful | Types should work without explicit initialization | | A little copying is better than a little dependency | Avoid unnecessary external dependencies | | Clear is better than clever | Prioritize readability over cleverness | | gofmt is no one's favorite but everyone's friend | Always format with gofmt/goimports | | Return early | Handle errors first, keep happy path unindented |
go// Bad: Naked returns in long functions func process() (result int, err error) { // ... 50 lines ... return // What is being returned? } // Bad: Using panic for control flow func GetUser(id string) *User { user, err := db.Find(id) if err != nil { panic(err) // Don't do this } return user } // Bad: Passing context in struct type Request struct { ctx context.Context // Context should be first param ID string } // Good: Context as first parameter func ProcessRequest(ctx context.Context, id string) error { // ... } // Bad: Mixing value and pointer receivers type Counter struct{ n int } func (c Counter) Value() int { return c.n } // Value receiver func (c *Counter) Increment() { c.n++ } // Pointer receiver // Pick one style and be consistent
Remember: Go code should be boring in the best way - predictable, consistent, and easy to understand. When in doubt, keep it simple.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-06 | pass→pass | 11,581 | 11,785 | +2% | 1 | 1 | 0% | 2,243 | 6,277 | +180% | 0 | 0 | — |
case-12 | pass→pass | 10,954 | 10,708 | -2% | 1 | 1 | 0% | 2,035 | 5,803 | +185% | 0 | 0 | — |
case-01 | pass→fail | 10,968 | 11,393 | +4% | 1 | 1 | 0% | 2,288 | 6,349 | +177% | 0 | 0 | — |
case-02 | pass→pass | 7,249 | 5,206 | -28% | 1 | 1 | 0% | 1,263 | 4,802 | +280% | 0 | 0 | — |
case-03 | pass→pass | 6,811 | 4,362 | -36% | 1 | 1 | 0% | 1,303 | 4,685 | +260% | 0 | 0 | — |
case-04 | pass→pass | 8,329 | 6,161 | -26% | 1 | 1 | 0% | 1,827 | 5,120 | +180% | 0 | 0 | — |
case-05 | pass→pass | 9,945 | 9,746 | -2% | 1 | 1 | 0% | 2,023 | 5,789 | +186% | 0 | 0 | — |
case-07 | pass→pass | 12,719 | 9,490 | -25% | 1 | 1 | 0% | 2,538 | 5,815 | +129% | 0 | 0 | — |
case-08 | pass→pass | 9,686 | 9,652 | -0% | 1 | 1 | 0% | 1,768 | 5,748 | +225% | 0 | 0 | — |
case-09 | pass→pass | 11,617 | 11,688 | +1% | 1 | 1 | 0% | 2,165 | 6,136 | +183% | 0 | 0 | — |
case-10 | pass→pass | 9,969 | 5,063 | -49% | 1 | 1 | 0% | 2,110 | 4,813 | +128% | 0 | 0 | — |
case-11 | pass→pass | 8,256 | 5,619 | -32% | 1 | 1 | 0% | 1,614 | 4,894 | +203% | 0 | 0 | — |
case-13 | pass→pass | 10,440 | 5,442 | -48% | 1 | 1 | 0% | 2,132 | 4,981 | +134% | 0 | 0 | — |
case-14 | pass→pass | 10,203 | 8,668 | -15% | 1 | 1 | 0% | 1,937 | 5,585 | +188% | 0 | 0 | — |
case-15 | pass→pass | 13,271 | 12,064 | -9% | 1 | 1 | 0% | 2,527 | 6,416 | +154% | 0 | 0 | — |
case-16 | pass→pass | 10,354 | 7,112 | -31% | 1 | 1 | 0% | 2,008 | 5,108 | +154% | 0 | 0 | — |
case-17 | fail→pass | 13,837 | 10,094 | -27% | 1 | 1 | 0% | 2,530 | 5,855 | +131% | 0 | 0 | — |
case-18 | pass→pass | 9,225 | 6,571 | -29% | 1 | 1 | 0% | 1,795 | 5,129 | +186% | 0 | 0 | — |
case-19 | pass→pass | 10,946 | 7,911 | -28% | 1 | 1 | 0% | 2,056 | 5,454 | +165% | 0 | 0 | — |
case-20 | pass→pass | 8,152 | 10,304 | +26% | 1 | 1 | 0% | 1,543 | 5,589 | +262% | 0 | 0 | — |
case-21 | pass→pass | 8,480 | 14,739 | +74% | 1 | 1 | 0% | 1,788 | 5,488 | +207% | 0 | 0 | — |
case-22 | pass→pass | 6,135 | 6,157 | +0% | 1 | 1 | 0% | 1,132 | 4,990 | +341% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of 0 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 7/27/2026 | -100% |
Other measured skills in the registry, with their headline benchmark lift.