跳转至

04 后端技术选型:为什么优先编译型语言

结论先说:AI 全流程开发的后端,优先选编译型语言(Go / Java / Rust)。 不是为了性能,而是为了把编译器、类型系统、静态分析和测试框架变成 AI 的“实时测谎仪”。


1. 编译型语言到底帮了什么忙

1.1 编译器是最快的“证伪器”

AI 生成代码时,最常犯的低级错误包括:

  • 调用了不存在的方法;
  • 参数类型不匹配;
  • 返回值没处理;
  • 结构体字段拼错;
  • 接口实现不完整;
  • 导入路径错误;
  • 并发原语使用错误(部分可被 race detector 发现)。

这些错误在动态语言里可能要到运行时、甚至线上才暴露;在编译型语言里,go build / mvn compile / cargo check 几秒钟就能报出来。Agent 可以在一个闭环里“写 → 编译 → 修 → 再编译”,不需要人介入。

1.2 类型系统是“契约的机器可读版本”

后端 API 的请求/响应结构、数据库扫描结构、领域对象,都可以用类型表达。当前端从 OpenAPI 生成类型、后端用类型校验请求时,字段不一致会在编译期或测试期被发现,而不是上线后。

1.3 重构更安全

AI 经常需要跨文件重命名、抽接口、改签名。编译型语言会在所有调用点报错,AI 可以顺着错误列表逐个修复。动态语言则可能在运行时才炸。

1.4 静态分析生态成熟

  • Go:go vet、staticcheck、golangci-lint、go test -race。
  • Java:Error Prone、SpotBugs、Checkstyle、SonarQube、mvn verify。
  • Rust:cargo clippy、cargo fmt、cargo test,所有权系统在编译期消除大量内存/并发错误。

1.5 部署简单

编译产物是单个二进制(Go/Rust)或自包含 jar(Java),部署时不需要在目标机器上装一堆运行时依赖,容器镜像也更小。

1.6 但编译通过 ≠ 业务正确

必须反复强调:

  • 编译器不知道“这个用户是否有权限删除这个任务”;
  • 编译器不知道“并发扣库存要加锁”;
  • 编译器不知道“这个字段应该是 UTC”;
  • 编译器不知道“这个 API 的语义和前端预期是否一致”。

所以编译型语言只是 第一道防线。后面还需要:

编译/类型检查 → 静态分析 → 单元测试 → 集成测试
→ 契约测试 → E2E → 人工验收

2. Go / Java / Rust 怎么选

2.1 对比表

维度 Go Java (Spring Boot) Rust
AI 生成可靠性 高:语法简单、约定少、错误信息清晰 高:生态成熟、模式固定、AI 训练数据多 中:所有权/生命周期对 AI 仍有挑战,编译错误多
编译速度 很快 中等(大型项目偏慢) 慢(尤其泛型/宏)
运行时性能 高 高(JVM 预热后) 极高
并发模型 goroutine + channel,简单 线程池 + 虚拟线程(新版本) async/await + 所有权,最严格
Web 生态 成熟(chi/gin/echo) 非常成熟(Spring 全家桶) 成长中(axum/actix)
数据库生态 成熟(pgx/sqlc/gorm) 非常成熟(JPA/MyBatis) 成熟(sqlx/diesel)
部署 单二进制,极简 jar + JVM,较重 单二进制,极简
学习曲线 低 中 高
适合场景 大多数 API/微服务/CLI 企业级、复杂业务、大团队 高性能、系统级、强安全
AI 全流程推荐度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐

2.2 推荐路径

默认选 Go,除非满足以下条件:

  • 团队已经是 Java 技术栈,或有大量 Spring 资产 → Java。
  • 有极致性能/内存/安全要求(高频交易、媒体处理、嵌入式网关) → Rust。
  • 需要复杂的领域建模、工作流引擎、规则引擎,且团队 Java 经验强 → Java。

单人 + AI 做产品:Go 的“简单、快、单二进制、部署省心”优势最大。

企业内网系统:Java + Spring Boot 的生态和招聘优势更大。

性能敏感的基础设施:Rust 值得投入,但要做好 AI 生成速度变慢、编译等待变长的准备。

2.3 一个决策记录模板(ADR)

# ADR-001 后端语言与框架选择

## 状态
已接受

## 背景
我们需要在 3 周内交付一个团队任务协作系统,团队 1 人 + AI 团队。
需要快速迭代、低运维成本、强类型约束。

## 决策
后端使用 Go 1.23 + chi + pgx/sqlc + PostgreSQL。

## 理由
1. 编译快,AI 可以在 1-2 秒内得到反馈。
2. 单二进制部署,容器镜像小,AWS 上运维简单。
3. 类型系统足以表达 API/DB 契约,配合 sqlc 可从 SQL 生成类型。
4. 团队有 Go 经验,AI 训练数据充分。
5. 并发模型简单,适合 API 服务。

## 备选方案
- Java/Spring Boot:生态更强,但启动慢、配置多,单人维护成本高。
- Rust/axum:性能更好,但 AI 编译-修复循环慢,交付风险高。

## 后果
- 需要自己搭建一部分基础设施(迁移、日志、错误处理)。
- 复杂领域建模不如 Java 生态丰富。
- 后续如果团队扩大,需要统一代码规范。

3. Go 技术栈推荐

3.1 核心选型

层 选择 说明
语言 Go 1.23+ 使用最新稳定版
HTTP chi 或 gin chi 更轻、标准库风格;gin 生态更大
数据库 PostgreSQL 16+ JSONB、事务、索引、扩展丰富
驱动 pgx/v5 性能好、类型支持好
查询 sqlc 从 SQL 生成类型安全代码,强烈推荐
迁移 goose 或 atlas 版本化、可回滚
配置 env + koanf/viper 12-factor,环境变量优先
日志 log/slog 标准库结构化日志
校验 go-playground/validator 请求校验
测试 testing + testify 标准库 + 断言
集成测试 testcontainers-go 真实 PostgreSQL/Redis
API 文档 OpenAPI 3.1 + oapi-codegen 契约优先,生成 server/client 类型
可观测 OpenTelemetry + Prometheus 日志、指标、链路
容器 Docker 多阶段构建 小镜像
CI GitHub Actions / GitLab CI lint/test/build/scan/deploy

3.2 为什么推荐 sqlc

传统 ORM 让 AI 写查询时容易“猜字段”。sqlc 的工作方式是:

  1. 你写 SQL;
  2. sqlc generate 解析 SQL 和 schema;
  3. 生成类型安全的 Go 代码;
  4. 字段拼错、类型不匹配会在生成期/编译期报错。

这对 AI 非常友好:SQL 是显式的,生成代码是确定的,编译器会兜底。


4. 架构:分层 + 依赖注入 + 请求隔离

4.1 推荐目录

backend/
├── cmd/
│   └── api/
│       └── main.go              # 入口:装配依赖、启动 HTTP
├── internal/
│   ├── config/                  # 配置加载
│   ├── domain/                  # 领域模型、错误、接口
│   │   ├── user.go
│   │   ├── project.go
│   │   ├── task.go
│   │   └── errors.go
│   ├── usecase/                 # 业务逻辑
│   │   ├── auth/
│   │   ├── project/
│   │   └── task/
│   ├── adapter/
│   │   ├── http/                # handler、middleware、路由
│   │   │   ├── router.go
│   │   │   ├── middleware/
│   │   │   ├── auth_handler.go
│   │   │   ├── project_handler.go
│   │   │   └── task_handler.go
│   │   └── postgres/            # repository 实现、sqlc 生成代码
│   │       ├── db/
│   │       └── repo/
│   ├── platform/                # 日志、数据库、缓存、时钟、ID 生成
│   └── observability/           # metrics、tracing
├── migrations/
│   ├── 00001_init.sql
│   └── 00002_add_tasks.sql
├── api/
│   └── openapi.yaml
├── sqlc.yaml
├── Dockerfile
├── Makefile
└── go.mod

4.2 依赖方向

adapter/http  ──► usecase ──► domain
adapter/postgres ──► usecase ──► domain
platform ──► 被所有层使用(只提供基础设施)

规则:

  • domain 不依赖任何外部框架。
  • usecase 只依赖 domain 的接口,不直接依赖 pgx。
  • adapter 实现接口,负责协议转换。
  • 依赖在 main.go 中一次性装配,运行期只读。

4.3 请求隔离:这是多 Agent 并行开发的底线

“保持请求间互不干扰”不是一句口号,它需要具体的设计约束:

规则 1:禁止包级可变状态

// 错误:全局可变 map,并发请求会互相污染
var cache = map[string]User{}

// 正确:依赖注入,只读,或使用并发安全且有清晰边界的缓存
type Handler struct {
    users UserRepository
    cache Cache
}

规则 2:每个请求有独立的 context

func (h *TaskHandler) Create(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()

    // 从 context 获取 request_id、user_id、tenant_id
    reqID := middleware.RequestIDFrom(ctx)
    userID := auth.UserIDFrom(ctx)
    tenantID := auth.TenantIDFrom(ctx)
    _ = reqID
    _ = userID
    _ = tenantID
}

规则 3:用户/租户信息只从 context 取,不存全局

// 错误:全局当前用户
var CurrentUser *User

// 正确:context
type ctxKey string
const userKey ctxKey = "user"

func WithUser(ctx context.Context, u *User) context.Context {
    return context.WithValue(ctx, userKey, u)
}

func UserFrom(ctx context.Context) (*User, bool) {
    u, ok := ctx.Value(userKey).(*User)
    return u, ok
}

规则 4:每个写请求一个事务

func (s *TaskService) Create(ctx context.Context, in CreateTaskInput) (*Task, error) {
    var task *Task
    err := s.db.WithTx(ctx, func(tx pgx.Tx) error {
        // 校验项目存在且用户有权限
        if err := s.projects.CheckMembership(ctx, tx, in.ProjectID, in.UserID); err != nil {
            return err
        }
        t, err := s.tasks.Insert(ctx, tx, in)
        if err != nil {
            return err
        }
        if err := s.audit.Log(ctx, tx, "task.created", t.ID, in.UserID); err != nil {
            return err
        }
        task = t
        return nil
    })
    return task, err
}

规则 5:连接池有上限,超时有兜底

pool, err := pgxpool.NewWithConfig(ctx, &pgxpool.Config{
    ConnConfig: &pgx.ConnConfig{...},
    MaxConns:   20,
    MinConns:   2,
    MaxConnLifetime: time.Hour,
    MaxConnIdleTime: 30 * time.Minute,
})

规则 6:幂等

创建类接口支持 Idempotency-Key:

POST /api/v1/tasks
Idempotency-Key: 6f1c2e9a-...

服务端在 Redis/DB 中记录 key → response,重复请求直接返回第一次结果。否则用户双击、网络重试会产生重复任务。

规则 7:panic 不拖垮进程

func Recover(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            defer func() {
                if rec := recover(); rec != nil {
                    logger.Error("panic recovered",
                        "request_id", middleware.RequestIDFrom(r.Context()),
                        "panic", rec,
                        "stack", string(debug.Stack()),
                    )
                    http.Error(w, `{"code":"INTERNAL_ERROR","message":"internal error"}`, 500)
                }
            }()
            next.ServeHTTP(w, r)
        })
    }
}

规则 8:优雅关闭

srv := &http.Server{Addr: ":8080", Handler: router}
go func() {
    if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)

规则 9:后台任务与请求路径分离

耗时操作(发邮件、生成报表、调用第三方)不要阻塞请求。写入任务表/队列,由 worker 处理。worker 也要有幂等和重试上限。

规则 10:缓存 key 必须带租户/用户维度

key := fmt.Sprintf("task:%s:tenant:%s", taskID, tenantID)

否则多租户场景会串数据。


5. 代码骨架

5.1 main.go

package main

import (
    "context"
    "errors"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    apphttp "example.com/taskapp/internal/adapter/http"
    "example.com/taskapp/internal/adapter/postgres/repo"
    "example.com/taskapp/internal/config"
    "example.com/taskapp/internal/platform/db"
    "example.com/taskapp/internal/usecase/task"
)

func main() {
    cfg, err := config.Load()
    if err != nil {
        slog.Error("load config", "err", err)
        os.Exit(1)
    }

    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: cfg.LogLevel,
    }))
    slog.SetDefault(logger)

    ctx := context.Background()
    pool, err := db.NewPool(ctx, cfg.DatabaseURL)
    if err != nil {
        logger.Error("connect db", "err", err)
        os.Exit(1)
    }
    defer pool.Close()

    taskRepo := repo.NewTaskRepo(pool)
    auditRepo := repo.NewAuditRepo(pool)
    taskSvc := task.NewService(taskRepo, auditRepo)

    router := apphttp.NewRouter(apphttp.Deps{
        Logger: logger,
        Tasks:  taskSvc,
        Health: db.NewHealthChecker(pool),
    })

    srv := &http.Server{
        Addr:              cfg.HTTPAddr,
        Handler:           router,
        ReadHeaderTimeout: 5 * time.Second,
        ReadTimeout:       15 * time.Second,
        WriteTimeout:      30 * time.Second,
        IdleTimeout:       60 * time.Second,
    }

    go func() {
        logger.Info("server started", "addr", cfg.HTTPAddr)
        if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            logger.Error("server error", "err", err)
            os.Exit(1)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        logger.Error("graceful shutdown failed", "err", err)
    }
    logger.Info("server stopped")
}

5.2 错误模型

// internal/domain/errors.go
package domain

import "errors"

type Code string

const (
    CodeValidation       Code = "VALIDATION_ERROR"
    CodeUnauthorized     Code = "UNAUTHORIZED"
    CodeForbidden        Code = "FORBIDDEN"
    CodeNotFound         Code = "NOT_FOUND"
    CodeConflict         Code = "CONFLICT"
    CodeInternal         Code = "INTERNAL_ERROR"
    CodeProjectArchived  Code = "PROJECT_ARCHIVED"
    CodeAssigneeNotInProj Code = "ASSIGNEE_NOT_IN_PROJECT"
)

type AppError struct {
    Code    Code
    Message string
    Err     error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return string(e.Code) + ": " + e.Message + ": " + e.Err.Error()
    }
    return string(e.Code) + ": " + e.Message
}

func (e *AppError) Unwrap() error { return e.Err }

func New(code Code, msg string) *AppError {
    return &AppError{Code: code, Message: msg}
}

func IsCode(err error, code Code) bool {
    var ae *AppError
    return errors.As(err, &ae) && ae.Code == code
}

统一响应:

{
  "code": "VALIDATION_ERROR",
  "message": "标题不能为空",
  "request_id": "01J...",
  "details": { "field": "title" }
}

所有 handler 的错误映射集中在一处,不要让每个 handler 自己拼 JSON。

5.3 middleware 顺序

RequestID → Logger → Recover → Timeout → CORS → Auth → RateLimit → Handler
  • RequestID 最先,保证后续日志都有 ID。
  • Recover 在业务之前,捕获 panic。
  • Auth 在业务之前,注入 user/tenant。
  • RateLimit 可以按用户/IP 限流。

5.4 handler 示例

func (h *TaskHandler) Create(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    user, ok := auth.UserFrom(ctx)
    if !ok {
        writeError(w, r, domain.New(domain.CodeUnauthorized, "未登录"))
        return
    }

    var req CreateTaskRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, r, domain.New(domain.CodeValidation, "请求格式错误"))
        return
    }
    if err := req.Validate(); err != nil {
        writeError(w, r, domain.New(domain.CodeValidation, err.Error()))
        return
    }

    t, err := h.svc.Create(ctx, task.CreateInput{
        ProjectID:   req.ProjectID,
        Title:       req.Title,
        Description: req.Description,
        AssigneeID:  req.AssigneeID,
        DueDate:     req.DueDate,
        Priority:    req.Priority,
        UserID:      user.ID,
    })
    if err != nil {
        writeError(w, r, err)
        return
    }
    writeJSON(w, http.StatusCreated, toTaskResponse(t))
}

5.5 错误映射

func writeError(w http.ResponseWriter, r *http.Request, err error) {
    var ae *domain.AppError
    if !errors.As(err, &ae) {
        ae = domain.New(domain.CodeInternal, "服务内部错误")
    }

    status := http.StatusInternalServerError
    switch ae.Code {
    case domain.CodeValidation:
        status = http.StatusBadRequest
    case domain.CodeUnauthorized:
        status = http.StatusUnauthorized
    case domain.CodeForbidden:
        status = http.StatusForbidden
    case domain.CodeNotFound:
        status = http.StatusNotFound
    case domain.CodeConflict, domain.CodeProjectArchived:
        status = http.StatusConflict
    }

    writeJSON(w, status, ErrorResponse{
        Code:      string(ae.Code),
        Message:   ae.Message,
        RequestID: middleware.RequestIDFrom(r.Context()),
    })
}

所有 handler 共享同一个 writeError,保证错误格式一致;request_id 从当前请求的 context 取,不能用 context.Background()。


6. 数据库设计

6.1 建模原则

  • 每张表有主键(UUID 或 BIGSERIAL)。
  • 有 created_at / updated_at(UTC)。
  • 需要并发控制时加 version(乐观锁)。
  • 软删除用 deleted_at,并加部分索引。
  • 外键和唯一约束尽量交给数据库。
  • 多租户表必须有 tenant_id,并在所有查询中带上。
  • 审计日志独立表,只追加,不修改。

6.2 示例 schema

CREATE TABLE users (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email         CITEXT NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,
    display_name  TEXT NOT NULL,
    status        TEXT NOT NULL DEFAULT 'active',
    created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE projects (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id   UUID NOT NULL,
    name        TEXT NOT NULL,
    short_code  TEXT NOT NULL,
    status      TEXT NOT NULL DEFAULT 'active',
    created_by  UUID NOT NULL REFERENCES users(id),
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    deleted_at  TIMESTAMPTZ,
    UNIQUE (tenant_id, short_code)
);

CREATE TABLE project_members (
    project_id UUID NOT NULL REFERENCES projects(id),
    user_id    UUID NOT NULL REFERENCES users(id),
    role       TEXT NOT NULL DEFAULT 'member',
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (project_id, user_id)
);

CREATE TABLE tasks (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id  UUID NOT NULL REFERENCES projects(id),
    title       TEXT NOT NULL CHECK (length(title) BETWEEN 1 AND 100),
    description TEXT,
    assignee_id UUID REFERENCES users(id),
    due_date    DATE,
    priority    TEXT NOT NULL DEFAULT 'medium',
    status      TEXT NOT NULL DEFAULT 'todo',
    version     INTEGER NOT NULL DEFAULT 0,
    created_by  UUID NOT NULL REFERENCES users(id),
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    deleted_at  TIMESTAMPTZ
);

CREATE INDEX idx_tasks_project_status_due
    ON tasks (project_id, status, due_date)
    WHERE deleted_at IS NULL;

CREATE TABLE audit_logs (
    id          BIGSERIAL PRIMARY KEY,
    tenant_id   UUID NOT NULL,
    actor_id    UUID NOT NULL,
    action      TEXT NOT NULL,
    resource    TEXT NOT NULL,
    detail      JSONB NOT NULL DEFAULT '{}',
    request_id  TEXT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_audit_tenant_created ON audit_logs (tenant_id, created_at DESC);

6.3 迁移规则

  • 迁移文件按时间戳命名:00001_init.sql、00002_add_tasks.sql。
  • 迁移必须可重复执行(IF NOT EXISTS)或有版本表。
  • 禁止修改已上线的迁移;新增变更用新文件。
  • 生产迁移遵循 expand → migrate → contract:
  • 先加新列/新表(兼容旧代码);
  • 双写/回填;
  • 切换读;
  • 删除旧列/旧代码。
  • 大表加索引用 CREATE INDEX CONCURRENTLY(PostgreSQL)。
  • 每次迁移前备份,准备回滚脚本。

7. 测试策略

7.1 分层

层 范围 工具 目标
单元测试 domain、usecase testing + testify 业务规则、边界、错误
集成测试 repository + DB testcontainers + 真实 PostgreSQL SQL、约束、事务
API 测试 handler + router httptest 状态码、错误格式、鉴权
契约测试 OpenAPI oapi-codegen + schema 校验 请求/响应符合契约
E2E 全链路 Chrome MCP / Playwright 用户流程

7.2 覆盖率

  • 核心业务(auth、权限、金额、状态机)≥ 80%。
  • 一般 CRUD ≥ 60%。
  • 不要为了覆盖率写“调用一下就通过”的假测试。
  • 关键路径用表驱动测试覆盖边界:
func TestCreateTask_Validation(t *testing.T) {
    tests := []struct{
        name string
        in   CreateInput
        want domain.Code
    }{
        {"empty title", CreateInput{Title: ""}, domain.CodeValidation},
        {"title too long", CreateInput{Title: strings.Repeat("a", 101)}, domain.CodeValidation},
        {"invalid priority", CreateInput{Title: "x", Priority: "urgent"}, domain.CodeValidation},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := svc.Create(context.Background(), tt.in)
            if !domain.IsCode(err, tt.want) {
                t.Fatalf("want %s, got %v", tt.want, err)
            }
        })
    }
}

7.3 并发测试

go test -race ./...

对共享状态、缓存、连接池、幂等逻辑,专门写并发测试。AI 很容易忽略竞态,-race 是最便宜的检查。

7.4 集成测试示例

func TestTaskRepo_Create(t *testing.T) {
    ctx := context.Background()
    pg, err := testcontainers.RunPostgres(ctx)
    if err != nil { t.Fatal(err) }
    defer pg.Close()

    require.NoError(t, migrate.Up(pg.URL))

    repo := postgres.NewTaskRepo(pg.Pool)
    task, err := repo.Insert(ctx, Task{Title: "test", ProjectID: pg.ProjectID})
    require.NoError(t, err)
    require.NotEmpty(t, task.ID)
}

8. 安全清单

  • 所有 SQL 使用参数化查询(sqlc/pgx 默认)。
  • 所有对象级权限在后端校验(不能只靠前端隐藏按钮)。
  • 密码用 Argon2id 或 bcrypt 哈希,不存明文。
  • 令牌短期有效,刷新令牌可撤销。
  • 敏感字段不写日志(密码、令牌、身份证、银行卡)。
  • CORS 白名单,不用 * 配凭证。
  • 请求体大小限制。
  • 速率限制(登录、发送验证码、创建资源)。
  • 文件上传校验类型/大小/病毒扫描(如需要)。
  • 依赖漏洞扫描(govulncheck / OWASP Dependency-Check / cargo audit)。
  • 密钥来自环境变量/密钥管理服务,不进仓库。
  • 审计日志不可篡改(至少只追加)。
  • 生产数据库不暴露公网。
  • 错误响应不泄露堆栈/SQL。

9. 可观测性

9.1 日志

结构化 JSON,字段统一:

{
  "time": "2026-10-25T12:00:00Z",
  "level": "INFO",
  "msg": "task created",
  "request_id": "01J...",
  "user_id": "uuid",
  "tenant_id": "uuid",
  "project_id": "uuid",
  "task_id": "uuid",
  "duration_ms": 12
}

9.2 指标

  • http_requests_total{method,path,status}
  • http_request_duration_seconds{method,path}
  • db_pool_connections{state}
  • task_created_total{project_id}
  • 业务指标:活跃用户、任务完成数、错误率。

9.3 健康检查

GET /healthz   → 进程存活
GET /readyz    → 数据库/依赖就绪

部署和负载均衡用不同的探针。


10. CI 门禁

# 简化示例
steps:
  - run: gofmt -l . | (! grep .)          # 格式
  - run: go vet ./...
  - run: golangci-lint run
  - run: go test -race -coverprofile=coverage.out ./...
  - run: go test -tags=integration ./...
  - run: govulncheck ./...
  - run: go build ./cmd/api
  - run: docker build -t app:$GITHUB_SHA .

规则:CI 不全绿,不允许合并。 Agent 说自己“完成”不算,CI 绿了才算。


11. Java 与 Rust 的补充建议

11.1 Java / Spring Boot

适合:

  • 企业级复杂业务;
  • 团队已有 Java 资产;
  • 需要成熟的工作流/权限/报表生态。

建议:

  • Spring Boot 3.x + Java 21+(虚拟线程)。
  • 用 Spring Data JPA 或 MyBatis,但不要让 AI 混用两套。
  • 用 mvn verify / gradle check 作为编译+测试门禁。
  • 用 ArchUnit 检查分层依赖。
  • 用 Testcontainers 做集成测试。
  • 用 Spring Security 统一鉴权,不要在 controller 手写权限判断。

11.2 Rust

适合:

  • 高性能网关、媒体处理、加密、系统工具;
  • 对内存安全/并发安全要求极高;
  • 团队愿意承担更长的编译和调试时间。

建议:

  • axum + tokio + sqlx。
  • cargo clippy -- -D warnings 作为门禁。
  • cargo test、cargo nextest。
  • 编译错误可能很多,Agent 需要更明确的提示词和更小的任务粒度。
  • 不要让 AI 一次生成 2000 行 Rust;按模块拆,小步编译。

12. 本章小结

  • 编译型语言的核心价值是:让 AI 的错误更早、更便宜地暴露。
  • Go 是单人 + AI 全流程的默认推荐;Java 适合企业复杂业务;Rust 适合高性能/强安全场景。
  • 架构上必须做到:依赖注入、无全局可变状态、每请求 context、每写请求事务、连接池上限、幂等、panic 恢复、优雅关闭。
  • 数据库建模要把约束、索引、审计、软删除、多租户隔离考虑进去。
  • 测试和 CI 才是“编译型语言优势”的放大器。

下一章:05 开发环境搭建 —— 在 Windows 上用 WSL2 打造 AI 友好的 Linux 开发环境。