Agent Developer Roadmap
全栈后端

Go / chi:标准库 HTTP、显式组合与 goroutine

核验日期:2026-07-16。本文以 Go 1.26.5 与 chi 5.3 作为主要讲解版本。Go 支持当前与前一 major,chi 文档声明兼容最近四个 Go major;生产固定 patch 并跟踪 Go 安全修订。

Go 后端的核心不是“语法少所以快”,而是标准库 net/http、接口组合、goroutine、channel、context、静态编译与直接部署形成一致工程模型。chi 是实现 http.Handler 的小型 router,不取代 server、验证、数据库或业务层。对前端开发者,主要迁移是从 Promise/exception/DI 容器转到显式 error、value/pointer、interface composition、context cancellation 与工具链。

请求链

http.Server 接受连接并为请求调度 handler;chi 匹配 method/path 并把 route params 放入 context;middleware 是 func(http.Handler) http.Handler;handler 解析协议;service 处理不变量。ResponseWriter 一旦写 header/body 就难改变 status,先完成可能失败的验证再写。

最小服务

package main

import (
    "encoding/json"
    "net/http"
    "strconv"

    "github.com/go-chi/chi/v5"
)

func listTechnologies(w http.ResponseWriter, r *http.Request) {
    limit := 20
    if raw := r.URL.Query().Get("limit"); raw != "" {
        parsed, err := strconv.Atoi(raw)
        if err != nil || parsed < 1 || parsed > 100 {
            writeProblem(w, r, http.StatusBadRequest, "INVALID_QUERY")
            return
        }
        limit = parsed
    }
    result, err := catalog.List(r.Context(), r.URL.Query().Get("query"), limit)
    if err != nil {
        writeError(w, r, err)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(result)
}

func routes() http.Handler {
    r := chi.NewRouter()
    r.Get("/technologies", listTechnologies)
    return r
}

环境 Go 1.26、chi 5.3;go run . 需在 main 创建带 timeout 的 http.Serverlimit=abc/101 返回统一 400。示例忽略 Encode 错误仅为局部展示,生产 wrapper 应在未写出前处理序列化;stream 中途失败只能终止连接/记录。

net/http Server 必须显式配置

http.ListenAndServe 适合示例,生产创建 http.Server 并设置 ReadHeaderTimeout、合理的 ReadTimeout/WriteTimeout/IdleTimeoutMaxHeaderBytes。timeout 语义与 streaming、proxy 有冲突,按 route/架构测试,不能复制固定数字。

请求 body 用 http.MaxBytesReader 限制,再 json.Decoder;通常 DisallowUnknownFields,并确认只有一个 JSON value。解析失败不回显原 body。压缩/代理后实际大小限制在正确层执行。

dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
dec.DisallowUnknownFields()
if err := dec.Decode(&input); err != nil {
    writeProblem(w, r, 400, "INVALID_BODY")
    return
}

Go struct tag 不自动执行语义验证。required、长度、枚举、跨字段要显式函数或小 validator 库。选择库时不要让 tag 变成不可读 DSL;业务不变量仍在 service/DB。

chi 组合与 middleware 次序

chi 与标准 http.Handler 兼容,可使用任何标准 middleware。推荐顺序通常 request id/recover/access log/security/auth/route-specific timeout,但错误和日志所有者要清楚。Recoverer 只处理 panic,不能把 panic 当普通 error;恢复后该请求返回 500,进程状态若涉及 unsafe/native 需评估退出。

func requestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := validIncomingID(r.Header.Get("X-Request-ID"))
        if id == "" { id = newID() }
        w.Header().Set("X-Request-ID", id)
        next.ServeHTTP(w, r.WithContext(withRequestID(r.Context(), id)))
    })
}

context key 使用私有自定义类型,避免冲突;context 只放 request-scoped metadata/deadline,不放可选 service。依赖通过 handler struct 字段显式注入。

type Handler struct { Catalog CatalogService; Log *slog.Logger }

小接口在消费方定义,只包含真实需要的方法。不要为每个 concrete struct 预先创建大 interface;测试可用函数/小 fake。constructor 验证非 nil 依赖,启动时装配一次。

goroutine、channel 与结构化生命周期

每个请求已经由 net/http 并发处理,不必为 handler 再 go func。goroutine 很轻但不是免费;无界创建会积累 stack、连接和内存。所有 goroutine 应有 owner、停止信号和 join/errgroup。

group, ctx := errgroup.WithContext(r.Context())
group.SetLimit(8)
for _, id := range ids {
    id := id
    group.Go(func() error { return fetchOne(ctx, id) })
}
if err := group.Wait(); err != nil { return err }

有界 errgroup 在一个失败时取消 siblings,但底层必须观察 context。不要把 loop variable 捕获问题的历史规则当永久假设;仍用清楚局部变量和当前 Go 版本测试。

channel 适合 ownership transfer/coordination,不是所有共享状态都要 channel。简单计数用 atomic/mutex;Map 并发写需锁或单 owner;channel close 由 sender/owner 负责,接收方不随意关闭。用 go test -race 发现数据竞争,但 race-free 不代表无死锁或业务竞态。

Context、deadline 与取消

r.Context() 在客户端断开、server 取消或 deadline 时 done。一路传入 DB、HTTP client 和 service;不要用 context.Background() 截断。不要把 context 存 struct 或传 nil。超时由入口预算,内层留更短预算;retry 共享总 deadline。

取消不等于回滚已提交数据库或撤回外部副作用。响应断开后写命令可能已完成,客户端重试必须依赖幂等。后台 durable task 不从 request context 启动;先持久化,再由 worker 自己的 context 处理。

错误:值、包装与 HTTP 映射

Go error 显式返回,使用 %w 包装 cause,errors.Is/As 分类;不要字符串比较。领域定义 sentinel/typed error,HTTP adapter 映射。

var ErrNotFound = errors.New("not found")

if errors.Is(err, ErrNotFound) {
    writeProblem(w, r, 404, "TECHNOLOGY_NOT_FOUND")
    return
}

底层 SQL error 不直接冒到客户端;repository 映射 duplicate/foreign key/deadlock 等已知类别,保留 cause 日志。panic 只用于不可能状态/程序 bug,普通输入和依赖失败返回 error。忽略 error(包括 Close/Flush/Rows.Err/Encode)会产生静默数据问题。

值、指针与可选字段

Go 零值有用,但 JSON patch 中无法区分未提供和 0/false/""。使用指针、自定义 optional 或字段集合表达协议,不把 database nullable、create required、patch optional 混一 struct。slice nil 编码 null 而 empty slice 编码 [],公共契约需规范化。

大 struct 指针减少复制不是默认理由;先按语义:需要共享/修改 identity 用指针,不可变小 value 用值。receiver 选择一致。map/slice 是 reference-like header,复制不等于深复制;并发共享需规则。

数据库与 database/sql

sql.DB 是并发安全连接池,不是一条连接,长寿命共享。设置 MaxOpenConns/MaxIdleConns/ConnMaxLifetime/IdleTime 并监控 Stats;每副本乘积不能压垮数据库。始终 context-aware query。

tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil { return err }
defer tx.Rollback()
// 业务写、outbox、idempotency 使用 tx;外部 HTTP 不在事务中。
if err := tx.Commit(); err != nil { return err }

defer Rollback 在 commit 后通常返回已结束,安全清理。transaction retry 要重跑纯 DB closure,外部副作用移出。参数化 query 防注入;动态排序/列名用 allowlist,不把 identifier 当参数。

ORM/query generator 选择:手写 SQL 最直接;sqlc 类生成器让 SQL 与静态类型结合;ORM 提升 CRUD 但隐藏 query/transaction。无论哪种都看真实 SQL、索引、N+1、migration 和 trace。测试真实数据库。

缓存、消息与 worker

进程内 cache 多副本不一致且需 mutex/容量;共享 cache key 含 tenant/version,序列化错误视 miss/告警。singleflight 可在单进程合并同 key,跨实例仍需源站保护。

consumer loop 用 context/errgroup,先停止拉取再 drain;消息至少一次,handler 幂等。业务事务 + outbox,relay publish 后标记;consumer inbox/unique event id。不要启动 goroutine 发布后立即返回 HTTP,进程退出会丢。

Auth 与安全

middleware 解析 token,service 授权。Go crypto/JWT 库选择维护活跃、算法固定;拒绝 alg=none/算法混淆;验证 issuer/audience/time。请求 body/header/path 限制,出站 URL allowlist,SQL 参数化,HTML/template 按上下文转义。os/exec 使用参数数组和 executable allowlist。

Go binary 仍可能含调试符号/字符串和 secret,secret 不编译进 ldflags。pprof endpoint 可能泄露内存/路径,只在受保护管理网络开启。

日志、metrics、trace、pprof

标准 log/slog 提供结构化日志;传 logger/context,字段稳定。route pattern 而不是 raw path 作为 metric label。OpenTelemetry instrumentation 跨 net/http、DB 和 client,验证 propagation。pprof 用于 CPU/heap/goroutine/block/mutex;采集有开销和敏感数据。

goroutine 泄漏表现为数量上升、stack 指向 channel/send/HTTP;保存 profile diff。GC/RSS 和 allocation profile解释内存,不先调 GOGC。Go 1.26 runtime/toolchain变化以 release notes为准。

测试与 fuzz

httptest.NewRecorder/NewRequest 测 handler,httptest.Server 测真实 client/protocol;table-driven tests覆盖输入测试范围;go test -race ./...;内置 fuzz 测 parser/cursor。DB container 集成;contract suite live server。

func TestRejectsLargeLimit(t *testing.T) {
    req := httptest.NewRequest("GET", "/technologies?limit=101", nil)
    rec := httptest.NewRecorder()
    routes().ServeHTTP(rec, req)
    if rec.Code != 400 { t.Fatalf("got %d", rec.Code) }
}

并发/idempotency 测试用 barrier 同时提交,不只顺序重复。用 fake clock 避免 sleep。race detector 会变慢,不用于性能结论。

Build、容器与 shutdown

go.mod/go.sum 固定模块;go mod verify、vulncheck;CGO 决定 libc/native 依赖,记录 CGO_ENABLED。静态单 binary 不等于无证书/时区文件需求。小镜像仍需 CA、非 root、可观测性和调试方案。

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() { <-ctx.Done(); shutdownCtx, cancel := context.WithTimeout(context.Background(), 8*time.Second); defer cancel(); server.Shutdown(shutdownCtx) }()
err := server.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) }

先 unready/摘流量,再 Shutdown;Hijacked/WebSocket/后台 worker需单独关闭。migration expand/contract,独立 job。

故障排查

高频错误:默认 server 无 timeout;无限 goroutine;context 被 Background 截断;response 写后再报错;rows/body 未关闭;session/tx 并发共享;nil/empty/null 契约漂移;error 字符串分类;pprof 公网暴露;pool 乘实例超限。

版本与来源

交付检查

保存 Go/chi 版本、server timeouts/body limits、handler contract、context propagation、goroutine owner/limit、race/profile、DB pool/transaction/idempotency/outbox、auth 负例、real DB tests、CGO/镜像、shutdown 与 p99/CPU/RSS。Go 的简洁来自显式边界,不来自省略错误处理。

运行演练:goroutine、连接与背压

对服务加测试指标:当前goroutine、HTTP in-flight、DB Stats、上游连接、队列、heap/GC。记录初始数据后增加上游延迟,观察每请求goroutine等待但pool/内存累积;context deadline后goroutine应退出、body/rows关闭。若goroutine profile长期停在channel/send/receive或client read,找到owner缺失。

模拟慢客户端写大响应。json.Encoder仍可能构建业务slice,真正流式需逐项编码/flush并处理disconnect;WriteTimeout与stream相冲突。避免每项Flush造成小包/CPU,按chunk。响应写一半错误不能换status,记录bytes/error并终止。

SetLimit/worker semaphore压CPU/外部fan-out,队列满快速拒绝。Channel buffer不是容量策略的全部:写明大小依据、满时行为、shutdown drain。不要通过巨大buffer隐藏下游慢,它会增加恢复时间和内存。

Race-free之外的并发正确性

go test -race发现未同步内存访问,但不会发现两个正确加锁transaction都通过“余额足够”后超扣。业务竞态由数据库constraint/lock/isolation或单owner处理。用barrier同时发请求、真实DB检查最终状态和返回。

Mutex保护范围小且不跨慢I/O;锁顺序固定防deadlock;RWMutex只有读远多且临界区合适才收益。sync.Map适合特定模式,不是类型安全cache替代。Atomic用于简单状态,多个字段不变量仍需锁/单owner。

goroutine panic若未recover会终止进程;HTTP server对handler panic的行为与自定义middleware需了解,但后台goroutine必须在owner捕获/上报或让进程fail。不要静默recover继续丢任务。

HTTP client正确配置

复用长寿命http.Client/Transport,不每请求新建,设置整体deadline通常用request context,配置Dial/TLS/response header/idle pool按场景。读取/关闭body以复用连接;非2xx也消费有限body后关闭。redirect回调重新验证host/credential,防SSRF和Authorization泄露。

默认Transport共享可用但全局可变配置危险。每上游建立语义client wrapper:base URL、auth、timeout/retry、metrics、error mapping。retry仅幂等method/带key写,退避抖动且总deadline;响应5xx/429按策略,网络unknown result谨慎。

Modules、生成代码与供应链

Go module使用semantic import version,v2+路径含/vNgo.modgo/toolchain影响语言/工具行为;固定CI toolchain。go.sum校验模块内容但不等于审计。GONOSUMDB/GOPRIVATE/GOPROXY与private source凭据记录。

go generate、codegen、cgo在构建执行工具/代码,生成文件要么提交并校验未漂移,要么CI确定性生成;版本固定。sqlc/OpenAPI/protobuf生成器是编译链依赖。不要让开发者本机不同generator产生噪声。

Go 1兼容承诺强,但Go每major runtime/stdlib/GODEBUG可能改变细节。支持当前/前一版本,双toolchain test;升级重点net/http/url/tls/crypto/template、GC和binary。Go 1.26安全patch已多次更新标准库,不能停在.0

运维与回滚

构建注入commit/version仅用非secret ldflags,/health不暴露完整build path。release binary保留符号或外部debug信息以pprof/perf;strip体积与诊断权衡。容器CPU quota下GOMAXPROCS当前行为按版本验证,worker/pool不按主机核心机械设置。

Shutdown演练包括HTTP、WebSocket、consumer、cron、goroutine tracker、DB/client。Server.Shutdown不等待任意后台goroutine;使用errgroup/tracker。第二次信号可强制退出。回滚旧binary前验证migration/data/event兼容。

上一篇:Python / FastAPI · 返回专栏导读 · 下一篇:Java / Spring Boot

在 GitHub 上编辑此页

本页目录