go-api-design
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo API Design
Go API设计
APIs are contracts. Once published, they're promises. Design them as if
you'll maintain them for a decade — because you probably will.
API是契约,一经发布便成承诺。设计时要做好维护十年的准备——因为你很可能真的需要维护这么久。
1. HTTP Handler Structure
1. HTTP处理器结构
Use the standard http.Handler
interface:
http.Handler使用标准的http.Handler
接口:
http.Handlergo
// ✅ Good — method on a struct with dependencies
type UserHandler struct {
store UserStore
logger *zap.Logger
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.handleGet(w, r)
case http.MethodPost:
h.handleCreate(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}go
// ✅ Good — method on a struct with dependencies
type UserHandler struct {
store UserStore
logger *zap.Logger
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.handleGet(w, r)
case http.MethodPost:
h.handleCreate(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}Handler function signature pattern:
处理器函数签名模式:
go
// Handler methods return nothing — they write directly to ResponseWriter.
// Errors are handled inside the handler, not returned.
func (h *UserHandler) handleGet(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := chi.URLParam(r, "id") // or mux.Vars(r)["id"]
if id == "" {
h.respondError(w, http.StatusBadRequest, "missing user id")
return
}
user, err := h.store.GetByID(ctx, id)
if err != nil {
if errors.Is(err, ErrNotFound) {
h.respondError(w, http.StatusNotFound, "user not found")
return
}
h.logger.Error("get user", zap.Error(err))
h.respondError(w, http.StatusInternalServerError, "internal error")
return
}
h.respondJSON(w, http.StatusOK, user)
}go
// Handler methods return nothing — they write directly to ResponseWriter.
// Errors are handled inside the handler, not returned.
func (h *UserHandler) handleGet(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := chi.URLParam(r, "id") // or mux.Vars(r)["id"]
if id == "" {
h.respondError(w, http.StatusBadRequest, "missing user id")
return
}
user, err := h.store.GetByID(ctx, id)
if err != nil {
if errors.Is(err, ErrNotFound) {
h.respondError(w, http.StatusNotFound, "user not found")
return
}
h.logger.Error("get user", zap.Error(err))
h.respondError(w, http.StatusInternalServerError, "internal error")
return
}
h.respondJSON(w, http.StatusOK, user)
}JSON response helpers:
JSON响应辅助函数:
go
func (h *UserHandler) respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
h.logger.Error("encode response", zap.Error(err))
}
}
func (h *UserHandler) respondError(w http.ResponseWriter, status int, msg string) {
h.respondJSON(w, status, map[string]string{"error": msg})
}go
func (h *UserHandler) respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
h.logger.Error("encode response", zap.Error(err))
}
}
func (h *UserHandler) respondError(w http.ResponseWriter, status int, msg string) {
h.respondJSON(w, status, map[string]string{"error": msg})
}2. Middleware Pattern
2. 中间件模式
Middleware wraps handlers. Use the standard signature:
func(http.Handler) http.Handlergo
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.New().String()
}
ctx := context.WithValue(r.Context(), requestIDKey, id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func Recoverer(logger *zap.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
logger.Error("panic recovered",
zap.Any("panic", rec),
zap.String("stack", string(debug.Stack())),
)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
}中间件用于包装处理器。使用标准的签名:
func(http.Handler) http.Handlergo
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.New().String()
}
ctx := context.WithValue(r.Context(), requestIDKey, id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func Recoverer(logger *zap.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
logger.Error("panic recovered",
zap.Any("panic", rec),
zap.String("stack", string(debug.Stack())),
)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
}Middleware ordering (outside → inside):
中间件执行顺序(从外到内):
Recoverer → RequestID → Logger → Auth → RateLimit → HandlerRecover MUST be outermost. Auth before business logic. Logger captures timing.
Recoverer → RequestID → Logger → Auth → RateLimit → HandlerRecover必须是最外层。认证要在业务逻辑之前。日志中间件用于捕获请求耗时。
3. Request Validation
3. 请求验证
Decode and validate in one step:
一步完成解码与验证:
go
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
}
func decodeAndValidate[T any](r *http.Request) (T, error) {
var req T
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return req, fmt.Errorf("decode: %w", err)
}
if err := validate.Struct(req); err != nil {
return req, fmt.Errorf("validate: %w", err)
}
return req, nil
}go
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
}
func decodeAndValidate[T any](r *http.Request) (T, error) {
var req T
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return req, fmt.Errorf("decode: %w", err)
}
if err := validate.Struct(req); err != nil {
return req, fmt.Errorf("validate: %w", err)
}
return req, nil
}Limit request body size:
限制请求体大小:
go
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MBgo
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB4. URL and Naming Conventions
4. URL与命名规范
GET /api/v1/users → list users
POST /api/v1/users → create user
GET /api/v1/users/{id} → get user
PUT /api/v1/users/{id} → replace user
PATCH /api/v1/users/{id} → partial update
DELETE /api/v1/users/{id} → delete user
GET /api/v1/users/{id}/orders → list user orders (nested resource)Rules:
- Plural nouns for resources: , not
/users/user - Kebab-case for multi-word paths:
/order-items - camelCase for JSON fields: ,
"createdAt""firstName" - Version in URL path:
/api/v1/... - No verbs in URLs: , NOT
/users/search?q=alice/searchUsers
GET /api/v1/users → 列出用户
POST /api/v1/users → 创建用户
GET /api/v1/users/{id} → 获取单个用户
PUT /api/v1/users/{id} → 替换用户资源
PATCH /api/v1/users/{id} → 部分更新用户
DELETE /api/v1/users/{id} → 删除用户
GET /api/v1/users/{id}/orders → 列出用户的订单(嵌套资源)规则:
- 资源使用复数名词:,而非
/users/user - 多词路径使用短横线分隔(Kebab-case):
/order-items - JSON字段使用小驼峰(camelCase):、
"createdAt""firstName" - 在URL路径中加入版本号:
/api/v1/... - URL中不使用动词:使用,而非
/users/search?q=alice/searchUsers
5. Pagination
5. 分页
go
type PageRequest struct {
Cursor string `json:"cursor"`
Limit int `json:"limit"`
}
type PageResponse[T any] struct {
Items []T `json:"items"`
NextCursor string `json:"next_cursor,omitempty"`
HasMore bool `json:"has_more"`
}Prefer cursor-based pagination over offset/limit for large datasets.
Offset pagination breaks under concurrent writes.
go
type PageRequest struct {
Cursor string `json:"cursor"`
Limit int `json:"limit"`
}
type PageResponse[T any] struct {
Items []T `json:"items"`
NextCursor string `json:"next_cursor,omitempty"`
HasMore bool `json:"has_more"`
}对于大型数据集,优先使用基于游标(Cursor-based)的分页,而非偏移量/限制(Offset/limit)分页。偏移量分页在并发写入场景下会出现数据不一致问题。
6. Graceful Shutdown
6. 优雅停机
go
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Start server
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Wait for interrupt
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("shutdown error: %v", err)
}
log.Println("server stopped gracefully")
}Programs should exit only in , preferably at most once.
main()go
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Start server
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Wait for interrupt
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("shutdown error: %v", err)
}
log.Println("server stopped gracefully")
}程序应仅在函数中退出,且最好只退出一次。
main()7. Health Check Endpoints
7. 健康检查端点
go
// Liveness: is the process alive?
// GET /healthz → 200 OK
// Readiness: can the process serve traffic?
// GET /readyz → 200 OK or 503 Service Unavailable
func (h *HealthHandler) handleReady(w http.ResponseWriter, r *http.Request) {
if err := h.db.PingContext(r.Context()); err != nil {
h.respondError(w, http.StatusServiceUnavailable, "database unavailable")
return
}
h.respondJSON(w, http.StatusOK, map[string]string{"status": "ready"})
}go
// Liveness: is the process alive?
// GET /healthz → 200 OK
// Readiness: can the process serve traffic?
// GET /readyz → 200 OK or 503 Service Unavailable
func (h *HealthHandler) handleReady(w http.ResponseWriter, r *http.Request) {
if err := h.db.PingContext(r.Context()); err != nil {
h.respondError(w, http.StatusServiceUnavailable, "database unavailable")
return
}
h.respondJSON(w, http.StatusOK, map[string]string{"status": "ready"})
}8. Error Response Format
8. 错误响应格式
Consistent error responses across the entire API:
json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "invalid request parameters",
"details": [
{"field": "email", "message": "must be a valid email"}
]
}
}Map internal errors to HTTP status codes at the handler boundary.
Internal errors should NEVER leak to clients.
整个API使用统一的错误响应格式:
json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "invalid request parameters",
"details": [
{"field": "email", "message": "must be a valid email"}
]
}
}在处理器边界将内部错误映射为HTTP状态码。内部错误绝对不能泄露给客户端。