| name | go-dev |
| description | 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. |
Go Development Skill
Use this skill when the user writes, modifies, tests, or builds Go code.
1. Build System Detection
Detect the project's build system and use the appropriate commands. Do not assume one build system over another.
Standard Go Toolchain (default)
Use when no BUILD, BUILD.bazel, or WORKSPACE files are present:
go build ./...
go test ./...
go test -race ./...
go vet ./...
Bazel Projects
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.
bazel build //path/to:target
bazel test //path/to:target_test
bazel query 'kind("go_library", //path/to/package/...)'
bazel query 'kind("go_test", //path/to/package/...)'
Makefile Projects
Use when a Makefile is present with Go-related targets:
make build
make test
make lint
2. Linting
Standard Go Toolchain
Use go vet and golangci-lint if available:
go vet ./...
golangci-lint run ./...
Bazel Nogo
In Bazel projects, nogo analyzers run during bazel build. These are compilation errors, not warnings.
Do not add //nolint directives carelessly:
- First: Understand why the linter is complaining.
- Second: Fix the code to comply.
- Only as last resort: Add
//nolint:<analyzer> with a comment explaining why.
x = computeValue()
When //nolint might be appropriate:
- Fix requires significant unrelated changes out of scope
- Linter false positive (e.g., function used via reflection)
- Implementing an external interface that requires a specific pattern
When //nolint is NOT appropriate:
- "I don't understand why it's complaining"
- "It's easier than fixing"
- "It is needed in the future"
- The fix is within scope of the current change
Common Analyzers
| 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. |
Unused Interfaces? Delete Them
If a linter reports an unused interface, delete the interface rather than adding artificial usage:
var _ MyInterface = (*MyImpl)(nil)
Interfaces should be defined where they are used. If nothing uses it, it shouldn't exist.
3. Gazelle (Bazel Projects Only)
Use Gazelle to manage BUILD files when the project has Gazelle configured:
bazel run //:gazelle -- update path/to/package
bazel run //:gazelle -- fix path/to/package
bazel run //:gazelle -- update-repos -from_file=go.mod
bazel run //:buildifier
Adding External Dependencies (Bazel)
- Add to
go.mod: go get github.com/external/package
- Tidy:
go mod tidy
- Sync to Bazel:
bazel run //:gazelle -- update-repos -from_file=go.mod
- Regenerate BUILD files:
bazel run //:gazelle -- update path/to/package
Adding External Dependencies (Standard)
- Add:
go get github.com/external/package
- Tidy:
go mod tidy
4. Directory and Package Naming
Directory names are hard to change later. Get them right from the start.
Go Package Naming Convention
| Rule | Good | Bad |
|---|
| Lowercase only | datastore | dataStore, DataStore |
| No underscores | loguploader | log_uploader |
| No hyphens | apiserver | api-server |
| Short, clear names | health | healthcheckservice |
Directory Structure
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
Package Documentation
Put package-level documentation in a file named after the package or in doc.go:
package health
5. Code Style
Keep CLI Flag and Command Definitions in main or cmd/ Packages
Library 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.
package auth
import "flag"
var verbose = flag.Bool("verbose", false, "enable verbose logging")
package auth
type Config struct {
Verbose bool
}
func NewService(cfg Config) *Service { ... }
Why:
- Keeps library packages reusable and testable without implicit global state.
- Makes dependency injection explicit — config flows down as parameters or structs.
- Avoids
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.
Error Handling: Handle OR Return, Not Both
Either handle the error or return it, but not both. Logging at error level and returning duplicates logs.
if err != nil {
slog.Error("operation failed", "reason", err)
return err
}
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
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.
Error Wrapping
Use standard library patterns:
return fmt.Errorf("failed to fetch user: %w", err)
return errors.Join(err1, err2)
Logging
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)
Interfaces
Define interfaces where they are used, not where implemented:
type Storage interface {
Save(ctx context.Context, data []byte) error
}
Context
- First parameter of functions that do I/O or may be cancelled.
- Never store in structs.
- Use
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)
}
6. Testing
Run Tests with Race Detector
go test -race ./...
bazel test //path/to:target_test --@io_bazel_rules_go//go/config:race
Table-Driven Tests
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)
}
})
}
}
Test Helpers
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)
}
}
7. Formatting
Standard Go
gofmt -w .
gofumpt -w .
goimports -w .
Bazel
Check if the project has a format target:
bazel run //:format
bazel run //:format.check