skills/go/SKILL.md
Guides Go development with idiomatic patterns, tooling, and project structure. ALWAYS trigger on "go project", "go module", "go mod", "goroutine", "channel", "go test", "go build", "golangci-lint", "go interface", "go error handling", "go concurrency", "go struct", "go anti-pattern", "go best practices", "go tooling", "go lint". Use when setting up Go projects, writing idiomatic Go, choosing concurrency patterns, or configuring tooling. Different from testing skill which covers general test strategy; this covers Go-specific testing patterns and tooling configs.
npx skillsauth add aj-geddes/unicorn-team goInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Go uses explicit error returns, not exceptions. Every error is a value you handle at the call site.
// Return errors, don't panic
func ParseConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
return &cfg, nil
}
// Sentinel errors for callers to check
var (
ErrNotFound = errors.New("not found")
ErrForbidden = errors.New("forbidden")
)
// Custom error types for rich context
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s: %s", e.Field, e.Message)
}
// Callers use errors.Is / errors.As
if errors.Is(err, ErrNotFound) { /* handle */ }
var ve *ValidationError
if errors.As(err, &ve) { /* access ve.Field */ }
See references/error-handling.md for wrapping strategies, panic/recover, error handling in goroutines, domain error design.
Go interfaces are satisfied implicitly -- no implements keyword. Define them where they're consumed, not where they're implemented.
// Small interfaces at the consumer site
type UserStore interface {
GetUser(ctx context.Context, id string) (*User, error)
}
// Accept interfaces, return structs
func NewUserService(store UserStore) *UserService {
return &UserService{store: store}
}
// Common stdlib interfaces to know
// io.Reader, io.Writer -- streaming data
// fmt.Stringer -- string representation
// sort.Interface -- custom sorting
// encoding.BinaryMarshaler -- serialization
// context.Context -- cancellation and deadlines
// http.Handler -- HTTP request handling
| Guideline | Why |
|-----------|-----|
| 1-3 method interfaces | Easier to implement, compose, mock |
| Define at consumer | Decouples packages, avoids import cycles |
| Accept interface, return struct | Callers get flexibility, producers stay concrete |
| Embed for composition | io.ReadWriter = io.Reader + io.Writer |
// Goroutines + channels for concurrent work
func FetchAll(ctx context.Context, urls []string) ([]Result, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]Result, len(urls))
for i, url := range urls {
g.Go(func() error {
res, err := fetch(ctx, url)
if err != nil {
return err
}
results[i] = res // safe: each goroutine owns its index
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
// Context for cancellation and timeouts
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Select for multiplexing channels
select {
case msg := <-msgCh:
handle(msg)
case <-ctx.Done():
return ctx.Err()
}
See references/concurrency-patterns.md for worker pools, fan-out/fan-in, pipelines, sync primitives, context propagation.
// Table-driven tests -- the Go standard
func TestParseSize(t *testing.T) {
tests := []struct {
name string
input string
want int64
wantErr bool
}{
{name: "bytes", input: "100B", want: 100},
{name: "kilobytes", input: "2KB", want: 2048},
{name: "empty", input: "", wantErr: true},
{name: "invalid", input: "abc", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseSize(tt.input)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("ParseSize(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
// HTTP handler testing
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
w := httptest.NewRecorder()
HealthHandler(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
Coverage target: 80%+
go test -cover -coverprofile=coverage.out ./...
go tool cover -func=coverage.out # summary
go tool cover -html=coverage.out # visual report
See references/testing-go.md for subtests, benchmarks, fuzz testing, testify, httptest patterns, test helpers.
go mod init github.com/org/project # initialize
go mod tidy # sync deps
go mod vendor # vendored deps (optional)
go get github.com/pkg/[email protected] # add/update dep
Runs 50+ linters in parallel. Single tool replaces go vet, staticcheck, errcheck, gosec, and more.
# .golangci.yml
linters:
enable:
- errcheck # unchecked errors
- govet # suspicious constructs
- staticcheck # advanced analysis
- unused # unused code
- gosimple # simplifications
- ineffassign # ineffective assignments
- gocritic # opinionated checks
- gosec # security issues
- errname # error naming conventions
- exhaustive # enum exhaustiveness
linters-settings:
govet:
enable-all: true
gocritic:
enabled-tags: [diagnostic, style, performance]
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
golangci-lint run ./...
See references/tooling-config.md for complete linter configs, Makefile patterns, CI setup, build tags.
// Struct with tags for serialization
type User struct {
ID string `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Email string `json:"email" db:"email"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// Constructor function (not a Go "constructor" -- just convention)
func NewUser(name, email string) *User {
return &User{
ID: uuid.NewString(),
Name: name,
Email: email,
CreatedAt: time.Now(),
}
}
// Value receiver: doesn't modify receiver, safe on copies
func (u User) DisplayName() string {
return fmt.Sprintf("%s <%s>", u.Name, u.Email)
}
// Pointer receiver: modifies receiver or is large
func (u *User) SetEmail(email string) error {
if !strings.Contains(email, "@") {
return &ValidationError{Field: "email", Message: "invalid format"}
}
u.Email = email
return nil
}
| Use | Receiver | Why |
|-----|----------|-----|
| Read-only, small struct | Value (u User) | No mutation, safe copy |
| Mutates state | Pointer (u *User) | Changes visible to caller |
| Large struct (>3 fields) | Pointer (u *User) | Avoid copy overhead |
| Implements interface with pointer methods | Pointer (u *User) | Consistency required |
myproject/
├── cmd/
│ └── myapp/
│ └── main.go # entry point, wiring only
├── internal/ # private to this module
│ ├── server/ # HTTP server setup
│ ├── user/ # user domain logic
│ └── storage/ # database layer
├── pkg/ # importable by other projects (use sparingly)
├── go.mod
├── go.sum
├── Makefile
└── .golangci.yml
See references/project-structure.md for multi-binary repos, dependency injection, config patterns, build tags.
| Anti-pattern | Fix |
|-------------|-----|
| panic() for expected errors | Return error |
| Ignoring errors _ = f() | Handle or explicitly document why safe |
| interface{} / any everywhere | Use generics (1.18+) or specific types |
| Goroutine leak (no exit path) | Use context.Context + select |
| Shared state without sync | sync.Mutex, channels, or atomic |
| init() with side effects | Explicit initialization in main() |
| Giant interfaces (>5 methods) | Split into focused 1-3 method interfaces |
| Package-level mutable state | Dependency injection |
| Premature channel/goroutine use | Start sequential, add concurrency when needed |
| log.Fatal in library code | Return errors, let caller decide |
| Naked returns in long functions | Named returns only for short functions or godoc |
| Missing defer for cleanup | defer f.Close() immediately after open |
| Element | Convention | Example |
|---------|-----------|---------|
| Package | short, lowercase, singular | user, http, json |
| Exported function | PascalCase, verb-noun | ParseConfig, NewServer |
| Unexported function | camelCase | validateInput, buildQuery |
| Interface (1 method) | Method + "er" | Reader, Stringer, Handler |
| Interface (multi) | Descriptive noun | UserStore, EventBus |
| Error variable | Err + description | ErrNotFound, ErrTimeout |
| Error type | Description + Error | ValidationError, TimeoutError |
| Constants | PascalCase (exported) or camelCase | MaxRetries, defaultTimeout |
| Acronyms | All caps | HTTPServer, userID, xmlParser |
# 1. Initialize module
go mod init github.com/org/project
# 2. Create directory structure
mkdir -p cmd/myapp internal/{server,storage}
# 3. Install tooling
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# 4. Create .golangci.yml (see Tooling section)
# 5. Create Makefile
# make build, make test, make lint, make run
# 6. Full quality check
go vet ./... && golangci-lint run ./... && go test -race -cover ./...
go build ./... # Build all packages
go test -v -race -cover ./... # Test with race detector + coverage
go test -run TestSpecific ./internal/user/ # Run specific test
go vet ./... # Static analysis
golangci-lint run ./... # Lint (50+ linters)
go mod tidy # Sync dependencies
development
Guides the user through test-first development and test strategy decisions. ALWAYS trigger on "write tests", "TDD", "test coverage", "mock", "test fails", "flaky test", "how to test", "unit test", "integration test", "e2e test", "test structure", "what to test", "test organization", "coverage report", "testing strategy", "arrange act assert". Use when writing new tests, choosing test types, setting up mocking, debugging flaky tests, improving coverage, or designing testable code. Different from qa-security agent which focuses on code review and security audits rather than test authoring.
development
Guides deliberate management of technical debt: recognition, tracking, prioritization, and paydown. ALWAYS trigger on "technical debt", "code shortcut", "pay down debt", "debt tracking", "just for now", "temporary hack", "hardcoded value", "copy-paste code", "missing tests", "TODO cleanup", "refactor plan", "debt priority", "interest cost", "boy scout rule", "code quality backlog". Use when taking a shortcut, discovering suboptimal code, planning debt paydown, or quantifying ongoing cost of compromises.
development
Guides the user through systematic pre-commit quality verification. ALWAYS trigger on "review my code", "check my work", "before commit", "self-review", "quality check", "am I ready to commit", "pre-commit review", "code quality", "verify my changes", "sanity check", "review before merge", "is this ready". Use before any commit, merge, or code review submission.
development
Guides secure development using defense-in-depth and attacker's mindset. ALWAYS trigger on "security review", "vulnerability", "authentication", "authorization", "input validation", "XSS", "SQL injection", "CSRF", "secrets management", "OWASP", "threat model", "security scan", "path traversal", "mass assignment", "privilege escalation", "security headers", "bandit", "dependency audit", "hardening". Use when implementing auth, handling user input, storing secrets, reviewing code for vulnerabilities, or preparing for production deployment. Different from devops skill which covers infrastructure; this covers application-level security patterns.