2. Go Web 服务基础搭建:使用 net/http 或 gin 框架快速搭建 RESTful API 服务
在契约压测闭环中,一个稳定、结构清晰的 Web 服务是后续所有测试与验证的基石。本章将聚焦于使用 Go 标准库 net/http 与流行框架 gin 快速搭建 RESTful API 服务,涵盖路由设计、中间件集成与错误处理机制。无论你选择哪种方式,核心目标都是构建一个可测试、可观测、易于扩展的服务骨架。
2.1 服务架构概览
一个典型的 Go Web 服务在接收到 HTTP 请求后,会依次经过路由匹配、中间件链处理、业务逻辑执行与错误响应封装。下图展示了请求在服务内部的流转路径:
2.2 使用 net/http 标准库搭建基础服务
Go 标准库 net/http 提供了构建 Web 服务所需的核心能力,无需引入第三方依赖。对于小型服务或对依赖管理有严格要求的项目,标准库是理想选择。
2.2.1 基础路由与处理器
标准库使用 http.HandleFunc 或 http.Handle 注册路由。以下示例创建了一个简单的用户管理 API:
package main
import (
"encoding/json"
"log"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var users = []User{{ID: 1, Name: "Alice"}}
func getUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var newUser User
if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil {
http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest)
return
}
newUser.ID = len(users) + 1
users = append(users, newUser)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newUser)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getUsers(w, r)
case http.MethodPost:
createUser(w, r)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
})
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
ServeMux 在 Go 1.22+ 中支持了方法匹配(如 GET /users),但在早期版本中需要手动通过 r.Method 进行分发。建议在路由处理器内部统一处理 HTTP 方法,保持代码清晰。
2.2.2 标准库中间件实现
标准库的中间件本质是一个接收 http.Handler 并返回 http.Handler 的函数。以下实现了一个日志中间件和一个恢复中间件:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
})
}
func recoveryMiddleware(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 recovered: %v", err)
http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// 使用方式
mux := http.NewServeMux()
mux.HandleFunc("/users", userHandler)
wrappedMux := recoveryMiddleware(loggingMiddleware(mux))
http.ListenAndServe(":8080", wrappedMux)
2.3 使用 Gin 框架搭建高性能服务
Gin 是目前 Go 社区最流行的 Web 框架之一,提供了更丰富的路由语法、内置中间件支持以及更高效的请求处理性能。对于需要快速迭代和复杂路由场景的项目,Gin 是更优选择。
2.3.1 Gin 路由与分组
Gin 支持路由分组,便于对同一资源的不同操作进行组织。以下示例展示了用户 API 的分组路由:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var users = []User{{ID: 1, Name: "Alice"}}
func main() {
r := gin.Default() // 默认包含 Logger 和 Recovery 中间件
// 路由分组:/api/v1
v1 := r.Group("/api/v1")
{
v1.GET("/users", func(c *gin.Context) {
c.JSON(http.StatusOK, users)
})
v1.POST("/users", func(c *gin.Context) {
var newUser User
if err := c.ShouldBindJSON(&newUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
newUser.ID = len(users) + 1
users = append(users, newUser)
c.JSON(http.StatusCreated, newUser)
})
v1.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
// 查找逻辑...
c.JSON(http.StatusOK, gin.H{"id": id, "name": "User" + id})
})
}
r.Run(":8080")
}
2.3.2 Gin 中间件链
Gin 的中间件通过 Use() 方法注册,支持全局中间件和分组中间件。以下实现了一个简单的鉴权中间件:
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" || token != "Bearer valid-token" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.Next() // 继续执行后续处理器
}
}
// 使用方式
r := gin.Default()
// 全局中间件
r.Use(gin.Logger())
r.Use(gin.Recovery())
// 分组中间件
v1 := r.Group("/api/v1")
v1.Use(AuthMiddleware())
{
v1.GET("/users", getUsers)
v1.POST("/users", createUser)
}
c.Next() 与 c.Abort() 控制中间件链的流转。在鉴权、限流等场景中,务必在条件不满足时调用 c.Abort() 并返回响应,避免后续处理器继续执行。
2.4 错误处理与响应标准化
统一的错误处理机制是服务可观测性的基础。无论是标准库还是 Gin,都应遵循一致的错误响应格式。
2.4.1 标准错误响应结构
推荐使用统一的 JSON 结构封装错误信息:
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
}
func writeError(w http.ResponseWriter, status int, message string, detail string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(ErrorResponse{
Code: status,
Message: message,
Detail: detail,
})
}
2.4.2 Gin 中的错误处理
Gin 提供了 c.Error() 方法收集错误,并通过中间件统一处理:
func ErrorHandlerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// 检查是否有错误发生
if len(c.Errors) > 0 {
err := c.Errors.Last()
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "internal server error",
"detail": err.Error(),
})
}
}
}
// 在业务处理器中
func createUser(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.Error(err) // 收集错误
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "invalid input",
"detail": err.Error(),
})
return
}
// 正常处理...
}
2.5 标准库 vs Gin:关键对比
| 特性 | net/http 标准库 | Gin 框架 |
|---|---|---|
| 依赖 | 无外部依赖 | 需引入 gin 包 |
| 路由语法 | 基础路径匹配,Go 1.22+ 支持方法 | 支持参数路由、分组、通配符 |
| 中间件 | 手动包装 Handler | 内置 Use() 链式注册 |
| 性能 | 中等,适合低并发 | 高,基于 Radix Tree 路由 |
| 错误处理 | 手动封装 | c.Error() + 中间件统一处理 |
| 学习曲线 | 低,直接使用标准库 | 中等,需学习框架 API |
| 适用场景 | 微服务、工具链、依赖敏感项目 | RESTful API、中大型项目、快速开发 |
2.6 服务启动与调试实践
无论使用哪种方式,服务启动后都应进行基本的健康检查与调试。以下是一个完整的启动流程:
- 配置端口与环境:通过环境变量或配置文件指定监听端口,避免硬编码。
- 启动日志:服务启动时打印监听地址与路由表,便于调试。
- 优雅关闭:监听系统信号,实现平滑退出,确保正在处理的请求完成。
- 健康检查端点:添加
/health或/ready端点,供负载均衡或压测工具探测。
// Gin 优雅关闭示例
func main() {
r := gin.Default()
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exited")
}
gin.Mode() 设置为 debug 模式,Gin 会自动输出路由表与请求详情。对于标准库,可以借助 httputil.DumpRequest 打印请求内容辅助调试。