[7天实战入门Go语言后端] Day 6:测试与 Docker 部署——单元测试与多阶段构建

本日关键词(实战):go test、表驱动测试、httptest、ResponseRecorder、Dockerfile、多阶段构建、镜像体积

本日语法/概念(实战):

语法/概念 实战用途 本日示例
*_test.gofunc TestXxx(t *testing.T) 单元测试,CI 必跑,改代码不慌 day6/handler 下 _test.go
表驱动 []struct{...} + 循环 多用例一次写清,易扩展 测试多组 path + 期望状态码
httptest.NewRequest + httptest.NewRecorder 测 HTTP Handler 不真起服务,速度快 HTTP 接口测试
go test ./day6/... 里的 ... 表示「该目录及所有子目录下的包」 否则只认 day6/ 一层,跑不到 handler
Dockerfile 多阶段构建 先 go build 再拷二进制到小镜像,体积小 day6/Dockerfile
docker build / docker run 本地或 CI 打镜像、跑容器 部署流程

获取实战代码 :如需在本地跑通本文示例,请克隆仓库 WenSongWang/go-quickstart-7days,本文示例在 day6 目录,克隆后在项目根目录执行下文中的命令即可。


一、本篇目标

学完本文并跑通本目录示例,你将掌握:

模块 内容
单元测试 _test.gotesting、表驱动测试
HTTP 测试 httptest.NewRequestNewRecorder,不占端口测 Handler
Docker 多阶段构建 Go 镜像(本目录 Dockerfile 构建 Day 7 的 server)

二、前置要求

  • 已完成 Day 1~5
  • 运行 Docker 相关命令需本机已安装 Docker(可选)。

三、本日目录结构与示例(先混个眼熟)

目录结构

复制代码
day6/
├── handler/
│   ├── handler.go        # 被测 Handler:GetUser,按路径 id 返回 JSON
│   └── handler_test.go   # 表驱动 + httptest 测 GetUser
├── Dockerfile            # 多阶段构建,产物为 day7 的 server
├── README.md
└── csdn.md
目录/文件 说明
handler/ 一个包:handler.go 提供 GetUser,handler_test.go 测它;同包故测试里直接调 GetUser,无需 import。
Dockerfile 两阶段:builder 用 golang 镜像编译,最终阶段用 alpine 只放二进制 + 运行;构建的是 ./day7/cmd/server

示例与知识点

示例 主要知识点
handler/handler.go GetUser:Method 校验、TrimPrefix 取 id、Atoi 解析、合法 200 / 非法 400
handler/handler_test.go 表驱动 []struct、httptest.NewRequest/NewRecorder、GetUser(rec, req)、断言 rec.Code
Dockerfile FROM AS builder、COPY go.mod、go build、COPY --from=builder、EXPOSE、CMD

四、核心概念与最小示例(不看代码也能懂)

为什么用「表驱动」?

把多组「输入 + 期望结果」写进一个 []struct,用 for 循环依次调被测函数并断言。这样加用例只需在切片里多写一行,易读、易维护,是 Go 里常见的测试写法。

为什么用 httptest,不直接起一个 HTTP 服务再请求?

起真实服务要占端口、要起 goroutine,测试慢且容易受端口占用影响。httptest.NewRequest 造一个「假的」*http.RequestNewRecorder() 造一个**「录响应」的 Writer**(「录」= 记录:Handler 往它写状态码、Body 时,不真发到网络,而是记在 Recorder 里,测试里用 rec.Coderec.Body 读出来做断言)。把这两个传给 Handler,Handler 像平时一样写,结果都记在 Recorder 里。不经过网络、不占端口,速度快,适合 CI。

go test ./day6/... 里的 ... 是什么?

表示「当前路径及所有子目录 下的包」。本项目中 .go 在 day6/handler/ 里,不在 day6/ 下;若只写 go test ./day6,Go 只看 day6 这一层,没有 .go 就没有包,不会跑测试。写成 ./day6/... 才会把 day6/handler 算进去,从而执行 handler_test.go

为什么测试里能直接调 GetUser,不用 import?

因为 handler_test.gohandler.go 都在同一目录且都是 package handler ,属于同一个包 。Go 规定:同一包内的所有 .go 文件共享命名空间,所以 handler.go 里导出的 GetUser 在测试里直接可见 ,无需 import。Go 里目录即包 ,不需要像 Python 那样放 __init__.py

Dockerfile 为什么是两阶段?为什么构建的是 day7?

  • 两阶段 :第一阶段用带 Go 编译器的镜像(如 golang:1.21-alpine)编译出二进制;第二阶段用很小的 alpine 只拷贝二进制和 ca-certificates,不包含源码和编译器,镜像体积小,部署快。
  • 构建 day7 :本系列把「可运行的应用」放在 Day 7,Day 6 只讲测试和镜像写法;所以 Dockerfile 里 go build ... ./day7/cmd/server,打出来的镜像跑的是 Day 7 的 server。学完 Day 7 后,同一 Dockerfile 即可用于上线部署。

易踩坑小结

原因 解法
go test ./day6 没跑测试 day6 下没有 .go,包在 day6/handler 写成 go test ./day6/...
测试文件不执行 文件名不是 _test.go 或函数名不是 TestXxx 必须 *_test.gofunc TestXxx(t *testing.T)
测试里找不到 GetUser 误以为要 import 同包直接可见,无需 import

五、Day 6 示例代码与逐段解读

1. handler/handler.go

go 复制代码
package handler

import (
	"encoding/json"
	"net/http"
	"strconv"
	"strings"
)

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func GetUser(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}
	path := strings.TrimPrefix(r.URL.Path, "/api/users/")
	id, err := strconv.Atoi(path)
	if err != nil || id <= 0 {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(map[string]string{"error": "invalid id"})
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(User{ID: id, Name: "用户" + strconv.Itoa(id)})
}

解读 :只接受 GET;从 r.URL.Path 去掉前缀 /api/users/ 得到 id 字符串,用 strconv.Atoi 转成整数;转换失败或 id≤0 返回 400 + JSON 错误信息,否则返回 200 + User JSON。为 Day 6 的 httptest 提供明确的状态码和 Body,便于断言。

2. handler/handler_test.go

go 复制代码
package handler

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestGetUser(t *testing.T) {
	tests := []struct {
		path       string
		wantStatus int
	}{
		{"/api/users/1", http.StatusOK},
		{"/api/users/42", http.StatusOK},
		{"/api/users/0", http.StatusBadRequest},
		{"/api/users/abc", http.StatusBadRequest},
	}
	for _, tt := range tests {
		req := httptest.NewRequest(http.MethodGet, tt.path, nil)
		rec := httptest.NewRecorder()
		GetUser(rec, req)
		if rec.Code != tt.wantStatus {
			t.Errorf("GetUser(%q) status = %d, want %d", tt.path, rec.Code, tt.wantStatus)
		}
	}
}

解读 :表驱动:tests 里每项是「路径 + 期望状态码」。循环里用 NewRequest 造 GET 请求、NewRecorder 录响应,直接 GetUser(rec, req)(同包无需 import),然后断言 rec.Code == tt.wantStatus,不等则 t.Errorf 报错。和 Python 的「for 用例 in 用例列表: 调接口、assert 状态码」逻辑一致。

3. Dockerfile

dockerfile 复制代码
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY go.sum* ./
COPY . .
RUN CGO_ENABLED=0 go build -o /server ./day7/cmd/server

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /server .
EXPOSE 8080
CMD ["./server"]

解读 :第一阶段 builder:拷 go.mod 先 go mod download(利用缓存),再拷全部源码,go build 得到 /server 二进制(关闭 CGO,便于跨平台)。第二阶段:用 alpine,只装 ca-certificates,从 builder 拷出 /server,暴露 8080,启动命令 ./server。最终镜像里没有 Go 环境,只有二进制,体积小。


六、运行当天代码

测试(项目根目录):

bash 复制代码
go test ./day6/...
# 或带详细输出:go test -v ./day6/...

预期输出示例(-v 时):

复制代码
=== RUN   TestGetUser
--- PASS: TestGetUser (0.00s)
PASS
ok      github.com/go-quickstart-7days/day6/handler     0.229s

Docker(可选,需已装 Docker):

bash 复制代码
docker build -f day6/Dockerfile -t go-7days-app .
docker run -p 8080:8080 go-7days-app

镜像内跑的是 Day 7 的 server,可访问 http://localhost:8080 验证。

七、学习建议

  1. 先跑测试go test -v ./day6/...,再看 handler_test.go 和 handler.go 对应关系。
  2. 理解同包:为什么测试里能直接写 GetUser,和「目录即包、同包无需 import」对上号。
  3. 看 Dockerfile :理解两阶段(编译镜像 → 运行镜像)、COPY --from=builder,以及为何最终镜像很小。

八、小结

Day 6 补齐「测试 + 镜像构建」:表驱动 + httptest 测 HTTP Handler 不占端口,Docker 多阶段构建出小镜像。Day 7 会把前几天的内容整合成一个完整小项目,并沿用这里的测试与 Docker 方式。

相关推荐
礼拜天没时间.2 小时前
Docker Compose 实战:从单容器命令到多服务编排
运维·网络·docker·云原生·容器·centos
礼拜天没时间.12 小时前
Docker自动化构建实战:从手工到多阶段构建的完美进化
运维·docker·容器·centos·自动化·sre
罗技12315 小时前
Docker启动Coco AI Server后,如何访问内置Easysearch?
人工智能·docker·容器
DeeplyMind15 小时前
第14章 挂载宿主机目录(Bind Mount)(最常用,重要)
运维·docker·云原生·容器·eureka
DeeplyMind15 小时前
第17章 Docker网络实战与高级管理
网络·docker·容器
DeeplyMind17 小时前
第19章 Docker Compose进阶
运维·docker·容器
小锋学长生活大爆炸17 小时前
【教程】PicoClaw:在嵌入式设备上部署OpenClaw
docker·github·教程·工具·openclaw·picoclaw
遇见你的雩风1 天前
【Golang】--- Channel
开发语言·golang
Tony Bai1 天前
Go 1.26 中值得关注的几个变化:从 new(expr) 真香落地、极致性能到智能工具链
开发语言·后端·golang