| name | go-pro |
| description | Idiomatic Go with proper error handling, concurrency patterns, slog, generics, and clean package design. Use when implementing, debugging, refactoring, or reviewing Go code; designing package boundaries; resolving race conditions or goroutine leaks; setting up table-driven tests; choosing between channels and mutexes; or tuning module layout. Applies to any Go work unless a more specific role overrides. |
Go Pro
Senior-level Go expertise for production projects. Focuses on idiomatic patterns, simplicity, and Go's design philosophy.
When Invoked
- Review
go.mod and .golangci.yml for project conventions
- For build system setup, invoke the just-pro skill
- Apply Go idioms and established project patterns
Core Standards
Required:
- All exported identifiers have doc comments
- All errors checked and handled (no
_ = err)
- NO
panic() for recoverable errors
- golangci-lint passes with project configuration
- Table-driven tests for multiple cases
Foundational Principles:
- Single Responsibility: One package = one purpose, one function = one job
- No God Objects: Split large structs; if it has 10+ fields or methods, decompose
- Dependency Injection: Pass dependencies, don't create them internally
- Small Interfaces: 1-3 methods max; compose larger behaviors from small interfaces
Project Setup (Go 1.25+)
Version Management
Pin Go version with mise: mise use go@1.25 (creates .mise.toml — commit it). Team members run mise install. See mise skill for setup.
New Project Quick Start
go mod init github.com/org/project
go mod edit -go=1.25
go get -tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
go get -tool golang.org/x/tools/cmd/goimports@latest
go get -tool golang.org/x/vuln/cmd/govulncheck@latest
just check
Developer Onboarding
git clone <repo> && cd <repo>
just setup
just check
Or manually:
mise trust && mise install
go mod download
Why go get -tool? Tools versioned in go.mod = reproducible builds, same versions for all devs, no separate installation needed.
Build System
Invoke the just-pro skill for build system setup. It covers:
- Simple repos vs monorepos
- Hierarchical justfile modules
- Go-specific templates (
references/package-go.just)
Why just? Consistent toolchain frontend between agents and humans. Instead of remembering go tool golangci-lint run --fix, use just fix.
Quality Assurance
Auto-Fix First - Always try auto-fix before manual fixes:
just fix
Verification:
just check
Quick Reference
Error Handling
| Pattern | Use |
|---|
return err | Propagate unchanged (internal errors) |
fmt.Errorf("context: %w", err) | Wrap with context (cross-boundary) |
errors.Is(err, target) | Check specific error |
errors.As(err, &target) | Extract typed error |
Sentinel Errors - Define package-level errors for expected conditions:
var ErrNotFound = errors.New("not found")
var ErrInvalidInput = errors.New("invalid input")
Generics
func Map[T, U any](items []T, fn func(T) U) []U {
result := make([]U, len(items))
for i, item := range items {
result[i] = fn(item)
}
return result
}
type Ordered interface {
~int | ~int64 | ~float64 | ~string
}
func Max[T Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Structured Logging (slog)
import "log/slog"
func NewService(logger *slog.Logger) *Service {
return &Service{
log: logger.With("component", "service"),
}
}
s.log.Info("request processed",
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start),
)
s.log.Error("operation failed",
"err", err,
"user_id", userID,
)
Iterators (Go 1.23+)
import "iter"
func (db *DB) Users() iter.Seq[User] {
return func(yield func(User) bool) {
rows, _ := db.Query("SELECT * FROM users")
defer rows.Close()
for rows.Next() {
var u User
rows.Scan(&u.ID, &u.Name)
if !yield(u) {
return
}
}
}
}
for user := range db.Users() {
process(user)
}
func (m *Map[K, V]) All() iter.Seq2[K, V]
Concurrency
| Pattern | Use |
|---|
sync.WaitGroup | Wait for goroutines |
sync.Mutex / RWMutex | Protect shared state |
context.Context | Cancellation/timeouts |
errgroup.Group | Concurrent with error collection |
func DoWork(ctx context.Context, arg string) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
}
Testing
func TestParse(t *testing.T) {
tests := []struct {
name string
input string
want Result
wantErr bool
}{
{name: "valid", input: "foo", want: Result{Value: "foo"}},
{name: "empty", input: "", wantErr: true},
{name: "special", input: "a@b", want: Result{Value: "a@b"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Parse() = %v, want %v", got, tt.want)
}
})
}
}
import "github.com/stretchr/testify/assert"
import "github.com/stretchr/testify/require"
func TestService(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, expected, actual)
assert.Len(t, items, 3)
assert.Contains(t, items, target)
}
Pointer vs Value Receivers
func (s *Service) UpdateConfig(cfg Config) { s.cfg = cfg }
func (p Point) Distance(other Point) float64 { ... }
Package Organization
project/
├── cmd/appname/main.go # Entry point
├── internal/ # Private packages
│ ├── api/ # Handlers
│ └── domain/ # Business logic
├── go.mod
├── .golangci.yml
└── justfile
Rules: One package = one purpose. Use internal/ for implementation. Avoid util, common, helpers packages.
DX Patterns
Doctor Recipe with Version Validation
Doctor scripts should validate that toolchain versions meet requirements, not just check existence:
# Validate toolchain versions meet requirements
doctor:
#!/usr/bin/env bash
set -euo pipefail
echo "Checking toolchain..."
# Validate Go version (requires 1.25+)
GO_VERSION=$(go version | grep -oE 'go[0-9]+\.[0-9]+' | sed 's/go//')
if [[ "$(printf '%s\n' "1.25" "$GO_VERSION" | sort -V | head -1)" != "1.25" ]]; then
echo "FAIL: Go $GO_VERSION < 1.25 required"
exit 1
fi
echo "✓ Go $GO_VERSION"
# Add more version checks as needed
echo "All checks passed"
Port Conflict Detection
For services that bind ports, check availability before starting:
# Check if required ports are available before starting
check-ports:
#!/usr/bin/env bash
for port in 8080 5432; do
if lsof -i :$port >/dev/null 2>&1; then
echo "FAIL: Port $port already in use"
exit 1
fi
done
echo "All ports available"
First-Run Detection
Avoid redundant setup work with first-run detection:
# Setup with first-run detection
setup:
#!/usr/bin/env bash
if [[ -f .setup-complete ]]; then
echo "Already set up. Run 'just setup-force' to reinstall."
exit 0
fi
mise trust && mise install
go mod download
touch .setup-complete
echo "Setup complete"
setup-force:
rm -f .setup-complete
@just setup
Responding to Limit Violations
These limits exist to improve code architecture, not to be gamed. When a file or function exceeds its linter limit (funlen, file-length-limit, gocognit), the correct response is to decompose by responsibility.
Extract, don't compress:
- Identify logical sections (validation, transformation, I/O, mapping)
- Extract each into a well-named function — the name documents what the section does
- Place in a companion file in the same package (e.g.,
order.go → order_validate.go, order_transform.go)
Go's package-level visibility makes extraction cheap — no parameter explosion since extracted functions in the same package access shared types naturally.
Prohibited responses to limit violations: combining statements onto single lines, removing or shortening comments, compressing whitespace, shortening descriptive names, inlining helpers. The goal is clean architecture, not metric compliance.
Anti-Patterns
panic() for recoverable errors (use return err)
- Ignoring errors with
_
- Exported package-level mutable variables
- Channels when mutex suffices
- Getter/setter methods (Go isn't Java)
init() with side effects
- God structs with 10+ fields/methods (extract into companion files, don't compress)
interface{} or any when specific types work
- Premature generics (concrete types first)
AI Agent Guidelines
Before writing code:
- Read
go.mod for module path and Go version
- Check
.golangci.yml for project-specific lint rules
- Identify existing patterns in the codebase to follow
When writing code:
- Handle all errors explicitly - never use
_ = err
- Add doc comments to exported identifiers immediately
- Use existing project abstractions over creating new ones
- Prefer concrete types; add generics only when pattern repeats 3+ times
Before committing:
- Run
just check (standard for projects using just)
- Fallback:
go tool golangci-lint run --fix && go tool golangci-lint run
- Fallback:
go test -race ./... to catch race conditions
- Fallback:
go tool govulncheck ./... to catch known vulnerabilities