Skip to content
Go
测试与部署

测试与部署

go-zero 服务开发完毕后,需要经过测试和部署两个环节进入生产环境。goctl 提供 Dockerfile 和 K8s 部署文件生成,测试方面遵循 Go 标准体系。


测试体系

单元测试

go-zero 的逻辑层结构天然适合单元测试——每个 Logic 是一个独立 struct,依赖通过 ServiceContext 注入,方便 Mock。

// internal/logic/getuserlogic_test.go
package logic

import (
    "context"
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

// Mock UserModel
type MockUserModel struct {
    mock.Mock
}

func (m *MockUserModel) FindOne(ctx context.Context, id int64) (*model.User, error) {
    args := m.Called(ctx, id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*model.User), args.Error(1)
}

func TestGetUserLogic_Success(t *testing.T) {
    mockModel := new(MockUserModel)
    mockModel.On("FindOne", mock.Anything, int64(1)).Return(&model.User{
        Id:   1,
        Name: "alice",
    }, nil)

    logic := &GetUserLogic{
        ctx:    context.Background(),
        svcCtx: &svc.ServiceContext{UserModel: mockModel},
    }

    resp, err := logic.GetUser(&types.GetUserReq{Id: 1})
    assert.NoError(t, err)
    assert.Equal(t, "alice", resp.Name)
}

func TestGetUserLogic_NotFound(t *testing.T) {
    mockModel := new(MockUserModel)
    mockModel.On("FindOne", mock.Anything, int64(999)).
        Return(nil, sqlx.ErrNotFound)

    logic := &GetUserLogic{
        ctx:    context.Background(),
        svcCtx: &svc.ServiceContext{UserModel: mockModel},
    }

    resp, err := logic.GetUser(&types.GetUserReq{Id: 999})
    assert.Error(t, err)
    assert.Nil(t, resp)
}

HTTP Handler 测试

func TestLoginHandler(t *testing.T) {
    // 构造请求
    body := `{"username":"admin","password":"123456"}`
    req := httptest.NewRequest("POST", "/user/login", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()

    // 模拟 ServiceContext(注入 Mock 依赖)
    svcCtx := &svc.ServiceContext{}

    // 调用 handler
    handler := LoginHandler(svcCtx)
    handler(w, req)

    // 断言
    assert.Equal(t, http.StatusOK, w.Code)
    var resp Response
    json.Unmarshal(w.Body.Bytes(), &resp)
    assert.Equal(t, 200, resp.Code)
}

RPC 测试

func TestGetUserRpc(t *testing.T) {
    // 启动 gRPC 服务端(使用随机端口)
    lis, _ := net.Listen("tcp", "127.0.0.1:0")
    s := zrpc.MustNewServer(c.RpcServerConf)
    user.RegisterUserServer(s, svcCtx)
    go s.Serve(lis)

    // 直连模式创建客户端
    conn, _ := zrpc.NewClient(zrpc.RpcClientConf{
        Endpoints: []string{lis.Addr().String()},
    })
    client := user.NewUser(conn)

    // 调用
    resp, err := client.GetUser(context.Background(), &user.GetUserReq{Id: 1})
    assert.NoError(t, err)
    assert.Equal(t, int64(1), resp.Id)
}

运行测试

# 运行所有测试
go test ./...

# 带覆盖率
go test -cover ./...

# 查看覆盖率详情
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# 指定测试函数
go test -run TestGetUser ./internal/logic/

Docker 部署

goctl 生成 Dockerfile

goctl docker -go user.go

生成的多阶段构建 Dockerfile:

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go env -w GOPROXY=https://goproxy.cn,direct
RUN go build -o user user.go

FROM alpine:latest
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/user /app/user
COPY --from=builder /app/etc /app/etc
EXPOSE 8888
CMD ["/app/user", "-f", "/app/etc/user-api.yaml"]

构建与运行

# 构建镜像
docker build -t user-api:v1.0.0 .

# 本地运行
docker run -p 8888:8888 \
    -e DB_DSN="root:password@tcp(host.docker.internal:3306)/dbname" \
    user-api:v1.0.0

Docker Compose(本地开发)

# docker-compose.yaml
version: "3.8"
services:
  etcd:
    image: quay.io/coreos/etcd:v3.5.9
    environment:
      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
      - ETCD_ADVERTISE_CLIENT_URLS=http://etcd:2379

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: dbname
    ports:
      - "3306:3306"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  jaeger:
    image: jaegertracing/all-in-one:1.57
    ports:
      - "16686:16686"
      - "4317:4317"
    environment:
      - COLLECTOR_OTLP_ENABLED=true

  user-rpc:
    build:
      context: ./service/user/rpc
    depends_on:
      - etcd
      - mysql
      - redis

  user-api:
    build:
      context: ./service/user/api
    ports:
      - "8888:8888"
    depends_on:
      - user-rpc
docker-compose up -d

Kubernetes 部署

goctl 生成 K8s YAML

goctl kube deploy \
    -name user-api \
    -namespace default \
    -image user-api:v1.0.0 \
    -secret docker-registry \
    -port 8888 \
    -nodePort 30080 \
    -requestCpu 100 \
    -requestMem 50 \
    -limitCpu 200 \
    -limitMem 100 \
    -replicas 3 \
    -o k8s/user-api-deploy.yaml

K8s 资源结构

生成的 YAML 包含:

# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-api
  template:
    metadata:
      labels:
        app: user-api
    spec:
      containers:
        - name: user-api
          image: user-api:v1.0.0
          ports:
            - containerPort: 8888
          resources:
            requests:
              cpu: 100m
              memory: 50Mi
            limits:
              cpu: 200m
              memory: 100Mi
          readinessProbe:
            tcpSocket:
              port: 8888
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            tcpSocket:
              port: 8888
            initialDelaySeconds: 15
            periodSeconds: 20
---
# Service
apiVersion: v1
kind: Service
metadata:
  name: user-api
spec:
  type: NodePort
  selector:
    app: user-api
  ports:
    - port: 8888
      nodePort: 30080

K8s 服务发现(Headless Service + DNS)

# user-rpc 的 Headless Service
apiVersion: v1
kind: Service
metadata:
  name: user-rpc-svc
spec:
  clusterIP: None           # Headless
  selector:
    app: user-rpc
  ports:
    - port: 8080
      name: grpc

API 端使用 DNS 直连:

UserRpcConf:
  Target: dns:///user-rpc-svc.default.svc.cluster.local:8080

ConfigMap 挂载配置

apiVersion: v1
kind: ConfigMap
metadata:
  name: user-api-config
data:
  user-api.yaml: |
    Name: user-api
    Host: 0.0.0.0
    Port: 8888
    UserRpcConf:
      Target: dns:///user-rpc-svc.default.svc.cluster.local:8080
# Deployment 中挂载
volumes:
  - name: config
    configMap:
      name: user-api-config
containers:
  - volumeMounts:
      - name: config
        mountPath: /app/etc

CI/CD 流程

标准流水线

代码提交 (git push)
单元测试 (go test)
代码分析 (golangci-lint)
构建镜像 (docker build)
推送镜像 (docker push Harbor)
部署 K8s (kubectl apply)
健康检查 (curl /health)

GitHub Actions 示例

name: Build and Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.21"

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

      - name: Build
        run: go build -o user-api .

      - name: Build Image
        run: |
          docker build -t harbor.example.com/app/user-api:${{ github.sha }} .
          docker push harbor.example.com/app/user-api:${{ github.sha }}

      - name: Deploy to K8s
        run: |
          kubectl set image deployment/user-api \
            user-api=harbor.example.com/app/user-api:${{ github.sha }}
          kubectl rollout status deployment/user-api

优雅下线

go-zero 服务自动处理 SIGTERM/SIGINT 信号:

// 自动执行的优雅停止流程:
// 1. 从 etcd 注销自身
// 2. 停止接受新连接
// 3. 等待现有请求完成(最多等待配置的 Timeout 时间)
// 4. 退出进程

自定义清理逻辑:

import "github.com/zeromicro/go-zero/core/proc"

proc.AddShutdownListener(func() {
    // 关闭数据库连接池
    db.Close()
    // 刷新缓冲区
    logger.Sync()
})

K8s 优雅停止配置

spec:
  terminationGracePeriodSeconds: 30  # 等待优雅停止的最长时间
  containers:
    - name: user-api
      lifecycle:
        preStop:
          exec:
            command: ["/bin/sh", "-c", "sleep 5"]  # 给负载均衡器时间摘除

压测与性能调优

使用 go-wrk / hey 压测

# 简单压测
hey -n 100000 -c 100 http://localhost:8888/user/1

# 查看结果:
# Requests/sec: 15000
# P50: 3ms, P95: 15ms, P99: 30ms

性能调优检查清单

方向 检查项
数据库 索引是否到位、是否走缓存、连接池大小
RPC 超时 是否设置合理的超时值
缓存 热点数据是否预热、过期时间是否合理
中间件 是否有多余的中间件、日志级别
并发 MaxConns、Processors 是否合理
容器 CPU/Memory 限制是否过紧

常见陷阱

陷阱 说明
🚨 生产镜像包含源代码 多阶段构建确保只复制二进制 + 配置
🚨 terminationGracePeriodSeconds 太短 K8s 没有给足够时间优雅停止,请求被强制中断
🚨 Readiness Probe 配置错误 服务还没完全启动就被分配流量,导致请求失败
🚨 数据库连接池默认值 生产环境高并发时默认连接池可能不够,需根据负载调整
🚨 配置硬编码在镜像中 通过 ConfigMap 或环境变量注入配置,才能实现一次构建多处部署
🚨 日志写到容器 rootfs 应使用 emptydir 或 sidecar 采集,避免写满磁盘