feat: add Go Lang development coverage (Milestone #49)

- Add go-developer agent for Go backend development
- Add 8 Go skills: web-patterns, middleware, db-patterns,
  error-handling, security, testing, concurrency, modules
- Add go.md rules file
- Update capability-index.yaml with Go capabilities
- Complete backend coverage for both NodeJS and Go
This commit is contained in:
¨NW¨
2026-04-05 03:40:32 +01:00
parent 8fcd8f8a9b
commit be4c586c8f
11 changed files with 5077 additions and 0 deletions

View File

@@ -0,0 +1,493 @@
---
description: Go backend specialist for Gin, Echo, APIs, and database integration
mode: subagent
model: ollama-cloud/deepseek-v3.2
color: "#00ADD8"
permission:
read: allow
edit: allow
write: allow
bash: allow
glob: allow
grep: allow
task:
"*": deny
---
# Kilo Code: Go Developer
## Role Definition
You are **Go Developer** — the Go backend specialist. Your personality is pragmatic, concurrency-focused, and idiomatic Go. You build performant services, design clean APIs, and leverage Go's strengths for concurrent systems.
## When to Use
Invoke this mode when:
- Building Go web services with Gin/Echo
- Designing REST/gRPC APIs
- Implementing concurrent patterns (goroutines, channels)
- Database integration with GORM/sqlx
- Creating Go microservices
- Authentication and middleware in Go
## Short Description
Go backend specialist for Gin, Echo, APIs, and concurrent systems.
## Behavior Guidelines
1. **Idiomatic Go** — Follow Go conventions and idioms
2. **Error Handling** — Always handle errors explicitly, wrap with context
3. **Concurrency** — Use goroutines and channels safely, prevent leaks
4. **Context Propagation** — Always pass context as first parameter
5. **Interface Design** — Accept interfaces, return concrete types
6. **Zero Values** — Design for zero-value usability
## Tech Stack
| Layer | Technologies |
|-------|-------------|
| Runtime | Go 1.21+ |
| Framework | Gin, Echo, net/http |
| Database | PostgreSQL, MySQL, SQLite |
| ORM | GORM, sqlx |
| Auth | JWT, OAuth2 |
| Validation | go-playground/validator |
| Testing | testing, testify, mockery |
## Output Format
```markdown
## Go Implementation: [Feature]
### API Endpoints Created
| Method | Path | Handler | Description |
|--------|------|---------|-------------|
| GET | /api/resource | ListResources | List resources |
| POST | /api/resource | CreateResource | Create resource |
| PUT | /api/resource/:id | UpdateResource | Update resource |
| DELETE | /api/resource/:id | DeleteResource | Delete resource |
### Database Changes
- Table: `resources`
- Columns: id (UUID), name (VARCHAR), created_at (TIMESTAMP), updated_at (TIMESTAMP)
- Indexes: idx_resources_name
### Files Created
- `internal/handlers/resource.go` - HTTP handlers
- `internal/services/resource.go` - Business logic
- `internal/repositories/resource.go` - Data access
- `internal/models/resource.go` - Data models
- `internal/middleware/auth.go` - Authentication middleware
### Security
- ✅ Input validation (go-playground/validator)
- ✅ SQL injection protection (parameterized queries)
- ✅ Context timeout handling
- ✅ Rate limiting middleware
---
Status: implemented
@CodeSkeptic ready for review
```
## Project Structure
```go
myapp/
cmd/
server/
main.go // Application entrypoint
internal/
config/
config.go // Configuration loading
handlers/
user.go // HTTP handlers
services/
user.go // Business logic
repositories/
user.go // Data access
models/
user.go // Data models
middleware/
auth.go // Middleware
app/
app.go // Application setup
pkg/
utils/
response.go // Public utilities
api/
openapi/
openapi.yaml // API definition
go.mod
go.sum
```
## Handler Template
```go
// internal/handlers/user.go
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/myorg/myapp/internal/models"
"github.com/myorg/myapp/internal/services"
)
type UserHandler struct {
service services.UserService
}
func NewUserHandler(service services.UserService) *UserHandler {
return &UserHandler{service: service}
}
// List handles GET /api/users
func (h *UserHandler) List(c *gin.Context) {
users, err := h.service.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, users)
}
// Create handles POST /api/users
func (h *UserHandler) Create(c *gin.Context) {
var req models.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.service.Create(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, user)
}
```
## Service Template
```go
// internal/services/user.go
package services
import (
"context"
"fmt"
"github.com/myorg/myapp/internal/models"
"github.com/myorg/myapp/internal/repositories"
)
type UserService interface {
GetByID(ctx context.Context, id string) (*models.User, error)
List(ctx context.Context) ([]models.User, error)
Create(ctx context.Context, req *models.CreateUserRequest) (*models.User, error)
Update(ctx context.Context, id string, req *models.UpdateUserRequest) (*models.User, error)
Delete(ctx context.Context, id string) error
}
type userService struct {
repo repositories.UserRepository
}
func NewUserService(repo repositories.UserRepository) UserService {
return &userService{repo: repo}
}
func (s *userService) GetByID(ctx context.Context, id string) (*models.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
return user, nil
}
func (s *userService) Create(ctx context.Context, req *models.CreateUserRequest) (*models.User, error) {
user := &models.User{
Email: req.Email,
FirstName: req.FirstName,
LastName: req.LastName,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return user, nil
}
```
## Repository Template
```go
// internal/repositories/user.go
package repositories
import (
"context"
"errors"
"fmt"
"gorm.io/gorm"
"github.com/myorg/myapp/internal/models"
)
type UserRepository interface {
FindByID(ctx context.Context, id string) (*models.User, error)
FindByEmail(ctx context.Context, email string) (*models.User, error)
Create(ctx context.Context, user *models.User) error
Update(ctx context.Context, user *models.User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context) ([]models.User, error)
}
type gormUserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &gormUserRepository{db: db}
}
func (r *gormUserRepository) FindByID(ctx context.Context, id string) (*models.User, error) {
var user models.User
if err := r.db.WithContext(ctx).First(&user, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("find user: %w", err)
}
return &user, nil
}
func (r *gormUserRepository) Create(ctx context.Context, user *models.User) error {
if err := r.db.WithContext(ctx).Create(user).Error; err != nil {
return fmt.Errorf("create user: %w", err)
}
return nil
}
```
## Model Template
```go
// internal/models/user.go
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type User struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primary_key" json:"id"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
FirstName string `gorm:"size:100" json:"first_name"`
LastName string `gorm:"size:100" json:"last_name"`
Role string `gorm:"default:'user'" json:"role"`
Active bool `gorm:"default:true" json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (User) TableName() string {
return "users"
}
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"`
FirstName string `json:"first_name" validate:"required"`
LastName string `json:"last_name" validate:"required"`
Password string `json:"password" validate:"required,min=8"`
}
type UpdateUserRequest struct {
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
}
```
## Middleware Template
```go
// internal/middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func Auth(jwtSecret string) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "missing authorization header",
})
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "invalid token",
})
return
}
claims := token.Claims.(jwt.MapClaims)
c.Set("userID", claims["sub"])
c.Next()
}
}
```
## Error Handling
```go
// pkg/errors/errors.go
package errors
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrBadRequest = errors.New("bad request")
ErrInternal = errors.New("internal error")
)
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
func NewNotFound(message string) *AppError {
return &AppError{Code: 404, Message: message, Err: ErrNotFound}
}
func NewBadRequest(message string) *AppError {
return &AppError{Code: 400, Message: message, Err: ErrBadRequest}
}
// internal/middleware/errors.go
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
for _, err := range c.Errors {
var appErr *errors.AppError
if errors.As(err.Err, &appErr) {
c.AbortWithStatusJSON(appErr.Code, gin.H{
"error": appErr.Message,
})
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "internal server error",
})
return
}
}
}
```
## Prohibited Actions
- DO NOT ignore errors — always handle or wrap
- DO NOT use panic in handlers
- DO NOT store contexts in structs
- DO NOT expose internal errors to clients
- DO NOT hardcode secrets or credentials
- DO NOT use global state for request data
## Skills Reference
This agent uses the following skills for comprehensive Go development:
### Core Skills
| Skill | Purpose |
|-------|---------|
| `go-web-patterns` | Gin, Echo, net/http patterns |
| `go-middleware` | Authentication, CORS, rate limiting |
| `go-error-handling` | Error types, wrapping, handling |
| `go-security` | OWASP, validation, security headers |
### Database
| Skill | Purpose |
|-------|---------|
| `go-db-patterns` | GORM, sqlx, migrations, transactions |
### Concurrency
| Skill | Purpose |
|-------|---------|
| `go-concurrency` | Goroutines, channels, context, sync |
### Testing & Quality
| Skill | Purpose |
|-------|---------|
| `go-testing` | Unit tests, table-driven, mocking |
### Package Management
| Skill | Purpose |
|-------|---------|
| `go-modules` | go.mod, dependencies, versioning |
### Rules
| File | Content |
|------|---------|
| `.kilo/rules/go.md` | Code style, error handling, best practices |
## Handoff Protocol
After implementation:
1. Run `go fmt ./...` and `go vet ./...`
2. Run `go test -race ./...`
3. Check for vulnerabilities: `govulncheck ./...`
4. Verify all handlers return proper status codes
5. Check context propagation throughout
6. Tag `@CodeSkeptic` for review
## Gitea Commenting (MANDATORY)
**You MUST post a comment to the Gitea issue after completing your work.**
Post a comment with:
1. ✅ Success: What was done, files changed, duration
2. ❌ Error: What failed, why, and blocker
3. ❓ Question: Clarification needed with options
Use the `post_comment` function from `.kilo/skills/gitea-commenting/SKILL.md`.
**NO EXCEPTIONS** - Always comment to Gitea.

View File

@@ -59,6 +59,27 @@ agents:
model: ollama-cloud/qwen3-coder:480b
mode: subagent
go-developer:
capabilities:
- go_api_development
- go_database_design
- go_concurrent_programming
- go_authentication
- go_microservices
receives:
- api_specifications
- database_requirements
- concurrent_requirements
produces:
- go_handlers
- go_database_schema
- go_api_documentation
- concurrent_solutions
forbidden:
- frontend_code
model: ollama-cloud/deepseek-v3.2
mode: subagent
# Quality Assurance
sdet-engineer:
capabilities:
@@ -484,6 +505,12 @@ capability_routing:
memory_retrieval: memory-manager
chain_of_thought: planner
tree_of_thoughts: planner
# Go Development
go_api_development: go-developer
go_database_design: go-developer
go_concurrent_programming: go-developer
go_authentication: go-developer
go_microservices: go-developer
# Parallelizable Tasks
parallel_groups:

283
.kilo/rules/go.md Normal file
View File

@@ -0,0 +1,283 @@
# Go Rules
Essential rules for Go development.
## Code Style
- Use `gofmt` for formatting
- Use `go vet` for static analysis
- Follow standard Go conventions
- Run `golangci-lint` before commit
```go
// ✅ Good
package user
import (
"context"
"errors"
"github.com/gin-gonic/gin"
)
type Service struct {
repo Repository
}
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
// ❌ Bad
package user
import "context"
import "errors"
import "github.com/gin-gonic/gin" // Wrong import grouping
```
## Error Handling
- Always handle errors
- Use `fmt.Errorf` with `%w` for wrapping
- Define custom error types
- Never panic in library code
```go
// ✅ Good
func GetUser(id int64) (*User, error) {
user, err := repo.FindByID(id)
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
return user, nil
}
// ❌ Bad
func GetUser(id int64) *User {
user, _ := repo.FindByID(id) // Ignoring error
return user
}
```
## Context
- Always pass `context.Context` as first parameter
- Use context for cancellation and timeouts
- Don't store context in structs
```go
// ✅ Good
func (s *Service) GetByID(ctx context.Context, id int64) (*User, error) {
return s.repo.FindByID(ctx, id)
}
// ❌ Bad
func (s *Service) GetByID(id int64) (*User, error) {
return s.repo.FindByID(context.Background(), id)
}
```
## Concurrency
- Use `sync.WaitGroup` for goroutine coordination
- Use channels for communication, not shared memory
- Always close channels
- Use context for cancellation
```go
// ✅ Good
func Process(items []int) error {
var wg sync.WaitGroup
errCh := make(chan error, 1)
for _, item := range items {
wg.Add(1)
go func(i int) {
defer wg.Done()
if err := processItem(i); err != nil {
select {
case errCh <- err:
default:
}
}
}(item)
}
go func() {
wg.Wait()
close(errCh)
}()
return <-errCh
}
```
## Testing
- Write tests for all exported functions
- Use table-driven tests
- Use `t.Parallel()` where appropriate
- Mock external dependencies
```go
// ✅ Good: Table-driven test
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
valid bool
}{
{"valid", "test@example.com", true},
{"invalid", "invalid", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateEmail(tt.email)
if got != tt.valid {
t.Errorf("got %v, want %v", got, tt.valid)
}
})
}
}
```
## Project Structure
```
myapp/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── config/
│ ├── handlers/
│ ├── services/
│ ├── repositories/
│ └── models/
├── pkg/
│ └── public/
├── go.mod
└── go.sum
```
## Security
- Validate all inputs
- Use parameterized queries
- Never store passwords in plain text
- Use environment variables for secrets
- Set security headers
```go
// ✅ Good: Parameterized query
func GetUser(db *sql.DB, id string) (*User, error) {
query := "SELECT * FROM users WHERE id = ?"
return db.QueryRow(query, id)
}
// ❌ Bad: SQL injection
func GetUser(db *sql.DB, id string) (*User, error) {
query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", id)
return db.QueryRow(query)
}
```
## Dependencies
- Use Go modules (`go.mod`)
- Run `go mod tidy` regularly
- Check for vulnerabilities: `govulncheck ./...`
- Don't overuse external dependencies
```bash
# ✅ Good practices
go mod init myapp
go get github.com/gin-gonic/gin
go mod tidy
govulncheck ./...
# Update dependencies
go get -u ./...
go mod tidy
```
## HTTP Handlers
- Keep handlers thin
- Return proper HTTP status codes
- Use middleware for cross-cutting concerns
- Validate input before processing
```go
// ✅ Good: Thin handler
func (h *Handler) GetUser(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
user, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"user": user})
}
// ❌ Bad: Logic in handler
func GetUser(c *gin.Context) {
db := getDB()
var user User
db.First(&user, c.Param("id"))
c.JSON(200, user)
}
```
## Interface Design
- Accept interfaces, return concrete types
- Keep interfaces small
- Use interfaces for testing
```go
// ✅ Good
type Repository interface {
FindByID(ctx context.Context, id int64) (*User, error)
Create(ctx context.Context, user *User) error
}
type UserService struct {
repo Repository
}
// ❌ Bad: Too large interface
type Service interface {
GetUser(id int64) (*User, error)
CreateUser(user *User) error
UpdateUser(user *User) error
DeleteUser(id int64) error
// ...many more methods
}
```
## Logging
- Use structured logging (zap, zerolog)
- Include context in logs
- Use appropriate log levels
- Don't log sensitive data
```go
// ✅ Good: Structured logging
logger.Info("user login",
zap.String("user_id", userID),
zap.String("ip", ip),
zap.Time("timestamp", time.Now()),
)
// ❌ Bad: Printf logging
log.Printf("user %s logged in from %s", userID, ip)
```

View File

@@ -0,0 +1,553 @@
# Go Concurrency Patterns
Idiomatic concurrency patterns for Go applications.
## Goroutines
```go
// Basic goroutine
go func() {
fmt.Println("Running in goroutine")
}()
// Anonymous function with parameters
for i := 0; i < 10; i++ {
go func(n int) {
fmt.Printf("Goroutine %d\n", n)
}(i)
}
// Wait for goroutine completion
func processData() error {
var wg sync.WaitGroup
errChan := make(chan error, 1)
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
if err := processItem(n); err != nil {
select {
case errChan <- err:
default:
}
}
}(i)
}
go func() {
wg.Wait()
close(errChan)
}()
return <-errChan
}
```
## Channels
```go
// Basic channel
ch := make(chan int)
// Buffered channel
ch := make(chan int, 10)
// Send and receive
go func() {
ch <- 42 // Send
}()
value := <-ch // Receive
// Close channel
close(ch)
// Range over channel
for value := range ch {
fmt.Println(value)
}
// Select statement
select {
case msg := <-ch1:
fmt.Println("Received from ch1:", msg)
case msg := <-ch2:
fmt.Println("Received from ch2:", msg)
case <-time.After(time.Second):
fmt.Println("Timeout")
default:
fmt.Println("No message available")
}
```
## Worker Pool
```go
package pool
import "sync"
type Task func() error
type WorkerPool struct {
tasks chan Task
results chan error
workers int
wg sync.WaitGroup
}
func NewWorkerPool(workers int) *WorkerPool {
return &WorkerPool{
tasks: make(chan Task, 100),
results: make(chan error, 100),
workers: workers,
}
}
func (p *WorkerPool) Start() {
for i := 0; i < p.workers; i++ {
p.wg.Add(1)
go p.worker()
}
}
func (p *WorkerPool) worker() {
defer p.wg.Done()
for task := range p.tasks {
if err := task(); err != nil {
p.results <- err
}
}
}
func (p *WorkerPool) Submit(task Task) {
p.tasks <- task
}
func (p *WorkerPool) Stop() {
close(p.tasks)
p.wg.Wait()
close(p.results)
}
func (p *WorkerPool) Results() <-chan error {
return p.results
}
// Usage
func main() {
pool := NewWorkerPool(5)
pool.Start()
for i := 0; i < 100; i++ {
i := i // Capture loop variable
pool.Submit(func() error {
return processItem(i)
})
}
go func() {
for err := range pool.Results() {
if err != nil {
log.Printf("Error: %v", err)
}
}
}()
pool.Stop()
}
```
## Fan-Out / Fan-In
```go
// Fan-out: Multiple workers process same channel
func FanOut(jobs <-chan int, workers int) []<-chan int {
results := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
results[i] = worker(jobs)
}
return results
}
func worker(jobs <-chan int) <-chan int {
results := make(chan int)
go func() {
defer close(results)
for job := range jobs {
results <- process(job)
}
}()
return results
}
// Fan-in: Merge multiple channels
func FanIn(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
// Usage
func main() {
jobs := make(chan int, 100)
// Fan-out
workers := FanOut(jobs, 5)
// Fan-in
results := FanIn(workers...)
go func() {
for result := range results {
fmt.Println(result)
}
}()
// Send jobs
for i := 0; i < 100; i++ {
jobs <- i
}
close(jobs)
}
```
## Context
```go
import "context"
// Context with timeout
func ProcessWithTimeout(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resultCh := make(chan error, 1)
go func() {
resultCh <- doWork()
}()
select {
case err := <-resultCh:
return err
case <-ctx.Done():
return ctx.Err()
}
}
// Context with cancel
func ProcessWithCancel(parent context.Context) {
ctx, cancel := context.WithCancel(parent)
defer cancel()
go func() {
// Some operation
if shouldCancel() {
cancel()
}
}()
select {
case <-ctx.Done():
fmt.Println("Context cancelled")
}
}
// Context with values
type key string
const (
UserIDKey key = "userID"
RoleKey key = "role"
)
func WithUser(ctx context.Context, userID, role string) context.Context {
ctx = context.WithValue(ctx, UserIDKey, userID)
ctx = context.WithValue(ctx, RoleKey, role)
return ctx
}
func GetUser(ctx context.Context) (userID, role string) {
return ctx.Value(UserIDKey).(string), ctx.Value(RoleKey).(string)
}
```
## Pipeline
```go
// Pipeline pattern
func Pipeline() {
// Stage 1: Generate numbers
nums := generate(1, 2, 3, 4, 5)
// Stage 2: Square numbers
squared := square(nums)
// Stage 3: Filter even numbers
evens := filter(squared, func(n int) bool {
return n%2 == 0
})
// Consume
for n := range evens {
fmt.Println(n)
}
}
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func filter(in <-chan int, predicate func(int) bool) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
if predicate(n) {
out <- n
}
}
}()
return out
}
```
## Rate Limiting
```go
import "golang.org/x/time/rate"
// Token bucket rate limiter
func RateLimitMiddleware(r rate.Limit, b int) gin.HandlerFunc {
limiter := rate.NewLimiter(r, b)
return func(c *gin.Context) {
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "rate limit exceeded",
})
c.Abort()
return
}
c.Next()
}
}
// Usage
router.Use(RateLimitMiddleware(100, 10)) // 100 requests/sec, burst 10
// Per-client rate limiting
type IPRateLimiter struct {
mu sync.Mutex
limiters map[string]*rate.Limiter
rate rate.Limit
burst int
}
func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
return &IPRateLimiter{
limiters: make(map[string]*rate.Limiter),
rate: r,
burst: b,
}
}
func (l *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
l.mu.Lock()
defer l.mu.Unlock()
limiter, exists := l.limiters[ip]
if !exists {
limiter = rate.NewLimiter(l.rate, l.burst)
l.limiters[ip] = limiter
}
return limiter
}
func (l *IPRateLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
limiter := l.GetLimiter(ip)
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "rate limit exceeded",
})
c.Abort()
return
}
c.Next()
}
}
```
## Graceful Shutdown
```go
func main() {
server := &http.Server{Addr: ":8080"}
// Start server in goroutine
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Create deadline
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Shutdown
if err := server.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exited")
}
// Shutdown with cleanup
func shutdownWithCleanup(server *http.Server, cleanup func()) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Starting graceful shutdown...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Shutdown HTTP server
if err := server.Shutdown(ctx); err != nil {
log.Printf("HTTP server shutdown error: %v", err)
}
// Run cleanup
cleanup()
log.Println("Shutdown complete")
}
```
## Best Practices
### Don't Do This
```go
// ❌ Bad: No synchronization
var counter int
for i := 0; i < 1000; i++ {
go func() {
counter++ // Race condition!
}()
}
// ❌ Bad: Goroutine leak
func process() {
ch := make(chan int)
go func() {
for {
ch <- getEvent() // Never exits!
}
}()
// ... function returns, goroutine leaks
}
// ❌ Bad: Blocking main goroutine
func main() {
ch := make(chan int)
ch <- 1 // Blocks forever, no receiver
}
```
### Do This
```go
// ✅ Good: Use mutex for shared state
var (
counter int
mu sync.Mutex
)
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
// ✅ Good: Use atomic for simple counters
var counter int64
for i := 0; i < 1000; i++ {
go func() {
atomic.AddInt64(&counter, 1)
}()
}
// ✅ Good: Proper goroutine termination
func process(ctx context.Context) {
ch := make(chan int)
go func() {
defer close(ch)
for {
select {
case <-ctx.Done():
return
case ch <- getEvent():
}
}
}()
}
// ✅ Good: Buffered channel for non-blocking send
func main() {
ch := make(chan int, 1)
ch <- 1 // Doesn't block
}
```

View File

@@ -0,0 +1,628 @@
# Go Database Patterns Skill
Comprehensive guide to database patterns in Go applications using GORM, sqlx, and database/sql.
## Overview
This skill covers database connection management, CRUD operations, transactions, migrations, and best practices for building robust data layers in Go applications.
## Connection Management
### Basic Connection (database/sql)
```go
package database
import (
"database/sql"
"time"
_ "github.com/lib/pq" // PostgreSQL driver
)
// ✅ Good: Connection pool configuration
func NewDB(connectionString string) (*sql.DB, error) {
db, err := sql.Open("postgres", connectionString)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
// Connection pool settings
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(1 * time.Minute)
// Verify connection
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("ping database: %w", err)
}
return db, nil
}
// ❌ Bad: No connection pool settings
func NewDBBad(connectionString string) (*sql.DB, error) {
return sql.Open("postgres", connectionString)
}
```
### Transaction Management
```go
// ✅ Good: Transaction pattern
func (s *Service) CreateUserWithProfile(ctx context.Context, user *User, profile *Profile) error {
return s.db.Transaction(ctx, func(tx *sql.Tx) error {
// Create user
userQuery := `INSERT INTO users (email, password) VALUES ($1, $2) RETURNING id`
if err := tx.QueryRowContext(ctx, userQuery, user.Email, user.Password).Scan(&user.ID); err != nil {
return fmt.Errorf("create user: %w", err)
}
// Create profile with user ID
profileQuery := `INSERT INTO profiles (user_id, bio) VALUES ($1, $2)`
if _, err := tx.ExecContext(ctx, profileQuery, user.ID, profile.Bio); err != nil {
return fmt.Errorf("create profile: %w", err)
}
return nil
})
}
// ✅ Good: Generic transaction helper
type Transactor struct {
db *sql.DB
}
func (t *Transactor) Transaction(ctx context.Context, fn func(*sql.Tx) error) error {
tx, err := t.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
if err := fn(tx); err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return fmt.Errorf("rollback error: %v, original error: %w", rbErr, err)
}
return err
}
return tx.Commit()
}
```
## GORM Patterns
### Model Definition
```go
// ✅ Good: GORM model with proper conventions
type User struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primary_key"`
Email string `gorm:"uniqueIndex;not null"`
Password string `gorm:"not null"`
FirstName string `gorm:"size:100"`
LastName string `gorm:"size:100"`
Role string `gorm:"default:'user'"`
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
// Relationships
Profile *Profile `gorm:"foreignKey:UserID"`
Posts []Post `gorm:"foreignKey:AuthorID"`
}
type Profile struct {
gorm.Model
UserID uuid.UUID `gorm:"not null;index"`
Bio string `gorm:"size:500"`
Avatar string `gorm:"size:255"`
}
// ❌ Bad: Missing indexes and constraints
type UserBad struct {
gorm.Model // Uses uint ID instead of UUID
Email string // No unique constraint
Name string // No size limit
}
```
### GORM Repository Pattern
```go
// ✅ Good: Repository interface
type UserRepository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id uuid.UUID) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, params ListParams) ([]User, int64, error)
}
// ✅ Good: GORM repository implementation
type GormUserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &GormUserRepository{db: db}
}
func (r *GormUserRepository) Create(ctx context.Context, user *User) error {
if err := r.db.WithContext(ctx).Create(user).Error; err != nil {
return fmt.Errorf("create user: %w", err)
}
return nil
}
func (r *GormUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
var user User
if err := r.db.WithContext(ctx).First(&user, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("get user: %w", err)
}
return &user, nil
}
func (r *GormUserRepository) GetByEmail(ctx context.Context, email string) (*User, error) {
var user User
if err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("get user by email: %w", err)
}
return &user, nil
}
func (r *GormUserRepository) Update(ctx context.Context, user *User) error {
result := r.db.WithContext(ctx).Save(user)
if result.Error != nil {
return fmt.Errorf("update user: %w", result.Error)
}
if result.RowsAffected == 0 {
return ErrNotFound
}
return nil
}
func (r *GormUserRepository) Delete(ctx context.Context, id uuid.UUID) error {
result := r.db.WithContext(ctx).Delete(&User{}, "id = ?", id)
if result.Error != nil {
return fmt.Errorf("delete user: %w", result.Error)
}
if result.RowsAffected == 0 {
return ErrNotFound
}
return nil
}
func (r *GormUserRepository) List(ctx context.Context, params ListParams) ([]User, int64, error) {
var users []User
var total int64
query := r.db.WithContext(ctx).Model(&User{})
// Apply filters
if params.Search != "" {
search := "%" + params.Search + "%"
query = query.Where("email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?",
search, search, search)
}
if params.Role != "" {
query = query.Where("role = ?", params.Role)
}
// Get total count
if err := query.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("count users: %w", err)
}
// Apply pagination
offset := (params.Page - 1) * params.PageSize
if err := query.Offset(offset).Limit(params.PageSize).Find(&users).Error; err != nil {
return nil, 0, fmt.Errorf("list users: %w", err)
}
return users, total, nil
}
```
### GORM Associations
```go
// ✅ Good: Loading associations
func (r *GormUserRepository) GetWithProfile(ctx context.Context, id uuid.UUID) (*User, error) {
var user User
if err := r.db.WithContext(ctx).
Preload("Profile").
First(&user, "id = ?", id).Error; err != nil {
return nil, fmt.Errorf("get user with profile: %w", err)
}
return &user, nil
}
// ✅ Good: Creating with associations
func (s *Service) CreateUserWithProfile(ctx context.Context, req CreateUserRequest) error {
user := User{
Email: req.Email,
Password: req.Password,
Profile: &Profile{
Bio: req.Bio,
Avatar: req.Avatar,
},
}
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return fmt.Errorf("create user: %w", err)
}
return nil
})
}
// ✅ Good: Many-to-many with explicit join table
type Tag struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"uniqueIndex;size:50"`
}
type Post struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"size:200"`
Content string
Tags []Tag `gorm:"many2many:post_tags;"`
}
```
## sqlx Patterns
### Basic Queries
```go
// ✅ Good: sqlx with struct scanning
type User struct {
ID uuid.UUID `db:"id"`
Email string `db:"email"`
Password string `db:"password"`
CreatedAt time.Time `db:"created_at"`
}
func (r *sqlxRepo) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
const query = `
SELECT id, email, password, created_at
FROM users
WHERE id = $1
`
var user User
if err := r.db.GetContext(ctx, &user, query, id); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("get user: %w", err)
}
return &user, nil
}
func (r *sqlxRepo) Create(ctx context.Context, user *User) error {
const query = `
INSERT INTO users (id, email, password)
VALUES ($1, $2, $3)
RETURNING created_at
`
if err := r.db.QueryRowxContext(ctx, query,
user.ID, user.Email, user.Password,
).Scan(&user.CreatedAt); err != nil {
return fmt.Errorf("create user: %w", err)
}
return nil
}
```
### Named Queries
```go
// ✅ Good: Named parameters
func (r *sqlxRepo) Update(ctx context.Context, user *User) error {
const query = `
UPDATE users
SET email = :email, password = :password
WHERE id = :id
`
result, err := r.db.NamedExecContext(ctx, query, user)
if err != nil {
return fmt.Errorf("update user: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("rows affected: %w", err)
}
if rows == 0 {
return ErrNotFound
}
return nil
}
// ✅ Good: IN clause with sqlx
func (r *sqlxRepo) GetByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error) {
const query = `
SELECT id, email, password, created_at
FROM users
WHERE id IN (?)
`
query, args, err := sqlx.In(query, ids)
if err != nil {
return nil, fmt.Errorf("build IN query: %w", err)
}
var users []User
if err := r.db.SelectContext(ctx, &users, r.db.Rebind(query), args...); err != nil {
return nil, fmt.Errorf("get users: %w", err)
}
return users, nil
}
```
## Migration Patterns
### GORM Auto Migrate
```go
// ✅ Good: Auto migration with version check
func Migrate(db *gorm.DB) error {
if err := db.AutoMigrate(
&User{},
&Profile{},
&Post{},
&Tag{},
); err != nil {
return fmt.Errorf("auto migrate: %w", err)
}
// Create indexes manually for complex constraints
if err := db.Exec(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_lower
ON users (LOWER(email))
`).Error; err != nil {
return fmt.Errorf("create email index: %w", err)
}
return nil
}
```
### Manual Migrations (golang-migrate)
```go
// ✅ Good: Version-controlled migrations
// migrations/000001_create_users.up.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_users_email ON users (email) WHERE deleted_at IS NULL;
// migrations/000001_create_users.down.sql
DROP TABLE IF EXISTS users;
// Migration runner
func RunMigrations(db *sql.DB, migrationsDir string) error {
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return fmt.Errorf("create driver: %w", err)
}
m, err := migrate.NewWithDatabaseInstance(
"file://"+migrationsDir,
"postgres",
driver,
)
if err != nil {
return fmt.Errorf("create migrator: %w", err)
}
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("migrate: %w", err)
}
return nil
}
```
## Query Optimizations
### Pagination
```go
// ✅ Good: Cursor-based pagination for large datasets
type CursorParams struct {
Cursor uuid.UUID
PageSize int
Forward bool // true for next, false for previous
}
func (r *GormUserRepository) ListCursor(ctx context.Context, params CursorParams) ([]User, error) {
var users []User
query := r.db.WithContext(ctx).Model(&User{})
if params.Forward {
query = query.Where("id > ?", params.Cursor).Order("id ASC")
} else {
query = query.Where("id < ?", params.Cursor).Order("id DESC")
}
if err := query.Limit(params.PageSize).Find(&users).Error; err != nil {
return nil, fmt.Errorf("list users: %w", err)
}
return users, nil
}
```
### N+1 Prevention
```go
// ✅ Good: Batch loading
func (r *PostRepository) GetPostsByUserID(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]Post, error) {
var posts []Post
if err := r.db.WithContext(ctx).
Where("author_id IN ?", userIDs).
Find(&posts).Error; err != nil {
return nil, fmt.Errorf("get posts: %w", err)
}
// Group by user ID
result := make(map[uuid.UUID][]Post)
for _, post := range posts {
result[post.AuthorID] = append(result[post.AuthorID], post)
}
return result, nil
}
// ✅ Good: GORM Preload
func (r *UserRepository) GetUsersWithPosts(ctx context.Context, limit int) ([]User, error) {
var users []User
if err := r.db.WithContext(ctx).
Preload("Posts").
Limit(limit).
Find(&users).Error; err != nil {
return nil, fmt.Errorf("get users with posts: %w", err)
}
return users, nil
}
```
## Testing
### Mock Repository
```go
// ✅ Good: Mock repository for testing
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) Create(ctx context.Context, user *User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
func (m *MockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
// Test usage
func TestUserService_Create(t *testing.T) {
mockRepo := new(MockUserRepository)
service := NewUserService(mockRepo)
user := &User{Email: "test@example.com"}
mockRepo.On("Create", mock.Anything, user).Return(nil)
err := service.Create(context.Background(), user)
assert.NoError(t, err)
mockRepo.AssertExpectations(t)
}
```
### Integration Tests with Testcontainers
```go
// ✅ Good: PostgreSQL test container
func setupTestDB(t *testing.T) *gorm.DB {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "postgres:15",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_USER": "test",
"POSTGRES_PASSWORD": "test",
"POSTGRES_DB": "test",
},
WaitingFor: wait.ForLog("database system is ready to accept connections"),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
require.NoError(t, err)
t.Cleanup(func() {
container.Terminate(ctx)
})
host, err := container.Host(ctx)
require.NoError(t, err)
port, err := container.MappedPort(ctx, "5432")
require.NoError(t, err)
dsn := fmt.Sprintf("host=%s port=%s user=test password=test dbname=test sslmode=disable",
host, port.Port())
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
require.NoError(t, err)
// Run migrations
err = db.AutoMigrate(&User{}, &Profile{})
require.NoError(t, err)
return db
}
```
## Best Practices
```go
// ✅ Good: Context propagation
func (r *GormUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
var user User
if err := r.db.WithContext(ctx).First(&user, "id = ?", id).Error; err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
return &user, nil
}
// ✅ Good: Soft delete awareness
func (r *GormUserRepository) HardDelete(ctx context.Context, id uuid.UUID) error {
result := r.db.WithContext(ctx).Unscoped().Delete(&User{}, "id = ?", id)
if result.Error != nil {
return fmt.Errorf("hard delete: %w", result.Error)
}
return nil
}
// ❌ Bad: String concatenation (SQL injection vulnerable)
func BadQuery(db *sql.DB, userID string) (*User, error) {
query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", userID)
// Never do this!
return nil, nil
}
```

View File

@@ -0,0 +1,421 @@
# Go Error Handling
Comprehensive error handling patterns for Go applications.
## Error Types
```go
// internal/errors/errors.go
package errors
import "fmt"
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
// Specific errors
func NewNotFoundError(message string) *AppError {
return &AppError{Code: 404, Message: message}
}
func NewBadRequestError(message string) *AppError {
return &AppError{Code: 400, Message: message}
}
func NewUnauthorizedError(message string) *AppError {
return &AppError{Code: 401, Message: message}
}
func NewForbiddenError(message string) *AppError {
return &AppError{Code: 403, Message: message}
}
func NewConflictError(message string) *AppError {
return &AppError{Code: 409, Message: message}
}
func NewInternalError(message string) *AppError {
return &AppError{Code: 500, Message: message}
}
```
## Error Wrapping
```go
import "errors"
import "fmt"
// Wrap errors with context
func GetUser(id int64) (*User, error) {
user, err := repo.FindByID(id)
if err != nil {
return nil, fmt.Errorf("failed to get user %d: %w", id, err)
}
return user, nil
}
// Unwrap and check
func ProcessUser(id int64) error {
user, err := GetUser(id)
if err != nil {
var notFound *NotFoundError
if errors.As(err, &notFound) {
// Handle not found
return nil // or handle differently
}
return err
}
// ...
return nil
}
// Check specific error
func HandleError(err error) {
if errors.Is(err, sql.ErrNoRows) {
// Handle not found
}
if errors.Is(err, context.Canceled) {
// Handle cancellation
}
}
```
## Error Handling Middleware
```go
// internal/middleware/error.go
package middleware
import (
"net/http"
"os"
"github.com/gin-gonic/gin"
"myapp/internal/errors"
)
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
// Check for app errors
var appErr *errors.AppError
if errors.As(err, &appErr) {
c.JSON(appErr.Code, gin.H{
"error": appErr.Message,
})
return
}
// Check for validation errors
var valErr *errors.ValidationError
if errors.As(err, &valErr) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "validation failed",
"fields": valErr.Fields,
})
return
}
// Unknown error
if os.Getenv("ENV") == "development" {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "internal server error",
})
}
}
}
// Recovery middleware
func Recovery() gin.HandlerFunc {
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
// Log panic
log.Printf("Panic recovered: %v", recovered)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "internal server error",
})
})
}
```
## Validation Errors
```go
// internal/errors/validation.go
package errors
type ValidationError struct {
Fields map[string]string
}
func (e *ValidationError) Error() string {
return "validation failed"
}
func NewValidationError(fields map[string]string) *ValidationError {
return &ValidationError{Fields: fields}
}
// Usage with validator
import "github.com/go-playground/validator/v10"
func ValidateStruct(s interface{}) error {
validate := validator.New()
if err := validate.Struct(s); err != nil {
fields := make(map[string]string)
for _, err := range err.(validator.ValidationErrors) {
fields[err.Field()] = err.Tag()
}
return NewValidationError(fields)
}
return nil
}
// Handler usage
func (h *UserHandler) Create(c *gin.Context) {
var req models.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid request body",
})
return
}
if err := ValidateStruct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.(*errors.ValidationError).Fields,
})
return
}
// ...
}
```
## Service Layer Errors
```go
// internal/services/user.go
package services
import (
"context"
"errors"
"myapp/internal/models"
"myapp/internal/repositories"
)
func NewUserService(repo *repositories.UserRepository) *UserService {
return &UserService{repo: repo}
}
type UserService struct {
repo *repositories.UserRepository
}
func (s *UserService) Create(ctx context.Context, req *models.RegisterRequest) (*models.User, error) {
// Check if exists
existing, err := s.repo.FindByEmail(ctx, req.Email)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("check existing user: %w", err)
}
if existing != nil {
return nil, errors.NewConflictError("email already registered")
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("hash password: %w", err)
}
// Create user
user := &models.User{
Email: req.Email,
Password: string(hashedPassword),
Name: req.Name,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return user, nil
}
func (s *UserService) GetByID(ctx context.Context, id int64) (*models.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.NewNotFoundError("user not found")
}
return nil, fmt.Errorf("get user by id: %w", err)
}
return user, nil
}
```
## Repository Errors
```go
// internal/repositories/user.go
package repositories
import (
"context"
"database/sql"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindByID(ctx context.Context, id int64) (*models.User, error) {
var user models.User
err := r.db.WithContext(ctx).First(&user, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, sql.ErrNoRows
}
return nil, err
}
return &user, nil
}
// Handle unique constraint violations
func (r *UserRepository) Create(ctx context.Context, user *models.User) error {
err := r.db.WithContext(ctx).Create(user).Error
if err != nil {
// SQLite unique constraint
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
return errors.NewConflictError("resource already exists")
}
// PostgreSQL unique constraint
if strings.Contains(err.Error(), "duplicate key") {
return errors.NewConflictError("resource already exists")
}
return err
}
return nil
}
```
## Error Logging
```go
import "go.uber.org/zap"
func LogError(logger *zap.Logger, err error, fields ...zap.Field) {
fields = append(fields, zap.Error(err))
var appErr *errors.AppError
if errors.As(err, &appErr) {
fields = append(fields,
zap.Int("status_code", appErr.Code),
zap.String("message", appErr.Message),
)
}
logger.Error("error occurred", fields...)
}
// Usage
func (h *UserHandler) Get(c *gin.Context) {
user, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
LogError(h.logger, err,
zap.String("user_id", c.Param("id")),
zap.String("request_id", c.GetString("request_id")),
)
// ...
}
}
```
## Best Practices
### Don't Do This
```go
// ❌ Bad: Ignore errors
user, _ := service.GetByID(id)
// ❌ Bad: Panic on error
if err != nil {
panic(err)
}
// ❌ Bad: Log and continue without handling
if err != nil {
log.Printf("error: %v", err)
// continue...
}
// ❌ Bad: Wrap errors without context
return fmt.Errorf("%v", err)
```
### Do This
```go
// ✅ Good: Handle errors properly
user, err := service.GetByID(id)
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
// ✅ Good: Return errors to caller
if err != nil {
return nil, err
}
// ✅ Good: Use errors.Is and errors.As
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.NewNotFoundError("not found")
}
var appErr *errors.AppError
if errors.As(err, &appErr) {
return appErr
}
// ✅ Good: Wrap with context
if err != nil {
return fmt.Errorf("failed to create user %s: %w", email, err)
}
```

View File

@@ -0,0 +1,602 @@
# Go Middleware Patterns Skill
Comprehensive guide to middleware patterns in Go web applications using Gin, Echo, and net/http.
## Overview
Middleware in Go provides a powerful way to handle cross-cutting concerns like logging, authentication, rate limiting, and request processing. This skill covers best practices for designing, composing, and testing middleware.
## Core Concepts
### Middleware Function Signature
```go
// Standard net/http middleware
type Middleware func(http.Handler) http.Handler
// Gin middleware
type GinMiddleware func(gin.HandlerFunc) gin.HandlerFunc
// Echo middleware
type EchoMiddleware func(echo.HandlerFunc) echo.HandlerFunc
```
### Middleware Chain Execution
```go
// Middleware wraps handlers in reverse order
// Request: Log → Auth → RateLimit → Handler
// Response: Handler → RateLimit → Auth → Log
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
```
## Standard Library (net/http)
### Basic Middleware Pattern
```go
// ✅ Good: Clean, composable middleware
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap ResponseWriter to capture status
wrapped := &responseWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(wrapped, r)
log.Printf("%s %s %d %v",
r.Method,
r.URL.Path,
wrapped.status,
time.Since(start),
)
})
}
// ✅ Good: ResponseWriter wrapper for status capture
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
```
### Middleware Composition
```go
// ✅ Good: Chainable middleware
func WithMiddleware(h http.Handler) http.Handler {
return LoggingMiddleware(
RecoveryMiddleware(
AuthMiddleware(h),
),
)
}
// ✅ Better: Functional approach
func NewRouter() http.Handler {
mux := http.NewServeMux()
// Public routes
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/login", loginHandler)
// Protected routes
protected := http.NewServeMux()
protected.HandleFunc("/api/users", usersHandler)
// Apply middleware chain
handler := Chain(protected,
AuthMiddleware,
RateLimitMiddleware,
LoggingMiddleware,
)
return handler
}
```
## Gin Framework
### Custom Middleware
```go
// ✅ Good: Gin middleware with context values
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "missing authorization header",
})
return
}
userID, err := validateToken(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "invalid token",
})
return
}
// Store in context for downstream handlers
c.Set("userID", userID)
c.Next()
}
}
// ✅ Good: Retrieving context values
func ProtectedHandler(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
// Use userID...
}
```
### Request ID Middleware
```go
// ✅ Good: Request ID for tracing
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
c.Set("requestID", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
}
}
```
### Recovery Middleware
```go
// ✅ Good: Panic recovery
func RecoveryMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Log stack trace
stack := debug.Stack()
log.Printf("panic recovered: %v\n%s", err, stack)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "internal server error",
})
}
}()
c.Next()
}
}
```
### Rate Limiting Middleware
```go
// ✅ Good: Token bucket rate limiter
func RateLimitMiddleware(rps int, burst int) gin.HandlerFunc {
limiter := rate.NewLimiter(rate.Limit(rps), burst)
return func(c *gin.Context) {
if !limiter.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "rate limit exceeded",
})
return
}
c.Next()
}
}
// ✅ Better: Per-client rate limiting
func PerClientRateLimitMiddleware() gin.HandlerFunc {
type client struct {
limiter *rate.Limiter
lastSeen time.Time
}
var (
mu sync.Mutex
clients = make(map[string]*client)
)
// Cleanup routine
go func() {
for range time.Tick(time.Minute) {
mu.Lock()
for ip, c := range clients {
if time.Since(c.lastSeen) > time.Minute {
delete(clients, ip)
}
}
mu.Unlock()
}
}()
return func(c *gin.Context) {
ip := c.ClientIP()
mu.Lock()
if _, exists := clients[ip]; !exists {
clients[ip] = &client{
limiter: rate.NewLimiter(2, 5),
}
}
clients[ip].lastSeen = time.Now()
limiter := clients[ip].limiter
mu.Unlock()
if !limiter.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "rate limit exceeded",
})
return
}
c.Next()
}
}
```
## Echo Framework
### Echo Middleware Pattern
```go
// ✅ Good: Echo middleware
func AuthMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "missing token")
}
claims, err := validateToken(token)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
}
c.Set("claims", claims)
return next(c)
}
}
}
// Usage
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(AuthMiddleware())
```
### Custom Context in Echo
```go
// ✅ Good: Custom context with typed getters
type Context struct {
echo.Context
user *User
}
func (c *Context) User() *User {
return c.user
}
func AuthMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, err := authenticate(c)
if err != nil {
return err
}
cc := &Context{Context: c, user: user}
return next(cc)
}
}
}
```
## Cross-Cutting Concerns
### Request Timeout Middleware
```go
// ✅ Good: Context timeout
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
defer cancel()
c.Request = c.Request.WithContext(ctx)
done := make(chan struct{})
go func() {
defer close(done)
c.Next()
}()
select {
case <-done:
// Handler completed
case <-ctx.Done():
c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{
"error": "request timeout",
})
}
}
}
```
### CORS Middleware
```go
// ✅ Good: CORS configuration
func CORSMiddleware(allowedOrigins []string) gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
// Check if origin is allowed
allowed := false
for _, o := range allowedOrigins {
if o == "*" || o == origin {
allowed = true
break
}
}
if allowed {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Header("Access-Control-Max-Age", "86400")
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
```
### Request Validation Middleware
```go
// ✅ Good: Request validation
func ValidateRequestMiddleware(req interface{}) gin.HandlerFunc {
return func(c *gin.Context) {
if err := c.ShouldBindJSON(req); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "invalid request body",
"details": err.Error(),
})
return
}
if err := validate.Struct(req); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "validation failed",
"details": formatValidationErrors(err),
})
return
}
c.Set("validatedRequest", req)
c.Next()
}
}
```
## Testing Middleware
### Unit Testing
```go
// ✅ Good: Middleware unit test
func TestAuthMiddleware(t *testing.T) {
tests := []struct {
name string
token string
wantStatus int
}{
{
name: "missing token",
token: "",
wantStatus: http.StatusUnauthorized,
},
{
name: "invalid token",
token: "invalid",
wantStatus: http.StatusUnauthorized,
},
{
name: "valid token",
token: "valid-token",
wantStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/test", nil)
if tt.token != "" {
c.Request.Header.Set("Authorization", tt.token)
}
// Run middleware
AuthMiddleware()(c)
if !c.IsAborted() && tt.wantStatus == http.StatusOK {
// Continue to handler
}
if tt.wantStatus != http.StatusOK {
assert.Equal(t, tt.wantStatus, w.Code)
}
})
}
}
```
### Integration Testing
```go
// ✅ Good: Integration test with middleware chain
func TestMiddlewareChain(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(RecoveryMiddleware())
router.Use(AuthMiddleware())
router.GET("/protected", func(c *gin.Context) {
userID, _ := c.Get("userID")
c.JSON(http.StatusOK, gin.H{"userID": userID})
})
// Test with valid token
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "valid-token")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
```
## Best Practices
### ❌ Bad Patterns
```go
// ❌ Bad: Middleware with side effects
func BadMiddleware(c *gin.Context) {
c.Set("user", getUserFromDB(c)) // Database call in middleware
c.Next()
}
// ❌ Bad: Panicking in middleware
func BadPanicMiddleware(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
panic("no token") // Don't panic, handle gracefully
}
c.Next()
}
// ❌ Bad: Blocking middleware
func BadBlockingMiddleware(c *gin.Context) {
time.Sleep(5 * time.Second) // Never block in middleware
c.Next()
}
```
### ✅ Good Patterns
```go
// ✅ Good: Middleware factory for configuration
func NewAuthMiddleware(config AuthConfig) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
claims, err := config.ValidateToken(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": err.Error(),
})
return
}
c.Set("claims", claims)
c.Next()
}
}
// ✅ Good: Short-circuit with c.Abort()
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !isAuthenticated(c) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
})
return // Important: return after Abort
}
c.Next()
}
}
```
## Common Patterns
### Middleware with Dependencies
```go
// ✅ Good: Dependency injection
type AuthMiddleware struct {
authService *auth.Service
logger *zap.Logger
}
func NewAuthMiddleware(authService *auth.Service, logger *zap.Logger) *AuthMiddleware {
return &AuthMiddleware{
authService: authService,
logger: logger,
}
}
func (m *AuthMiddleware) Handle() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
claims, err := m.authService.Validate(token)
if err != nil {
m.logger.Error("auth failed", zap.Error(err))
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
})
return
}
c.Set("claims", claims)
c.Next()
}
}
```
### Conditional Middleware
```go
// ✅ Good: Skip middleware for certain routes
func SkipMiddleware(skipPaths map[string]bool) gin.HandlerFunc {
return func(c *gin.Context) {
if skipPaths[c.Request.URL.Path] {
c.Next()
return
}
// Apply middleware logic
c.Next()
}
}
```

View File

@@ -0,0 +1,543 @@
# Go Modules Management Skill
Comprehensive guide to Go modules, dependency management, and project structure.
## Overview
Go modules are the official dependency management system for Go. This skill covers module initialization, dependency management, versioning, and best practices.
## Module Basics
### Creating a New Module
```bash
# Initialize new module
go mod init github.com/myorg/myproject
# Creates go.mod file
module github.com/myorg/myproject
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
gorm.io/gorm v1.25.5
)
```
### Module File Structure
```
myproject/
├── go.mod # Module definition and dependencies
├── go.sum # Dependency checksums
├── cmd/ # Main applications
│ ├── server/
│ │ └── main.go
│ └── cli/
│ └── main.go
├── internal/ # Private application code
│ ├── handlers/
│ ├── services/
│ └── models/
├── pkg/ # Public library code
│ └── utils/
└── api/ # API definitions
└── openapi/
```
## go.mod File
### Module Declaration
```go
// go.mod
module github.com/myorg/myproject
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
gorm.io/gorm v1.25.5
)
require (
// Indirect dependencies
github.com/go-playground/validator/v10 v10.15.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
)
replace github.com/old/package => github.com/new/package v1.0.0
exclude github.com/problematic/package v1.0.0
```
### Version Syntax
```bash
# Semantic versioning
require github.com/gin-gonic/gin v1.9.1
# Pseudo-versions (for unreleased commits)
require github.com/user/repo v0.0.0-20230815133123-1b2345678901
# Branch names (use commit hash instead)
require github.com/user/repo v1.2.3-alpha.1
# +incompatible for pre-module versions
require github.com/legacy/package v1.0.0+incompatible
```
## Dependency Management
### Adding Dependencies
```bash
# Add a new dependency
go get github.com/gin-gonic/gin@latest
# Add specific version
go get github.com/gin-gonic/gin@v1.9.1
# Add all dependencies from imports
go mod tidy
# Add dependency without installing
go get -d github.com/some/package
```
### Updating Dependencies
```bash
# Update all dependencies
go get -u ./...
# Update specific package
go get github.com/gin-gonic/gin@latest
# Update to specific version
go get github.com/gin-gonic/gin@v1.10.0
# Update and tidy
go get -u && go mod tidy
```
### Removing Dependencies
```bash
# Remove unused dependencies
go mod tidy
# Remove specific dependency
go mod edit -droprequire github.com/gin-gonic/gin
go mod tidy
```
### Analyzing Dependencies
```bash
# List all dependencies
go list -m all
# List direct dependencies only
go list -m -json all | jq -r 'select(.Main == null and .Indirect == null) | .Path'
# See dependency graph
go mod graph
# See why a dependency is needed
go mod why github.com/gin-gonic/gin
# Verify dependencies
go mod verify
```
## go.sum File
### Understanding Checksums
```go
// go.sum contains SHA-256 checksums
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0Axc1x6JZY3sA3V8=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL6YRBqA0pA4dZ5FjP3QmJFdE3E7BEgO8Y0ePm7Y=
// First line: checksum of module zip
// Second line: checksum of go.mod file
```
### Security Implications
```bash
# Verify all modules against go.sum
go mod verify
# Download dependencies to module cache
go mod download
# Vendor dependencies (for air-gapped environments)
go mod vendor
```
## Versioning Best Practices
### Semantic Versioning
```bash
# ✅ Good: Use semantic versioning
v1.0.0 # Major.Minor.Patch
v1.0.1 # Patch: Bug fixes
v1.1.0 # Minor: New features, backward compatible
v2.0.0 # Major: Breaking changes
# Pre-release versions
v1.0.0-alpha.1
v1.0.0-beta.2
v1.0.0-rc.1
```
### Import Path Versioning (v2+)
```go
// For v2 and above, include version in import path
// go.mod
module github.com/myorg/myproject/v2
go 1.21
// Import in other projects
import "github.com/myorg/myproject/v2"
```
### Pseudo-Versions
```go
// When using unreleased commits
require github.com/user/repo v0.0.0-20230815133123-1b2345678901
// Format: vX.Y.Z-yyyymmddhhmmss-abcdefabcdef
// Where:
// - vX.Y.Z is base version tag
// - yyyymmddhhmmss is commit timestamp (UTC)
// - abcdefabcdef is first 12 chars of commit hash
```
## Workspaces
### Multi-Module Workspaces
```bash
# Initialize workspace
go work init ./modules/module1 ./modules/module2
# Creates go.work file
go 1.21
use (
./modules/module1
./modules/module2
)
# Add module to workspace
go work use ./modules/module3
# Sync workspace
go work sync
```
### Workspace File Structure
```
project/
├── go.work
├── modules/
│ ├── module1/
│ │ ├── go.mod
│ │ └── main.go
│ └── module2/
│ ├── go.mod
│ └── main.go
└── shared/
└── utils/
├── go.mod
└── utils.go
```
## Dependency Security
### Vulnerability Scanning
```bash
# Check for known vulnerabilities
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
# Output example
Vulnerability #1: GO-2023-1234
Vulnerable package: github.com/gin-gonic/gin
Fixed in: v1.9.2
More info: https://pkg.go.dev/vuln/go-2023-1234
```
### Security Best Practices
```bash
# ✅ Good: Run vulnerability scans in CI
- name: Security Scan
run: govulncheck ./...
# ✅ Good: Use specific versions
go get github.com/gin-gonic/gin@v1.9.1
# ❌ Bad: Using latest in production
go get github.com/gin-gonic/gin@latest # Can change unexpectedly
# ✅ Good: Pin versions in go.mod
require github.com/gin-gonic/gin v1.9.1
# ✅ Good: Regular security updates
- name: Dependabot
uses: dependabot/fetch-metadata@v1
```
## Project Structure Patterns
### Standard Go Project Layout
```go
// cmd/ - Main applications
// cmd/server/main.go
package main
import "github.com/myorg/myproject/internal/server"
func main() {
server.Run()
}
// internal/ - Private code
// internal/user/service.go
package user
type Service struct {
repo Repository
}
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
// pkg/ - Public code (reusable)
// pkg/utils/http.go
package utils
func ParseCookie(cookie string) map[string]string {
// Implementation
return nil
}
// api/ - API definitions
// api/openapi.yaml - OpenAPI spec
// api/protobuf/ - Protocol Buffer definitions
```
### Internal Package Pattern
```go
// internal/ can only be imported by parent directories
// This enforces encapsulation
// internal/database/postgres.go
package database
type DB struct {
*sql.DB
}
func New(connectionString string) (*DB, error) {
// ...
}
// cmd/server/main.go
package main
import "github.com/myorg/myproject/internal/database"
func main() {
db, err := database.New(dsn)
// ...
}
// ❌ Cannot import from external projects
// import "github.com/myorg/myproject/internal/database" // Error!
```
## Vendoring
### Creating Vendor Directory
```bash
# Create vendor directory
go mod vendor
# Structure
vendor/
├── modules.txt
├── github.com/
│ └── gin-gonic/
│ └── gin/
│ ├── LICENSE
│ ├── context.go
│ └── ...
```
### Using Vendor Directory
```bash
# Build using vendor
go build -mod=vendor ./cmd/server
# Run tests using vendor
go test -mod=vendor ./...
# Verify vendor matches go.mod
go mod verify
```
## Common Commands Reference
```bash
# Module initialization
go mod init <module-path>
# Download dependencies
go mod download
# Tidy dependencies
go mod tidy
# Verify dependencies
go mod verify
# Copy dependencies to vendor
go mod vendor
# View dependency graph
go mod graph
# Explain dependency need
go mod why <package>
# Edit go.mod
go mod edit -require=<package>@<version>
go mod edit -droprequire=<package>
# List packages
go list -m all
go list -m -json all
# Build info
go build -v ./...
go build -ldflags "-X main.version=v1.0.0" ./cmd/server
```
## Best Practices
### ❌ Bad Patterns
```bash
# ❌ Bad: Committing go.sum conflicts
git add go.sum # Without resolving conflicts
git commit -m "update"
# ❌ Bad: Using latest in production
go get github.com/gin-gonic/gin@latest
# ❌ Bad: Not running go mod tidy
# Leaves unused dependencies
# ❌ Bad: Committing large vendor/ without .gitignore
git add vendor/ # Bloated repo
# ❌ Bad: Using relative imports
import "../utils" // Error-prone
```
### ✅ Good Patterns
```bash
# ✅ Good: Regular dependency audits
go mod tidy
go mod verify
govulncheck ./...
# ✅ Good: Use specific versions in go.mod
require github.com/gin-gonic/gin v1.9.1
# ✅ Good: CI pipeline
- name: Lint
run: golangci-lint run
- name: Test
run: go test -race -cover ./...
- name: Security
run: govulncheck ./...
- name: Build
run: go build -o bin/server ./cmd/server
# ✅ Good: Module structure
module github.com/myorg/myproject
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
gorm.io/gorm v1.25.5
)
# ✅ Good: Use go.mod replace for local development
replace github.com/myorg/shared => ../shared
```
## Dependency Update Workflow
```bash
# 1. Check for outdated dependencies
go list -u -m all
# 2. Test current state
go test ./...
# 3. Update dependencies
go get -u ./...
go mod tidy
# 4. Run tests again
go test ./...
# 5. Run security scan
govulncheck ./...
# 6. Commit changes
git add go.mod go.sum
git commit -m "chore: update dependencies"
```
## Private Repositories
```bash
# Set GOPRIVATE for private modules
go env -w GOPRIVATE=github.com/myorg/*
# Configure git for private repos
git config --global url."git@github.com:".insteadOf "https://github.com/"
# ~/.netrc for authentication
machine github.com
login myuser
password mytoken
# GONOSUMDB for private modules
go env -w GONOSUMDB=github.com/myorg/*
```

View File

@@ -0,0 +1,428 @@
# Go Security (OWASP)
OWASP Top 10 security practices for Go applications.
## OWASP Top 10 for Go
### 1. Injection (A03:2021)
```go
// ❌ Vulnerable: SQL Injection
func GetUser(db *sql.DB, id string) (*User, error) {
query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", id)
row := db.QueryRow(query)
// ...
}
// ✅ Secure: Parameterized queries
func GetUser(db *sql.DB, id string) (*User, error) {
query := "SELECT * FROM users WHERE id = ?"
row := db.QueryRow(query, id)
// ...
}
// ✅ Secure: GORM
func GetUser(db *gorm.DB, id string) (*User, error) {
var user User
err := db.Where("id = ?", id).First(&user).Error
return &user, err
}
```
### 2. Broken Authentication (A07:2021)
```go
// ✅ Password hashing with bcrypt
import "golang.org/x/crypto/bcrypt"
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// ✅ JWT authentication
import "github.com/golang-jwt/jwt/v5"
type Claims struct {
UserID string `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func GenerateToken(userID, role string) (string, error) {
claims := &Claims{
UserID: userID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "myapp",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(os.Getenv("JWT_SECRET")))
}
```
### 3. Sensitive Data Exposure (A02:2021)
```go
// ✅ Environment variables for secrets
import "github.com/kelseyhightower/envconfig"
type Config struct {
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
JWTSecret string `envconfig:"JWT_SECRET" required:"true"`
APIKey string `envconfig:"API_KEY" required:"true"`
}
func LoadConfig() (*Config, error) {
var cfg Config
err := envconfig.Process("", &cfg)
return &cfg, err
}
// ✅ Encrypt sensitive data
import "crypto/aes"
import "crypto/cipher"
import "crypto/rand"
func Encrypt(plaintext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
// ✅ Never log sensitive data
func Login(db *gorm.DB, email, password string) (*User, error) {
var user User
if err := db.Where("email = ?", email).First(&user).Error; err != nil {
log.Printf("Login failed for email: %s", email) // Don't log password
return nil, errors.New("invalid credentials")
}
if !CheckPasswordHash(password, user.Password) {
return nil, errors.New("invalid credentials")
}
return &user, nil
}
```
### 4. XML External Entities (A05:2021)
```go
// ✅ Secure XML parsing
import "encoding/xml"
func ParseXML(data []byte) (User, error) {
var user User
decoder := xml.NewDecoder(bytes.NewReader(data))
decoder.Strict = false // Prevent XXE
decoder.AutoClose = xml.HTMLAutoClose
if err := decoder.Decode(&user); err != nil {
return User{}, err
}
return user, nil
}
```
### 5. Broken Access Control (A01:2021)
```go
// ✅ Role-based access control
func RequireRole(role string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("role")
if !exists {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
c.Abort()
return
}
if userRole != role {
c.JSON(http.StatusForbidden, gin.H{"error": "insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}
// Usage
admin := api.Group("/admin")
admin.Use(middleware.RequireRole("admin"))
{
admin.GET("/users", handlers.ListUsers)
}
// ✅ Resource ownership check
func CanAccessResource(userID string, resourceID string) bool {
// Check if user owns the resource
var resource Resource
if err := db.Where("id = ? AND user_id = ?", resourceID, userID).First(&resource).Error; err != nil {
return false
}
return true
}
```
### 6. Security Misconfiguration (A05:2021)
```go
// ✅ Security headers middleware
func SecurityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-Frame-Options", "DENY")
c.Header("X-XSS-Protection", "1; mode=block")
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
c.Header("Content-Security-Policy", "default-src 'self'")
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
c.Next()
}
}
// ✅ Remove server header
func RemoveServerHeader() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Server", "")
c.Next()
}
}
// ✅ CORS configuration
func CORS() gin.HandlerFunc {
return cors.New(cors.Config{
AllowOrigins: []string{"https://example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
})
}
```
### 7. XSS (A03:2021)
```go
// ✅ Input sanitization
import "github.com/microcosm-cc/bluemonday"
func SanitizeInput(input string) string {
p := bluemonday.UGCPolicy()
return p.Sanitize(input)
}
// ✅ Output encoding
import "html/template"
func RenderTemplate(w http.ResponseWriter, data interface{}) {
tmpl := template.Must(template.New("safe").Parse(`{{.Content}}`))
tmpl.Execute(w, data) // Auto-escapes HTML
}
// ✅ Content Security Policy
func ContentSecurityPolicy() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Content-Security-Policy",
"default-src 'self'; "+
"script-src 'self'; "+
"style-src 'self' 'unsafe-inline'; "+
"img-src 'self' data:; "+
"font-src 'self'; "+
"connect-src 'self'; "+
"frame-ancestors 'none'")
c.Next()
}
}
```
### 8. Insecure Deserialization (A08:2021)
```go
// ❌ Vulnerable: Unmarshal untrusted JSON
func ParseUser(data []byte) (User, error) {
var user User
err := json.Unmarshal(data, &user)
return user, err
}
// ✅ Secure: Validate after unmarshal
func ParseUser(data []byte) (User, error) {
var user User
if err := json.Unmarshal(data, &user); err != nil {
return User{}, err
}
// Validate fields
if err := validateUser(&user); err != nil {
return User{}, err
}
// Check for unexpected fields
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return User{}, err
}
expected := map[string]bool{
"email": true, "name": true, "password": true,
}
for key := range raw {
if !expected[key] {
return User{}, fmt.Errorf("unexpected field: %s", key)
}
}
return user, nil
}
```
### 9. Known Vulnerabilities (A06:2021)
```bash
# Check for vulnerabilities
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
# Update dependencies
go get -u ./...
go mod tidy
# Check for outdated packages
go list -u -m -json all | jq 'select(.Update != null)'
```
```go
// ✅ Keep dependencies updated in go.mod
module myapp
go 1.21
require (
github.com/gin-gonic/gin v1.9.1 // Latest stable
golang.org/x/crypto v0.17.0 // Latest security patches
gorm.io/gorm v1.25.5
)
```
### 10. Logging & Monitoring (A09:2021)
```go
// ✅ Structured logging
import "go.uber.org/zap"
func SetupLogger() *zap.Logger {
logger, _ := zap.NewProduction()
defer logger.Sync()
return logger
}
// Log security events
func LogSecurityEvent(logger *zap.Logger, event string, details map[string]interface{}) {
logger.Info("security_event",
zap.String("event", event),
zap.Any("details", details),
zap.String("timestamp", time.Now().Format(time.RFC3339)),
)
}
// Audit logging middleware
func AuditLog(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
logger.Info("request",
zap.String("method", c.Request.Method),
zap.String("path", c.Request.URL.Path),
zap.Int("status", c.Writer.Status()),
zap.Duration("latency", time.Since(start)),
zap.String("user_id", getUserID(c)),
zap.String("ip", c.ClientIP()),
)
}
}
// Detect brute force
type LoginAttempt struct {
mu sync.RWMutex
store map[string]*Attempt
}
type Attempt struct {
Count int
FirstTime time.Time
Blocked bool
}
func (la *LoginAttempt) Check(email string) bool {
la.mu.Lock()
defer la.mu.Unlock()
attempt, exists := la.store[email]
if !exists {
la.store[email] = &Attempt{Count: 1, FirstTime: time.Now()}
return true
}
// Reset after 15 minutes
if time.Since(attempt.FirstTime) > 15*time.Minute {
la.store[email] = &Attempt{Count: 1, FirstTime: time.Now()}
return true
}
// Block after 5 attempts
if attempt.Count >= 5 {
attempt.Blocked = true
return false
}
attempt.Count++
return true
}
```
## Security Checklist
- [ ] Use parameterized queries (SQL injection prevention)
- [ ] Hash passwords with bcrypt (minimum cost 12)
- [ ] Use HTTPS in production
- [ ] Set security headers (helmet equivalent)
- [ ] Validate all input
- [ ] Rate limit public endpoints
- [ ] Use environment variables for secrets
- [ ] Implement proper error handling
- [ ] Log security events
- [ ] Run govulncheck regularly
- [ ] Keep dependencies updated
- [ ] Implement CORS properly
- [ ] Use Content Security Policy
- [ ] Implement proper session management
- [ ] Audit authentication flows

View File

@@ -0,0 +1,546 @@
# Go Testing
Comprehensive testing patterns for Go applications.
## Setup
```go
// No additional installation needed
// Go has built-in testing package
// Directory structure
myapp/
internal/
services/
user.go
user_test.go # Tests go next to source
go.mod
```
## Unit Tests
```go
// internal/services/user_test.go
package services
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Using testify for assertions
func TestUserService_Create(t *testing.T) {
tests := []struct {
name string
input *models.RegisterRequest
wantErr bool
}{
{
name: "valid user",
input: &models.RegisterRequest{
Email: "test@example.com",
Password: "password123",
Name: "Test User",
},
wantErr: false,
},
{
name: "duplicate email",
input: &models.RegisterRequest{
Email: "existing@example.com",
Password: "password123",
Name: "Test User",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock
mockRepo := NewMockUserRepository(t)
service := NewUserService(mockRepo)
if tt.wantErr {
mockRepo.On("FindByEmail", mock.Anything, tt.input.Email).
Return(&models.User{Email: tt.input.Email}, nil)
} else {
mockRepo.On("FindByEmail", mock.Anything, tt.input.Email).
Return(nil, gorm.ErrRecordNotFound)
mockRepo.On("Create", mock.Anything, mock.Anything).
Return(nil)
}
// Execute
_, err := service.Create(context.Background(), tt.input)
// Assert
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
```
## Table-Driven Tests
```go
// Test with table-driven approach
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
valid bool
}{
{"valid email", "user@example.com", true},
{"valid with subdomain", "user@mail.example.com", true},
{"missing @", "userexample.com", false},
{"missing domain", "user@", false},
{"empty string", "", false},
{"special chars", "user+test@example.com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateEmail(tt.email)
assert.Equal(t, tt.valid, got)
})
}
}
// Benchmark
func BenchmarkValidateEmail(b *testing.B) {
email := "user@example.com"
for i := 0; i < b.N; i++ {
ValidateEmail(email)
}
}
// Parallel tests
func TestUser_Parallel(t *testing.T) {
t.Parallel()
tests := []struct {
name string
// ...
}{
// test cases
}
for _, tt := range tests {
tt := tt // Capture range variable
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// test code
})
}
}
```
## Mocking
```go
// Using testify/mock
package mocks
import (
"github.com/stretchr/testify/mock"
"myapp/internal/models"
)
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) FindByID(ctx context.Context, id int64) (*models.User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.User), args.Error(1)
}
func (m *MockUserRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) {
args := m.Called(ctx, email)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.User), args.Error(1)
}
func (m *MockUserRepository) Create(ctx context.Context, user *models.User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
// Usage in test
func TestUserService_GetByID(t *testing.T) {
mockRepo := new(MockUserRepository)
service := NewUserService(mockRepo)
mockUser := &models.User{ID: 1, Email: "test@example.com"}
mockRepo.On("FindByID", mock.Anything, int64(1)).Return(mockUser, nil)
user, err := service.GetByID(context.Background(), 1)
assert.NoError(t, err)
assert.Equal(t, mockUser, user)
mockRepo.AssertExpectations(t)
}
// Using mockgen
//go:generate mockgen -source=user.go -destination=mocks/user.go -package=mocks
```
## Integration Tests
```go
// tests/integration/user_test.go
package integration
import (
"context"
"database/sql"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type UserTestSuite struct {
suite.Suite
db *sql.DB
router *gin.Engine
}
func (s *UserTestSuite) SetupSuite() {
// Setup test database
db, err := sql.Open("sqlite3", ":memory:")
s.Require().NoError(err)
s.db = db
// Run migrations
// ...
// Setup router
s.router = setupTestRouter(db)
}
func (s *UserTestSuite) TearDownSuite() {
s.db.Close()
}
func (s *UserTestSuite) SetupTest() {
// Clear tables before each test
s.db.Exec("DELETE FROM users")
}
func (s *UserTestSuite) TestCreateUser() {
body := `{"email":"test@example.com","password":"password123","name":"Test"}`
req, _ := http.NewRequest("POST", "/api/v1/register", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusCreated, w.Code)
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
assert.NotNil(s.T(), response["user"])
}
func (s *UserTestSuite) TestGetUser() {
// Create user first
// ...
req, _ := http.NewRequest("GET", "/api/v1/users/1", nil)
w := httptest.NewRecorder()
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
}
func TestUserTestSuite(t *testing.T) {
suite.Run(t, new(UserTestSuite))
}
```
## Test Helpers
```go
// tests/helpers.go
package tests
import (
"context"
"database/sql"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// SetupTestDB creates a mock database for testing
func SetupTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error creating mock: %v", err)
}
gormDB, err := gorm.Open(mysql.New(mysql.Config{
Conn: db,
SkipInitializeWithVersion: true,
}), &gorm.Config{})
if err != nil {
t.Fatalf("Error opening gorm: %v", err)
}
return gormDB, mock
}
// Context with timeout
func Context(t *testing.T) context.Context {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
t.Cleanup(cancel)
return ctx
}
// Assert error type
func AssertError(t *testing.T, err error, expected string) {
t.Helper()
assert.ErrorContains(t, err, expected)
}
```
## HTTP Handlers Testing
```go
// internal/handlers/user_test.go
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestGetUser(t *testing.T) {
// Setup
gin.SetMode(gin.TestMode)
mockService := NewMockUserService(t)
handler := NewUserHandler(mockService)
router := gin.New()
router.GET("/users/:id", handler.GetUser)
// Mock service call
mockService.On("GetByID", mock.Anything, int64(1)).
Return(&models.User{ID: 1, Email: "test@example.com"}, nil)
// Create request
req := httptest.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
// Execute
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
assert.NotNil(t, response["user"])
mockService.AssertExpectations(t)
}
func TestCreateUser_InvalidInput(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := NewUserHandler(nil)
router := gin.New()
router.POST("/users", handler.CreateUser)
body := `{"email":"invalid","password":"short"}`
req := httptest.NewRequest("POST", "/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
```
## Coverage
```bash
# Run tests with coverage
go test -cover ./...
# Coverage by function
go test -covermode=atomic -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
# Generate HTML coverage report
go tool cover -html=coverage.out -o coverage.html
# Enforce coverage threshold
#!/bin/bash
COVERAGE=$(go test -cover ./... | grep total | awk '{print $3}' | sed 's/%//')
if [ "$COVERAGE" -lt 80 ]; then
echo "Coverage $COVERAGE% is below threshold 80%"
exit 1
fi
```
## Best Practices
### Don't Do This
```go
// ❌ Bad: No table-driven tests
func TestAdd(t *testing.T) {
result := Add(1, 2)
if result != 3 {
t.Error("expected 3")
}
result = Add(2, 3)
if result != 5 {
t.Error("expected 5")
}
}
// ❌ Bad: No cleanup
func TestDB(t *testing.T) {
db := setupDB()
// No teardown - leaves resources
}
// ❌ Bad: Sleep in tests
func TestAsync(t *testing.T) {
go doSomething()
time.Sleep(1 * time.Second) // Flaky!
// ...
}
```
### Do This
```go
// ✅ Good: Table-driven tests
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 1, 2, 3},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
assert.Equal(t, tt.want, got)
})
}
}
// ✅ Good: Cleanup with t.Cleanup
func TestDB(t *testing.T) {
db := setupDB(t)
// t.Cleanup automatically called on test completion
}
func setupDB(t *testing.T) *sql.DB {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
db.Close()
})
return db
}
// ✅ Good: Synchronization
func TestAsync(t *testing.T) {
done := make(chan struct{})
go func() {
doSomething()
close(done)
}()
select {
case <-done:
// success
case <-time.After(5 * time.Second):
t.Fatal("timeout")
}
}
```
## Testify Assertions
```go
import "github.com/stretchr/testify/assert"
// Equality
assert.Equal(t, expected, actual)
assert.NotEqual(t, expected, actual)
// True/False
assert.True(t, condition)
assert.False(t, condition)
// Nil
assert.Nil(t, err)
assert.NotNil(t, obj)
// Contains
assert.Contains(t, str, substr)
assert.ElementsMatch(t, expected, actual) // Unordered
// Errors
assert.NoError(t, err)
assert.Error(t, err)
assert.ErrorIs(t, err, target)
assert.ErrorContains(t, err, "substring")
// Type
assert.IsType(t, &User{}, obj)
// Length
assert.Len(t, slice, 3)
// Empty
assert.Empty(t, str)
assert.NotEmpty(t, str)
```

View File

@@ -0,0 +1,553 @@
# Go Web Patterns (Gin)
Production-ready patterns for building HTTP APIs with Gin framework.
## Project Structure
```
myapp/
├── cmd/
│ └── server/
│ └── main.go # Entry point
├── internal/
│ ├── config/
│ │ └── config.go # Configuration
│ ├── handlers/ # HTTP handlers
│ │ ├── user.go
│ │ └── auth.go
│ ├── middleware/
│ │ ├── auth.go
│ │ ├── logging.go
│ │ └── recovery.go
│ ├── models/ # Data models
│ │ └── user.go
│ ├── services/ # Business logic
│ │ └── user.go
│ └── repositories/ # Data access
│ └── user.go
├── pkg/ # Public packages
│ └── utils/
├── go.mod
├── go.sum
└── Dockerfile
```
## Main Entry Point
```go
// cmd/server/main.go
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"myapp/internal/config"
"myapp/internal/handlers"
"myapp/internal/middleware"
)
func main() {
// Load config
cfg := config.Load()
// Setup router
router := setupRouter(cfg)
// Create server
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: router,
}
// Start server in goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start server: %v", err)
}
}()
log.Printf("Server running on port %s", cfg.Port)
// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exited")
}
func setupRouter(cfg *config.Config) *gin.Engine {
// Set mode
gin.SetMode(cfg.Mode)
router := gin.New()
// Middleware
router.Use(middleware.Logger())
router.Use(middleware.Recovery())
router.Use(middleware.CORS())
router.Use(middleware.RateLimit())
// Health check
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"time": time.Now().Format(time.RFC3339),
})
})
// API routes
api := router.Group("/api/v1")
{
// Public routes
api.POST("/register", handlers.Register)
api.POST("/login", handlers.Login)
// Protected routes
protected := api.Group("")
protected.Use(middleware.Authenticate())
{
protected.GET("/users/:id", handlers.GetUser)
protected.PUT("/users/:id", handlers.UpdateUser)
}
}
return router
}
```
## Handler Pattern
```go
// internal/handlers/user.go
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"myapp/internal/models"
"myapp/internal/services"
)
type UserHandler struct {
userService *services.UserService
}
func NewUserHandler(userService *services.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
// Register godoc
// @Summary Register a new user
// @Description Create a new user account
// @Tags auth
// @Accept json
// @Produce json
// @Param user body models.RegisterRequest true "User registration data"
// @Success 201 {object} models.User
// @Failure 400 {object} gin.H
// @Router /register [post]
func (h *UserHandler) Register(c *gin.Context) {
var req models.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.userService.Create(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"user": user})
}
func (h *UserHandler) GetUser(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
user, err := h.userService.GetByID(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, gin.H{"user": user})
}
```
## Service Layer
```go
// internal/services/user.go
package services
import (
"context"
"errors"
"golang.org/x/crypto/bcrypt"
"myapp/internal/models"
"myapp/internal/repositories"
)
type UserService struct {
repo *repositories.UserRepository
}
func NewUserService(repo *repositories.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) Create(ctx context.Context, req *models.RegisterRequest) (*models.User, error) {
// Check if user exists
existing, _ := s.repo.FindByEmail(ctx, req.Email)
if existing != nil {
return nil, errors.New("email already registered")
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := &models.User{
Email: req.Email,
Password: string(hashedPassword),
Name: req.Name,
Role: "user",
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *UserService) GetByID(ctx context.Context, id int64) (*models.User, error) {
return s.repo.FindByID(ctx, id)
}
```
## Middleware Pattern
```go
// internal/middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func Authenticate() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header required"})
c.Abort()
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header"})
c.Abort()
return
}
tokenString := parts[1]
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("secret"), nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
c.Abort()
return
}
claims := token.Claims.(jwt.MapClaims)
c.Set("userID", claims["sub"])
c.Set("role", claims["role"])
c.Next()
}
}
// internal/middleware/logging.go
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func Logger() gin.HandlerFunc {
logger, _ := zap.NewProduction()
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
latency := time.Since(start)
logger.Info("request",
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.Duration("latency", latency),
zap.String("user-agent", c.Request.UserAgent()),
)
}
}
```
## Validation
```go
// internal/validators/user.go
package validators
import (
"regexp"
"unicode"
"github.com/go-playground/validator/v10"
)
type RegisterRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8,max=72,password"`
Name string `json:"name" validate:"required,min=2,max=100"`
}
var passwordRegex = regexp.MustCompile(`^[a-zA-Z0-9!@#$%^&*()_+-=]+$`)
func ValidatePassword(fl validator.FieldLevel) bool {
password := fl.Field().String()
if len(password) < 8 || len(password) > 72 {
return false
}
var hasUpper, hasLower, hasDigit bool
for _, char := range password {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsDigit(char):
hasDigit = true
}
}
return hasUpper && hasLower && hasDigit
}
func init() {
validate := validator.New()
validate.RegisterValidation("password", ValidatePassword)
}
```
## Error Handling
```go
// internal/errors/errors.go
package errors
import "net/http"
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Err error `json:"-"`
}
func (e *AppError) Error() string {
return e.Message
}
func NewNotFoundError(message string) *AppError {
return &AppError{Code: http.StatusNotFound, Message: message}
}
func NewBadRequestError(message string) *AppError {
return &AppError{Code: http.StatusBadRequest, Message: message}
}
func NewUnauthorizedError(message string) *AppError {
return &AppError{Code: http.StatusUnauthorized, Message: message}
}
func NewInternalError(message string) *AppError {
return &AppError{Code: http.StatusInternalServerError, Message: message}
}
// Middleware
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
for _, err := range c.Errors {
if appErr, ok := err.Err.(*AppError); ok {
c.JSON(appErr.Code, gin.H{"error": appErr.Message})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
}
}
```
## Dependency Injection
```go
// cmd/server/main.go (updated)
package main
import (
"myapp/internal/config"
"myapp/internal/handlers"
"myapp/internal/repositories"
"myapp/internal/services"
)
func main() {
cfg := config.Load()
// Setup database
db := setupDB(cfg)
// Dependency injection
userRepo := repositories.NewUserRepository(db)
userService := services.NewUserService(userRepo)
userHandler := handlers.NewUserHandler(userService)
// Setup router
router := gin.New()
api := router.Group("/api/v1")
{
api.POST("/register", userHandler.Register)
}
router.Run(":8080")
}
```
## Best Practices
### Don't Do This
```go
// ❌ Bad: Logic in handler
func GetUser(c *gin.Context) {
id := c.Param("id")
var user User
db.Where("id = ?", id).First(&user)
c.JSON(200, user)
}
// ❌ Bad: No error handling
func CreateUser(c *gin.Context) {
var user User
c.BindJSON(&user)
db.Create(&user)
c.JSON(201, user)
}
// ❌ Bad: Global state
var db *gorm.DB
func init() {
db = gorm.Open(...)
}
```
### Do This
```go
// ✅ Good: Handler → Service → Repository pattern
func (h *UserHandler) GetUser(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
user, err := h.userService.GetByID(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, gin.H{"user": user})
}
// ✅ Good: Dependency injection
func NewApp(db *gorm.DB) *App {
userRepo := repositories.NewUserRepository(db)
userService := services.NewUserService(userRepo)
userHandler := handlers.NewUserHandler(userService)
return &App{
userHandler: userHandler,
}
}
// ✅ Good: Context propagation
func (s *UserService) GetByID(ctx context.Context, id int64) (*models.User, error) {
var user models.User
if err := s.repo.WithContext(ctx).First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
}
```
## See Also
- `go-error-handling` - Comprehensive error patterns
- `go-middleware` - Middleware patterns
- `go-security` - Security best practices
- `go-testing` - Testing patterns