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/gorm
2. 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 builds
3. 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