Comprehensive Claude Code guidance system with: - 5 agents: tdd-guardian, code-reviewer, security-scanner, refactor-scan, dependency-audit - 18 skills covering languages (Python, TypeScript, Rust, Go, Java, C#), infrastructure (AWS, Azure, GCP, Terraform, Ansible, Docker/K8s, Database, CI/CD), testing (TDD, UI, Browser), and patterns (Monorepo, API Design, Observability) - 3 hooks: secret detection, auto-formatting, TDD git pre-commit - Strict TDD enforcement with 80%+ coverage requirements - Multi-model strategy: Opus for planning, Sonnet for execution (opusplan) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
687 lines
14 KiB
Markdown
687 lines
14 KiB
Markdown
---
|
|
name: go-development
|
|
description: Go development patterns with testing, error handling, and idiomatic practices. Use when writing Go code.
|
|
---
|
|
|
|
# Go Development Skill
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
project/
|
|
├── cmd/
|
|
│ └── server/
|
|
│ └── main.go # Application entry point
|
|
├── internal/
|
|
│ ├── domain/ # Business logic
|
|
│ │ ├── user.go
|
|
│ │ └── user_test.go
|
|
│ ├── repository/ # Data access
|
|
│ │ ├── user_repo.go
|
|
│ │ └── user_repo_test.go
|
|
│ ├── service/ # Application services
|
|
│ │ ├── user_service.go
|
|
│ │ └── user_service_test.go
|
|
│ └── handler/ # HTTP handlers
|
|
│ ├── user_handler.go
|
|
│ └── user_handler_test.go
|
|
├── pkg/ # Public packages
|
|
│ └── validation/
|
|
├── go.mod
|
|
├── go.sum
|
|
└── Makefile
|
|
```
|
|
|
|
## Testing Patterns
|
|
|
|
### Table-Driven Tests
|
|
```go
|
|
package user
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestValidateEmail(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
email string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid email",
|
|
email: "user@example.com",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "missing @ symbol",
|
|
email: "userexample.com",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty email",
|
|
email: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "missing domain",
|
|
email: "user@",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ValidateEmail(tt.email)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ValidateEmail(%q) error = %v, wantErr %v",
|
|
tt.email, err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
### Test Fixtures with Helper Functions
|
|
```go
|
|
package service_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"myapp/internal/domain"
|
|
)
|
|
|
|
// Test helper - creates valid user with optional overrides
|
|
func newTestUser(t *testing.T, opts ...func(*domain.User)) *domain.User {
|
|
t.Helper()
|
|
|
|
user := &domain.User{
|
|
ID: "user-123",
|
|
Email: "test@example.com",
|
|
Name: "Test User",
|
|
Role: domain.RoleUser,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(user)
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
// Option functions for customization
|
|
func withEmail(email string) func(*domain.User) {
|
|
return func(u *domain.User) {
|
|
u.Email = email
|
|
}
|
|
}
|
|
|
|
func withRole(role domain.Role) func(*domain.User) {
|
|
return func(u *domain.User) {
|
|
u.Role = role
|
|
}
|
|
}
|
|
|
|
func TestUserService_CreateUser(t *testing.T) {
|
|
t.Run("creates user with valid data", func(t *testing.T) {
|
|
user := newTestUser(t)
|
|
|
|
svc := NewUserService(mockRepo)
|
|
result, err := svc.CreateUser(user)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result.ID == "" {
|
|
t.Error("expected user to have an ID")
|
|
}
|
|
})
|
|
|
|
t.Run("rejects user with invalid email", func(t *testing.T) {
|
|
user := newTestUser(t, withEmail("invalid-email"))
|
|
|
|
svc := NewUserService(mockRepo)
|
|
_, err := svc.CreateUser(user)
|
|
|
|
if err == nil {
|
|
t.Error("expected error for invalid email")
|
|
}
|
|
})
|
|
}
|
|
```
|
|
|
|
### Mocking with Interfaces
|
|
```go
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
|
|
"myapp/internal/domain"
|
|
)
|
|
|
|
// Repository interface for dependency injection
|
|
type UserRepository interface {
|
|
GetByID(ctx context.Context, id string) (*domain.User, error)
|
|
Create(ctx context.Context, user *domain.User) error
|
|
Update(ctx context.Context, user *domain.User) error
|
|
}
|
|
|
|
// Service with injected dependencies
|
|
type UserService struct {
|
|
repo UserRepository
|
|
}
|
|
|
|
func NewUserService(repo UserRepository) *UserService {
|
|
return &UserService{repo: repo}
|
|
}
|
|
|
|
// Test file
|
|
package service_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"myapp/internal/domain"
|
|
"myapp/internal/service"
|
|
)
|
|
|
|
// Mock implementation
|
|
type mockUserRepo struct {
|
|
users map[string]*domain.User
|
|
err error
|
|
}
|
|
|
|
func newMockUserRepo() *mockUserRepo {
|
|
return &mockUserRepo{
|
|
users: make(map[string]*domain.User),
|
|
}
|
|
}
|
|
|
|
func (m *mockUserRepo) GetByID(ctx context.Context, id string) (*domain.User, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
user, ok := m.users[id]
|
|
if !ok {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (m *mockUserRepo) Create(ctx context.Context, user *domain.User) error {
|
|
if m.err != nil {
|
|
return m.err
|
|
}
|
|
m.users[user.ID] = user
|
|
return nil
|
|
}
|
|
|
|
func (m *mockUserRepo) Update(ctx context.Context, user *domain.User) error {
|
|
if m.err != nil {
|
|
return m.err
|
|
}
|
|
m.users[user.ID] = user
|
|
return nil
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Custom Error Types
|
|
```go
|
|
package domain
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// Sentinel errors for comparison
|
|
var (
|
|
ErrNotFound = errors.New("not found")
|
|
ErrUnauthorized = errors.New("unauthorized")
|
|
ErrInvalidInput = errors.New("invalid input")
|
|
)
|
|
|
|
// Structured error with context
|
|
type ValidationError struct {
|
|
Field string
|
|
Message string
|
|
}
|
|
|
|
func (e *ValidationError) Error() string {
|
|
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
|
|
}
|
|
|
|
// Error wrapping
|
|
func GetUser(ctx context.Context, id string) (*User, error) {
|
|
user, err := repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting user %s: %w", id, err)
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// Error checking
|
|
func handleError(err error) {
|
|
if errors.Is(err, ErrNotFound) {
|
|
// Handle not found
|
|
}
|
|
|
|
var validationErr *ValidationError
|
|
if errors.As(err, &validationErr) {
|
|
// Handle validation error
|
|
fmt.Printf("Field %s: %s\n", validationErr.Field, validationErr.Message)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Result Pattern (Optional)
|
|
```go
|
|
package result
|
|
|
|
// Result type for explicit error handling
|
|
type Result[T any] struct {
|
|
value T
|
|
err error
|
|
}
|
|
|
|
func Ok[T any](value T) Result[T] {
|
|
return Result[T]{value: value}
|
|
}
|
|
|
|
func Err[T any](err error) Result[T] {
|
|
return Result[T]{err: err}
|
|
}
|
|
|
|
func (r Result[T]) IsOk() bool {
|
|
return r.err == nil
|
|
}
|
|
|
|
func (r Result[T]) IsErr() bool {
|
|
return r.err != nil
|
|
}
|
|
|
|
func (r Result[T]) Unwrap() (T, error) {
|
|
return r.value, r.err
|
|
}
|
|
|
|
func (r Result[T]) UnwrapOr(defaultValue T) T {
|
|
if r.err != nil {
|
|
return defaultValue
|
|
}
|
|
return r.value
|
|
}
|
|
|
|
// Usage
|
|
func ProcessPayment(amount float64) Result[*Receipt] {
|
|
if amount <= 0 {
|
|
return Err[*Receipt](ErrInvalidInput)
|
|
}
|
|
|
|
receipt := &Receipt{Amount: amount}
|
|
return Ok(receipt)
|
|
}
|
|
```
|
|
|
|
## HTTP Handlers
|
|
|
|
### Chi Router Pattern
|
|
```go
|
|
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
|
|
"myapp/internal/service"
|
|
)
|
|
|
|
type UserHandler struct {
|
|
userService *service.UserService
|
|
}
|
|
|
|
func NewUserHandler(userService *service.UserService) *UserHandler {
|
|
return &UserHandler{userService: userService}
|
|
}
|
|
|
|
func (h *UserHandler) Routes() chi.Router {
|
|
r := chi.NewRouter()
|
|
|
|
r.Use(middleware.Logger)
|
|
r.Use(middleware.Recoverer)
|
|
|
|
r.Get("/", h.ListUsers)
|
|
r.Post("/", h.CreateUser)
|
|
r.Get("/{userID}", h.GetUser)
|
|
r.Put("/{userID}", h.UpdateUser)
|
|
r.Delete("/{userID}", h.DeleteUser)
|
|
|
|
return r
|
|
}
|
|
|
|
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
|
userID := chi.URLParam(r, "userID")
|
|
|
|
user, err := h.userService.GetUser(r.Context(), userID)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrNotFound) {
|
|
http.Error(w, "User not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, user)
|
|
}
|
|
|
|
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
|
var req CreateUserRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := req.Validate(); err != nil {
|
|
respondJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
user, err := h.userService.CreateUser(r.Context(), req.ToUser())
|
|
if err != nil {
|
|
http.Error(w, "Failed to create user", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusCreated, user)
|
|
}
|
|
|
|
func respondJSON(w http.ResponseWriter, status int, data any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|
|
```
|
|
|
|
### Request Validation
|
|
```go
|
|
package handler
|
|
|
|
import (
|
|
"regexp"
|
|
)
|
|
|
|
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
|
|
|
type CreateUserRequest struct {
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (r *CreateUserRequest) Validate() error {
|
|
if r.Email == "" {
|
|
return &domain.ValidationError{
|
|
Field: "email",
|
|
Message: "email is required",
|
|
}
|
|
}
|
|
|
|
if !emailRegex.MatchString(r.Email) {
|
|
return &domain.ValidationError{
|
|
Field: "email",
|
|
Message: "invalid email format",
|
|
}
|
|
}
|
|
|
|
if r.Name == "" {
|
|
return &domain.ValidationError{
|
|
Field: "name",
|
|
Message: "name is required",
|
|
}
|
|
}
|
|
|
|
if len(r.Name) > 100 {
|
|
return &domain.ValidationError{
|
|
Field: "name",
|
|
Message: "name must be 100 characters or less",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *CreateUserRequest) ToUser() *domain.User {
|
|
return &domain.User{
|
|
Email: r.Email,
|
|
Name: r.Name,
|
|
}
|
|
}
|
|
```
|
|
|
|
## Context Usage
|
|
|
|
```go
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// Context with timeout
|
|
func (s *UserService) ProcessOrder(ctx context.Context, orderID string) error {
|
|
// Create a timeout context
|
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Check context before expensive operations
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
// Process order...
|
|
return s.repo.UpdateOrder(ctx, orderID)
|
|
}
|
|
|
|
// Context values for request-scoped data
|
|
type contextKey string
|
|
|
|
const (
|
|
userIDKey contextKey = "userID"
|
|
requestIDKey contextKey = "requestID"
|
|
)
|
|
|
|
func WithUserID(ctx context.Context, userID string) context.Context {
|
|
return context.WithValue(ctx, userIDKey, userID)
|
|
}
|
|
|
|
func UserIDFromContext(ctx context.Context) (string, bool) {
|
|
userID, ok := ctx.Value(userIDKey).(string)
|
|
return userID, ok
|
|
}
|
|
```
|
|
|
|
## Concurrency Patterns
|
|
|
|
### Worker Pool
|
|
```go
|
|
package worker
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
)
|
|
|
|
type Job func(ctx context.Context) error
|
|
|
|
type Pool struct {
|
|
workers int
|
|
jobs chan Job
|
|
results chan error
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func NewPool(workers int) *Pool {
|
|
return &Pool{
|
|
workers: workers,
|
|
jobs: make(chan Job, workers*2),
|
|
results: make(chan error, workers*2),
|
|
}
|
|
}
|
|
|
|
func (p *Pool) Start(ctx context.Context) {
|
|
for i := 0; i < p.workers; i++ {
|
|
p.wg.Add(1)
|
|
go p.worker(ctx)
|
|
}
|
|
}
|
|
|
|
func (p *Pool) worker(ctx context.Context) {
|
|
defer p.wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case job, ok := <-p.jobs:
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := job(ctx); err != nil {
|
|
p.results <- err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Pool) Submit(job Job) {
|
|
p.jobs <- job
|
|
}
|
|
|
|
func (p *Pool) Close() {
|
|
close(p.jobs)
|
|
p.wg.Wait()
|
|
close(p.results)
|
|
}
|
|
```
|
|
|
|
### Error Group
|
|
```go
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
func (s *OrderService) ProcessBatch(ctx context.Context, orderIDs []string) error {
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
|
|
// Limit concurrency
|
|
g.SetLimit(10)
|
|
|
|
for _, orderID := range orderIDs {
|
|
orderID := orderID // Capture for goroutine
|
|
|
|
g.Go(func() error {
|
|
return s.ProcessOrder(ctx, orderID)
|
|
})
|
|
}
|
|
|
|
return g.Wait()
|
|
}
|
|
```
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# Testing
|
|
go test ./... # Run all tests
|
|
go test -v ./... # Verbose output
|
|
go test -race ./... # Race detection
|
|
go test -cover ./... # Coverage summary
|
|
go test -coverprofile=coverage.out ./... # Coverage file
|
|
go tool cover -html=coverage.out # HTML coverage report
|
|
|
|
# Linting
|
|
go vet ./... # Built-in checks
|
|
golangci-lint run # Comprehensive linting
|
|
|
|
# Building
|
|
go build -o bin/server ./cmd/server # Build binary
|
|
go build -ldflags="-s -w" -o bin/server ./cmd/server # Smaller binary
|
|
|
|
# Dependencies
|
|
go mod tidy # Clean up dependencies
|
|
go mod verify # Verify dependencies
|
|
|
|
# Code generation
|
|
go generate ./... # Run generate directives
|
|
```
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
```go
|
|
// ❌ BAD - Returning nil error with nil value
|
|
func GetUser(id string) (*User, error) {
|
|
user := db.Find(id)
|
|
return user, nil // user might be nil!
|
|
}
|
|
|
|
// ✅ GOOD - Explicit error for not found
|
|
func GetUser(id string) (*User, error) {
|
|
user := db.Find(id)
|
|
if user == nil {
|
|
return nil, ErrNotFound
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// ❌ BAD - Ignoring errors
|
|
result, _ := SomeFunction()
|
|
|
|
// ✅ GOOD - Handle or propagate errors
|
|
result, err := SomeFunction()
|
|
if err != nil {
|
|
return fmt.Errorf("calling SomeFunction: %w", err)
|
|
}
|
|
|
|
// ❌ BAD - Naked goroutine without error handling
|
|
go processItem(item)
|
|
|
|
// ✅ GOOD - Error handling with channels or error groups
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
g.Go(func() error {
|
|
return processItem(ctx, item)
|
|
})
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// ❌ BAD - Mutex embedded in struct
|
|
type Service struct {
|
|
sync.Mutex
|
|
data map[string]string
|
|
}
|
|
|
|
// ✅ GOOD - Private mutex
|
|
type Service struct {
|
|
mu sync.Mutex
|
|
data map[string]string
|
|
}
|
|
```
|