测试与部署
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.0Docker 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-rpcdocker-compose up -dKubernetes 部署
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.yamlK8s 资源结构
生成的 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: 30080K8s 服务发现(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: grpcAPI 端使用 DNS 直连:
UserRpcConf:
Target: dns:///user-rpc-svc.default.svc.cluster.local:8080ConfigMap 挂载配置
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/etcCI/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 采集,避免写满磁盘 |