Spring Boot vs Gin:Java 人搭第一个 Go REST API

本文为《Java工程师转Go实战》连载 第 12 篇 / 共 20 篇

上一篇:context.Context 全解析

下一篇:数据库 ORM 选型


类比开场

Spring Boot 起手式:

java 复制代码
@RestController
@RequestMapping("/api/users")
public class UserController {
    @Autowired private UserService userService;

    @GetMapping("/{id}")
    public ResponseEntity<User> get(@PathVariable Long id) {
        return ResponseEntity.ok(userService.getById(id));
    }

    @PostMapping
    public ResponseEntity<User> create(@RequestBody @Valid CreateUserReq req) {
        return ResponseEntity.status(201).body(userService.create(req));
    }
}

Go + Gin 同样几行搞定,而且 没有反射启动、没有 JVM 预热、没有 Bean 扫描------启动时间从秒级变成毫秒级。


一、框架全景对照

Spring Boot 概念 Go 生态对应 说明
Spring MVC Gin / Echo / Fiber / Chi Web 框架
Spring WebFlux Gin + goroutine(天然异步) Go 不需要响应式框架
@RestController gin.Engine + handler function
@RequestMapping r.Group("/api") 路由分组
@Autowired / @Inject 构造函数注入(手动或 wire)
@Valid / @Validated binding tag + validator
@ControllerAdvice 错误处理中间件
Filter / HandlerInterceptor gin.HandlerFunc 中间件链
Actuator /health 自建 /health + Prometheus
application.yml viper + yaml/env
Spring Security casbin / 自建 JWT 中间件
@Async go func(){}()
DevTools 热更新 air / reflex

为什么选 Gin?

  • GitHub 70k+ stars,Go Web 框架使用率第一
  • 基于 httprouter 的 radix tree 路由,性能极高
  • 中间件生态丰富(CORS、限流、JWT、Swagger......)
  • API 简洁,学习曲线平缓
  • 生产案例多:字节、B站、七牛等大厂使用

二、最小 REST 服务(完整可运行)

go 复制代码
package main

import (
    "net/http"
    "strconv"
    "sync"

    "github.com/gin-gonic/gin"
)

// ========== Model ==========

type User struct {
    ID   int64  `json:"id"`
    Name string `json:"name" binding:"required,min=1,max=64"`
    Age  int    `json:"age" binding:"gte=0,lte=150"`
}

type CreateUserReq struct {
    Name string `json:"name" binding:"required,min=1,max=64"`
    Age  int    `json:"age" binding:"gte=0,lte=150"`
}

// ========== 模拟存储 ==========

var (
    users  = make(map[int64]*User)
    nextID int64 = 1
    mu     sync.Mutex
)

// ========== Handler ==========

func getUser(c *gin.Context) {
    idStr := c.Param("id")
    id, err := strconv.ParseInt(idStr, 10, 64)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"code": 40000, "msg": "invalid id"})
        return
    }

    mu.Lock()
    u, exists := users[id]
    mu.Unlock()

    if !exists {
        c.JSON(http.StatusNotFound, gin.H{"code": 40400, "msg": "user not found"})
        return
    }
    c.JSON(http.StatusOK, gin.H{"code": 0, "data": u})
}

func createUser(c *gin.Context) {
    var req CreateUserReq
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"code": 40000, "msg": err.Error()})
        return
    }

    mu.Lock()
    u := &User{ID: nextID, Name: req.Name, Age: req.Age}
    users[nextID] = u
    nextID++
    mu.Unlock()

    c.JSON(http.StatusCreated, gin.H{"code": 0, "data": u})
}

func listUsers(c *gin.Context) {
    mu.Lock()
    list := make([]*User, 0, len(users))
    for _, u := range users {
        list = append(list, u)
    }
    mu.Unlock()

    c.JSON(http.StatusOK, gin.H{"code": 0, "data": list})
}

// ========== Main ==========

func main() {
    r := gin.Default() // 包含 Logger + Recovery 中间件

    // 健康检查
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    // REST 路由
    api := r.Group("/api")
    {
        userGroup := api.Group("/users")
        userGroup.GET("", listUsers)
        userGroup.GET("/:id", getUser)
        userGroup.POST("", createUser)
    }

    r.Run(":8080") // 默认监听 0.0.0.0:8080
}

启动后测试:

bash 复制代码
# 创建用户
curl -X POST http://localhost:8080/api/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"张三","age":28}'

# 查询用户
curl http://localhost:8080/api/users/1

# 列表
curl http://localhost:8080/api/users

三、路由详解(对标 @RequestMapping)

路由分组

go 复制代码
r := gin.Default()

// 版本化 API
v1 := r.Group("/api/v1")
{
    v1.GET("/users", listUsers)
    v1.GET("/users/:id", getUser)
    v1.POST("/users", createUser)
    v1.PUT("/users/:id", updateUser)
    v1.DELETE("/users/:id", deleteUser)
}

v2 := r.Group("/api/v2")
{
    v2.GET("/users", listUsersV2) // 新版本
}

路由参数

go 复制代码
// 路径参数:/users/:id → c.Param("id")
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id") // "123"
})

// 查询参数:/users?page=1&size=10
r.GET("/users", func(c *gin.Context) {
    page := c.DefaultQuery("page", "1")
    size := c.DefaultQuery("size", "10")
})

// 通配符:/static/*filepath
r.Static("/static", "./public")

参数绑定(对标 @Valid + DTO)

go 复制代码
type SearchReq struct {
    Keyword  string `form:"keyword" binding:"required"`
    Page     int    `form:"page" binding:"gte=1"`
    PageSize int    `form:"pageSize" binding:"gte=1,lte=100"`
    Sort     string `form:"sort" binding:"oneof=asc desc"`
}

func searchUsers(c *gin.Context) {
    var req SearchReq
    if err := c.ShouldBindQuery(&req); err != nil {
        c.JSON(400, gin.H{"msg": err.Error()})
        return
    }
    // req.Keyword, req.Page, req.PageSize 都已验证
}

// 绑定方法对照
// c.ShouldBindJSON(&req)  ← application/json body
// c.ShouldBindQuery(&req) ← URL query parameters
// c.ShouldBind(&req)      ← 自动判断 Content-Type
// c.ShouldBindUri(&req)   ← 路径参数 (:id)

自定义校验器

go 复制代码
import "github.com/go-playground/validator/v10"

// 自定义校验:手机号
func validPhone(fl validator.FieldLevel) bool {
    phone := fl.Field().String()
    matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
    return matched
}

func init() {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("phone", validPhone)
    }
}

type RegisterReq struct {
    Phone string `json:"phone" binding:"required,phone"` // 使用自定义校验
}

四、中间件详解(对标 Filter / Interceptor)

中间件执行流程

复制代码
Request → [Logger] → [Recovery] → [Auth] → [Handler] → Response
                                             │
            c.Next() 前 = 前置处理            │
            c.Next() 后 = 后置处理  ←─────────┘

自定义中间件模板

go 复制代码
func MyMiddleware() gin.HandlerFunc {
    // 这里做一次性初始化(中间件创建时执行一次)
    return func(c *gin.Context) {
        // ====== 前置处理(Request 进来时) ======
        start := time.Now()
        
        // ====== 调用下一个 handler ======
        c.Next()
        
        // ====== 后置处理(Response 出去时) ======
        duration := time.Since(start)
        status := c.Writer.Status()
        log.Info("request completed",
            zap.String("method", c.Request.Method),
            zap.String("path", c.Request.URL.Path),
            zap.Int("status", status),
            zap.Duration("duration", duration),
        )
    }
}

实用中间件示例

请求 ID 中间件

go 复制代码
func RequestID() gin.HandlerFunc {
    return func(c *gin.Context) {
        requestID := c.GetHeader("X-Request-ID")
        if requestID == "" {
            requestID = uuid.New().String()
        }
        c.Set("requestID", requestID)
        c.Header("X-Request-ID", requestID)
        c.Next()
    }
}

限流中间件

go 复制代码
func RateLimit(qps int) gin.HandlerFunc {
    limiter := rate.NewLimiter(rate.Limit(qps), qps)
    return func(c *gin.Context) {
        if !limiter.Allow() {
            c.AbortWithStatusJSON(429, gin.H{
                "code": 42900,
                "msg":  "too many requests",
            })
            return
        }
        c.Next()
    }
}

CORS 中间件

go 复制代码
func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization")
        
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        c.Next()
    }
}

超时中间件

go 复制代码
func Timeout(timeout time.Duration) gin.HandlerFunc {
    return func(c *gin.Context) {
        ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
        defer cancel()
        c.Request = c.Request.WithContext(ctx)
        
        done := make(chan struct{})
        go func() {
            c.Next()
            close(done)
        }()
        
        select {
        case <-done:
            // 正常完成
        case <-ctx.Done():
            c.AbortWithStatusJSON(504, gin.H{"msg": "request timeout"})
        }
    }
}

中间件注册范围

go 复制代码
r := gin.New()

// 全局中间件
r.Use(gin.Logger(), gin.Recovery(), RequestID(), CORS())

// 分组中间件
api := r.Group("/api", RateLimit(1000))
{
    // 需要认证的路由
    auth := api.Group("", AuthMiddleware())
    auth.POST("/orders", createOrder)
    auth.GET("/profile", getProfile)
    
    // 不需要认证的路由
    api.POST("/login", login)
    api.POST("/register", register)
}

五、统一响应体(对标 Spring ResponseEntity)

定义统一响应结构

go 复制代码
package response

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

type Response struct {
    Code int    `json:"code"`
    Msg  string `json:"msg"`
    Data any    `json:"data,omitempty"`
}

func OK(c *gin.Context, data any) {
    c.JSON(http.StatusOK, Response{Code: 0, Msg: "success", Data: data})
}

func Created(c *gin.Context, data any) {
    c.JSON(http.StatusCreated, Response{Code: 0, Msg: "created", Data: data})
}

func BadRequest(c *gin.Context, msg string) {
    c.JSON(http.StatusBadRequest, Response{Code: 40000, Msg: msg})
}

func NotFound(c *gin.Context, msg string) {
    c.JSON(http.StatusNotFound, Response{Code: 40400, Msg: msg})
}

func ServerError(c *gin.Context) {
    c.JSON(http.StatusInternalServerError, Response{Code: 50000, Msg: "internal error"})
}

// 分页响应
type PageData struct {
    List     any   `json:"list"`
    Total    int64 `json:"total"`
    Page     int   `json:"page"`
    PageSize int   `json:"pageSize"`
}

func OKPage(c *gin.Context, list any, total int64, page, pageSize int) {
    OK(c, PageData{List: list, Total: total, Page: page, PageSize: pageSize})
}

使用

go 复制代码
func getUser(c *gin.Context) {
    id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
    user, err := userService.GetByID(c.Request.Context(), id)
    if err != nil {
        if errors.Is(err, service.ErrNotFound) {
            response.NotFound(c, "用户不存在")
            return
        }
        response.ServerError(c)
        return
    }
    response.OK(c, user)
}

六、项目结构建议

小型项目

复制代码
user-service/
├── main.go
├── handler/
│   └── user.go
├── service/
│   └── user.go
├── repository/
│   └── user.go
├── model/
│   ├── user.go
│   └── request.go
├── middleware/
│   ├── auth.go
│   └── logger.go
├── pkg/
│   └── response/
│       └── response.go
├── configs/
│   └── app.yaml
└── go.mod

中大型项目

复制代码
order-platform/
├── cmd/
│   └── server/
│       └── main.go          # 入口:组装路由、启动
├── internal/                 # 不可被外部 import
│   ├── handler/             # HTTP Handler(薄层)
│   │   ├── user.go
│   │   ├── order.go
│   │   └── router.go       # 路由注册
│   ├── service/             # 业务逻辑
│   │   ├── user.go
│   │   └── order.go
│   ├── repository/          # 数据访问
│   │   ├── user.go
│   │   └── order.go
│   ├── model/               # 数据模型
│   │   ├── entity.go       # DB 实体
│   │   ├── request.go      # 请求 DTO
│   │   └── response.go     # 响应 DTO
│   └── middleware/
│       ├── auth.go
│       ├── trace.go
│       └── ratelimit.go
├── pkg/                     # 可被外部引用的公共包
│   ├── response/
│   └── errcode/
├── configs/
├── api/                     # OpenAPI / proto 定义
├── scripts/
├── Dockerfile
├── Makefile
└── go.mod

原则

  • cmd/ 只做组装(依赖注入),不放业务逻辑
  • internal/ 锁住实现细节
  • handler 层薄:参数绑定 + 调用 service + 写响应
  • service 层放业务规则和编排
  • repository 层只碰数据库

七、热更新开发(对标 Spring DevTools)

bash 复制代码
# 安装 air
go install github.com/air-verse/air@latest

# 项目根目录初始化
air init
# 生成 .air.toml 配置文件

# 启动(代替 go run)
air

.air.toml 关键配置:

toml 复制代码
[build]
cmd = "go build -o ./tmp/main ./cmd/server"
bin = "tmp/main"
include_ext = ["go", "yaml", "html"]
exclude_dir = ["tmp", "vendor", "node_modules"]

修改 Go 文件后自动重新编译运行,体验接近 Spring DevTools。


八、和 Spring Boot 的核心差异总结

维度 Spring Boot Gin
启动速度 3-15 秒(Bean 扫描、AOP 织入) 10-50 毫秒
内存占用 200MB-1GB(JVM 堆) 10-50MB
二进制 JAR (需要 JRE) 单二进制(编译好直接跑)
注解魔法 大量(方便但隐式) 几乎没有(显式路由,一眼看清)
IoC 容器 强大的 DI 容器 手动或 wire 代码生成
生态广度 全家桶(什么都有) 自己拼 middleware + 库
配置方式 application.yml + Profile viper + env
性能天花板 高(需调优 JVM) 极高(无 GC 长暂停)
适合场景 复杂企业应用、大团队 高并发 API、微服务、云原生
部署 容器化需要 JRE 基础镜像 FROM scratchdistroless

Docker 镜像对比

dockerfile 复制代码
# Java Spring Boot: ~300MB+
FROM eclipse-temurin:21-jre
COPY target/app.jar /app.jar
CMD ["java", "-jar", "/app.jar"]

# Go Gin: ~10-20MB
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server ./cmd/server

FROM scratch
COPY --from=builder /app/server /server
CMD ["/server"]

九、实战:完整的用户注册登录 API

go 复制代码
// internal/handler/auth.go
type AuthHandler struct {
    svc       *service.AuthService
    jwtSecret []byte
}

func (h *AuthHandler) Register(c *gin.Context) {
    var req model.RegisterReq
    if err := c.ShouldBindJSON(&req); err != nil {
        response.BadRequest(c, err.Error())
        return
    }
    
    user, err := h.svc.Register(c.Request.Context(), req)
    if err != nil {
        handleError(c, err)
        return
    }
    response.Created(c, user)
}

func (h *AuthHandler) Login(c *gin.Context) {
    var req model.LoginReq
    if err := c.ShouldBindJSON(&req); err != nil {
        response.BadRequest(c, err.Error())
        return
    }
    
    token, err := h.svc.Login(c.Request.Context(), req)
    if err != nil {
        handleError(c, err)
        return
    }
    response.OK(c, gin.H{"token": token})
}

// 路由注册
func RegisterRoutes(r *gin.Engine, authHandler *AuthHandler, userHandler *UserHandler) {
    // 公开路由
    r.POST("/api/register", authHandler.Register)
    r.POST("/api/login", authHandler.Login)
    
    // 需要认证的路由
    auth := r.Group("/api", AuthMiddleware())
    {
        auth.GET("/profile", userHandler.GetProfile)
        auth.PUT("/profile", userHandler.UpdateProfile)
    }
}

📦 面试追问

  1. Gin 为什么快?

    基于 httprouter 的 radix tree 路由匹配(O(k) k=URL 长度);减少反射使用;Context 对象池化(sync.Pool)复用。但实际瓶颈在 DB/IO,框架本身差异在真实场景中可忽略。

  2. 标准库 net/http 够用吗?

    够。Gin 是 net/http 的封装。K8s、Docker、Prometheus 等项目都直接用 net/http。Gin 提供的是路由分组、参数绑定、中间件链等开发体验。

  3. 怎么做优雅关闭?

    http.Server.Shutdown(ctx) 停止接收新请求,等在途请求完成,配合 os.Signal 监听 SIGTERM。Gin 的 r.Run() 底层就是 http.Server。

  4. Gin vs Echo vs Fiber?

    Gin 社区最大、教程最多、生产案例最广。Echo 更现代(泛型)。Fiber 基于 fasthttp(非标准库,兼容性有坑)。新项目选 Gin 或 Echo 都行。

  5. Go 怎么做 Swagger 文档?

    swaggo/swag:注释写在 handler 上,运行 swag init 生成 docs/,然后 r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))


💡 一句话总结

把 @RestController 换成 gin.HandlerFunc,把 @RequestMapping 换成 Group + GET/POST,把 @Valid 换成 binding tag,把 Filter 换成 Use() 中间件------第一个 Go API 半小时就能跑起来。启动快、镜像小、部署简单,这是 Java 人立刻能感受到的体验差距。


建议标签Golang Gin Spring Boot Java REST Web 微服务

相关推荐
神明不懂浪漫2 小时前
【第七章】Java中的常用类
java·开发语言·前端·经验分享·笔记
码兄科技9 小时前
实战:基于Spring Boot + UniApp的地理信息小程序开发
spring boot·后端·uni-app
Jelena1577958579211 小时前
电商运营分析数据比价接口实战:多平台价格监控与智能决策系统
java·大数据·数据库
神明不懂浪漫12 小时前
【第五章】Java中的继承与多态
java·开发语言
AI多Agent协作实战派14 小时前
AI多Agent协作系统实战(十七):凌晨4点,我的AI系统在“假装工作“——3个bug同时爆炸的5小时
java·前端·bug
gaolei_eit14 小时前
Java+Ai+vue
java·spring·maven
qq_25183645714 小时前
基于java Web 动漫视频网站毕业论文
java·开发语言·前端
LayZhangStrive14 小时前
JUC相关的函数、注解、变量杂记
java·面试·多线程·juc
未秃头的程序猿14 小时前
给公司做了个AI客服Agent,用的Spring AI 1.0,3天上线领导拍板了
java·后端·ai编程