100 Go Mistakes and How to Avoid Them 阅读笔记。4-Control Structures, 6-Functions and Methods, 7-Error Management
Table of Contents
引言
最近发现了一本书籍,叫做 100 Go Mistakes and How to Avoid Them。主要描述了使用 Go 语言编程时的常见问题。
其中第一章文中即很有趣地引用了句谚语:
Tell me and I forget.
Teach me and I remember.
Involve me and I learn.
于是博主决定挑出其中自己阅读过程中印象深刻的示例,结合自己粗浅的经验,跑一跑例程,同时也激励自己勤耕不辍写一点点笔记出来。
笔者不想完全 copy 书中的内容,所以随记尽量精简。如果读者能在碎片化时间中对某些 "Mistakes" 有印象,是本系列笔记的荣幸!同时推荐有时间的读者直接阅读原文。
第 4 章: Control Structures 控制结构
本章主要描述循环迭代的技巧和陷阱。
#30 忽视在 range
循环中,元素被复制
该部分大部分 Go 入门时都会着重描述,这篇笔记不再赘述。
开发者在迭代中更改被迭代的对象值时应当额外注意,
- 牢记在
range
循环中,元素被复制,更改是否生效在我们真正希望的元素上// wrong: accounts := []account{ {balance: 100.}, {balance: 200.}, {balance: 300.}, } for _, a := range accounts { a.balance += 1000 }
- 是否如期望改变了
range
迭代的逻辑s := []int{0, 1, 2} for range s { s = append(s, 10) } // 3 次迭代后将停止
#32 忽视指针迭代陷阱
func (s *Store) storeCustomers(customers []Customer) {
for _, customer := range customers {
fmt.Printf("%p\n", &customer)
s.m[customer.ID] = &customer
}
}
// output
0xc000096020
0xc000096020
0xc000096020
Extra
迭代的 index
value
一直是 Go 开发者最容易犯的错误。扩展阅读:
十多年了,这个最容易犯错的Go语法终于要改了 - 鸟窝的博客
#33 对 map
迭代抱有错误假设
迭代 map 时候,开发者不应该依赖于迭代的顺序。不能假设数据被添加的顺序。
Go 甚至添加了一些随机化来确保开发者不会依赖假设的顺序。
这也意味着,在迭代中,试图通过修改 map 数据来控制迭代逻辑是完全不可预料的。
m := map[int]bool{
0: true,
1: false,
2: true,
}
for k, v := range m {
if v {
m[10+k] = true
}
}
fmt.Println(m)
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true]
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true 32:true]
map[0:true 1:false 2:true 10:true 12:true 20:true]
一个可选的方法,是复制一份 map 出来,再进行更改。
#34 忽略了 break
对应的生效位置
break
可以对应 switch
,使用时容易弄混。
可以使用 break 行标签的方式,在复杂循环中跳转
readlines:
for {
line, err := rw.Body.ReadString('\n')
switch {
case err == io.EOF:
break readlines
case err != nil:
t.Fatalf("unexpected error reading from CGI: %v", err)
}
// ...
}
第 6 章: Functions and Methods 函数和方法
#45: Returning a nil receiver
函数返回 interface
时候容易出现的反直觉错误。
引出例
书中给出了如下使用例。
假如我们封装了自己的多 error 结构体 MultiError
。 其实现了 Error
接口。
type MultiError struct {
errs []string
}
func (m *MultiError) Add(err error) {
m.errs = append(m.errs, err.Error())
}
func (m *MultiError) Error() string {
return strings.Join(m.errs, ";")
}
实际使用中如下:
func (c Customer) Validate() error {
var m *MultiError
if c.Age < 0 {
m = &MultiError{}
m.Add(errors.New("age is negative"))
}
if c.Name == "" {
if m == nil {
m = &MultiError{}
}
m.Add(errors.New("name is nil"))
}
return m
}
customer := Customer{Age: 33, Name: "John"}
if err := customer.Validate(); err != nil {
log.Fatalf("customer is invalid: %v", err)
}
将得到一个非常怪异的输出
2021/05/08 13:47:28 customer is invalid: <nil>
pointer receiver
允许为 nil
, 以及 interface wrap 机制
虽然我们想要返回的 MultiError
为 nil
,但是其实现了 Error
接口。
返回的 Error
不为 nil
。
在 Go 中,接口实际上是一个 warpper
。我们的空 MultiError
被包装成了为非空的 Error
。
为了改正这个错误,我们可以在检查后直接返回 nil
。
if c.Age < 0 {
...
}
if c.Name == "" {
...
}
return nil
本错误建议在开发者返回 interface
时候留有意识检查。
Extra
参考阅读
Go Doc 关于 nil error 的描述: https://go.dev/doc/faq#nil_error
相关讨论: https://github.com/golang/go/issues/42663
#47: Ignoring how defer arguments and receivers are evaluated
注意 defer 的闭包使用
func f() error {
var status string
defer func() {
notify(status)
incrementCounter(status)
}()
// The rest of the function
}
第 7 章:Error management 错误管理
#49: Ignoring when to wrap an error
#50: Checking an error type inaccurately
#51: Checking an error value inaccurately
3 节主要讲述了 error warp 的由来,和 errors.As()
errors.Is()
配套使用方法。
应当使用 warp error,让 debug 更清晰。
#52: Handling an error twice
这里主要指在返回 error
的时候,不要多次打印日志,难于排查原因。
一个方式,是在上层调用处打印 error 日志
func GetRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {
err := validateCoordinates(srcLat, srcLng)
if err != nil {
// 此处不必打印日志,在最初调用处打印即可
return Route{}, err
}
err = validateCoordinates(dstLat, dstLng)
if err != nil {
// 此处不必打印日志,在最初调用处打印即可
return Route{}, err
}
return getRoute(srcLat, srcLng, dstLat, dstLng)
}
func validateCoordinates(lat, lng float32) error {
if lat > 90.0 || lat < -90.0 {
return fmt.Errorf("invalid latitude: %f", lat)
}
if lng > 180.0 || lng < -180.0 {
return fmt.Errorf("invalid longitude: %f", lng)
}
return nil
}
笔者自己在 code review 时也曾被指出这个问题。多次打印 error 将在复杂调用中增加排查问题的难度。
但这种处理方式仍然不是最好的,因为我们很难找到具体发生 error 的地方,因此可以套用上述的 wrap 方法,在每层返回都追加信息。如此,在最上层调用处打印的 log 可以反应整个的调用链路。
func GetRoute(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {
err := validateCoordinates(srcLat, srcLng)
if err != nil {
return Route{},
fmt.Errorf("failed to validate source coordinates: %w",
err)
}
err = validateCoordinates(dstLat, dstLng)
if err != nil {
return Route{},
fmt.Errorf("failed to validate target coordinates: %w",
err)
}
return getRoute(srcLat, srcLng, dstLat, dstLng)
}