Go Web API
Go 很适合编写轻量、高并发、部署简单的 Web API。标准库 net/http 已经能完成基础服务,复杂项目可以使用 Gin、Echo、Fiber 等框架。但不管用不用框架,核心都一样:请求进入、路由匹配、参数解析、业务处理、下游调用、统一响应、日志和超时控制。
零基础学习 Go Web API 不要只背框架写法,要先理解标准库如何工作。理解 http.Handler、http.Request、http.ResponseWriter、Context、中间件和分层之后,再使用框架才不会写成“能跑但不可维护”的接口。
学习目标
学完本页你应该能回答:
- Go 标准库 Web 服务怎么启动。
http.Handler、ResponseWriter、Request分别是什么。- 一次请求从客户端到 Handler 经历哪些步骤。
- Handler、Service、Repository 如何分层。
- 中间件为什么本质上是包装 Handler。
- 如何做统一响应、统一错误、日志、panic 恢复。
- 为什么所有下游调用都要传递 Context。
- 线上接口慢、panic、超时、连接耗尽怎么排查。
最小可运行服务
main.go:
package main
import (
"encoding/json"
"log"
"net/http"
)
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "UP"})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
log.Println("server started at :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}运行:
go run main.go访问:
curl http://127.0.0.1:8080/health这个 Demo 涉及几个核心对象:
| 对象 | 作用 |
|---|---|
http.Server | HTTP 服务器 |
ServeMux | 路由匹配器 |
HandlerFunc | 把函数适配成 Handler |
ResponseWriter | 写响应头和响应体 |
Request | 请求方法、路径、Header、Body、Context |
请求处理流程
flowchart TD
A["客户端发起 HTTP 请求"] --> B["Go HTTP Server 接收连接"]
B --> C["解析请求行 Header Body"]
C --> D["ServeMux 路由匹配"]
D --> E["中间件链"]
E --> F["Handler 解析参数"]
F --> G["Service 执行业务"]
G --> H["Repository 或外部服务"]
H --> I["返回业务结果"]
I --> J["Handler 写 JSON 响应"]Go 标准库会为每个请求创建一个 *http.Request,其中 r.Context() 会在客户端断开、请求结束或服务端取消时被取消。
Handler 是什么
标准库核心接口:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}只要实现了 ServeHTTP 方法,就是一个 Handler。
type HealthHandler struct{}
func (h HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}函数也可以变成 Handler:
func health(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}
mux.HandleFunc("/health", health)HandleFunc 的本质是把普通函数转换成 HandlerFunc,而 HandlerFunc 实现了 ServeHTTP。
Request 和 ResponseWriter
*http.Request 包含请求信息:
| 字段/方法 | 作用 |
|---|---|
r.Method | HTTP 方法 |
r.URL.Path | 请求路径 |
r.URL.Query() | Query 参数 |
r.Header | 请求头 |
r.Body | 请求体 |
r.Context() | 请求 Context |
http.ResponseWriter 用来写响应:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"ok":true}`))注意:WriteHeader 只能有效调用一次。写 Body 前如果没有显式调用 WriteHeader,Go 会默认返回 200。
参数解析
Query 参数:
func listAssets(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
page := query.Get("page")
size := query.Get("size")
json.NewEncoder(w).Encode(map[string]string{
"page": page,
"size": size,
})
}JSON Body:
type CreateAssetRequest struct {
Name string `json:"name"`
SourceSystem string `json:"source_system"`
}
func createAsset(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var req CreateAssetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "请求体不是合法 JSON")
return
}
if req.Name == "" {
writeError(w, http.StatusBadRequest, "ASSET_NAME_REQUIRED", "资产名称不能为空")
return
}
writeJSON(w, http.StatusOK, map[string]any{"name": req.Name})
}Go 标准库不会自动做参数校验。你要自己校验,或者使用 validator、框架绑定能力。
统一响应和错误
商业项目不要每个接口随便返回不同结构。推荐统一响应:
type APIResponse struct {
Code string `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(APIResponse{
Code: "SUCCESS",
Message: "success",
Data: data,
})
}
func writeError(w http.ResponseWriter, status int, code string, message string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(APIResponse{
Code: code,
Message: message,
})
}为什么要统一:
- 前端处理简单。
- 错误码可统计。
- 日志和监控能聚合。
- 面试和项目表达更专业。
分层结构
小 Demo 可以写一个文件,商业项目建议分层。
project/
cmd/server/main.go
internal/handler/asset_handler.go
internal/service/asset_service.go
internal/repository/asset_repository.go
internal/domain/asset.go调用方向:
flowchart TD
A["Handler HTTP 入参出参"] --> B["Service 业务规则"]
B --> C["Repository 数据访问"]
C --> D["Database"]
B --> E["Client 外部服务"]职责:
| 层 | 负责 | 不应该负责 |
|---|---|---|
| Handler | 参数解析、鉴权结果接收、响应转换 | 复杂业务规则 |
| Service | 业务规则、流程编排、事务边界 | 解析 HTTP Header |
| Repository | SQL、数据库访问 | 判断复杂业务规则 |
| Client | 外部 HTTP/RPC/AI 调用 | 保存核心业务数据 |
完整分层 Demo
domain/asset.go:
package domain
type Asset struct {
ID int64 `json:"id"`
Name string `json:"name"`
SourceSystem string `json:"source_system"`
}repository/asset_repository.go:
package repository
import (
"context"
"sync"
"project/internal/domain"
)
type AssetRepository struct {
mu sync.Mutex
nextID int64
rows []domain.Asset
}
func NewAssetRepository() *AssetRepository {
return &AssetRepository{nextID: 1}
}
func (r *AssetRepository) Save(ctx context.Context, asset domain.Asset) (domain.Asset, error) {
select {
case <-ctx.Done():
return domain.Asset{}, ctx.Err()
default:
}
r.mu.Lock()
defer r.mu.Unlock()
asset.ID = r.nextID
r.nextID++
r.rows = append(r.rows, asset)
return asset, nil
}service/asset_service.go:
package service
import (
"context"
"errors"
"project/internal/domain"
"project/internal/repository"
)
type AssetService struct {
repo *repository.AssetRepository
}
func NewAssetService(repo *repository.AssetRepository) *AssetService {
return &AssetService{repo: repo}
}
func (s *AssetService) Create(ctx context.Context, name string, source string) (domain.Asset, error) {
if name == "" {
return domain.Asset{}, errors.New("资产名称不能为空")
}
if source != "HIS" && source != "LIS" && source != "PACS" && source != "EMR" {
return domain.Asset{}, errors.New("来源系统不支持")
}
return s.repo.Save(ctx, domain.Asset{Name: name, SourceSystem: source})
}handler/asset_handler.go:
package handler
import (
"encoding/json"
"net/http"
"project/internal/service"
)
type AssetHandler struct {
service *service.AssetService
}
func NewAssetHandler(service *service.AssetService) *AssetHandler {
return &AssetHandler{service: service}
}
type createAssetRequest struct {
Name string `json:"name"`
SourceSystem string `json:"source_system"`
}
func (h *AssetHandler) Create(w http.ResponseWriter, r *http.Request) {
var req createAssetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "请求体不是合法 JSON")
return
}
asset, err := h.service.Create(r.Context(), req.Name, req.SourceSystem)
if err != nil {
writeError(w, http.StatusBadRequest, "BIZ_ERROR", err.Error())
return
}
writeJSON(w, http.StatusOK, asset)
}这个 Demo 的重点不是代码多,而是职责清楚:HTTP 逻辑不污染业务逻辑,业务逻辑不直接依赖响应对象。
中间件原理
中间件本质上是接收一个 Handler,返回一个新的 Handler。
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("method=%s path=%s cost=%s", r.Method, r.URL.Path, time.Since(start))
})
}执行链:
flowchart TD
A["请求进入"] --> B["日志中间件 before"]
B --> C["鉴权中间件 before"]
C --> D["业务 Handler"]
D --> E["鉴权中间件 after"]
E --> F["日志中间件 after"]常见中间件:
| 中间件 | 作用 |
|---|---|
| 日志 | 记录方法、路径、状态码、耗时 |
| Recovery | 捕获 panic,避免进程崩溃 |
| Auth | 鉴权,解析用户 |
| CORS | 处理浏览器跨域 |
| Rate Limit | 限流 |
| Request ID | 生成链路 ID |
Panic 恢复
Go Web 服务中某个请求 panic,不应该让整个进程崩溃。
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 path=%s err=%v", r.URL.Path, err)
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "系统异常")
}
}()
next.ServeHTTP(w, r)
})
}Recovery 不是让你忽略 panic。它只是保护服务不崩,并记录日志。panic 的根因仍然要修。
Context 和超时
Handler 中所有下游调用都应该传 r.Context()。
func (h *AssetHandler) Get(w http.ResponseWriter, r *http.Request) {
asset, err := h.service.Get(r.Context(), 1001)
if err != nil {
writeError(w, http.StatusInternalServerError, "QUERY_ERROR", "查询失败")
return
}
writeJSON(w, http.StatusOK, asset)
}下游慢调用可以设置局部超时:
func (s *AssetService) SyncRemote(ctx context.Context) error {
remoteCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return s.remoteClient.Sync(remoteCtx)
}不要创建新根 Context:
ctx := context.Background() // Handler 里通常不应该这样这样会切断请求取消信号,客户端断开后下游还会继续跑。
Server 超时配置
不要直接使用:
http.ListenAndServe(":8080", mux)生产更推荐显式配置 http.Server:
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 2 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Fatal(server.ListenAndServe())这些超时的意义:
| 配置 | 作用 |
|---|---|
ReadHeaderTimeout | 限制读取请求头时间,防慢速攻击 |
ReadTimeout | 限制读取整个请求时间 |
WriteTimeout | 限制写响应时间 |
IdleTimeout | Keep-Alive 空闲连接保留时间 |
Graceful Shutdown
服务发布或重启时,不应该粗暴杀掉正在处理的请求。
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
server := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("server shutdown failed: %v", err)
}流程:
flowchart TD
A["收到 SIGTERM"] --> B["停止接收新请求"]
B --> C["等待正在处理的请求结束"]
C --> D{"是否超过关闭超时"}
D -- "否" --> E["正常退出"]
D -- "是" --> F["强制退出"]商业场景:数据资产 API
医疗数据资产平台常见 API:
| 接口 | 作用 |
|---|---|
POST /assets | 创建资产 |
GET /assets/{id} | 查询资产 |
POST /collect-jobs | 创建采集任务 |
GET /collect-jobs/{id} | 查询任务状态 |
设计要点:
- Handler 解析参数,Service 判断业务规则。
- 创建采集任务后不要同步采集全部数据,应返回任务 ID。
- 任务处理要有 Context、超时、日志和状态机。
- 所有错误返回统一结构和错误码。
- 日志带 request_id、user_id、cost_ms。
- 数据库和外部医院接口调用都要设置超时。
线上排查
接口慢
flowchart TD
A["接口慢"] --> B["查看 access log cost_ms"]
B --> C["按 request_id 查链路"]
C --> D{"慢在哪里"}
D -- "Handler" --> E["参数解析 Body 大小 JSON 解码"]
D -- "Service" --> F["业务循环 锁 并发控制"]
D -- "Repository" --> G["SQL 慢 连接池 锁等待"]
D -- "外部服务" --> H["HTTP 超时 重试 下游慢"]Panic
排查:
- Recovery 日志是否记录堆栈。
- panic 是 nil 指针、数组越界、类型断言失败还是并发 map 写。
- 是否缺少参数校验。
- 是否需要补测试覆盖。
连接耗尽
常见原因:
resp.Body没关闭。- 数据库 rows 没关闭。
- 请求没有超时,慢调用长期占连接。
- 下游连接池配置太小或下游真的慢。
面试标准回答
Go Web API 一次请求怎么走?
客户端请求到达 Go HTTP Server,标准库解析请求后交给路由器匹配,经过中间件链,最后进入 Handler。Handler 解析参数并调用 Service,Service 执行业务并调用 Repository 或外部服务,最后 Handler 将结果写成 JSON 响应。
中间件原理是什么?
中间件本质是一个函数:接收 http.Handler,返回新的 http.Handler。它在调用 next.ServeHTTP 前后执行逻辑,例如日志、鉴权、panic 恢复、限流。多个中间件层层包装,形成调用链。
Handler 为什么不写业务逻辑?
Handler 负责 HTTP 入参出参,Service 负责业务规则,Repository 负责数据访问。这样业务逻辑可以被 CLI、任务、消息消费者复用,也更容易写单元测试。所有逻辑写 Handler 会导致难测、难复用、难排查。
Go Web 服务生产要注意什么?
要配置 Server 超时、panic recovery、统一响应、日志 request_id、Context 传递、下游超时、连接池、限流和优雅关闭。接口慢时按 request_id 拆 Handler、Service、DB、外部服务耗时。
关联知识点
小结
Go Web API 的核心不是框架,而是 HTTP Server、Handler、中间件、Context、分层、统一响应、日志和超时。标准库理解清楚后,再用 Gin/Echo 只是换一层工具;真正决定项目质量的是职责清晰、生命周期可控、错误可观测、性能可排查。
