04_HTTP数据库与测试
使用标准库建立 HTTP 服务,理解 database/sql、接口测试和构建发布。
Go HTTP、数据库与测试
学习目标:用标准库建立小型 HTTP 服务,明确数据库与测试边界。
1. 最小 HTTP 服务
Go 1.22+ 的 ServeMux 支持带 HTTP 方法的路由模式。在独立目录执行 go mod init example.com/notes,将代码保存为 main.go,然后执行 go run .。
package main
import (
"encoding/json"
"log"
"net/http"
"time"
)
func routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
return mux
}
func main() {
server := &http.Server{
Addr: ":8080", Handler: routes(),
ReadHeaderTimeout: 5 * time.Second,
}
log.Fatal(server.ListenAndServe())
}
/health 只证明进程能应答;依赖数据库的就绪检查应单独设计。生产服务还需优雅退出、请求体大小限制、写超时、反向代理与 TLS 配置。不要假设客户端提交的 JSON 已经可信。
2. 数据库边界
database/sql 提供通用接口,但不内置具体数据库驱动。sql.DB 是连接池句柄,通常在应用生命周期内复用;创建后用 PingContext 验证可连接。查询使用 QueryContext/ExecContext 与参数绑定,具体占位符如 ? 或 $1 由数据库驱动决定。
事务流程:开始事务 → 逐步执行 → 遇错回滚 → 全部成功后提交。处理 sql.ErrNoRows 时可以返回业务上的“未找到”,但其他数据库错误应保留内部日志并映射为稳定 API 错误。
3. HTTP 测试
将上面的 routes() 与下面测试放在同一包的 main_test.go 中。
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHealth(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/health", nil)
response := httptest.NewRecorder()
routes().ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", response.Code)
}
}
运行 go test ./...,并对输入错误、未找到、重复写入、无权限和数据库故障补充用例。依赖数据库的集成测试使用隔离数据库,不要接真实生产实例。
4. 构建与发布
go.mod、go.sum 记录依赖;CI 中运行格式检查、go vet ./...、测试和构建。应用配置从环境变量读取;发布时设超时、健康检查、结构化日志、优雅关闭和回滚方案。共用原则见 测试、CI 与部署。
自测
sql.Open成功是否证明数据库可连接?不一定,还需PingContext或实际操作。- 单元测试与集成测试为什么应分开?前者快速验证规则,后者验证数据库、迁移和驱动的真实行为。
5. 严格处理 JSON 请求
先限制请求体大小,再解码、校验字段并检查是否有多余 JSON 值。默认解码器会忽略未知字段;如果 API 契约要求拒绝拼错的字段,可调用 DisallowUnknownFields()。响应写入前先设置 Content-Type 和状态码;http.Error 有自己的纯文本格式,若契约规定 JSON 错误,应统一封装。
type CreateNote struct { Title string `json:"title"` }
func decodeCreate(w http.ResponseWriter, r *http.Request) (CreateNote, error) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var input CreateNote
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&input); err != nil { return input, err }
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
return input, errors.New("请求体只能有一个 JSON 值")
}
if strings.TrimSpace(input.Title) == "" {
return input, errors.New("标题不能为空")
}
return input, nil
}
这是 handler 辅助函数片段,需导入 encoding/json、errors、io、net/http、strings。真实服务还要限制 Content-Type、字段长度和认证身份;解码错误需区分请求过大、非法 JSON 和业务校验失败。
6. 数据库事务的可检验写法
sql.DB 是并发安全的连接池句柄,一般在启动时创建并在关闭服务时释放。事务中的所有 SQL 都通过同一个 *sql.Tx 执行;若混用 db.ExecContext,操作会跑到事务外。回滚可以在成功提交后安全地调用而得到“事务已结束”错误,因此常用 defer tx.Rollback() 作兜底,同时只把 Commit 成功视为完成。
tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()
if _, err := tx.ExecContext(ctx,
"INSERT INTO notes(owner_id, title) VALUES (?, ?)", ownerID, title); err != nil {
return err
}
return tx.Commit()
上段是返回 error 的函数体片段,占位符需按数据库驱动调整。实际创建接口还应校验用户身份、执行唯一约束、处理冲突及返回新 ID。集成测试要在临时数据库上运行迁移,再验证提交与回滚;httptest 只验证 HTTP 处理,不会自动证明持久化正确。
7. 可运行练习与参考
在现有 /health 服务上增加 POST /notes:限制请求体、拒绝空标题、返回 201 和 JSON;用 httptest 验证成功、坏 JSON、未知字段和过大请求体。之后接测试数据库,验证重复标题冲突与事务回滚。