Skip to content

Go Web API ​

Go 很适合编写轻量、高并发、部署简单的 Web API。标准库 net/http 已经能完成基础服务,复杂项目可以使用 Gin、Echo、Fiber 等框架。但不管用不用框架,核心都一样:请求进入、路由匹配、参数解析、业务处理、下游调用、统一响应、日志和超时控制。

零基础学习 Go Web API 不要只背框架写法,要先理解标准库如何工作。理解 http.Handler、http.Request、http.ResponseWriter、Context、中间件和分层之后,再使用框架才不会写成“能跑但不可维护”的接口。

学习目标 ​

学完本页你应该能回答:

  1. Go 标准库 Web 服务怎么启动。
  2. http.Handler、ResponseWriter、Request 分别是什么。
  3. 一次请求从客户端到 Handler 经历哪些步骤。
  4. Handler、Service、Repository 如何分层。
  5. 中间件为什么本质上是包装 Handler。
  6. 如何做统一响应、统一错误、日志、panic 恢复。
  7. 为什么所有下游调用都要传递 Context。
  8. 线上接口慢、panic、超时、连接耗尽怎么排查。

最小可运行服务 ​

main.go:

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)
    }
}

运行:

bash
go run main.go

访问:

bash
curl http://127.0.0.1:8080/health

这个 Demo 涉及几个核心对象:

对象作用
http.ServerHTTP 服务器
ServeMux路由匹配器
HandlerFunc把函数适配成 Handler
ResponseWriter写响应头和响应体
Request请求方法、路径、Header、Body、Context

请求处理流程 ​

mermaid
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 是什么 ​

标准库核心接口:

go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

只要实现了 ServeHTTP 方法,就是一个 Handler。

go
type HealthHandler struct{}

func (h HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("ok"))
}

函数也可以变成 Handler:

go
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.MethodHTTP 方法
r.URL.Path请求路径
r.URL.Query()Query 参数
r.Header请求头
r.Body请求体
r.Context()请求 Context

http.ResponseWriter 用来写响应:

go
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"ok":true}`))

注意:WriteHeader 只能有效调用一次。写 Body 前如果没有显式调用 WriteHeader,Go 会默认返回 200。

参数解析 ​

Query 参数:

go
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:

go
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、框架绑定能力。

统一响应和错误 ​

商业项目不要每个接口随便返回不同结构。推荐统一响应:

go
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,
    })
}

为什么要统一:

  1. 前端处理简单。
  2. 错误码可统计。
  3. 日志和监控能聚合。
  4. 面试和项目表达更专业。

分层结构 ​

小 Demo 可以写一个文件,商业项目建议分层。

text
project/
  cmd/server/main.go
  internal/handler/asset_handler.go
  internal/service/asset_service.go
  internal/repository/asset_repository.go
  internal/domain/asset.go

调用方向:

mermaid
flowchart TD
    A["Handler HTTP 入参出参"] --> B["Service 业务规则"]
    B --> C["Repository 数据访问"]
    C --> D["Database"]
    B --> E["Client 外部服务"]

职责:

层负责不应该负责
Handler参数解析、鉴权结果接收、响应转换复杂业务规则
Service业务规则、流程编排、事务边界解析 HTTP Header
RepositorySQL、数据库访问判断复杂业务规则
Client外部 HTTP/RPC/AI 调用保存核心业务数据

完整分层 Demo ​

domain/asset.go:

go
package domain

type Asset struct {
    ID           int64  `json:"id"`
    Name         string `json:"name"`
    SourceSystem string `json:"source_system"`
}

repository/asset_repository.go:

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:

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:

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。

go
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))
    })
}

执行链:

mermaid
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,不应该让整个进程崩溃。

go
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()。

go
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)
}

下游慢调用可以设置局部超时:

go
func (s *AssetService) SyncRemote(ctx context.Context) error {
    remoteCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()
    return s.remoteClient.Sync(remoteCtx)
}

不要创建新根 Context:

go
ctx := context.Background() // Handler 里通常不应该这样

这样会切断请求取消信号,客户端断开后下游还会继续跑。

Server 超时配置 ​

不要直接使用:

go
http.ListenAndServe(":8080", mux)

生产更推荐显式配置 http.Server:

go
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限制写响应时间
IdleTimeoutKeep-Alive 空闲连接保留时间

Graceful Shutdown ​

服务发布或重启时,不应该粗暴杀掉正在处理的请求。

go
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)
}

流程:

mermaid
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}查询任务状态

设计要点:

  1. Handler 解析参数,Service 判断业务规则。
  2. 创建采集任务后不要同步采集全部数据,应返回任务 ID。
  3. 任务处理要有 Context、超时、日志和状态机。
  4. 所有错误返回统一结构和错误码。
  5. 日志带 request_id、user_id、cost_ms。
  6. 数据库和外部医院接口调用都要设置超时。

线上排查 ​

接口慢 ​

mermaid
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 ​

排查:

  1. Recovery 日志是否记录堆栈。
  2. panic 是 nil 指针、数组越界、类型断言失败还是并发 map 写。
  3. 是否缺少参数校验。
  4. 是否需要补测试覆盖。

连接耗尽 ​

常见原因:

  1. resp.Body 没关闭。
  2. 数据库 rows 没关闭。
  3. 请求没有超时,慢调用长期占连接。
  4. 下游连接池配置太小或下游真的慢。

面试标准回答 ​

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 只是换一层工具;真正决定项目质量的是职责清晰、生命周期可控、错误可观测、性能可排查。