Appearance
RESTful API 与 Gin 集成
本篇是 Go 微服务系列的第四篇。上一篇我们学习了 gRPC——它是内部服务间通信的最佳选择。但对外(前端、移动端、第三方)暴露的接口通常还是 RESTful API,因为浏览器和移动端对 HTTP/JSON 支持最好。本篇将系统讲解 RESTful API 设计规范、Gin 框架的微服务集成、统一响应格式、API 版本管理、Swagger 文档,以及如何在一个进程中同时提供 gRPC 和 RESTful 双端口服务。
一、RESTful API 设计规范
1. REST 的核心思想
REST(Representational State Transfer)由 Roy Fielding 在 2000 年的博士论文中提出。核心思想:
- 资源(Resource):所有事物都抽象为资源,每个资源有唯一 URI。
- 统一接口:使用标准 HTTP 方法表达对资源的操作。
- 无状态:每个请求包含全部所需信息,服务端不保存会话状态。
- 分层架构:客户端不感知中间是否存在代理、网关。
2. HTTP 方法语义
| 方法 | 含义 | 安全 | 幂等 | 示例 |
|---|---|---|---|---|
| GET | 获取资源 | 是 | 是 | GET /users/1 |
| POST | 创建资源 | 否 | 否 | POST /users |
| PUT | 全量更新资源 | 否 | 是 | PUT /users/1 |
| PATCH | 部分更新资源 | 否 | 是 | PATCH /users/1 |
| DELETE | 删除资源 | 否 | 是 | DELETE /users/1 |
- 安全:不修改服务端状态。
- 幂等:多次执行结果相同(重试安全)。
3. URI 设计原则
- 用名词复数表示资源集合:
/users、/orders。 - 用 ID 表示具体资源:
/users/123。 - 用路径表达从属关系:
/users/123/orders。 - 用查询参数表达过滤、排序、分页:
/users?role=admin&page=2&page_size=20。 - 不在 URI 中放动词:
/getUser?id=1❌ →GET /users/1✅。 - 使用 kebab-case:
/order-items而不是/order_items或/orderItems。
4. HTTP 状态码
| 分类 | 含义 | 常见码 |
|---|---|---|
| 2xx | 成功 | 200 OK / 201 Created / 204 No Content |
| 3xx | 重定向 | 301 / 302 / 304 Not Modified |
| 4xx | 客户端错误 | 400 / 401 / 403 / 404 / 409 / 422 / 429 |
| 5xx | 服务端错误 | 500 / 502 / 503 / 504 |
RESTful 实践中,应当用 HTTP 状态码表达结果,而不是统一返回 200 然后在 body 里写 code: 500。但国内不少团队采用「统一 200 + 业务码」的方案,后面我们会讲如何取舍。
5. RESTful 与 gRPC 的选择
| 场景 | 推荐 | 理由 |
|---|---|---|
| 浏览器 / 移动端调用 | REST | 原生支持 |
| 第三方开放 API | REST | 通用、易调试 |
| 内部服务间同步调用 | gRPC | 高性能、强类型 |
| 大数据量、低延迟 | gRPC | Protobuf 二进制 + HTTP/2 多路复用 |
| 流式传输 | gRPC | 原生支持双向流 |
| 简单 CRUD、对外服务 | REST | 工具链丰富、可读性好 |
实际项目:对外 REST + 内部 gRPC,由 API 网关做协议转换。
二、Gin 构建 RESTful 微服务
1. 最简 RESTful 服务
go
package main
import (
"log"
"net/http"
"strconv"
"sync"
"sync/atomic"
"github.com/gin-gonic/gin"
)
type Product struct {
ID int64 `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
}
type Store struct {
mu sync.RWMutex
products map[int64]*Product
nextID int64
}
func NewStore() *Store {
return &Store{
products: make(map[int64]*Product),
nextID: 1,
}
}
func (s *Store) Create(p *Product) *Product {
s.mu.Lock()
defer s.mu.Unlock()
p.ID = atomic.AddInt64(&s.nextID, 1) - 1
s.products[p.ID] = p
return p
}
func (s *Store) Get(id int64) (*Product, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.products[id]
return p, ok
}
func (s *Store) List() []*Product {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]*Product, 0, len(s.products))
for _, p := range s.products {
out = append(out, p)
}
return out
}
func (s *Store) Delete(id int64) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.products[id]; !ok {
return false
}
delete(s.products, id)
return true
}
func main() {
store := NewStore()
r := gin.Default()
// RESTful 路由
r.GET("/products", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": store.List()})
})
r.GET("/products/:id", func(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
p, ok := store.Get(id)
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": p})
})
r.POST("/products", func(c *gin.Context) {
var p Product
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
created := store.Create(&p)
c.JSON(http.StatusCreated, gin.H{"data": created})
})
r.DELETE("/products/:id", func(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if !store.Delete(id) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.Status(http.StatusNoContent)
})
log.Println("rest server on :8080")
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}2. 参数绑定与校验
Gin 内置了 validator 库,可以通过结构体 tag 进行参数校验:
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=32"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Age int `json:"age" binding:"gte=0,lte=150"`
}
func main() {
r := gin.Default()
r.POST("/users", func(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid request",
"detail": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{"user": req.Username})
})
_ = r.Run(":8080")
}常用校验 tag:
required:必填min/max:长度或数值范围email:邮箱格式oneof=a b c:枚举url:URL 格式datetime=2006-01-02:日期格式dive:对切片/Map元素递归校验
三、统一响应格式设计
1. 为什么要统一
前后端协作最大的痛点之一就是响应格式不统一。如果 /users 返回 {...},/orders 返回 [...],错误返回 {error: "..."},前端就要为每个接口写适配代码。统一格式让前端封装统一的请求库成为可能。
2. 推荐格式
json
{
"code": 0,
"message": "ok",
"data": {...},
"request_id": "abc-123"
}code:0 表示成功,非 0 表示业务错误码。message:人类可读的提示信息。data:业务数据,失败时为null。request_id:请求 ID,便于排查问题。
3. 实现
go
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
RequestID string `json:"request_id"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: "ok",
Data: data,
RequestID: c.GetString("request_id"),
})
}
func Fail(c *gin.Context, httpStatus, code int, msg string) {
c.JSON(httpStatus, Response{
Code: code,
Message: msg,
RequestID: c.GetString("request_id"),
})
}
// RequestIDMiddleware 注入 request id
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
rid := c.GetHeader("X-Request-ID")
if rid == "" {
rid = uuid.NewString()
}
c.Set("request_id", rid)
c.Header("X-Request-ID", rid)
c.Next()
}
}
// LoggingMiddleware 简单访问日志
func LoggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
c.Logger().Printf("%s %s %d %v rid=%s",
c.Request.Method, c.Request.URL.Path,
c.Writer.Status(), time.Since(start), c.GetString("request_id"))
}
}
func main() {
r := gin.New()
r.Use(RequestIDMiddleware(), LoggingMiddleware(), gin.Recovery())
r.GET("/ping", func(c *gin.Context) {
Success(c, gin.H{"message": "pong"})
})
r.GET("/error", func(c *gin.Context) {
Fail(c, http.StatusBadRequest, 10001, "invalid parameter")
})
_ = r.Run(":8080")
}4. 业务错误码体系
设计一套清晰的业务错误码体系很重要,建议分段:
0:成功1xxxx:通用错误(参数、认证、限流)2xxxx:用户模块3xxxx:订单模块4xxxx:支付模块5xxxx:商品模块
go
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type BizError struct {
HTTPStatus int
Code int
Message string
}
func (e *BizError) Error() string {
return fmt.Sprintf("biz error %d: %s", e.Code, e.Message)
}
var (
ErrInvalidParam = &BizError{http.StatusBadRequest, 10001, "参数错误"}
ErrUnauthorized = &BizError{http.StatusUnauthorized, 10002, "未认证"}
ErrRateLimited = &BizError{http.StatusTooManyRequests, 10003, "请求过于频繁"}
ErrUserNotFound = &BizError{http.StatusNotFound, 20001, "用户不存在"}
ErrUserExists = &BizError{http.StatusConflict, 20002, "用户已存在"}
)
// BizErrorHandler 统一业务错误处理中间件
func BizErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) == 0 {
return
}
err := c.Errors.Last().Err
if bizErr, ok := err.(*BizError); ok {
c.JSON(bizErr.HTTPStatus, Response{
Code: bizErr.Code,
Message: bizErr.Message,
RequestID: c.GetString("request_id"),
})
return
}
c.JSON(http.StatusInternalServerError, Response{
Code: 50000,
Message: "internal error",
RequestID: c.GetString("request_id"),
})
}
}
func main() {
r := gin.New()
r.Use(RequestIDMiddleware(), BizErrorHandler(), gin.Recovery())
r.GET("/users/:id", func(c *gin.Context) {
// 模拟查不到用户
_ = c.Error(ErrUserNotFound)
})
_ = r.Run(":8080")
}四、API 版本管理策略
API 一旦上线就会有多版本并存的需求。常见策略:
1. URI 版本(最常用)
GET /v1/users
GET /v2/users优点:清晰、易于调试、CDN 友好。缺点:URI 不再纯粹表达资源。
2. Header 版本
GET /users
API-Version: 2优点:URI 干净。缺点:不直观、难调试。
3. Accept Header 版本
GET /users
Accept: application/vnd.example.v2+json优点:符合 REST 规范。缺点:复杂、不易调试。
4. Gin 实现版本分组
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func v1Routes(r *gin.RouterGroup) {
r.GET("/users", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"version": "v1", "users": []string{"alice"}})
})
}
func v2Routes(r *gin.RouterGroup) {
r.GET("/users", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"version": "v2", "users": []string{"alice", "bob"}})
})
}
func main() {
r := gin.Default()
v1 := r.Group("/v1")
v1Routes(v1)
v2 := r.Group("/v2")
v2Routes(v2)
_ = r.Run(":8080")
}版本演进建议:v2 必须兼容 v1 至少一个发布周期,给客户端充足时间迁移;废弃 v1 时通过响应头 Sunset 和 Deprecation 提示。
五、Swagger / OpenAPI 集成
1. OpenAPI 简介
OpenAPI(前身 Swagger)是描述 RESTful API 的标准规范,一个 YAML/JSON 文件描述了 API 的所有路径、参数、响应。围绕它有完整的工具链:
- Swagger UI:可视化 API 文档
- 代码生成:从 OpenAPI 生成多语言客户端
- Mock 服务:基于规范生成 mock 数据
- API 网关:基于规范做校验和路由
2. swaggo/swag
swaggo/swag 通过解析 Go 代码注释生成 OpenAPI 文档,并集成 Swagger UI。安装:
bash
go install github.com/swaggo/swag/cmd/swag@latest3. 注释规范
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// @Summary 创建用户
// @Description 创建一个新用户
// @Tags 用户
// @Accept json
// @Produce json
// @Param body body CreateUserRequest true "用户信息"
// @Success 201 {object} User
// @Failure 400 {object} Response
// @Router /v1/users [post]
func createUser(c *gin.Context) {}
// @Summary 获取用户
// @Description 根据 ID 获取用户详情
// @Tags 用户
// @Produce json
// @Param id path int true "用户 ID"
// @Success 200 {object} User
// @Failure 404 {object} Response
// @Router /v1/users/{id} [get]
func getUser(c *gin.Context) {}
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type CreateUserRequest struct {
Name string `json:"name"`
}
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
// @title 用户服务 API
// @version 1.0
// @description 示例用户微服务
// @host localhost:8080
// @BasePath /v1
func main() {
r := gin.Default()
v1 := r.Group("/v1")
v1.POST("/users", createUser)
v1.GET("/users/:id", getUser)
_ = r.Run(":8080")
}4. 生成并集成文档
执行:
bash
swag init -g cmd/server/main.go -o api/docs然后在代码里集成 Swagger UI:
go
package main
import (
_ "github.com/example/user-service/api/docs" // 引入生成的 docs
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
_ = r.Run(":8080")
}访问 http://localhost:8080/swagger/index.html 即可看到可视化文档。
六、Gin + gRPC 双端口服务
实际项目最常见的架构:一个进程同时提供 gRPC(内部调用)和 RESTful(对外)两种接口,业务逻辑共享。
go
package main
import (
"context"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
// 业务逻辑(共享给 gRPC 和 REST)
type UserService struct {
users map[int64]*User
}
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
func (s *UserService) Create(name string) *User {
id := int64(len(s.users) + 1)
u := &User{ID: id, Name: name}
s.users[id] = u
return u
}
func (s *UserService) Get(id int64) (*User, bool) {
u, ok := s.users[id]
return u, ok
}
// startREST 启动 REST 端口
func startREST(svc *UserService) *http.Server {
r := gin.Default()
r.POST("/users", func(c *gin.Context) {
var req struct {
Name string `json:"name" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u := svc.Create(req.Name)
c.JSON(http.StatusCreated, u)
})
r.GET("/users/:id", func(c *gin.Context) {
// 简化处理
c.JSON(http.StatusOK, gin.H{"id": 1, "name": "alice"})
})
srv := &http.Server{Addr: ":8080", Handler: r}
go func() {
log.Printf("rest server on :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("rest: %v", err)
}
}()
return srv
}
// startGRPC 启动 gRPC 端口(这里用 net.Listen 模拟)
func startGRPC(svc *UserService) net.Listener {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}
go func() {
log.Printf("grpc server on :50051")
// 真实场景:grpc.NewServer() + Register + Serve(lis)
for {
conn, err := lis.Accept()
if err != nil {
return
}
go func(c net.Conn) {
defer c.Close()
_ = c
}(conn)
}
}()
return lis
}
func main() {
svc := &UserService{users: map[int64]*User{}}
svc.Create("alice")
svc.Create("bob")
restSrv := startREST(svc)
grpcLis := startGRPC(svc)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = restSrv.Shutdown(ctx)
_ = grpcLis.Close()
log.Println("server exited")
}这种模式的好处:内部调用走 gRPC 享受高性能和强类型,对外走 REST 兼容前端和第三方。
七、完整示例:电商商品服务
下面把本篇内容整合为一个更完整的电商商品服务示例:
go
package main
import (
"log"
"net/http"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// === 数据模型 ===
type Product struct {
ID int64 `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
Category string `json:"category"`
CreatedAt time.Time `json:"created_at"`
}
// === 存储层 ===
type ProductRepo struct {
mu sync.RWMutex
products map[int64]*Product
nextID int64
}
func NewProductRepo() *ProductRepo {
return &ProductRepo{products: make(map[int64]*Product)}
}
func (r *ProductRepo) Create(p *Product) *Product {
p.ID = atomic.AddInt64(&r.nextID, 1)
p.CreatedAt = time.Now()
r.mu.Lock()
r.products[p.ID] = p
r.mu.Unlock()
return p
}
func (r *ProductRepo) Get(id int64) (*Product, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
p, ok := r.products[id]
return p, ok
}
func (r *ProductRepo) List(page, pageSize int) ([]*Product, int) {
r.mu.RLock()
defer r.mu.RUnlock()
total := len(r.products)
out := make([]*Product, 0, pageSize)
for _, p := range r.products {
out = append(out, p)
}
start := (page - 1) * pageSize
if start >= len(out) {
return nil, total
}
end := start + pageSize
if end > len(out) {
end = len(out)
}
return out[start:end], total
}
func (r *ProductRepo) Update(id int64, patch map[string]interface{}) (*Product, bool) {
r.mu.Lock()
defer r.mu.Unlock()
p, ok := r.products[id]
if !ok {
return nil, false
}
if name, ok := patch["name"].(string); ok {
p.Name = name
}
if price, ok := patch["price"].(float64); ok {
p.Price = price
}
if stock, ok := patch["stock"].(float64); ok {
p.Stock = int(stock)
}
if cat, ok := patch["category"].(string); ok {
p.Category = cat
}
return p, true
}
func (r *ProductRepo) Delete(id int64) bool {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.products[id]; !ok {
return false
}
delete(r.products, id)
return true
}
// === 统一响应 ===
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
RequestID string `json:"request_id"`
}
func main() {
repo := NewProductRepo()
// 预置数据
repo.Create(&Product{Name: "iPhone 16", Price: 7999, Stock: 100, Category: "phone"})
repo.Create(&Product{Name: "MacBook Pro", Price: 19999, Stock: 50, Category: "laptop"})
r := gin.New()
r.Use(func() gin.HandlerFunc {
return func(c *gin.Context) {
rid := c.GetHeader("X-Request-ID")
if rid == "" {
rid = uuid.NewString()
}
c.Set("request_id", rid)
c.Header("X-Request-ID", rid)
c.Next()
}
}(), gin.Recovery(), gin.Logger())
v1 := r.Group("/v1")
// 创建商品
v1.POST("/products", func(c *gin.Context) {
var p Product
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(http.StatusBadRequest, Response{Code: 10001, Message: err.Error(), RequestID: c.GetString("request_id")})
return
}
if p.Name == "" || p.Price <= 0 {
c.JSON(http.StatusBadRequest, Response{Code: 10002, Message: "name and price required", RequestID: c.GetString("request_id")})
return
}
created := repo.Create(&p)
c.JSON(http.StatusCreated, Response{Code: 0, Message: "ok", Data: created, RequestID: c.GetString("request_id")})
})
// 获取单个商品
v1.GET("/products/:id", func(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, Response{Code: 10001, Message: "invalid id", RequestID: c.GetString("request_id")})
return
}
p, ok := repo.Get(id)
if !ok {
c.JSON(http.StatusNotFound, Response{Code: 30001, Message: "not found", RequestID: c.GetString("request_id")})
return
}
c.JSON(http.StatusOK, Response{Code: 0, Message: "ok", Data: p, RequestID: c.GetString("request_id")})
})
// 列表(分页)
v1.GET("/products", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
list, total := repo.List(page, pageSize)
c.JSON(http.StatusOK, Response{
Code: 0, Message: "ok", RequestID: c.GetString("request_id"),
Data: gin.H{"items": list, "total": total, "page": page, "page_size": pageSize},
})
})
// 部分更新
v1.PATCH("/products/:id", func(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
var patch map[string]interface{}
if err := c.ShouldBindJSON(&patch); err != nil {
c.JSON(http.StatusBadRequest, Response{Code: 10001, Message: err.Error(), RequestID: c.GetString("request_id")})
return
}
p, ok := repo.Update(id, patch)
if !ok {
c.JSON(http.StatusNotFound, Response{Code: 30001, Message: "not found", RequestID: c.GetString("request_id")})
return
}
c.JSON(http.StatusOK, Response{Code: 0, Message: "ok", Data: p, RequestID: c.GetString("request_id")})
})
// 删除
v1.DELETE("/products/:id", func(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if !repo.Delete(id) {
c.JSON(http.StatusNotFound, Response{Code: 30001, Message: "not found", RequestID: c.GetString("request_id")})
return
}
c.Status(http.StatusNoContent)
})
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
log.Println("product service on :8080")
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}启动后可以用 curl 测试完整 CRUD 流程:
bash
# 创建
curl -X POST http://localhost:8080/v1/products \
-H "Content-Type: application/json" \
-d '{"name":"iPad Pro","price":6999,"stock":30,"category":"tablet"}'
# 列表
curl http://localhost:8080/v1/products
# 获取单个
curl http://localhost:8080/v1/products/1
# 部分更新
curl -X PATCH http://localhost:8080/v1/products/1 \
-H "Content-Type: application/json" \
-d '{"price":7499}'
# 删除
curl -X DELETE http://localhost:8080/v1/products/1八、小结
本篇我们学习了用 Gin 构建 RESTful 微服务的核心知识:
- REST 设计规范:用名词复数表达资源、用 HTTP 方法表达操作、用状态码表达结果。
- REST vs gRPC:对外 REST,内部 gRPC,网关做转换。
- 统一响应格式:
code/message/data/request_id四件套,便于前端封装统一请求库。 - 业务错误码体系:按模块分段,避免与 HTTP 状态码混淆。
- API 版本管理:URI 版本(
/v1)最实用,Gin 用RouterGroup自然支持。 - Swagger 文档:用 swaggo/swag 解析注释生成 OpenAPI + Swagger UI。
- 双端口服务:一个进程同时跑 gRPC 和 REST,业务逻辑共享。
下一篇我们会进入 API 网关模式,学习如何用 Gin + httputil.ReverseProxy 实现一个统一的微服务入口,承担路由、鉴权、限流、协议转换等职责。
延伸阅读:
- Gin 官方文档:https://gin-gonic.com/docs/
- REST API 设计指南:https://restfulapi.net/
- OpenAPI 规范:https://swagger.io/specification/
- swaggo/swag:https://github.com/swaggo/swag