--- 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 } ```