feat: initial Claude Code configuration scaffold
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>
This commit is contained in:
686
.claude/skills/languages/go/SKILL.md
Normal file
686
.claude/skills/languages/go/SKILL.md
Normal file
@@ -0,0 +1,686 @@
|
||||
---
|
||||
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
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user