返回真实目录数据

Go 语言Beginner
立即练习

介绍

社区服务台收藏了一小批失物。准备好的网页能够显示物品,但示例 API 只返回一条记录。你将把真实的 Go 记录连接到列表和详情响应,让访客找到正确的领取地点。

你应已了解 Go 的函数、结构体、切片、映射和模块,以及基本的终端路径操作。本实验将介绍 HTTP、JSON 和处理函数测试。浏览器环境已备妥 Go 和可用页面。本单元独立初始化,不需要其他实验的文件。包括阅读和处理普通错误,预计需要约 55 分钟;这是作者估计。

选择单件物品并保留列表功能

本步骤在保持单卡片集合可用的同时,添加真实的物品详情。从 Workspace 标签页开始。其 Shell 进入 /home/labex/project/lost-found;如果稍后返回,下面的 cd 可以恢复到这个位置。

Go 安装在 /usr/local/go/bin。export 将该目录加入当前 Shell 的命令搜索路径,同时保留已有的 $PATH。这只对当前终端会话生效;新建 Shell 后需要再次执行。不会修改全局 Shell 设置。

cd /home/labex/project/lost-found
export PATH=/usr/local/go/bin:$PATH

curl 用于发送请求。-i 包含响应头,单引号将整个 URL 保持为一个整体。GET 请求用于读取资源。/api/items 标识物品集合;127.0.0.1:8080 是同一台机器上的服务。先使用正常示例:

curl -i 'http://127.0.0.1:8080/api/items'

预期看到 200 OK、Content-Type: application/json 响应头,以及包含 LF-101 的数组,也就是图书馆的那把蓝色雨伞。HTTP 状态描述请求的结果;正文承载数据。JSON 用方括号表示数组,用花括号表示对象。仅有一个 200 响应,不能证明集合完整。切换到 Lost and Found,查看其中的一张卡片,再返回 Workspace。

cat 用于显示文件,不会编辑文件。阅读源记录、输入结构和起始的小型处理函数:

cat items.json
cat main.go
cat handler.go

源数据共有六条记录。Item 将 ID、Name、Category 和 Location 组合在一起;首字母大写的导出字段可以被编码。`json:"id"` 这样的标签用于选择小写的 JSON 键。main.go 加载集合、提供准备好的页面,并将物品切片传给 catalogHandler。保留 main.go、go.mod、items.json、serve.py 和 web/;只编辑 handler.go 以及其他新增的 Go 文件或测试。

修改代码之前,预测请求 LF-101 与请求 LF-103 的区别。两者名称都是 Blue umbrella,但地点分别是 Library desk 和 Cafe counter。哪个字段能只选中一条记录?现在请求第二个 ID:

curl -i 'http://127.0.0.1:8080/api/items/LF-103'

起始代码仍返回只含第一条记录的数组。这是一个已收到的响应,但不是正确的详情。你的任务是返回由 ID 选中的对象,同时保留当前列表响应。不要让每个路径都返回第一个对象来解决问题。

处理函数是每个请求到来时调用的 Go 代码。r *http.Request 指向传入的请求。w http.ResponseWriter 是响应的写入目标:Header 设置响应头,写入操作发送正文。http.Handler 描述这种行为;http.HandlerFunc 将一个函数适配为该接口。返回的匿名函数会保留传入的 items。你不需要自己实现 HTTP 解析器。

编辑之前,保存一个副本。cp 复制文件;特意使用 .backup 扩展名而不是 .go,避免 Go 编译包时产生重复定义。

cp handler.go handler.go.backup

用终端编辑器打开实际文件。仅作用于这条命令的 LC_ALL=C.UTF-8 让 nano 按 UTF-8 解释文本,包括带重音的字符。它只影响这一次编辑器调用,不改变全局 Shell 设置:

LC_ALL=C.UTF-8 nano handler.go

在 nano 中,Ctrl 表示按住 Control,再按指定的键。用方向键移动。按 Ctrl+W,输入 catalogHandler,再按 Enter 找到函数;Ctrl+C 取消未完成的搜索。通过普通输入、Backspace 和 Delete 编辑。Ctrl+K 剪切当前行;只在确实打算替换的行使用它。底部快捷键标签用 ^ 表示 Control。你是在编辑已有处理函数,不是另建第二个程序。

在返回函数内部、已有 content-type 响应头之后,实现下列判断。一次处理一个分支:

首先,读取 r.URL.Path。若它等于 /api/items,保留起始代码对 items[:1] 的数组编码,并立即返回。这能在处理详情时保留正常的单卡片列表。

接着,在其他情况下识别 /api/items/ 前缀。在已有导入旁添加标准库 strings。strings.HasPrefix(path, prefix) 检查前缀,strings.TrimPrefix(path, prefix) 移除前缀。把剩余的 ID 保存到局部变量。

然后,用 for _, item := range items 遍历传入的切片。将 item.ID 与请求的 ID 比较,不要比较名称。匹配时编码该 item 对象,并立即返回。json.NewEncoder(w).Encode(value) 将选中的 Go 值写为 JSON;切片变为数组,一个结构体变为对象。循环必须检查每条记录,不能只检查 items[0]。

之后,如果没有 ID 匹配,或路径不属于这些路由,在写 JSON 错误之前设置 404。w.WriteHeader(http.StatusNotFound) 设置状态。一个简短的 JSON 兼容错误值是 map[string]string{"error": "item not found"};error 字符串必须非空。编码这个映射。不要在错误后再写一个列表。

响应的第一次写入通常会提交 200,因此应在编码前设置其他状态。Encode 返回一个 error:nil 表示没有失败。本示例的全部值都是兼容 JSON 的字符串。客户端断开仍可能导致写入失败;正文开始发送后再发第二个响应,并不能修复连接。提前返回也能防止两个答案被追加在一起。

按 Ctrl+O 保存,用 Enter 确认已有文件名 handler.go,再按 Ctrl+X 退出。检查已保存文件:

cat handler.go

如果打开了错误文件,不保存退出:按 Ctrl+X,如果询问是否保存,按 N。运行 pwd 查看当前位置,执行最开始的 cd 恢复目录,再打开正确文件名。如果保存提示显示了非预期名称,用 Ctrl+C 取消。不要覆盖源数据来弥补编辑错误。

gofmt -w 就地格式化指定的 Go 文件。提供的 serve.py 先构建你的代码,再仅替换此工作区记录的服务器。它不提供处理函数实现。构建出错会保留之前正常工作的进程,因此浏览器的旧响应不能证明新代码已编译成功。

gofmt -w handler.go
python3 serve.py

预期看到 Lost and Found ready on port 8080。格式化或编译失败时,阅读文件名和行号。重新打开 nano handler.go,用 Ctrl+W 找到附近表达式,仅修复该区域,保存、退出,再重复两个命令。保留已经正常工作的逻辑;不要用初始备份覆盖后续进度。如果出现 No such file,使用最开始的 cd 恢复目录。服务诊断信息位于 .work/server.log,可用 cat .work/server.log 阅读;不要因为输入错误就重置 VM。

curl -i 'http://127.0.0.1:8080/api/items/LF-103'

预期得到一个对象,其中 id 为 LF-103,location 为 Cafe counter,而不是数组,也不是图书馆的雨伞。检查正常列表仍返回样例数组:

curl -i 'http://127.0.0.1:8080/api/items'

你在保留正常页面所需集合格式的同时,添加了选择功能。下一步会将列表扩展为全部记录。

返回完整集合

本步骤将单条记录列表改为完整的只读目录,同时保留你写好的详情功能。从 Workspace 开始,工作目录为 /home/labex/project/lost-found。

编辑前,阅读集合分支中的切片表达式。items[:1] 选择索引一之前的前缀,因此隐藏了另外五条记录。预测将 items 本身传给编码器会得到什么响应。与硬编码六件物品的数组不同,这个参数也适用于其他传入的切片。

LC_ALL=C.UTF-8 nano handler.go

用 Ctrl+W 搜索 items[:1],按 Enter。只将集合中被编码的值改为完整输入切片。保留 JSON 响应头、详情循环、缺失 ID 错误和提前返回。按 Ctrl+O、Enter 保存到同一个文件,再按 Ctrl+X 退出。

空集合必须编码为 [],不能是 null。空的非 nil 切片编码为 [];nil 切片则编码为 null。本单元的独立空输入是 []Item{}。不要将空结果替换为物品缺失错误:列表和详情含义不同。数组顺序和对象键顺序不限,但属于每个 ID 的名称、类别和地点必须保持关联。

gofmt -w handler.go
python3 serve.py

先请求集合,再分别使用两把雨伞各自的 ID 请求详情:

curl -i 'http://127.0.0.1:8080/api/items'
curl -i 'http://127.0.0.1:8080/api/items/LF-101'
curl -i 'http://127.0.0.1:8080/api/items/LF-103'

预期数组包含全部六条完整记录。即使名称都是 Blue umbrella,LF-101 仍对应 Library desk,LF-103 仍对应 Cafe counter。如果任一详情的地点变了,检查 ID 比较和被编码的循环值。

下一个请求故意使用不存在的 ID。预期得到 HTTP 404 和包含非空 error 字符串的 JSON 对象,而不是空列表。curl 收到 HTTP 错误响应时通常仍会成功退出:收到响应与无法连接不同。

curl -i 'http://127.0.0.1:8080/api/items/no-such-id'

这个处理函数还没有筛选功能,也没有限制 HTTP 方法;下一个引导实验会介绍这些行为。不要把正常显示的首页或任意一个 200 当作详情端点正常工作的证据。你现在在改变集合范围的同时,保留了列表和详情功能。

编写能区分身份的测试

本步骤自行设计一个针对性处理函数测试,无需运行第二个在线服务器,然后使用实际页面。从 Workspace 开始,保持相同工作目录。

先决定测试必须捕捉什么问题:处理函数可能返回名称匹配的第一件物品,而不是请求的 ID。自行选择两条简短记录,名称相同,但 ID 和地点不同,且不属于网页的集合。请求第二个 ID。写下应返回的状态、ID 和地点。这个独立 fixture 能让固定在线响应无法通过,使测试在六条准备好的记录之外也有价值。

Go 会在以 _test.go 结尾的文件中发现测试。测试函数名称以 Test 开头,接受 t *testing.T。用 nano 创建测试文件:

LC_ALL=C.UTF-8 nano handler_test.go

以 package main 开头。导入 encoding/json、net/http/httptest 和 testing。添加一个函数,例如 func TestSecondIdentity(t *testing.T) { ... },将省略号替换为测试语句。函数体分为四部分:

首先,用 main.go 中的导出字段名构造自己的两条记录 []Item fixture。将它传给 catalogHandler,不要加载或修改 items.json。

接着,为第二条记录的详情创建请求。httptest.NewRequest("GET", path, nil) 构造请求;最后一个参数表示没有请求正文。httptest.NewRecorder() 返回响应记录器。调用处理函数的 ServeHTTP(recorder, request) 会运行实际处理函数,捕获其响应头、状态和正文。

然后,声明 var got Item。用 json.Unmarshal(recorder.Body.Bytes(), &got) 解码。&got 是保存字段的地址。先检查返回的 error:不能把无效 JSON 误当作有效空结构体。例如,if err != nil { t.Fatal(err) } 会停止测试并显示可读的失败信息。

之后,断言预期状态和身份。recorder.Code 包含状态,got.ID 和 got.Location 包含解码后的字段。将它们与第二条 fixture 记录比较。用 if 和 t.Fatalf 报告失败,并显示实际值。%d 显示数字,%+v 显示结构体字段。不要只断言存在某些 JSON。

在请求和断言中一致地使用你选择的 ID 和地点。将 fixture 数据、执行和断言保留在同一个测试函数中,方便理解推理过程。按 Ctrl+O、Enter 保存,再按 Ctrl+X 退出。环境不会替你提供能通过的测试。

go test -v . 运行此包的测试:-v 显示测试名称,. 选择当前包。

gofmt -w handler_test.go
go test -v .

预期看到你的测试名称,随后是 PASS。失败时应打印实际状态或物品。如果失败,判断是你预期的 fixture 关系有误,还是处理函数有误;用 nano 只修正该部分,再运行测试。编译错误会指出应检查的文件和行。修正处理函数后,用 python3 serve.py 重新构建,让浏览器也使用当前代码。在工作区中保留至少一个能通过的 Go 处理函数测试;最终检查会在隔离环境中运行你的测试,并用其他输入记录检查 API。

切换到 Lost and Found 并重新加载。预期看到 6 items found。在 LF-103 上打开 View item,阅读其编号和领取地点,再选择 Back to collection。两把雨伞必须保持可区分。本单元提供集合和详情浏览;下一个引导实验会同时添加搜索控件和对应的 API 筛选行为。

显示全部源记录的实际目录页面

观察六张卡片及其各自的编号。页面使用正在运行的 API,不包含备用集合。如需修正,返回 Workspace。现在,你既有可用目录,也有一个通过自行选择 fixture 和断言来暴露真实身份错误的测试。

总结

你将 Go 记录编码为 JSON,根据路径选择列表和详情响应,保留了同名物品各自的 ID,并为不存在的物品返回真实的错误状态。你还编写了独立的 httptest 测试并使用了实际网页。搜索和更严格的错误处理是下一个独立引导实验的任务。