| name | go-patterns |
| description | Go (Golang) development principles and decision-making. Modern Go 1.22+ patterns, concurrency, project structure, API design. |
Go Patterns - Modern Go 1.22+
Go development principles and decision-making.
Learn to THINK, not copy-paste patterns.
⚠️ How to Use This Skill
This skill teaches decision-making principles for Go development.
- ASK user for use case (API, CLI, Microservice, Worker)
- Choose Go idioms, not Java/Python patterns
- Don't assume async if not needed (Go is already concurrent)
1. Framework Selection (Go)
Decision Tree
What are you building?
│
├── HTTP API / REST
│ ├── Gin (most popular, fast)
│ ├── Chi (minimalist, Go idioms)
│ ├── Fiber (Echo-compatible, fast)
│ └── Echo (full-featured)
│
├── gRPC / Microservices
│ ├── gRPC (Google)
│ └── Connect (Buf)
│
├── CLI Tool
│ ├── Cobra (complete CLI framework)
│ ├── Kingpin (flags only)
│ └── Cli (simple)
│
├── Background Worker
│ └── Asynq (Redis-based)
│ └── Machinery (Celery-like)
│
└── Full-stack Web
│ └── Go doesn't do full-stack; use templ + HTMX
└── Go templates + HTMX
Comparison Table
| Factor | Gin | Chi | Echo | Fiber |
|---|
| Performance | Very High | High | High | Very High |
| Routing | Tree-based | Middleware | Flexible | Express-like |
| Learning Curve | Low | Low | Low | Medium |
| Middleware | Many available | Chain-based | Rich | Many |
| Best For | Production APIs | Clean APIs | Full-featured | Speed |
2. Go Modules & Project Structure
Standard Module Structure
myproject/
├── go.mod
├── go.sum
├── cmd/
│ └── myapp/
│ └── main.go
├── internal/
│ ├── handler/
│ ├── service/
│ ├── repository/
│ └── middleware/
├── pkg/
│ └── utils/
├── api/
│ └── openapi.yaml
├── configs/
├── migrations/
└── tests/
Go 1.22+ Routing Patterns
func SetupRoutes(r *http.ServeMux) {
r.HandleFunc("GET /users", handler.ListUsers)
r.HandleFunc("POST /users", handler.CreateUser)
r.HandleFunc("GET /users/{id}", handler.GetUser)
r.HandleFunc("PUT /users/{id}", handler.UpdateUser)
r.HandleFunc("DELETE /users/{id}", handler.DeleteUser)
}
3. Concurrency Patterns
Go Idioms vs Other Languages
Go's philosophy (DON'T):
├── Don't use goroutines when not needed
├── Don't use channels for simple syncs (use sync.Mutex)
├── Don't over-engineer with patterns from other languages
└── Don't abuse "go run" for everything
Go's philosophy (DO):
├── Use goroutines for I/O-bound operations
├── Use channels for communication, not shared memory
├── Keep it simple - clear > clever
└── Let Go handle the concurrency, don't fight it
Common Patterns
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go func(t Task) {
defer wg.Done()
process(t)
}(task)
}
wg.Wait()
func Handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
result, err := doWork(ctx, r.URL.Query())
if err != nil {
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(result)
}
func StreamUsers(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
for user := range userChan {
fmt.Fprintf(w, "data: %s\n\n", user)
flusher.Flush()
}
}
4. Error Handling
Go Idiomatic Error Handling
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
if err != nil {
return fmt.Errorf("failed to get user %d: %w", userID, err)
}
if errors.Is(err, ErrNotFound) {
}
if errors.As(err, &validationErr) {
}
5. Database Patterns
ORMs & SQL
| Solution | Best For | Notes |
|---|
| GORM | Full ORM | Rich features, popular |
| Ent | Type-safe queries | Code generation, GraphQL |
| sqlx | Raw SQL + struct mapping | Lightweight, fast |
| Squirrel | Query builder | Flexible |
| Kitex | gRPC + DB | When using gRPC |
Pattern: Repository
type UserRepository interface {
FindByID(ctx context.Context, id int) (*User, error)
FindAll(ctx context.Context) ([]User, error)
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id int) error
}
type PostgresUserRepo struct {
db *sqlx.DB
}
func (r *PostgresUserRepo) FindByID(ctx context.Context, id int) (*User, error) {
var user User
err := r.db.GetContext(ctx, &user, "SELECT * FROM users WHERE id = $1", id)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return &user, err
}
6. Testing Patterns
Test Structure
func TestCalculateArea(t *testing.T) {
tests := []struct {
name string
width float64
height float64
expected float64
}{
{"square", 10, 10, 100},
{"rectangle", 5, 10, 50},
{"zero", 0, 10, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CalculateArea(tt.width, tt.height)
if result != tt.expected {
t.Errorf("expected %.2f, got %.2f", tt.expected, result)
}
})
}
}
type UserRepository interface {
FindByID(ctx context.Context, id int) (*User, error)
}
type MockUserRepo struct {
mock.Mock
}
func (m *MockUserRepo) FindByID(ctx context.Context, id int) (*User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get().(*User), args.Error()
}
Testing Tools
| Tool | Purpose |
|---|
testify | Assertions, mocking |
gocheck | More testing features |
ginkgo | BDD testing |
goconvey | BDD with assertion sugar |
monkey | Runtime patching (for tough cases) |
7. Configuration
Best Practices
import "github.com/spf13/viper"
func LoadConfig() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/app/")
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, err
}
return &config, nil
}
type Config struct {
DatabaseURL string `env:"DATABASE_URL"`
Port string `env:"PORT" default:"8080"`
}
8. Middleware Patterns
HTTP Middleware
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
r := chi.NewRouter()
r.Use(Logger)
r.Use(Recovery)
r.Use(Auth)
r.Route("/api", func(r chi.Router) {
r.Use(RateLimit)
r.Mount("/users", users.Routes())
})
9. Decision Checklist
Before implementing:
❌ Anti-Patterns to Avoid
❌ DON'T:
- Use goroutines for CPU-bound work (use worker pool)
- Create custom mutex when sync package suffices
- Use frameworks when net/http is enough
- Skip error handling ("if err != nil { return }")
- Use global variables
- Panic for expected errors
✅ DO:
- Go 1.22+ routing with http.ServeMux
- Context for timeout/cancellation
- Interfaces for dependency injection
- Think about Go idioms, not other languages
- Keep it simple and clear
Remember: Go is about simplicity and clarity.
Don't bring patterns from Java/Python/Ruby.
Think in Go.