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 的语义和前端预期是否一致”。
所以编译型语言只是 第一道防线。后面还需要:
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 的工作方式是:
- 你写 SQL;
sqlc generate解析 SQL 和 schema;- 生成类型安全的 Go 代码;
- 字段拼错、类型不匹配会在生成期/编译期报错。
这对 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:
服务端在 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 必须带租户/用户维度¶
否则多租户场景会串数据。
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最先,保证后续日志都有 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 并发测试¶
对共享状态、缓存、连接池、幂等逻辑,专门写并发测试。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 健康检查¶
部署和负载均衡用不同的探针。
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 开发环境。