Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Idiomatic Go patterns for error handling, interfaces, concurrency, testing, and module management
.claude/skills/bilal140202-golang-idioms/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 52% | 0% |
| case-07 | ✓→✓ | = Same ✓ | 110% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 42% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 70% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 554% | 0% |
go// Return errors, never panic in library code func LoadConfig(path string) (Config, error) { data, err := os.ReadFile(path) if err != nil { return Config{}, fmt.Errorf("reading config %s: %w", path, err) } var cfg Config if err := json.Unmarshal(data, &cfg); err != nil { return Config{}, fmt.Errorf("parsing config: %w", err) } return cfg, nil }
Rules:
fmt.Errorf("context: %w", err)%w to allow callers to use errors.Is and errors.Asgovar ( ErrNotFound = errors.New("not found") ErrUnauthorized = errors.New("unauthorized") ) func GetUser(id string) (User, error) { user, ok := store[id] if !ok { return User{}, fmt.Errorf("user %s: %w", id, ErrNotFound) } return user, nil } // Caller user, err := GetUser(id) if errors.Is(err, ErrNotFound) { http.Error(w, "user not found", http.StatusNotFound) return }
go// Keep interfaces small (1-3 methods) type Reader interface { Read(p []byte) (n int, err error) } type UserStore interface { GetUser(ctx context.Context, id string) (User, error) CreateUser(ctx context.Context, u User) error } // Accept interfaces, return structs func NewService(store UserStore, logger *slog.Logger) *Service { return &Service{store: store, logger: logger} }
Rules:
io.Reader, io.Writer, fmt.Stringer from the standard libraryer suffixgofunc process(ctx context.Context, jobs <-chan Job, workers int) <-chan Result { results := make(chan Result, workers) var wg sync.WaitGroup for range workers { wg.Add(1) go func() { defer wg.Done() for job := range jobs { select { case <-ctx.Done(): return case results <- job.Execute(): } } }() } go func() { wg.Wait() close(results) }() return results }
gofunc fanOut[T, R any](ctx context.Context, items []T, fn func(T) R, concurrency int) []R { sem := make(chan struct{}, concurrency) results := make([]R, len(items)) var wg sync.WaitGroup for i, item := range items { wg.Add(1) sem <- struct{}{} go func() { defer func() { <-sem; wg.Done() }() results[i] = fn(item) }() } wg.Wait() return results }
Rules:
context.Context as the first parametersync.WaitGroup to wait for goroutine completiongofunc (s *Service) HandleRequest(ctx context.Context, req Request) (Response, error) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() user, err := s.store.GetUser(ctx, req.UserID) if err != nil { return Response{}, fmt.Errorf("getting user: %w", err) } ctx = context.WithValue(ctx, userKey, user) return s.processRequest(ctx, req) }
Rules:
context.WithTimeout or context.WithDeadline for all external callsdefer cancel() after creating a cancellable contextcontext.WithValue sparingly (request-scoped values only: trace IDs, auth info)gofunc TestValidateEmail(t *testing.T) { tests := []struct { name string email string want bool }{ {"valid email", "user@example.com", true}, {"missing @", "userexample.com", false}, {"empty string", "", false}, {"multiple @", "user@@example.com", false}, {"valid with subdomain", "user@mail.example.com", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ValidateEmail(tt.email) if got != tt.want { t.Errorf("ValidateEmail(%q) = %v, want %v", tt.email, got, tt.want) } }) } }
gofunc newTestServer(t *testing.T) *httptest.Server { t.Helper() handler := setupRoutes() srv := httptest.NewServer(handler) t.Cleanup(srv.Close) return srv } func assertEqual[T comparable](t *testing.T, got, want T) { t.Helper() if got != want { t.Errorf("got %v, want %v", got, want) } }
Use t.Helper() in all test utility functions. Use t.Cleanup() instead of defer for test resource cleanup. Use testdata/ directory for test fixtures.
go.mod structure:
module github.com/org/project
go 1.23
require (
github.com/lib/pq v1.10.9
golang.org/x/sync v0.7.0
)Commands:
bashgo mod tidy # remove unused, add missing go mod verify # verify checksums go list -m -u all # check for updates go get -u ./... # update all dependencies go mod vendor # vendor dependencies (optional)
Use go mod tidy before every commit. Pin major versions. Review changelogs before updating.
Design types so their zero value is useful:
go// sync.Mutex zero value is an unlocked mutex (ready to use) var mu sync.Mutex // bytes.Buffer zero value is an empty buffer (ready to use) var buf bytes.Buffer buf.WriteString("hello") // Custom types: make zero value meaningful type Server struct { Addr string // defaults to "" Handler http.Handler // defaults to nil Timeout time.Duration // defaults to 0 (no timeout) } func (s *Server) ListenAndServe() error { addr := s.Addr if addr == "" { addr = ":8080" // useful default } handler := s.Handler if handler == nil { handler = http.DefaultServeMux } // ... }
Rules:
goimport "log/slog" logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, })) logger.Info("request handled", slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.Int("status", status), slog.Duration("latency", time.Since(start)), )
Use log/slog (standard library, Go 1.21+). Use structured fields, never string interpolation. Include request ID, user ID, and operation name in every log entry.
interface{} / any instead of concrete typesinit() for complex setup (makes testing hard)_ without comment| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-07 | pass→pass | 8,530 | 8,900 | +4% | 1 | 1 | 0% | 1,858 | 3,899 | +110% | 0 | 0 | — |
case-01 | pass→pass | 12,953 | 8,682 | -33% | 1 | 1 | 0% | 2,796 | 3,977 | +42% | 0 | 0 | — |
case-02 | fail→pass | 13,925 | 11,111 | -20% | 1 | 1 | 0% | 3,140 | 4,776 | +52% | 0 | 0 | — |
case-03 | pass→pass | 12,061 | 8,471 | -30% | 1 | 1 | 0% | 2,283 | 3,870 | +70% | 0 | 0 | — |
case-04 | pass→pass | 2,267 | 1,890 | -17% | 1 | 1 | 0% | 379 | 2,477 | +554% | 0 | 0 | — |
case-05 | pass→pass | 9,348 | 7,522 | -20% | 1 | 1 | 0% | 2,152 | 3,827 | +78% | 0 | 0 | — |
case-06 | pass→pass | 8,501 | 6,162 | -28% | 1 | 1 | 0% | 1,760 | 3,446 | +96% | 0 | 0 | — |
case-08 | pass→pass | 4,794 | 4,572 | -5% | 1 | 1 | 0% | 974 | 3,191 | +228% | 0 | 0 | — |
case-09 | pass→pass | 10,443 | 8,273 | -21% | 1 | 1 | 0% | 2,229 | 4,037 | +81% | 0 | 0 | — |
case-10 | pass→pass | 6,897 | 11,345 | +64% | 1 | 1 | 0% | 1,605 | 4,684 | +192% | 0 | 0 | — |
case-11 | pass→pass | 10,786 | 10,225 | -5% | 1 | 1 | 0% | 2,222 | 4,254 | +91% | 0 | 0 | — |
case-12 | pass→pass | 7,795 | 7,908 | +1% | 1 | 1 | 0% | 1,735 | 3,827 | +121% | 0 | 0 | — |
case-13 | fail→fail | 10,333 | 8,638 | -16% | 1 | 1 | 0% | 1,984 | 3,792 | +91% | 0 | 0 | — |
case-14 | pass→pass | 10,777 | 8,367 | -22% | 1 | 1 | 0% | 2,308 | 3,762 | +63% | 0 | 0 | — |
case-15 | pass→pass | 2,831 | 1,677 | -41% | 1 | 1 | 0% | 496 | 2,499 | +404% | 0 | 0 | — |
case-16 | pass→pass | 9,296 | 8,102 | -13% | 1 | 1 | 0% | 2,023 | 3,750 | +85% | 0 | 0 | — |
case-17 | pass→pass | 8,902 | 7,608 | -15% | 1 | 1 | 0% | 1,782 | 3,681 | +107% | 0 | 0 | — |
case-18 | pass→pass | 10,491 | 9,274 | -12% | 1 | 1 | 0% | 2,082 | 4,028 | +93% | 0 | 0 | — |
case-19 | fail→fail | 6,999 | 5,067 | -28% | 1 | 1 | 0% | 1,383 | 3,094 | +124% | 0 | 0 | — |
case-20 | pass→pass | 5,127 | 3,619 | -29% | 1 | 1 | 0% | 962 | 2,831 | +194% | 0 | 0 | — |
case-21 | pass→pass | 9,607 | 9,501 | -1% | 1 | 1 | 0% | 1,798 | 3,872 | +115% | 0 | 0 | — |
case-22 | pass→pass | 6,223 | 5,381 | -14% | 1 | 1 | 0% | 1,244 | 3,078 | +147% | 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 +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
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.
Other measured skills in the registry, with their headline benchmark lift.