1、未知的枚举值
示例:
type Status uint32 const ( StatusOpen Status = iota StatusClosed StatusUnknown )
示例中使用了 iota 创建了枚举值,其结果就是:
StatusOpen = 0 StatusClosed = 1 StatusUnknown = 2
现在假设上述 Status 类型将会作为 JSON request 的一部分:
type Request struct { ID int `json:"Id"` Timestamp int `json:"Timestamp"` Status Status `json:"Status"` }
然后你收到的数据可能是:
{ "Id": 1234, "Timestamp": 1563362390, "Status": 0 }
这看起来似乎没有任何问题,status 将会被解码为 StatusOpen 。
但是如果另一个请求的数据是这样:
{ "Id": 1235, "Timestamp": 1563362390 }
这时 status 即使没有传值(也就是 unknown 未知状态),但由于默认零值,其将会被解码为 StatusOpen ,显然不符合业务语义上的 StatusUnknown 。
最佳实践是将未知的枚举值设置为 0 :
type Status uint32 const ( StatusUnknown Status = iota StatusOpen StatusClosed )
2、基准测试
基准测试受到多方面的影响,因此想得到正确的结果比较困难。
最常见的一种错误情况就是被编译器优化了,例如:
func clear(n uint64, i, j uint8) uint64 { return (math.MaxUint64<<j | ((1 << i) - 1)) & n }
这个函数的作用是清除指定范围的 bit 位,基准测试可能会这样写:
func BenchmarkWrong(b *testing.B) { for i := 0; i < b.N; i++ { clear(1221892080809121, 10, 63) } }
在这个基准测试中,编译器将会注意到这个 clear 是一个 leaf 函数(没有调用其它函数)因此会将其 inline 。一旦这个函数被 inline 了,编译器也会注意到它没有 side-effects(副作用)。因此 clear 函数的调用将会被简单的移除从而导致不准确的结果。
解决这个问题的一种方式是将函数的返回结果设置给一个全局变量:
var result uint64 func BenchmarkCorrect(b *testing.B) { var r uint64 for i := 0; i < b.N; i++ { r = clear(1221892080809121, 10, 63) } result = r }
此时,编译器不知道这个函数的调用是否会产生 side-effect ,因此基准测试的结果将会是准确的。
3、指针
按值传递变量将会创建此变量的副本(简称“值拷贝”),而通过指针传递则只会复制变量的内存地址。
因此,指针传递总是更快吗?显然不是,尤其是对于小数据而言,值拷贝更快性能更好。
原因与 Go 中的内存管理有关。让我们简单的解释一下。
变量可以被分配到 heap 或者 stack 中:
- stack 包含了指定 goroutine 中的将会被用到的变量。一旦函数返回,变量将会从 stack 中 pop 移除。
- heap 包含了需要共享的变量(例如全局变量等)。
示例:
func getFooValue() foo { var result foo // Do something return result }
这里,result 变量由当前的 goroutine 创建,并且将会被 push 到当前的 stack 中。一旦这个函数返回了,调用者将会收到 result 变量的值拷贝副本,而这个 result 变量本身将会被从 stack 中 pop 移除掉。它仍存在于内存中,直到它被另一个变量擦除,但是它无法被访问到。
现在看下指针的示例:
func getFooPointer() *foo { var result foo // Do something return &result }
result 变量仍然由当前 goroutine 创建,但是函数的调用者将会接受的是一个指针(result 变量内存地址的副本)。如果 result 变量被从 stack 中 pop 移除,那么函数调用者显然无法再访问它。
在这种情况下,为了正常使用 result 变量,Go 编译器将会把 result 变量 escape(转移)到一个可以共享变量的位置,也就是 heap 中。
传递指针也会有另一种情况,例如:
func main() { p := &foo{} f(p) }
由于我们在相同的 goroutine(main 函数)中调用 f 函数,这里的 p 变量无需被 escape 到 heap 中,它只会被推送到 stack 中,并且 sub-function 也就是这里的 f 函数是可以直接访问到 p 变量的。
stack 为什么更快?主要有两个原因:
- stack 几乎没有垃圾回收。正如上文所述,一个变量创建后 push 到 stack 中,其函数返回后则从 stack 中 pop 掉。对于未使用的变量无需复杂的过程来回收它们。
- stack 从属于一个 goroutine ,与 heap 相比,stack 中的变量不需要同步,这也导致了 stack 性能上的优势。
总之,当我们创建一个函数时,我们的默认行为应该是使用值而不是指针,只有当我们想用共享变量时才应该使用指针。
如果我们遇到性能问题,一种可能的优化就是检查指针在某些特定情况下是否有帮助。如果你想要知道编译器何时将变量 escape 到 heap ,可以使用以下命令:
go build -gcflags "-m -m"
4、从 for/switch 或 for/select 中 break
例如:
for { switch f() { case true: break case false: // Do something } } for { select { case <-ch: // Do something case <-ctx.Done(): break } }
注意,break 将会跳出 switch 或 select ,但不会跳出 for 循环。
为了跳出 for 循环,一种解决方式是使用带标签的 break :
loop: for { select { case <-ch: // Do something case <-ctx.Done(): break loop } }
5、errors 管理
Go 中的错误处理一直以来颇具争议。
推荐使用 https://github.com/pkg/errors 库,这个库遵循如下规则:
An error should be handled only once. Logging an error is handling an error. So an error should either be logged or propagated.
而当前的标准库(只有一个 New 函数)却很难去遵循这一点,因为我们可能希望为错误添加一些上下文并具有某种形式的层次结构。
假设我们在调用某个 REST 请求操作数据库时会碰到以下问题:
unable to server HTTP POST request for customer 1234 |_ unable to insert customer contract abcd |_ unable to commit transaction
通过上述 pkg/errors 库,我们可以处理如下:
func postHandler(customer Customer) Status { err := insert(customer.Contract) if err != nil { log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID) return Status{ok: false} } return Status{ok: true} } func insert(contract Contract) error { err := dbQuery(contract) if err != nil { return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID) } return nil } func dbQuery(contract Contract) error { // Do something then fail return errors.New("unable to commit transaction") }
最底层通过 errors.New 初始化一个 error ,中间层 insert 函数向其添加更多上下文信息来包装此 error ,然后父级调用者通过记录日志来处理错误,每一层都对错误进行了返回或者处理。
我们可能还想检查错误原因以进行重试。例如我们有一个外部库 db 处理数据库访问,其可能会返回一个 db.DBError 的错误,为了实现重试,我们必须检查具体的错误原因:
func postHandler(customer Customer) Status { err := insert(customer.Contract) if err != nil { switch errors.Cause(err).(type) { default: log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID) return Status{ok: false} case *db.DBError: return retry(customer) } } return Status{ok: true} } func insert(contract Contract) error { err := db.dbQuery(contract) if err != nil { return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID) } return nil }
如上所示,通过 pkg/errors 库的 errors.Cause 即可轻松实现。
一种经常会犯的错误是只部分使用 pkg/errors 库,例如:
switch err.(type) { default: log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID) return Status{ok: false} case *db.DBError: return retry(customer) }
这里直接使用 err.(type) 是无法捕获到 db.DBError 然后进行重试的。