这组材料面向已经熟悉一门或多门编程语言的开发者。读完一个例子,你应该能解释依赖从哪里来、哪个函数负责做事、错误怎样返回,以及资源在什么时候关闭。
从哪里开始
- 八种常见组合:先认出构造函数、接口、错误处理、清理、中间件、锁、slice 和取消各自怎样搭配。
- 把同步服务逐步写完整:从一次查价开始,加上可替换的数据源、错误判断、测试和 context。
- 加入并发与取消:把价格和运费并发读取,再加入并发上限与任务收尾。
- 回到 Spine 读真实代码:在工具注册、Runner 和 Future 中找到前面见过的组合。
第一次建议按这四章顺序阅读。基础语法卡住时再查 Go 语法 或 值与内存。
每段代码先看四件事
依赖从哪里来
看结构体字段和构造函数。字段中出现接口,通常意味着这一层通过方法约定使用外部能力。字段中出现函数,通常意味着调用方传入了一段行为。
构造函数也说明哪些依赖需要长期保存。每次请求都变化的参数,通常继续放在方法参数里。
状态活多久
先圈出函数局部变量,再看被结构体或闭包保存的值。接着找 Close、defer、cancel 和等待操作。
指针说明可以间接访问哪个值;关闭责任要结合创建位置、API 约定和调用顺序判断。
失败在哪里处理
沿着 return err 向上找。当前函数有可补充的信息时,用 %w 包装错误。需要判断原因的调用方用 errors.Is 或 errors.As。
日志一般留在真正处理这次失败的位置。让每一层都记录同一个错误,会让一件故障产生多份重复日志。
哪个时刻才算完成
同步调用通常跟到函数返回。出现 goroutine 后,再找等待路径:谁接收结果、谁关闭通知、取消后是否还有工作没退出。
cancel() 发出通知。函数什么时候可以返回,要看它承诺等待哪些任务。
用你熟悉的语言建立联系
C# / Java:依赖注入与服务对象
Go 常用普通构造函数把依赖放进结构体。入口代码负责选择具体实现,业务方法使用这些依赖。小项目可以直接在入口把对象组装起来。
接口的方法集合匹配后即可赋值。是否提取接口,主要看这一层实际需要调用哪些方法,以及是否需要不同实现。
TypeScript:结构化接口与函数包装
Go 的隐式接口实现,能接上你对结构化类型的理解。方法集合、指针接收者和包可见性仍需要按 Go 的规则判断。
中间件可以写成“接收一个函数,返回同类型函数”。外层通过闭包保存配置,内层在每次调用时补充行为。
Python:资源作用域与可变对象
defer 很适合把清理安排放在资源获取旁边。它在所在函数退出时执行,循环里的资源应注意函数作用域是否过大。
struct、array、slice 和 map 的复制方式各有规则。跨函数交出一份 slice 时,先决定双方是否允许共享底层数据。
异步调用:显式安排收尾
goroutine 启动后,结果传递和等待方式由代码安排。context 可以把取消通知传下去,WaitGroup 可以等待任务结束,channel 可以传值或发布完成信号。后面的渐进示例会把这些用途逐项放进具体调用中。
如何运行例子
在仓库根目录进入独立教学 module:
cd learning/go-patterns
go test ./...
go test -race ./...
go vet ./...
每个目录都是一个独立可读的小包。测试是例子的运行入口,包含正常结果和需要关注的失败路径。文章中给出单包命令,便于只跑正在读的内容。
教学 module 只使用 Go 标准库。运行这些例子不需要加载 Wasm,也不需要重建 Guest。
七个版本怎样使用
七个阶段保存在不同目录,可以来回比较。教学中有意保留一些重复代码,让你直接看到某个需求加入后,函数签名、结构体和测试发生了什么变化。
- 先读当前版本,预测输入和输出。
- 运行测试,核对预测。
- 改一个需求,再看下一阶段如何处理。
- 找到新结构承担的职责,并想想当前项目是否也需要它。
这些版本用于比较设计。真正的业务代码可以保留最终需要的版本,再根据实际复用情况提取公共部分。
一轮练习结束后
试着独立回答:
- 换一个数据源,需要改哪些文件?
- 调用失败时,哪一层能区分“找不到”与“请求被取消”?
- 并发数达到上限后,新工作在哪里等待?
- 调用已经返回时,还有没有属于它的 goroutine?
- 返回的 slice 与内部存储是否共享数据?
能沿代码说明这些问题,就已经可以开始读项目中的组合写法。API 的细节仍可以随用随查。