Skip to content

Go Web API

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

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

学习目标

学完本页你应该能回答:

  1. Go 标准库 Web 服务怎么启动。
  2. http.HandlerResponseWriterRequest 分别是什么。
  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 只是换一层工具;真正决定项目质量的是职责清晰、生命周期可控、错误可观测、性能可排查。