
Golang Web
- 91 installs
- 253 repo stars
- Updated August 4, 2026
- majiayu000/claude-arsenal
Helps with ai & agent building tasks during AI-assisted development.
About
golang-web is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- golang-web
- AI & Agent Building
- AI-coding skill
Golang Web by the numbers
- 91 all-time installs (skills.sh)
- Ranked #4,765 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/majiayu000/claude-arsenal --skill golang-webAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 91 |
|---|---|
| repo stars | ★ 253 |
| Last updated | August 4, 2026 |
| Repository | majiayu000/claude-arsenal ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Go Web Architecture
Core Principles
- Standard layout — Follow cmd/internal/pkg convention
- Explicit dependencies — Wire dependencies in main.go, no globals
- Interface-driven — Define interfaces where you use them, not where you implement
- Error wrapping — Wrap errors with context, use error codes
- No backwards compatibility — Delete, don't deprecate. Change directly
- LiteLLM for LLM APIs — Use LiteLLM proxy for all LLM integrations
---
No Backwards Compatibility
Delete unused code. Change directly. No compatibility layers.
// ❌ BAD: Deprecated function kept around
// Deprecated: Use NewUserService instead
func CreateUserService() *UserService { ... }
// ❌ BAD: Alias for renamed types
type OldName = NewName // "for backwards compatibility"
// ❌ BAD: Unused parameters
func Process(_ context.Context, data Data) { ... }
// ✅ GOOD: Just delete and update all usages
func NewUserService(repo UserRepository) *UserService { ... }---
LiteLLM for LLM APIs
Use LiteLLM proxy. Don't call provider APIs directly.
// adapters/llm/client.go
package llm
import (
"github.com/sashabaranov/go-openai"
)
// Connect to LiteLLM proxy using OpenAI-compatible SDK
func NewClient(cfg Config) *openai.Client {
config := openai.DefaultConfig(cfg.APIKey)
config.BaseURL = cfg.BaseURL // LiteLLM proxy URL
return openai.NewClientWithConfig(config)
}---
Quick Start
1. Initialize Project
mkdir myapp && cd myapp
go mod init github.com/yourname/myapp
# Install core dependencies
go get github.com/gin-gonic/gin
go get github.com/spf13/viper
go get github.com/sirupsen/logrus
go get gorm.io/gorm2. Apply Tech Stack
| Layer | Recommendation |
|---|---|
| HTTP Framework | Gin / Chi / Echo |
| Configuration | Viper |
| Logging | Logrus / Zap / Slog |
| Database ORM | GORM / sqlx / sqlc |
| Validation | go-playground/validator |
| Testing | testify / go test |
Version Strategy
Always get latest. Never pin in templates.
# Always fetch latest
go get -u github.com/gin-gonic/gin
go get -u ./...
# go.mod handles version locking
# go.sum ensures reproducible builds3. Use Standard Structure
myapp/
├── cmd/
│ └── myapp/
│ └── main.go # Entry point, dependency wiring
├── configs/
│ └── config.go # Configuration struct + loader
├── internal/ # Private application code
│ ├── handlers/ # HTTP handlers
│ ├── services/ # Business logic
│ ├── repositories/ # Data access
│ ├── models/ # Domain models
│ ├── middleware/ # HTTP middleware
│ └── router/ # Route definitions
├── pkg/ # Public reusable packages
│ ├── errors/ # Error types
│ ├── logger/ # Logging setup
│ ├── response/ # Unified response format
│ └── database/ # Database connection
├── config.yaml # Configuration file
├── Makefile # Build automation
├── Dockerfile
└── go.mod---
Architecture Layers
cmd/ — Entry Point
Wire all dependencies here. No business logic.
// cmd/myapp/main.go
func main() {
// Load config
cfg := configs.Load()
// Initialize infrastructure
db := database.New(cfg.Database)
cache := cache.New(cfg.Redis)
logger := logger.New(cfg.Log)
// Initialize repositories
userRepo := repositories.NewUserRepository(db)
// Initialize services
userService := services.NewUserService(userRepo)
// Initialize handlers
userHandler := handlers.NewUserHandler(userService)
// Setup router
r := router.Setup(cfg, userHandler)
// Start server with graceful shutdown
server.Run(r, cfg.Server)
}internal/ — Private Business Code
handlers/ — HTTP Layer
// internal/handlers/user.go
type UserHandler struct {
service services.UserService
}
func NewUserHandler(s services.UserService) *UserHandler {
return &UserHandler{service: s}
}
func (h *UserHandler) Create(c *gin.Context) {
var input CreateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
response.Error(c, errors.ErrInvalidParams)
return
}
user, err := h.service.Create(c.Request.Context(), input)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, user)
}services/ — Business Logic
// internal/services/user.go
type UserService interface {
Create(ctx context.Context, input CreateUserInput) (*models.User, error)
GetByID(ctx context.Context, id string) (*models.User, error)
}
type userService struct {
repo repositories.UserRepository
}
func NewUserService(repo repositories.UserRepository) UserService {
return &userService{repo: repo}
}
func (s *userService) Create(ctx context.Context, input CreateUserInput) (*models.User, error) {
existing, _ := s.repo.FindByEmail(ctx, input.Email)
if existing != nil {
return nil, errors.ErrUserExists
}
user := &models.User{
ID: uuid.New().String(),
Email: input.Email,
Name: input.Name,
}
return s.repo.Save(ctx, user)
}repositories/ — Data Access
// internal/repositories/user.go
type UserRepository interface {
FindByID(ctx context.Context, id string) (*models.User, error)
FindByEmail(ctx context.Context, email string) (*models.User, error)
Save(ctx context.Context, user *models.User) (*models.User, error)
Delete(ctx context.Context, id string) error
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) 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, nil
}
return nil, err
}
return &user, nil
}pkg/ — Reusable Packages
errors/ — Error Handling
// pkg/errors/errors.go
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Cause error `json:"-"`
}
func (e *AppError) Error() string { return e.Message }
func (e *AppError) Unwrap() error { return e.Cause }
func New(code int, message string) *AppError {
return &AppError{Code: code, Message: message}
}
func Wrap(err error, code int, message string) *AppError {
return &AppError{Code: code, Message: message, Cause: err}
}
// Predefined errors
var (
ErrInternal = New(500, "internal server error")
ErrInvalidParams = New(400, "invalid parameters")
ErrNotFound = New(404, "resource not found")
ErrUnauthorized = New(401, "unauthorized")
ErrUserExists = New(409, "user already exists")
)response/ — Unified Response
// pkg/response/response.go
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: "success",
Data: data,
})
}
func Error(c *gin.Context, err error) {
var appErr *errors.AppError
if errors.As(err, &appErr) {
c.JSON(appErr.Code/100, Response{
Code: appErr.Code,
Message: appErr.Message,
})
return
}
c.JSON(http.StatusInternalServerError, Response{
Code: 500,
Message: "internal server error",
})
}---
Configuration
Viper + YAML + Environment Variables
// configs/config.go
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Log LogConfig `mapstructure:"log"`
LLM LLMConfig `mapstructure:"llm"`
}
type LLMConfig struct {
BaseURL string `mapstructure:"base_url"`
APIKey string `mapstructure:"api_key"`
DefaultModel string `mapstructure:"default_model"`
}
func Load() *Config {
viper.SetConfigFile("config.yaml")
viper.AutomaticEnv()
viper.SetEnvPrefix("APP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Defaults
viper.SetDefault("server.port", 8080)
viper.SetDefault("llm.base_url", "http://localhost:4000")
viper.SetDefault("llm.default_model", "gpt-4o")
viper.ReadInConfig()
var cfg Config
viper.Unmarshal(&cfg)
return &cfg
}---
Graceful Shutdown
// pkg/server/server.go
func Run(handler http.Handler, cfg ServerConfig) {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
Handler: handler,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)
}---
Makefile
.PHONY: build run test lint clean
APP_NAME=myapp
build:
go build -o bin/$(APP_NAME) ./cmd/$(APP_NAME)
run:
go run ./cmd/$(APP_NAME)
dev:
air
test:
go test -v ./...
lint:
golangci-lint run
clean:
rm -rf bin/
tidy:
go mod tidy
upgrade:
go get -u ./...
go mod tidy---
Checklist
## Project Setup
- [ ] Go 1.21+ installed
- [ ] Standard directory structure (cmd/internal/pkg)
- [ ] go.mod initialized
- [ ] Makefile created
## Architecture
- [ ] Dependencies wired in main.go
- [ ] Handlers → Services → Repositories layers
- [ ] Interfaces defined at usage site
- [ ] No circular dependencies
## Infrastructure
- [ ] Configuration with Viper
- [ ] Structured logging
- [ ] Custom error types
- [ ] Unified response format
- [ ] Graceful shutdown
## Quality
- [ ] Tests for services
- [ ] golangci-lint configured
- [ ] go vet passes
- [ ] Race detection tested---
See Also
- reference/architecture.md — Detailed architecture patterns
- reference/tech-stack.md — Tech stack comparison
- reference/patterns.md — Go design patterns
Architecture Reference
Table of Contents
1. Standard Go Project Layout 2. Layered Architecture 3. Dependency Injection 4. Interface Design 5. Error Handling
---
Standard Go Project Layout
Overview
project/
├── cmd/ # Main applications
│ └── myapp/
│ └── main.go # Entry point
├── internal/ # Private code (not importable)
│ ├── handlers/ # HTTP handlers
│ ├── services/ # Business logic
│ ├── repositories/ # Data access
│ ├── models/ # Domain models
│ ├── middleware/ # HTTP middleware
│ └── router/ # Route setup
├── pkg/ # Public reusable code
│ ├── errors/
│ ├── logger/
│ ├── response/
│ └── database/
├── configs/ # Configuration
├── api/ # OpenAPI/Swagger specs
├── scripts/ # Build/deploy scripts
├── deployments/ # Docker, K8s configs
└── docs/ # DocumentationDirectory Purposes
| Directory | Purpose | Importable? |
|---|---|---|
cmd/ | Application entry points | No (main packages) |
internal/ | Private application code | No (Go enforced) |
pkg/ | Public library code | Yes |
configs/ | Configuration loading | Internal use |
api/ | API definitions (OpenAPI) | N/A |
Why internal/?
// internal/ cannot be imported from outside the module
// This is enforced by Go compiler
// ✅ OK: Import within same module
import "github.com/myorg/myapp/internal/services"
// ❌ ERROR: Cannot import from another module
import "github.com/myorg/myapp/internal/services" // Compilation error---
Layered Architecture
Layer Diagram
┌─────────────────────────────────────────────────────────────┐
│ cmd/main.go │
│ (Dependency Wiring) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ internal/handlers/ │
│ HTTP Handlers (Gin/Chi/Echo) │
│ Parse request → Call service → Return response │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ internal/services/ │
│ Business Logic │
│ Orchestration, validation, domain rules │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ internal/repositories/ │
│ Data Access │
│ Database queries, cache operations │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ pkg/ │
│ Shared Infrastructure │
│ errors, logger, response, database, cache │
└─────────────────────────────────────────────────────────────┘Layer Responsibilities
Handlers (Presentation)
// internal/handlers/user.go
// Responsibilities:
// - Parse HTTP request
// - Validate input format
// - Call service
// - Format HTTP response
// - NO business logic
func (h *UserHandler) Create(c *gin.Context) {
// 1. Parse request
var input CreateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
response.Error(c, errors.ErrInvalidParams)
return
}
// 2. Call service (business logic happens there)
user, err := h.service.Create(c.Request.Context(), input)
if err != nil {
response.Error(c, err)
return
}
// 3. Return response
response.Success(c, user)
}Services (Business)
// internal/services/user.go
// Responsibilities:
// - Business rules and validation
// - Orchestrate multiple repositories
// - Transaction management
// - NO HTTP concerns, NO SQL queries
func (s *userService) Create(ctx context.Context, input CreateUserInput) (*models.User, error) {
// Business rule: Check duplicate
existing, _ := s.repo.FindByEmail(ctx, input.Email)
if existing != nil {
return nil, errors.ErrUserExists
}
// Business rule: Hash password
hashedPassword, err := s.hasher.Hash(input.Password)
if err != nil {
return nil, errors.Wrap(err, 500, "failed to hash password")
}
user := &models.User{
ID: uuid.New().String(),
Email: input.Email,
Password: hashedPassword,
}
return s.repo.Save(ctx, user)
}Repositories (Data)
// internal/repositories/user.go
// Responsibilities:
// - Database queries
// - Data mapping (DB row → Model)
// - NO business logic
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) {
var user models.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // Not found is not an error
}
return &user, err
}---
Dependency Injection
Manual DI in main.go
// cmd/myapp/main.go
func main() {
cfg := configs.Load()
// Infrastructure (bottom of dependency graph)
db := database.New(cfg.Database)
cache := cache.New(cfg.Redis)
hasher := security.NewArgon2Hasher()
// Repositories (depend on infrastructure)
userRepo := repositories.NewUserRepository(db)
orderRepo := repositories.NewOrderRepository(db)
// Services (depend on repositories)
userService := services.NewUserService(userRepo, hasher)
orderService := services.NewOrderService(orderRepo, userService)
// Handlers (depend on services)
userHandler := handlers.NewUserHandler(userService)
orderHandler := handlers.NewOrderHandler(orderService)
// Router (depend on handlers)
r := router.Setup(userHandler, orderHandler)
server.Run(r, cfg.Server)
}Why Manual DI?
| Approach | Pros | Cons |
|---|---|---|
| Manual DI | Explicit, type-safe, no magic | Verbose for large apps |
| Wire (Google) | Compile-time DI generation | Learning curve |
| Fx (Uber) | Runtime DI, lifecycle management | Runtime errors possible |
| dig (Uber) | Simpler runtime DI | Less type safety |
Recommendation: Start with manual DI. Move to Wire only when wiring becomes painful.
---
Interface Design
Define Interfaces at Point of Use
// ❌ BAD: Interface defined with implementation
// internal/repositories/user.go
type UserRepository interface { ... }
type userRepository struct { ... }
// ✅ GOOD: Interface defined where it's used
// internal/services/user.go
type UserRepository interface {
FindByID(ctx context.Context, id string) (*models.User, error)
Save(ctx context.Context, user *models.User) (*models.User, error)
}
type userService struct {
repo UserRepository // Accepts any implementation
}
// internal/repositories/user.go
type userRepository struct { db *gorm.DB }
// Implicitly implements services.UserRepositoryAccept Interfaces, Return Structs
// ✅ GOOD: Accept interface
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// ❌ BAD: Accept concrete type
func NewUserService(repo *PostgresUserRepository) *UserService {
return &UserService{repo: repo}
}Keep Interfaces Small
// ❌ BAD: Large interface
type UserRepository interface {
FindByID(id string) (*User, error)
FindByEmail(email string) (*User, error)
FindByPhone(phone string) (*User, error)
FindAll() ([]*User, error)
FindByRole(role string) ([]*User, error)
Save(user *User) (*User, error)
Update(user *User) error
Delete(id string) error
// ... 20 more methods
}
// ✅ GOOD: Small, focused interfaces
type UserFinder interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type UserSaver interface {
Save(ctx context.Context, user *User) (*User, error)
}
// Compose when needed
type UserRepository interface {
UserFinder
UserSaver
}---
Error Handling
Custom Error Types
// pkg/errors/errors.go
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
HTTPStatus int `json:"-"`
Cause error `json:"-"`
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Cause)
}
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Cause
}Error Wrapping Pattern
// Repository layer: Wrap with context
func (r *userRepository) 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, nil // Not found is OK
}
return nil, errors.Wrap(err, 500, "failed to query user")
}
return &user, nil
}
// Service layer: Add business context
func (s *userService) GetByID(ctx context.Context, id string) (*models.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, err // Already wrapped
}
if user == nil {
return nil, errors.ErrNotFound
}
return user, nil
}
// Handler layer: Just pass through
func (h *UserHandler) Get(c *gin.Context) {
user, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
response.Error(c, err) // Error type determines HTTP status
return
}
response.Success(c, user)
}Error Handling Best Practices
// ✅ DO: Check errors immediately
result, err := doSomething()
if err != nil {
return nil, err
}
// ✅ DO: Wrap with context
if err != nil {
return errors.Wrap(err, 500, "failed to process order")
}
// ✅ DO: Use errors.Is for comparison
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
// ❌ DON'T: Ignore errors
result, _ := doSomething() // Never do this
// ❌ DON'T: Create new error losing context
if err != nil {
return fmt.Errorf("failed") // Lost original error
}Go Design Patterns Reference
Table of Contents
1. Repository Pattern 2. Service Pattern 3. Functional Options 4. Middleware Pattern 5. Graceful Shutdown 6. Context Usage
---
Repository Pattern
Abstract data access behind interfaces:
// internal/repositories/user.go
// Interface - defined where it's USED (in services)
type UserRepository interface {
FindByID(ctx context.Context, id string) (*models.User, error)
FindByEmail(ctx context.Context, email string) (*models.User, error)
Save(ctx context.Context, user *models.User) (*models.User, error)
Delete(ctx context.Context, id string) error
}
// Implementation
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *userRepository {
return &userRepository{db: db}
}
func (r *userRepository) 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, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepository) Save(ctx context.Context, user *models.User) (*models.User, error) {
if err := r.db.WithContext(ctx).Save(user).Error; err != nil {
return nil, err
}
return user, nil
}In-Memory Repository for Testing
// internal/repositories/user_memory.go
type inMemoryUserRepository struct {
users map[string]*models.User
mu sync.RWMutex
}
func NewInMemoryUserRepository() *inMemoryUserRepository {
return &inMemoryUserRepository{
users: make(map[string]*models.User),
}
}
func (r *inMemoryUserRepository) FindByID(ctx context.Context, id string) (*models.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
user, ok := r.users[id]
if !ok {
return nil, nil
}
return user, nil
}
func (r *inMemoryUserRepository) Save(ctx context.Context, user *models.User) (*models.User, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.users[user.ID] = user
return user, nil
}---
Service Pattern
Encapsulate business logic:
// internal/services/user.go
type UserService interface {
Create(ctx context.Context, input CreateUserInput) (*models.User, error)
GetByID(ctx context.Context, id string) (*models.User, error)
Update(ctx context.Context, id string, input UpdateUserInput) (*models.User, error)
Delete(ctx context.Context, id string) error
}
type userService struct {
repo UserRepository
hasher PasswordHasher
events EventPublisher
}
func NewUserService(repo UserRepository, hasher PasswordHasher, events EventPublisher) UserService {
return &userService{
repo: repo,
hasher: hasher,
events: events,
}
}
func (s *userService) Create(ctx context.Context, input CreateUserInput) (*models.User, error) {
// Business rule: Check duplicate email
existing, err := s.repo.FindByEmail(ctx, input.Email)
if err != nil {
return nil, errors.Wrap(err, 500, "failed to check email")
}
if existing != nil {
return nil, errors.ErrUserExists
}
// Business rule: Hash password
hashedPassword, err := s.hasher.Hash(input.Password)
if err != nil {
return nil, errors.Wrap(err, 500, "failed to hash password")
}
user := &models.User{
ID: uuid.New().String(),
Email: input.Email,
Password: hashedPassword,
CreatedAt: time.Now(),
}
saved, err := s.repo.Save(ctx, user)
if err != nil {
return nil, errors.Wrap(err, 500, "failed to save user")
}
// Publish event (async)
s.events.Publish("user.created", UserCreatedEvent{UserID: saved.ID})
return saved, nil
}---
Functional Options
Configure structs with optional parameters:
// pkg/server/server.go
type Server struct {
port int
readTimeout time.Duration
writeTimeout time.Duration
handler http.Handler
}
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithReadTimeout(d time.Duration) Option {
return func(s *Server) {
s.readTimeout = d
}
}
func WithWriteTimeout(d time.Duration) Option {
return func(s *Server) {
s.writeTimeout = d
}
}
func NewServer(handler http.Handler, opts ...Option) *Server {
// Defaults
s := &Server{
port: 8080,
readTimeout: 30 * time.Second,
writeTimeout: 30 * time.Second,
handler: handler,
}
// Apply options
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer(handler,
WithPort(9090),
WithReadTimeout(60*time.Second),
)Builder Alternative
type ServerBuilder struct {
server *Server
}
func NewServerBuilder(handler http.Handler) *ServerBuilder {
return &ServerBuilder{
server: &Server{
port: 8080,
handler: handler,
},
}
}
func (b *ServerBuilder) Port(p int) *ServerBuilder {
b.server.port = p
return b
}
func (b *ServerBuilder) ReadTimeout(d time.Duration) *ServerBuilder {
b.server.readTimeout = d
return b
}
func (b *ServerBuilder) Build() *Server {
return b.server
}
// Usage
server := NewServerBuilder(handler).
Port(9090).
ReadTimeout(60 * time.Second).
Build()---
Middleware Pattern
Chain HTTP handlers:
// internal/middleware/middleware.go
// Middleware type
type Middleware func(http.Handler) http.Handler
// Chain combines multiple middleware
func Chain(middlewares ...Middleware) Middleware {
return func(next http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
next = middlewares[i](next)
}
return next
}
}
// Logger middleware
func Logger(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap response writer to capture status
ww := &responseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
logger.Info("http request",
"method", r.Method,
"path", r.URL.Path,
"status", ww.status,
"duration", time.Since(start),
)
})
}
}
// Recovery middleware
func Recovery() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
slog.Error("panic recovered", "error", err)
}
}()
next.ServeHTTP(w, r)
})
}
}
// RequestID middleware
func RequestID() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
ctx := context.WithValue(r.Context(), "request_id", requestID)
w.Header().Set("X-Request-ID", requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// Usage
handler := Chain(
Recovery(),
RequestID(),
Logger(logger),
)(router)Gin Middleware
// internal/middleware/gin.go
func GinLogger(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
logger.Info("http request",
"method", c.Request.Method,
"path", c.Request.URL.Path,
"status", c.Writer.Status(),
"duration", time.Since(start),
"client_ip", c.ClientIP(),
)
}
}
func GinRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
slog.Error("panic recovered", "error", err)
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}---
Graceful Shutdown
Handle shutdown signals properly:
// pkg/server/server.go
func (s *Server) Run() error {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
Handler: s.handler,
ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout,
}
// Channel to listen for errors from ListenAndServe
errChan := make(chan error, 1)
go func() {
slog.Info("server starting", "port", s.port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errChan <- err
}
}()
// Channel to listen for OS signals
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Block until signal or error
select {
case err := <-errChan:
return fmt.Errorf("server error: %w", err)
case sig := <-quit:
slog.Info("shutdown signal received", "signal", sig)
}
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
return fmt.Errorf("server shutdown error: %w", err)
}
slog.Info("server stopped gracefully")
return nil
}With Cleanup Functions
// pkg/server/server.go
type CleanupFunc func(context.Context) error
func (s *Server) RunWithCleanup(cleanups ...CleanupFunc) error {
// ... server start code ...
// After shutdown signal
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Shutdown HTTP server
if err := srv.Shutdown(ctx); err != nil {
slog.Error("server shutdown error", "error", err)
}
// Run cleanup functions
for _, cleanup := range cleanups {
if err := cleanup(ctx); err != nil {
slog.Error("cleanup error", "error", err)
}
}
return nil
}
// Usage
server.RunWithCleanup(
db.Close,
cache.Close,
eventBus.Close,
)---
Context Usage
Pass context through the call chain:
// ✅ GOOD: Context as first parameter
func (s *userService) GetByID(ctx context.Context, id string) (*models.User, error) {
return s.repo.FindByID(ctx, id)
}
// ✅ GOOD: Extract values from context
func GetRequestID(ctx context.Context) string {
if id, ok := ctx.Value("request_id").(string); ok {
return id
}
return ""
}
// ✅ GOOD: Use context for cancellation
func (s *userService) LongOperation(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// Do work
}
}
}
// ❌ BAD: Context not first parameter
func BadFunc(id string, ctx context.Context) error { ... }
// ❌ BAD: Storing context in struct
type BadService struct {
ctx context.Context // Never do this
}
// ❌ BAD: Using context.Background() in handlers
func (h *Handler) Get(c *gin.Context) {
user, _ := h.service.GetByID(context.Background(), id) // Use c.Request.Context()
}Context Keys
// pkg/ctxkeys/keys.go
type contextKey string
const (
RequestIDKey contextKey = "request_id"
UserIDKey contextKey = "user_id"
TraceIDKey contextKey = "trace_id"
)
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, RequestIDKey, id)
}
func RequestID(ctx context.Context) string {
if id, ok := ctx.Value(RequestIDKey).(string); ok {
return id
}
return ""
}Tech Stack Reference
Table of Contents
1. Version Strategy 2. HTTP Framework 3. Configuration 4. Logging 5. Database 6. Validation 7. Testing 8. Linting 9. Decision Matrix
---
Version Strategy
Always use latest. `go.mod` handles version locking.
Why No Pinned Versions
- Go modules automatically track versions in
go.mod go.sumensures reproducible buildsgo get -uupdates to latest compatible version
How to Stay Current
# Update all dependencies
go get -u ./...
go mod tidy
# Update specific package
go get -u github.com/gin-gonic/gin
# Check for available updates
go list -m -u all
# Verify dependencies
go mod verifyPackage Installation
# Always install without version
go get github.com/gin-gonic/gin # Gets latest
go get github.com/spf13/viper # Gets latest
# Never do this in templates
go get github.com/gin-gonic/gin@v1.9.0 # Pinned = outdated---
HTTP Framework
Gin (Recommended)
go get github.com/gin-gonic/ginimport "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/users/:id", getUser)
r.POST("/users", createUser)
r.Run(":8080")
}Pros:
- Most popular, largest ecosystem
- Fast (uses httprouter)
- Good middleware support
- Excellent documentation
Chi (Lightweight alternative)
go get github.com/go-chi/chi/v5import "github.com/go-chi/chi/v5"
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/users/{id}", getUser)
r.Post("/users", createUser)
http.ListenAndServe(":8080", r)
}Pros:
- Lightweight, stdlib compatible
- Composable middleware
- No external dependencies
Echo (Feature-rich)
go get github.com/labstack/echo/v4import "github.com/labstack/echo/v4"
func main() {
e := echo.New()
e.GET("/users/:id", getUser)
e.POST("/users", createUser)
e.Start(":8080")
}Pros:
- Built-in features (validation, binding)
- Good performance
- Clean API
Comparison
| Framework | Performance | Ecosystem | Learning Curve | stdlib Compatible |
|---|---|---|---|---|
| Gin | Excellent | Large | Low | No |
| Chi | Excellent | Medium | Low | Yes |
| Echo | Excellent | Medium | Low | No |
| Fiber | Fastest | Growing | Low | No |
| stdlib | Good | N/A | Medium | Yes |
---
Configuration
Viper (Recommended)
go get github.com/spf13/viperimport "github.com/spf13/viper"
type Config struct {
Server struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
} `mapstructure:"server"`
Database struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
} `mapstructure:"database"`
}
func Load() *Config {
viper.SetConfigFile("config.yaml")
viper.AutomaticEnv()
viper.SetEnvPrefix("APP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Defaults
viper.SetDefault("server.port", 8080)
viper.ReadInConfig()
var cfg Config
viper.Unmarshal(&cfg)
return &cfg
}Features:
- YAML, JSON, TOML, ENV support
- Environment variable override
- Default values
- Live config reload
Envconfig (Simpler)
go get github.com/kelseyhightower/envconfigimport "github.com/kelseyhightower/envconfig"
type Config struct {
Port int `envconfig:"PORT" default:"8080"`
DBHost string `envconfig:"DB_HOST" required:"true"`
LogLevel string `envconfig:"LOG_LEVEL" default:"info"`
}
func Load() *Config {
var cfg Config
envconfig.Process("APP", &cfg)
return &cfg
}Pros: Simple, environment-variable focused
---
Logging
Slog (Go 1.21+ stdlib)
import "log/slog"
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
slog.Info("server started", "port", 8080)
slog.Error("failed to connect", "error", err)
}Pros: Stdlib, structured, no dependencies
Logrus (Battle-tested)
go get github.com/sirupsen/logrusimport "github.com/sirupsen/logrus"
var log = logrus.New()
func init() {
log.SetFormatter(&logrus.JSONFormatter{})
log.SetLevel(logrus.InfoLevel)
}
func main() {
log.WithFields(logrus.Fields{
"port": 8080,
}).Info("server started")
}Pros: Popular, feature-rich, hooks support
Zap (Performance)
go get go.uber.org/zapimport "go.uber.org/zap"
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("server started",
zap.Int("port", 8080),
)
}Pros: Fastest, zero-allocation
Comparison
| Library | Performance | Ease of Use | Stdlib |
|---|---|---|---|
| slog | Good | Easy | Yes |
| Logrus | Medium | Easy | No |
| Zap | Excellent | Medium | No |
| Zerolog | Excellent | Easy | No |
---
Database
GORM (Full ORM)
go get gorm.io/gorm
go get gorm.io/driver/postgresimport (
"gorm.io/gorm"
"gorm.io/driver/postgres"
)
type User struct {
ID string `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex"`
Name string
}
func main() {
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db.AutoMigrate(&User{})
// Create
db.Create(&User{ID: "1", Email: "test@test.com"})
// Query
var user User
db.First(&user, "email = ?", "test@test.com")
}Pros: Feature-rich, migrations, associations
sqlx (SQL + Struct mapping)
go get github.com/jmoiron/sqlximport "github.com/jmoiron/sqlx"
type User struct {
ID string `db:"id"`
Email string `db:"email"`
}
func main() {
db := sqlx.MustConnect("postgres", dsn)
var user User
db.Get(&user, "SELECT * FROM users WHERE id = $1", "1")
var users []User
db.Select(&users, "SELECT * FROM users WHERE active = $1", true)
}Pros: Direct SQL control, good performance
sqlc (Compile-time type-safe SQL)
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest-- query.sql
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- name: CreateUser :one
INSERT INTO users (id, email) VALUES ($1, $2) RETURNING *;// Generated code
func (q *Queries) GetUser(ctx context.Context, id string) (User, error)
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)Pros: Type-safe SQL, compile-time checks
Comparison
| Library | Type Safety | Performance | Learning Curve |
|---|---|---|---|
| GORM | Good | Medium | Low |
| sqlx | Medium | Good | Low |
| sqlc | Excellent | Excellent | Medium |
| Ent | Excellent | Good | High |
---
Validation
go-playground/validator
go get github.com/go-playground/validator/v10import "github.com/go-playground/validator/v10"
type CreateUserInput struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Age int `json:"age" validate:"gte=0,lte=120"`
}
var validate = validator.New()
func ValidateStruct(s interface{}) error {
return validate.Struct(s)
}Pros: Standard choice, many validators, custom rules
ozzo-validation
go get github.com/go-ozzo/ozzo-validation/v4import validation "github.com/go-ozzo/ozzo-validation/v4"
type CreateUserInput struct {
Email string
Password string
}
func (i CreateUserInput) Validate() error {
return validation.ValidateStruct(&i,
validation.Field(&i.Email, validation.Required, is.Email),
validation.Field(&i.Password, validation.Required, validation.Length(8, 100)),
)
}Pros: Fluent API, validation in structs
---
Testing
testify (Recommended)
go get github.com/stretchr/testifyimport (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
func TestUserService_Create(t *testing.T) {
// Arrange
repo := NewMockUserRepository()
service := NewUserService(repo)
// Act
user, err := service.Create(ctx, input)
// Assert
require.NoError(t, err)
assert.Equal(t, "test@test.com", user.Email)
}
// Table-driven tests
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{"valid email", "test@test.com", false},
{"invalid email", "invalid", true},
{"empty email", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}mockery (Mock generation)
go install github.com/vektra/mockery/v2@latest//go:generate mockery --name=UserRepository
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
}
// Generated mock in mocks/UserRepository.go---
Linting
golangci-lint (Recommended)
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest# .golangci.yml
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
linters-settings:
errcheck:
check-type-assertions: true
govet:
check-shadowing: truegolangci-lint run---
Decision Matrix
For New Projects
| Layer | Recommended | Alternative |
|---|---|---|
| HTTP | Gin | Chi |
| Config | Viper | Envconfig |
| Logging | slog (stdlib) | Logrus |
| Database | GORM | sqlx / sqlc |
| Validation | validator | ozzo-validation |
| Testing | testify | stdlib |
| Linting | golangci-lint | - |
Stack Combinations
Feature-rich stack:
Gin + Viper + Logrus + GORM + validator + testifyLightweight stack:
Chi + Envconfig + slog + sqlx + validator + stdlib testingType-safe stack:
Chi + Viper + slog + sqlc + validator + testify// cmd/myapp/main.go
package main
import (
"log/slog"
"os"
"github.com/yourname/myapp/configs"
"github.com/yourname/myapp/internal/handlers"
"github.com/yourname/myapp/internal/repositories"
"github.com/yourname/myapp/internal/router"
"github.com/yourname/myapp/internal/services"
"github.com/yourname/myapp/pkg/database"
"github.com/yourname/myapp/pkg/server"
)
func main() {
// Initialize logger
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
// Load configuration
cfg := configs.Load()
// Initialize database
db, err := database.New(cfg.Database)
if err != nil {
slog.Error("failed to connect to database", "error", err)
os.Exit(1)
}
defer db.Close()
// Initialize repositories
userRepo := repositories.NewUserRepository(db.DB())
// Initialize services
userService := services.NewUserService(userRepo)
// Initialize handlers
userHandler := handlers.NewUserHandler(userService)
// Setup router
r := router.Setup(cfg, userHandler)
// Start server
srv := server.New(r,
server.WithPort(cfg.Server.Port),
server.WithReadTimeout(cfg.Server.ReadTimeout),
server.WithWriteTimeout(cfg.Server.WriteTimeout),
)
if err := srv.Run(); err != nil {
slog.Error("server error", "error", err)
os.Exit(1)
}
}
# Application Configuration
# Environment variables override these values with APP_ prefix
# e.g., APP_SERVER_PORT=9090
server:
port: 8080
mode: debug # debug, release
read_timeout: 30s
write_timeout: 30s
database:
driver: sqlite # sqlite, postgres, mysql
host: localhost
port: 5432
username: postgres
password: ""
database: data/app.db
ssl_mode: disable
log:
level: info # debug, info, warn, error
format: json # json, text
# LiteLLM proxy configuration
llm:
base_url: http://localhost:4000
api_key: ${LITELLM_API_KEY}
default_model: gpt-4o
// configs/config.go
package configs
import (
"strings"
"time"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Log LogConfig `mapstructure:"log"`
LLM LLMConfig `mapstructure:"llm"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
}
type DatabaseConfig struct {
Driver string `mapstructure:"driver"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
SSLMode string `mapstructure:"ssl_mode"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
}
type LLMConfig struct {
BaseURL string `mapstructure:"base_url"`
APIKey string `mapstructure:"api_key"`
DefaultModel string `mapstructure:"default_model"`
}
func Load() *Config {
viper.SetConfigFile("config.yaml")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
// Environment variables
viper.AutomaticEnv()
viper.SetEnvPrefix("APP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Defaults
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.mode", "debug")
viper.SetDefault("server.read_timeout", 30*time.Second)
viper.SetDefault("server.write_timeout", 30*time.Second)
viper.SetDefault("database.driver", "sqlite")
viper.SetDefault("database.database", "data/app.db")
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", "json")
viper.SetDefault("llm.base_url", "http://localhost:4000")
viper.SetDefault("llm.default_model", "gpt-4o")
// Read config file (optional)
_ = viper.ReadInConfig()
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
panic("failed to unmarshal config: " + err.Error())
}
return &cfg
}
module github.com/yourname/myapp
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.6.0
github.com/spf13/viper v1.18.2
gorm.io/driver/postgres v1.5.7
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.7
)
// internal/handlers/user.go
package handlers
import (
"github.com/gin-gonic/gin"
"github.com/yourname/myapp/internal/services"
"github.com/yourname/myapp/pkg/errors"
"github.com/yourname/myapp/pkg/response"
)
// UserHandler handles user-related HTTP requests
type UserHandler struct {
service services.UserService
}
// NewUserHandler creates a new UserHandler
func NewUserHandler(service services.UserService) *UserHandler {
return &UserHandler{service: service}
}
// Create handles POST /users
func (h *UserHandler) Create(c *gin.Context) {
var input services.CreateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
response.Error(c, errors.ErrInvalidParams)
return
}
user, err := h.service.Create(c.Request.Context(), input)
if err != nil {
response.Error(c, err)
return
}
response.Created(c, user)
}
// Get handles GET /users/:id
func (h *UserHandler) Get(c *gin.Context) {
id := c.Param("id")
user, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, user)
}
// Update handles PUT /users/:id
func (h *UserHandler) Update(c *gin.Context) {
id := c.Param("id")
var input services.UpdateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
response.Error(c, errors.ErrInvalidParams)
return
}
user, err := h.service.Update(c.Request.Context(), id, input)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, user)
}
// Delete handles DELETE /users/:id
func (h *UserHandler) Delete(c *gin.Context) {
id := c.Param("id")
if err := h.service.Delete(c.Request.Context(), id); err != nil {
response.Error(c, err)
return
}
response.NoContent(c)
}
// internal/models/user.go
package models
import "time"
// User represents a user in the system
type User struct {
ID string `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex"`
Name string `json:"name"`
Password string `json:"-"` // Never expose password
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName returns the table name for GORM
func (User) TableName() string {
return "users"
}
// internal/repositories/user.go
package repositories
import (
"context"
"errors"
"github.com/yourname/myapp/internal/models"
"gorm.io/gorm"
)
// UserRepository defines the interface for user data access
type UserRepository interface {
FindByID(ctx context.Context, id string) (*models.User, error)
FindByEmail(ctx context.Context, email string) (*models.User, error)
Save(ctx context.Context, user *models.User) (*models.User, error)
Delete(ctx context.Context, id string) error
}
type userRepository struct {
db *gorm.DB
}
// NewUserRepository creates a new UserRepository
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) 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, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) {
var user models.User
if err := r.db.WithContext(ctx).First(&user, "email = ?", email).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *userRepository) Save(ctx context.Context, user *models.User) (*models.User, error) {
if err := r.db.WithContext(ctx).Save(user).Error; err != nil {
return nil, err
}
return user, nil
}
func (r *userRepository) Delete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&models.User{}, "id = ?", id).Error
}
// internal/router/router.go
package router
import (
"github.com/gin-gonic/gin"
"github.com/yourname/myapp/configs"
"github.com/yourname/myapp/internal/handlers"
)
// Setup configures and returns the router
func Setup(cfg *configs.Config, userHandler *handlers.UserHandler) *gin.Engine {
// Set Gin mode
if cfg.Server.Mode == "release" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
// Middleware
r.Use(gin.Recovery())
r.Use(gin.Logger())
// Health check
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// API v1
v1 := r.Group("/api/v1")
{
// Users
users := v1.Group("/users")
{
users.POST("", userHandler.Create)
users.GET("/:id", userHandler.Get)
users.PUT("/:id", userHandler.Update)
users.DELETE("/:id", userHandler.Delete)
}
}
return r
}
// internal/services/user.go
package services
import (
"context"
"time"
"github.com/google/uuid"
"github.com/yourname/myapp/internal/models"
"github.com/yourname/myapp/internal/repositories"
"github.com/yourname/myapp/pkg/errors"
)
// CreateUserInput represents input for creating a user
type CreateUserInput struct {
Email string `json:"email" binding:"required,email"`
Name string `json:"name" binding:"required,min=2,max=100"`
}
// UpdateUserInput represents input for updating a user
type UpdateUserInput struct {
Name string `json:"name" binding:"omitempty,min=2,max=100"`
}
// UserService defines the interface for user business logic
type UserService interface {
Create(ctx context.Context, input CreateUserInput) (*models.User, error)
GetByID(ctx context.Context, id string) (*models.User, error)
Update(ctx context.Context, id string, input UpdateUserInput) (*models.User, error)
Delete(ctx context.Context, id string) error
}
type userService struct {
repo repositories.UserRepository
}
// NewUserService creates a new UserService
func NewUserService(repo repositories.UserRepository) UserService {
return &userService{repo: repo}
}
func (s *userService) Create(ctx context.Context, input CreateUserInput) (*models.User, error) {
// Check if email already exists
existing, err := s.repo.FindByEmail(ctx, input.Email)
if err != nil {
return nil, errors.Wrap(err, 500, "failed to check email")
}
if existing != nil {
return nil, errors.ErrUserExists
}
user := &models.User{
ID: uuid.New().String(),
Email: input.Email,
Name: input.Name,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
saved, err := s.repo.Save(ctx, user)
if err != nil {
return nil, errors.Wrap(err, 500, "failed to save user")
}
return saved, nil
}
func (s *userService) GetByID(ctx context.Context, id string) (*models.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, errors.Wrap(err, 500, "failed to get user")
}
if user == nil {
return nil, errors.ErrUserNotFound
}
return user, nil
}
func (s *userService) Update(ctx context.Context, id string, input UpdateUserInput) (*models.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, errors.Wrap(err, 500, "failed to get user")
}
if user == nil {
return nil, errors.ErrUserNotFound
}
if input.Name != "" {
user.Name = input.Name
}
user.UpdatedAt = time.Now()
saved, err := s.repo.Save(ctx, user)
if err != nil {
return nil, errors.Wrap(err, 500, "failed to update user")
}
return saved, nil
}
func (s *userService) Delete(ctx context.Context, id string) error {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return errors.Wrap(err, 500, "failed to get user")
}
if user == nil {
return errors.ErrUserNotFound
}
if err := s.repo.Delete(ctx, id); err != nil {
return errors.Wrap(err, 500, "failed to delete user")
}
return nil
}
.PHONY: build run dev test lint clean tidy upgrade help
APP_NAME=myapp
BUILD_DIR=bin
# Build the application
build:
@echo "Building $(APP_NAME)..."
go build -o $(BUILD_DIR)/$(APP_NAME) ./cmd/$(APP_NAME)
# Run the application
run:
@echo "Running $(APP_NAME)..."
go run ./cmd/$(APP_NAME)
# Run in development mode with hot reload (requires air)
dev:
@echo "Running $(APP_NAME) in development mode..."
air
# Run tests
test:
@echo "Running tests..."
go test -v -race ./...
# Run tests with coverage
test-coverage:
@echo "Running tests with coverage..."
go test -v -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Run linter (requires golangci-lint)
lint:
@echo "Running linter..."
golangci-lint run
# Format code
fmt:
@echo "Formatting code..."
go fmt ./...
# Run go vet
vet:
@echo "Running go vet..."
go vet ./...
# Clean build artifacts
clean:
@echo "Cleaning..."
rm -rf $(BUILD_DIR)
rm -f coverage.out coverage.html
# Tidy dependencies
tidy:
@echo "Tidying dependencies..."
go mod tidy
# Upgrade all dependencies to latest
upgrade:
@echo "Upgrading dependencies..."
go get -u ./...
go mod tidy
# Download dependencies
download:
@echo "Downloading dependencies..."
go mod download
# Run all checks
check: fmt vet lint test
@echo "All checks passed!"
# Generate mocks (requires mockery)
mock:
@echo "Generating mocks..."
mockery --all
# Help
help:
@echo "Available commands:"
@echo " build - Build the application"
@echo " run - Run the application"
@echo " dev - Run with hot reload (requires air)"
@echo " test - Run tests"
@echo " test-coverage - Run tests with coverage"
@echo " lint - Run linter (requires golangci-lint)"
@echo " fmt - Format code"
@echo " vet - Run go vet"
@echo " clean - Clean build artifacts"
@echo " tidy - Tidy dependencies"
@echo " upgrade - Upgrade all dependencies"
@echo " download - Download dependencies"
@echo " check - Run all checks"
@echo " mock - Generate mocks"
// pkg/database/database.go
package database
import (
"fmt"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Config holds database configuration
type Config struct {
Driver string
Host string
Port int
Username string
Password string
Database string
SSLMode string
}
// Database wraps gorm.DB
type Database struct {
db *gorm.DB
}
// New creates a new database connection
func New(cfg Config) (*Database, error) {
var dialector gorm.Dialector
switch cfg.Driver {
case "postgres":
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Database, cfg.SSLMode,
)
dialector = postgres.Open(dsn)
case "sqlite":
dialector = sqlite.Open(cfg.Database)
default:
return nil, fmt.Errorf("unsupported database driver: %s", cfg.Driver)
}
db, err := gorm.Open(dialector, &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
return &Database{db: db}, nil
}
// DB returns the underlying gorm.DB
func (d *Database) DB() *gorm.DB {
return d.db
}
// Close closes the database connection
func (d *Database) Close() error {
sqlDB, err := d.db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
// AutoMigrate runs auto migration for given models
func (d *Database) AutoMigrate(models ...interface{}) error {
return d.db.AutoMigrate(models...)
}
// pkg/errors/errors.go
package errors
import "fmt"
// AppError represents an application error with code and message
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Cause error `json:"-"`
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Cause)
}
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Cause
}
// HTTPStatus returns the HTTP status code for this error
func (e *AppError) HTTPStatus() int {
switch {
case e.Code >= 500:
return 500
case e.Code >= 400:
return e.Code
default:
return 500
}
}
// New creates a new AppError
func New(code int, message string) *AppError {
return &AppError{
Code: code,
Message: message,
}
}
// Wrap wraps an existing error with additional context
func Wrap(err error, code int, message string) *AppError {
if err == nil {
return nil
}
return &AppError{
Code: code,
Message: message,
Cause: err,
}
}
// Wrapf wraps an error with formatted message
func Wrapf(err error, code int, format string, args ...interface{}) *AppError {
if err == nil {
return nil
}
return &AppError{
Code: code,
Message: fmt.Sprintf(format, args...),
Cause: err,
}
}
// Predefined errors
var (
ErrInternal = New(500, "internal server error")
ErrInvalidParams = New(400, "invalid parameters")
ErrNotFound = New(404, "resource not found")
ErrUnauthorized = New(401, "unauthorized")
ErrForbidden = New(403, "forbidden")
ErrConflict = New(409, "resource already exists")
)
// Specific errors
var (
ErrUserNotFound = New(404, "user not found")
ErrUserExists = New(409, "user already exists")
ErrInvalidToken = New(401, "invalid token")
)
// pkg/response/response.go
package response
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
apperrors "github.com/yourname/myapp/pkg/errors"
)
// Response represents a unified API response
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Success sends a success response
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: "success",
Data: data,
})
}
// Created sends a 201 created response
func Created(c *gin.Context, data interface{}) {
c.JSON(http.StatusCreated, Response{
Code: 0,
Message: "created",
Data: data,
})
}
// NoContent sends a 204 no content response
func NoContent(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// Error sends an error response
func Error(c *gin.Context, err error) {
var appErr *apperrors.AppError
if errors.As(err, &appErr) {
c.JSON(appErr.HTTPStatus(), Response{
Code: appErr.Code,
Message: appErr.Message,
})
return
}
// Unknown error
c.JSON(http.StatusInternalServerError, Response{
Code: 500,
Message: "internal server error",
})
}
// ErrorWithMessage sends an error response with custom message
func ErrorWithMessage(c *gin.Context, status int, code int, message string) {
c.JSON(status, Response{
Code: code,
Message: message,
})
}
// BadRequest sends a 400 bad request response
func BadRequest(c *gin.Context, message string) {
c.JSON(http.StatusBadRequest, Response{
Code: 400,
Message: message,
})
}
// Unauthorized sends a 401 unauthorized response
func Unauthorized(c *gin.Context, message string) {
c.JSON(http.StatusUnauthorized, Response{
Code: 401,
Message: message,
})
}
// NotFound sends a 404 not found response
func NotFound(c *gin.Context, message string) {
c.JSON(http.StatusNotFound, Response{
Code: 404,
Message: message,
})
}
// pkg/server/server.go
package server
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
// Server represents an HTTP server with graceful shutdown
type Server struct {
port int
readTimeout time.Duration
writeTimeout time.Duration
handler http.Handler
}
// Option is a functional option for Server
type Option func(*Server)
// WithPort sets the server port
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
// WithReadTimeout sets the read timeout
func WithReadTimeout(d time.Duration) Option {
return func(s *Server) {
s.readTimeout = d
}
}
// WithWriteTimeout sets the write timeout
func WithWriteTimeout(d time.Duration) Option {
return func(s *Server) {
s.writeTimeout = d
}
}
// New creates a new Server with options
func New(handler http.Handler, opts ...Option) *Server {
s := &Server{
port: 8080,
readTimeout: 30 * time.Second,
writeTimeout: 30 * time.Second,
handler: handler,
}
for _, opt := range opts {
opt(s)
}
return s
}
// Run starts the server with graceful shutdown
func (s *Server) Run() error {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
Handler: s.handler,
ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout,
}
// Channel for server errors
errChan := make(chan error, 1)
go func() {
slog.Info("server starting", "port", s.port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errChan <- err
}
}()
// Channel for OS signals
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Block until signal or error
select {
case err := <-errChan:
return fmt.Errorf("server error: %w", err)
case sig := <-quit:
slog.Info("shutdown signal received", "signal", sig)
}
// Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
return fmt.Errorf("server shutdown error: %w", err)
}
slog.Info("server stopped gracefully")
return nil
}