From go-dev
Expert knowledge for Go development. Includes idiomatic patterns, error handling, testing, package naming, and build system detection (go toolchain, Bazel, Makefile). Use when writing, testing, or building Go code.
npx claudepluginhub jaeyeom/claude-toolbox --plugin go-devThis skill uses the workspace's default tool permissions.
Use this skill when the user **writes, modifies, tests, or builds Go code**.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Use this skill when the user writes, modifies, tests, or builds Go code.
Detect the project's build system and use the appropriate commands. Do not assume one build system over another.
Use when no BUILD, BUILD.bazel, or WORKSPACE files are present:
go build ./...
go test ./...
go test -race ./...
go vet ./...
Use when BUILD, BUILD.bazel, or WORKSPACE files are present. Do not use go build or go test directly in Bazel projects — generated code (protobufs, etc.) may not resolve with the standard toolchain.
# Build and test
bazel build //path/to:target
bazel test //path/to:target_test
# Query targets
bazel query 'kind("go_library", //path/to/package/...)'
bazel query 'kind("go_test", //path/to/package/...)'
Use when a Makefile is present with Go-related targets:
make build
make test
make lint
Use go vet and golangci-lint if available:
go vet ./...
golangci-lint run ./...
In Bazel projects, nogo analyzers run during bazel build. These are compilation errors, not warnings.
Do not add //nolint directives carelessly:
//nolint:<analyzer> with a comment explaining why.// BAD - suppressing without understanding
//nolint:ineffassign
x = computeValue()
// ACCEPTABLE - fix requires out-of-scope changes
//nolint:lintername // TODO: Requires updating callers across multiple packages
When //nolint might be appropriate:
When //nolint is NOT appropriate:
| Analyzer | Purpose |
|---|---|
godot | Comments should end with a period. |
ineffassign | Detects ineffectual assignments. |
staticcheck | Various Go best practices. |
govet | Unreachable code, format mismatches. |
errcheck | Unchecked error return values. |
gosimple | Suggests code simplifications. |
If a linter reports an unused interface, delete the interface rather than adding artificial usage:
// BAD - artificial usage to silence the linter
var _ MyInterface = (*MyImpl)(nil)
// GOOD - just delete the unused interface entirely
Interfaces should be defined where they are used. If nothing uses it, it shouldn't exist.
Use Gazelle to manage BUILD files when the project has Gazelle configured:
# Generate/update BUILD files for a directory
bazel run //:gazelle -- update path/to/package
# Fix BUILD files
bazel run //:gazelle -- fix path/to/package
# Update deps from go.mod
bazel run //:gazelle -- update-repos -from_file=go.mod
# Format BUILD files (if buildifier is configured)
bazel run //:buildifier
go.mod: go get github.com/external/packagego mod tidybazel run //:gazelle -- update-repos -from_file=go.modbazel run //:gazelle -- update path/to/packagego get github.com/external/packagego mod tidyDirectory names are hard to change later. Get them right from the start.
| Rule | Good | Bad |
|---|---|---|
| Lowercase only | datastore | dataStore, DataStore |
| No underscores | loguploader | log_uploader |
| No hyphens | apiserver | api-server |
| Short, clear names | health | healthcheckservice |
myproject/
├── cmd/ # Entry points
│ └── myapp/
│ └── main.go
├── internal/ # Private packages
│ └── auth/
│ ├── auth.go
│ └── auth_test.go
├── pkg/ # Public packages (optional)
│ └── client/
│ └── client.go
├── go.mod
└── go.sum
Put package-level documentation in a file named after the package or in doc.go:
// Package health provides health checking functionality for services.
//
// It supports multiple health check types including liveness and readiness
// probes compatible with Kubernetes.
package health
main or cmd/ PackagesLibrary and business-logic packages must not import flag, cobra, pflag, urfave/cli, or similar CLI frameworks. CLI concerns belong in main or cmd/ subpackages — library code receives configuration via function parameters or config structs.
// BAD - library package imports flag
package auth
import "flag"
var verbose = flag.Bool("verbose", false, "enable verbose logging")
// GOOD - library package accepts config as a parameter
package auth
type Config struct {
Verbose bool
}
func NewService(cfg Config) *Service { ... }
Why:
init()-time side effects from flag.Parse() scattered across packages.Cobra and subcommands: Cobra-style projects typically define subcommands in cmd/ subpackages (e.g., cmd/serve/, cmd/migrate/). This is fine — those packages are CLI entry points, not reusable libraries.
Either handle the error or return it, but not both. Logging at error level and returning duplicates logs.
// BAD - logs error AND returns it (duplicates logs)
if err != nil {
slog.Error("operation failed", "reason", err)
return err
}
// GOOD - just return (let caller decide how to handle)
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
// GOOD - handle it here (don't return the error)
if err != nil {
slog.Error("operation failed, using fallback", "reason", err)
return fallbackValue, nil
}
Exception: Log at service/process boundaries where you don't control the caller.
Use standard library patterns:
// Wrap with context
return fmt.Errorf("failed to fetch user: %w", err)
// Join multiple independent errors
return errors.Join(err1, err2)
Use log/slog for structured logging:
slog.Info("starting operation", "param", val)
slog.Debug("detailed info for debugging", "state", s)
slog.Error("operation failed", "err", err, "userID", id)
Define interfaces where they are used, not where implemented:
// In the consumer package, not the provider
type Storage interface {
Save(ctx context.Context, data []byte) error
}
context.WithTimeout or context.WithCancel to manage lifetimes.func (s *Service) Process(ctx context.Context, req *Request) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
return s.db.Query(ctx, req.ID)
}
# Standard Go
go test -race ./...
# Bazel
bazel test //path/to:target_test --@io_bazel_rules_go//go/config:race
Always include the test case name in failure messages:
func TestParse(t *testing.T) {
tests := []struct {
name string
input string
want int
wantErr bool
}{
{name: "valid number", input: "42", want: 42},
{name: "negative", input: "-1", want: -1},
{name: "invalid", input: "abc", wantErr: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := Parse(tc.input)
if (err != nil) != tc.wantErr {
t.Fatalf("Parse(%q) error = %v, wantErr %v", tc.input, err, tc.wantErr)
}
if got != tc.want {
t.Errorf("Parse(%q) = %d, want %d", tc.input, got, tc.want)
}
})
}
}
Use t.Helper() in test helper functions so failures report the caller's line:
func assertEqual(t *testing.T, got, want any) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
gofmt -w .
# or stricter
gofumpt -w .
# or with import grouping
goimports -w .
Check if the project has a format target:
bazel run //:format # Write changes
bazel run //:format.check # Dry run