工程化

工程化解决的是"代码之外"的问题:代码放在哪里、质量由谁把关、提交后如何验证、最终如何交付。本章给出一套被广泛验证的最小工程实践。

标准项目布局

Go 社区没有强制的目录规范,但存在事实标准。中型服务的典型布局:

shortener/
├── cmd/shortener/        # main.go:唯一的入口,只做装配
├── internal/             # 私有代码:其他模块无法 import(编译器强制)
│   ├── handler/          # HTTP 层:路由、参数解析、响应
│   ├── service/          # 业务逻辑层
│   ├── store/            # 存储层:接口定义 + 实现
│   └── auth/             # 领域组件:认证、限流
├── migrations/           # 数据库迁移脚本
├── deployments/          # Dockerfile、docker-compose.yml
├── .golangci.yml         # 静态检查配置
├── Makefile              # 常用命令入口
└── go.mod

两条核心原则:

  • internal/ 是编译器强制的私有边界internal 下的包只能被其父目录内的代码导入,外部项目无法引用,公共 API 与内部实现天然隔离。
  • cmd/<名称>/main.go 只做装配:解析配置、连接依赖、注入构造函数后启动服务;业务逻辑一律下沉到 internal

静态检查:golangci-lint

go vetgofmt 是底线;生产项目普遍使用 golangci-lint 聚合数十个检查器。仓库根目录放置 .golangci.yml(v2 配置格式):

version: "2"

linters:
  # standard 含 errcheck、govet、ineffassign、staticcheck、unused 五大核心
  default: standard
  enable:
    - errorlint    # 检查 errors.Is/As 的正确使用
    - gosec        # 安全漏洞扫描(SQL 注入、弱随机等)
    - bodyclose    # HTTP 响应体未关闭
    - noctx        # HTTP 请求缺少 context
    - revive       # 代码风格与命名

formatters:
  enable:
    - gofmt
    - goimports
golangci-lint run        # 检查当前模块
golangci-lint run --fix  # 自动修复可修复问题
Tip

配置原则:从 default: standard 起步,按团队痛点逐个启用额外检查器。一次性开满上百个 linter 只会产生海量告警,让所有人关掉检查。CI 与本地必须使用同一份配置

Makefile:统一命令入口

把团队约定固化为命令,降低"怎么跑测试/怎么构建"的沟通成本:

.PHONY: test lint build docker

test:      ## 运行全部测试(含竞态检测)
	go test -race -cover ./...

lint:      ## 静态检查
	golangci-lint run

build:     ## 本地构建二进制
	CGO_ENABLED=0 go build -o bin/shortener ./cmd/shortener

docker:    ## 构建镜像
	docker build -f deployments/Dockerfile -t shortener:dev .

CI:GitHub Actions

推送与 PR 时自动执行"检查 + 测试 + 构建",这是质量的最后一道闸门:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache: true

      - name: Vet
        run: go vet ./...

      - name: Test
        run: go test -race -cover ./...

      - name: Lint
        uses: golangci/golangci-lint-action@v7
        with:
          version: latest

      - name: Build
        run: go build ./...

要点:go-version-file: go.mod 让 CI 的 Go 版本跟随模块声明,避免环境漂移;测试永远带 -race

交付:Docker 多阶段构建

Go 编译出的是静态二进制,镜像只需极小的运行时层:

# 阶段一:构建
FROM golang:1.24-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download          # 先复制依赖清单,利用层缓存
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/shortener ./cmd/shortener

# 阶段二:运行(极小镜像)
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/shortener /shortener
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/shortener"]
docker build -f deployments/Dockerfile -t shortener:v1.0.0 .
docker run -p 8080:8080 -e DATABASE_URL=... shortener:v1.0.0

:::tip 多阶段构建的关键点:

  • CGO_ENABLED=0 生成纯静态二进制,可在 distroless/alpine 等无 libc 镜像中运行。
  • COPY go.mod go.sumgo mod download,源码变更时不重装依赖,构建缓存命中率大幅提升。
  • -ldflags="-s -w" 去除调试符号,镜像体积减小约 30%。
  • nonroot 用户运行,收窄容器被攻破后的权限;distroless 不含 shell,进一步减少攻击面。
  • .dockerignore 中排除 .gitbindoc_build 等目录,加快构建上下文上传。 :::

小结

  • 布局记两条就够:入口在 cmd/,实现沉到 internal/
  • .golangci.yml(v2 格式)统一本地与 CI 的质量标准,从 standard 起步。
  • CI 最小闭环:vet → test -race → lint → build。
  • 多阶段 Docker 构建 + distroless,交付产物小而安全。

下一章把这些实践全部落地:实现一个带认证、测试与容器化部署的短链接服务。