Go 的类型、错误返回与 goroutine 让服务代码可以写得直接,但简单语法不意味着没有隐含共享。读代码时先追踪值包含什么、谁能修改、谁负责关闭,以及错误沿什么路径传播。

切片复制没有复制底层数据

slice 包含对底层数组的引用、长度和容量。把 slice 赋给另一个变量,两个视图可能指向同一个数组;append 是否重新分配取决于容量。不能依据某次运行“append 后没互相影响”推断所有情况都独立。

base := []int{1, 2, 3}
alias := base[:2]
clone := append([]int(nil), base...)
alias[0] = 9
fmt.Println(base[0], clone[0]) // 9 1

复制元素后,int 列表独立;元素若是指针或嵌套 slice,仍只是浅拷贝。接口也保存动态类型和值,一个保存了有类型 nil 指针的接口不一定等于 nil。返回 error 时明确返回 nil,不要把某个未赋值的具体错误指针装进接口再返回。

错误是调用契约的一部分

在边界增加上下文时用 fmt.Errorf("load product: %w", err) 保留原因;上层通过 errors.Is/errors.As 判断可恢复类型。不能用字符串包含某几个字决定是否重试,也不要把所有错误统一变成空结果。

err := fmt.Errorf("load config: %w", os.ErrNotExist)
fmt.Println(errors.Is(err, os.ErrNotExist)) // true

完整代码:example.go。下载后运行 go run example.go,应输出两行 9 1 与 true。代码只用标准库。

谁创建,谁明确结束

打开文件后先检查 err,再安排 defer Close。循环中不断 defer 大量文件关闭会推迟释放到整个函数返回,应该将单次处理收进小函数,或在合适位置显式关闭。HTTP 客户端响应 body 也必须关闭,必要时处理读取与连接复用。

context 用于传播取消和 deadline,不应存进长期共享结构体充当万能参数袋。子任务必须知道何时退出,channel 的发送者通常负责关闭;多发送者不能各自随意 close。读取已关闭 channel 会得到零值与 ok=false,向已关闭 channel 发送会 panic。

包、接口与测试边界

使用 go.mod 记录模块与语言版本,go.sum 校验依赖内容。接口通常在使用者一侧定义,只列使用者真正需要的方法,避免为了抽象而复制整个数据库驱动接口。错误、超时和事务边界比“所有函数都通过接口”更重要。

普通 map 的并发读写需要同步;保护复合不变量时用 Mutex 或其他明确协议。goroutine 便宜但不是免费,生命周期和数量仍要受控。并发与背压与Go 任务工程会把这些规则放进服务。

练习与面试自查

把 base 改为 []*Item,给 clone 中指针指向的对象改字段,说明为什么复制 slice 仍会互相影响。再为一个可取消的等待函数写测试,保证超时后 goroutine 不永久停在发送结果上。

  • new、make、值复制与引用共享各适用于什么?说明 slice 与 map 的区别。
  • 接口值为什么可能“看起来是 nil”却不等于 nil?
  • defer、panic、recover 适合保护什么边界?为什么不应把普通错误都改成 panic?

参考资料