Go: Go REST API 实战
REST API 不是只有 CRUD——资源设计、状态码选择、错误格式统一、中间件链,每个细节都决定着 API 的生产级质量。
当你的 API 需要被前端和第三方同时调用时,统一的错误格式、合理的状态码、清晰的版本管理就不再是"加分项",而是"必须项"。
1. 你将学到
- RESTful 设计原则(资源/动词/状态码)
- Go 1.22 路由构建 REST API
- 请求参数验证
- 中间件链集成
- 统一错误响应格式
- API 版本管理策略
2. 一个前端合作者的真实故事
(1) 痛点:API 错误格式每接口不同,前端崩溃
Alice 的后端团队和前端团队合作一个电商项目:
"前端同事说'你们每个接口的错误格式都不一样。用户列表返回
{"error":"not found"},订单接口返回{"message":"Order not found","code":404},商品接口直接返回 500 页面。我每个接口要写不同的错误处理!'"
// 坏代码:错误格式不统一
// GET /users/1 → {"error":"not found"} ← 格式 A
// GET /orders/1 → {"message":"Order not found","code":404} ← 格式 B
// GET /products → <html>500 Internal Error</html> ← 格式 C
(2) Go 的解法:统一错误响应
// 统一错误格式
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
}
func (e *APIError) Error() string {
return e.Message
}
// 工厂函数
func NotFound(msg string) *APIError {
return &APIError{Code: 404, Message: "not_found", Detail: msg}
}
func BadRequest(msg string) *APIError {
return &APIError{Code: 400, Message: "bad_request", Detail: msg}
}
func InternalError(msg string) *APIError {
return &APIError{Code: 500, Message: "internal_error", Detail: msg}
}
(3) 收益:统一前后
| 维度 | 各自为政 | 统一格式 |
|---|---|---|
| 前端处理 | 每个接口写不同逻辑 | if (resp.error) handleError(resp) |
| 文档成本 | 每个接口单独文档 | 一句话描述格式 |
| SDK 生成 | 无法自动化 | OpenAPI 直接生成客户端 |
| 调试成本 | 每次看具体格式 | 统一的字段名 |
3. RESTful 设计原则
(1) 资源设计
// 好的 RESTful URL 设计:
// 资源(名词) + 动词(HTTP 方法)
// 单个资源
GET /users → 列表(collection)
POST /users → 创建
GET /users/{id} → 查看单个
PUT /users/{id} → 全量更新
PATCH /users/{id} → 部分更新
DELETE /users/{id} → 删除
// 子资源
GET /users/{id}/orders → 用户的订单列表
POST /users/{id}/orders → 为用户创建订单
GET /users/{id}/orders/{oid} → 用户的某个订单
// 动作(非 CRUD 用动词)
POST /users/{id}/activate → 激活用户
POST /orders/{id}/cancel → 取消订单
(2) 状态码选择
package main
import (
"encoding/json"
"fmt"
"net/http"
)
// 统一响应
type Response struct {
Data interface{} `json:"data,omitempty"`
Error *APIError `json:"error,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
}
type Meta struct {
Total int `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func main() {
fmt.Println("Response types: Response, APIError, Meta")
fmt.Println("Helper: writeJSON(w, status, data)")
}
| 状态码 | 方法 | 含义 |
|---|---|---|
| 200 OK | GET | 查询成功 |
| 201 Created | POST | 创建成功 |
| 204 No Content | DELETE | 删除成功 |
| 400 Bad Request | — | 请求参数错误 |
| 401 Unauthorized | — | 未认证 |
| 403 Forbidden | — | 无权限 |
| 404 Not Found | — | 资源不存在 |
| 409 Conflict | POST/PUT | 资源冲突(如重复创建) |
| 422 Unprocessable | POST/PUT | 请求体语义错误 |
| 429 Too Many | — | 频率限制 |
| 500 Internal | — | 服务端错误 |
4. REST API 实战
▶ 示例:用户 CRUD API
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"sync"
"time"
)
// ---------- Model ----------
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type UpdateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
// ---------- Store ----------
type UserStore struct {
mu sync.RWMutex
users map[int]User
nextID int
}
func NewUserStore() *UserStore {
return &UserStore{
users: make(map[int]User),
nextID: 1,
}
}
func (s *UserStore) List() []User {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]User, 0, len(s.users))
for _, u := range s.users {
result = append(result, u)
}
return result
}
func (s *UserStore) Create(req CreateUserRequest) User {
s.mu.Lock()
defer s.mu.Unlock()
u := User{
ID: s.nextID,
Name: req.Name,
Email: req.Email,
CreatedAt: time.Now(),
}
s.nextID++
s.users[u.ID] = u
return u
}
func (s *UserStore) Get(id int) (User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
return u, ok
}
func (s *UserStore) Update(id int, req UpdateUserRequest) (User, bool) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.users[id]
if !ok {
return User{}, false
}
u.Name = req.Name
u.Email = req.Email
s.users[id] = u
return u, true
}
func (s *UserStore) Delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.users[id]
if !ok {
return false
}
delete(s.users, id)
return true
}
// ---------- Handler ----------
type UserHandler struct {
store *UserStore
}
func NewUserHandler(store *UserStore) *UserHandler {
return &UserHandler{store: store}
}
func (h *UserHandler) Register(mux *http.ServeMux) {
mux.HandleFunc("GET /api/v1/users", h.ListUsers)
mux.HandleFunc("POST /api/v1/users", h.CreateUser)
mux.HandleFunc("GET /api/v1/users/{id}", h.GetUser)
mux.HandleFunc("PUT /api/v1/users/{id}", h.UpdateUser)
mux.HandleFunc("DELETE /api/v1/users/{id}", h.DeleteUser)
}
// 统一响应
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]interface{}{
"error": map[string]interface{}{
"code": status,
"message": message,
},
})
}
func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
users := h.store.List()
writeJSON(w, http.StatusOK, map[string]interface{}{
"data": users,
"meta": map[string]int{"total": len(users)},
})
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
// 参数验证
if req.Name == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
if req.Email == "" {
writeError(w, http.StatusBadRequest, "email is required")
return
}
user := h.store.Create(req)
writeJSON(w, http.StatusCreated, map[string]interface{}{
"data": user,
})
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid user ID")
return
}
user, ok := h.store.Get(id)
if !ok {
writeError(w, http.StatusNotFound, "user not found")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"data": user,
})
}
func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid user ID")
return
}
var req UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
user, ok := h.store.Update(id, req)
if !ok {
writeError(w, http.StatusNotFound, "user not found")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"data": user,
})
}
func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid user ID")
return
}
if !h.store.Delete(id) {
writeError(w, http.StatusNotFound, "user not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
func main() {
store := NewUserStore()
handler := NewUserHandler(store)
mux := http.NewServeMux()
handler.Register(mux)
log.Println("User API 启动于 :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
5. 参数验证
▶ 示例:结构化验证
package main
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
)
// 验证器
type Validator struct {
errors []string
}
func (v *Validator) Required(field, value string) {
if strings.TrimSpace(value) == "" {
v.errors = append(v.errors, fmt.Sprintf("%s is required", field))
}
}
func (v *Validator) MinLength(field, value string, min int) {
if len(value) < min {
v.errors = append(v.errors, fmt.Sprintf("%s must be at least %d characters", field, min))
}
}
func (v *Validator) MaxLength(field, value string, max int) {
if len(value) > max {
v.errors = append(v.errors, fmt.Sprintf("%s must be at most %d characters", field, max))
}
}
func (v *Validator) Email(field, value string) {
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(pattern, value)
if !matched {
v.errors = append(v.errors, fmt.Sprintf("%s is not a valid email", field))
}
}
func (v *Validator) Valid() bool {
return len(v.errors) == 0
}
func (v *Validator) Errors() []string {
return v.errors
}
// ---------- 使用 ----------
type SignupRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
Age int `json:"age"`
}
func validateSignup(req SignupRequest) *Validator {
v := &Validator{}
v.Required("name", req.Name)
v.MinLength("name", req.Name, 2)
v.MaxLength("name", req.Name, 50)
v.Required("email", req.Email)
v.Email("email", req.Email)
v.Required("password", req.Password)
v.MinLength("password", req.Password, 8)
return v
}
func signupHandler(w http.ResponseWriter, r *http.Request) {
var req SignupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
if v := validateSignup(req); !v.Valid() {
writeJSON(w, http.StatusUnprocessableEntity, map[string]interface{}{
"error": map[string]interface{}{
"code": 422,
"message": "validation failed",
"details": v.Errors(),
},
})
return
}
writeJSON(w, http.StatusCreated, map[string]string{"status": "ok"})
}
func main() {
v := validateSignup(SignupRequest{Name: "A", Email: "bad", Password: "123"})
if !v.Valid() {
fmt.Println("Validation errors:", v.Errors())
}
}
6. 中间件链集成
▶ 示例:API 中间件链
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"strings"
"time"
)
type contextKey string
const UserContextKey contextKey = "user"
// ---------- 中间件 ----------
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
// 请求日志
func RequestLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
})
}
// 认证(简单 Token)
func Auth(token string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
writeError(w, http.StatusUnauthorized, "missing or invalid token")
return
}
if auth[7:] != token {
writeError(w, http.StatusForbidden, "invalid token")
return
}
// 注入用户信息到 Context
ctx := context.WithValue(r.Context(), UserContextKey, "admin")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// 请求超时
func RequestTimeout(timeout time.Duration) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// CORS
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// 恢复
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
writeError(w, http.StatusInternalServerError, "internal server error")
}
}()
next.ServeHTTP(w, r)
})
}
// 工具函数
func writeError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": map[string]interface{}{
"code": status,
"message": msg,
},
})
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// ---------- 主程序 ----------
func helloHandler(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(UserContextKey)
writeJSON(w, http.StatusOK, map[string]interface{}{
"message": "Hello, " + user.(string),
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/hello", helloHandler)
// 中间件链
handler := Chain(mux,
Recovery,
RequestLogging,
CORS,
Auth("secret-token"),
RequestTimeout(5*time.Second),
)
log.Println("API 启动于 :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}
7. 完整示例:图书管理系统 API
// book_api.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
// ---------- Models ----------
type Book struct {
ID int `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
ISBN string `json:"isbn"`
Year int `json:"year"`
Available bool `json:"available"`
CreatedAt time.Time `json:"created_at"`
}
type CreateBookRequest struct {
Title string `json:"title"`
Author string `json:"author"`
ISBN string `json:"isbn"`
Year int `json:"year"`
}
type UpdateBookRequest struct {
Title string `json:"title"`
Author string `json:"author"`
Available *bool `json:"available"`
}
// ---------- Validator ----------
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
func validateCreateBook(req CreateBookRequest) []ValidationError {
var errs []ValidationError
if strings.TrimSpace(req.Title) == "" {
errs = append(errs, ValidationError{"title", "title is required"})
}
if strings.TrimSpace(req.Author) == "" {
errs = append(errs, ValidationError{"author", "author is required"})
}
if strings.TrimSpace(req.ISBN) == "" {
errs = append(errs, ValidationError{"isbn", "ISBN is required"})
}
if req.Year < 1000 || req.Year > 2100 {
errs = append(errs, ValidationError{"year", "year must be between 1000 and 2100"})
}
return errs
}
// ---------- Store ----------
type BookStore struct {
mu sync.RWMutex
books map[int]Book
nextID int
}
func NewBookStore() *BookStore {
return &BookStore{
books: make(map[int]Book),
nextID: 1,
}
}
func (s *BookStore) List() []Book {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Book, 0, len(s.books))
for _, b := range s.books {
result = append(result, b)
}
return result
}
func (s *BookStore) Create(req CreateBookRequest) (Book, error) {
s.mu.Lock()
defer s.mu.Unlock()
// 检查 ISBN 唯一
for _, b := range s.books {
if b.ISBN == req.ISBN {
return Book{}, fmt.Errorf("ISBN already exists: %s", req.ISBN)
}
}
book := Book{
ID: s.nextID,
Title: req.Title,
Author: req.Author,
ISBN: req.ISBN,
Year: req.Year,
Available: true,
CreatedAt: time.Now(),
}
s.nextID++
s.books[book.ID] = book
return book, nil
}
func (s *BookStore) Get(id int) (Book, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
b, ok := s.books[id]
return b, ok
}
func (s *BookStore) Update(id int, req UpdateBookRequest) (Book, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
b, ok := s.books[id]
if !ok {
return Book{}, false, nil
}
if req.Title != "" {
b.Title = req.Title
}
if req.Author != "" {
b.Author = req.Author
}
if req.Available != nil {
b.Available = *req.Available
}
s.books[id] = b
return b, true, nil
}
func (s *BookStore) Delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.books[id]
if !ok {
return false
}
delete(s.books, id)
return true
}
// ---------- API Response ----------
type APIResponse struct {
Data interface{} `json:"data,omitempty"`
Error *APIError `json:"error,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
Details []ValidationError `json:"details,omitempty"`
}
type Meta struct {
Total int `json:"total"`
}
func respond(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(APIResponse{Data: data})
}
func respondError(w http.ResponseWriter, status int, msg string, details ...[]ValidationError) {
err := APIError{Code: status, Message: msg}
if len(details) > 0 {
err.Details = details[0]
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(APIResponse{Error: &err})
}
// ---------- Handlers ----------
type BookHandler struct {
store *BookStore
}
func NewBookHandler(store *BookStore) *BookHandler {
return &BookHandler{store: store}
}
func (h *BookHandler) Register(mux *http.ServeMux, basePath string) {
mux.HandleFunc("GET "+basePath, h.ListBooks)
mux.HandleFunc("POST "+basePath, h.CreateBook)
mux.HandleFunc("GET "+basePath+"/{id}", h.GetBook)
mux.HandleFunc("PUT "+basePath+"/{id}", h.UpdateBook)
mux.HandleFunc("DELETE "+basePath+"/{id}", h.DeleteBook)
}
func (h *BookHandler) ListBooks(w http.ResponseWriter, r *http.Request) {
books := h.store.List()
respond(w, http.StatusOK, map[string]interface{}{
"items": books,
"meta": Meta{Total: len(books)},
})
}
func (h *BookHandler) CreateBook(w http.ResponseWriter, r *http.Request) {
var req CreateBookRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if errs := validateCreateBook(req); len(errs) > 0 {
respondError(w, http.StatusUnprocessableEntity, "validation failed", errs)
return
}
book, err := h.store.Create(req)
if err != nil {
respondError(w, http.StatusConflict, err.Error())
return
}
respond(w, http.StatusCreated, book)
}
func (h *BookHandler) GetBook(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
respondError(w, http.StatusBadRequest, "invalid book ID")
return
}
book, ok := h.store.Get(id)
if !ok {
respondError(w, http.StatusNotFound, "book not found")
return
}
respond(w, http.StatusOK, book)
}
func (h *BookHandler) UpdateBook(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
respondError(w, http.StatusBadRequest, "invalid book ID")
return
}
var req UpdateBookRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid JSON body")
return
}
book, ok, err := h.store.Update(id, req)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
if !ok {
respondError(w, http.StatusNotFound, "book not found")
return
}
respond(w, http.StatusOK, book)
}
func (h *BookHandler) DeleteBook(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
respondError(w, http.StatusBadRequest, "invalid book ID")
return
}
if !h.store.Delete(id) {
respondError(w, http.StatusNotFound, "book not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
// ---------- Middleware ----------
func RequestLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
})
}
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
respondError(w, http.StatusInternalServerError, "internal server error")
}
}()
next.ServeHTTP(w, r)
})
}
// ---------- Main ----------
func main() {
store := NewBookStore()
handler := NewBookHandler(store)
mux := http.NewServeMux()
handler.Register(mux, "/api/v1/books")
// 健康检查
mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
respond(w, http.StatusOK, map[string]string{"status": "ok"})
})
// 中间件链
app := Recovery(RequestLogging(mux))
log.Println("Book API 启动于 :8080")
log.Println("端点:")
log.Println(" GET /api/v1/books — 图书列表")
log.Println(" POST /api/v1/books — 创建图书")
log.Println(" GET /api/v1/books/{id} — 查看图书")
log.Println(" PUT /api/v1/books/{id} — 更新图书")
log.Println(" DELETE /api/v1/books/{id} — 删除图书")
log.Fatal(http.ListenAndServe(":8080", app))
}
graph LR
subgraph API [REST API Layer]
H[Handler]
M[Middleware Chain]
V[Validator]
end
subgraph Store [Store Layer]
R[Repository<br/>map + RWMutex]
end
subgraph Model [Model Layer]
Book
User
Order
end
Request --> M
M --> H
H --> V
V --> R
R --> Book
interface{} 作为响应类型时要小心——map 零值会被 JSON 序列化为 null 而不是 {}。好的实践是定义一个明确的响应结构体(如 APIResponse{Data, Error, Meta}),确保字段在有值时出现,空值时忽略(omitempty)。
❓ 常见问题
/api/v1/(最常用);(2) 请求头 Accept: application/vnd.api+json; version=1;(3) 查询参数 ?v=1;(4) 子域名 v1.api.example.com。推荐 URL 路径——最直观,开发和调试成本最低。APIError 结构体,包含 code/message/details field。所有 handler 使用统一的 respondError(w, status, msg) function。前端只需 if resp.error 就显示错误,不关心具体接口。mux.HandleFunc("/", func(w, r) { writeError(w, 404, "not found") })。注意这个 handler 应该注册在最后,因为 ServeMux 按最长匹配。?page=1&per_page=20。Handler 解析参数,Store 层实现带 LIMIT/OFFSET 的查询。响应中返回 meta: {total, page, per_page} 供前端计算分页组件。Go 1.22 路由中的 {path...} 不适合分页参数——参数应该在 query string 中。📖 小节
- REST 设计:资源(名词)+ method(动词)+ 状态码
- URL 版本管理:
/api/v1/方式最推荐 - 统一错误格式:
{error: {code, message, details}} - 参数验证:基础验证(Handler)+ 业务验证(Service)
- 中间件链:Recovery → log → CORS → Auth → Timeout → Handler
- 统一响应函数:
respond()/respondError() - 分页:
?page=N&per_page=N+ meta 信息
📝 作业
-
基础题(难度⭐):搭建一个作者(Author)REST API。要求:(1) CRUD 完整操作;(2) 使用
/api/v1/authors路径;(3) 统一错误格式;(4) 基础参数验证(name 必填)。用 curl 测试所有端点。 -
进阶题(难度⭐⭐):实现一个文章(Article)API,关联作者。要求:(1)
POST /articles创建文章(关联到已有作者);(2)GET /articles?author_id=X按作者筛选;(3) 分页支持(page/per_page parameter);(4) 统一响应格式{data, meta};(5) 验证 title 和 content 不为空。 -
挑战题(难度⭐⭐⭐):实现一个带完整中间件链的用户管理系统。要求:(1) Routes: users(CRUD)+ auth(登录/注册);(2) middleware:Recovery → Logging → CORS → RateLimit (令牌桶) → Auth (Bearer Token) → Timeout;(3) 注册时密码 bcrypt 加密,登录返回 JWT;(4) 认证中间件从 JWT 解析 userID 注入 Context;(5) 用
-race验证并发安全。