100 Go Mistakes and How to Avoid Them 一书主要描述了使用 Go 语言编程时的常见问题。本文是博主对第三章 - 数据类型 Data type 的阅读笔记。
引言
最近发现了一本书籍,叫做 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" 有印象,是本系列笔记的荣幸!同时推荐有时间的读者直接阅读原文。
第 3 章:数据类型 Data type
#18 忽略 integer
溢出
溢出在 c/c++ 中也比较常见。主要原因是 Go 虽然会在编译时检查溢出,但在运行时候忽略潜在的溢出。
在关键的流程控制代码中使用以下防御性编程:
func IncUint(counter uint) uint {
if counter == math.MaxUint {
panic("uint overflow")
}
return counter + 1
}
func MultiplyInt(a, b int) int {
if a == 0 || b == 0 {
return 0
}
result := a * b
if a == 1 || b == 1 {
return result
}
if a == math.MinInt || b == math.MinInt {
panic("integer overflow")
}
if result/b != a {
panic("integer overflow")
}
return result
}
如果涉及到大数且必须精确,需要使用 math/big
#19 不理解浮点数表示原理
此部分和 c/c++ 很像。总之是要注意浮点计算带来的误差,以及写单元测试时,比较浮点数,控制在一个小的 \delta 误差即可。
#20 不理解 slice 的 length
和 capacity
是 Go 语言 slice
的基本原理。
Tips: slice
扩增的时候,小于 1024 个元素每次扩增 1 倍;之后每次扩增 1/4。
#21 低效率的 slice 初始化
slice 的原理和扩充原理本笔记不再赘述。
对于长度固定的情况,可以提前 make
slice,避免循环中多次扩充 slice 导致的内存复制。
func convert(foos []Foo) []Bar {
n := len(foos)
bars := make([]Bar, n)
for i, foo := range foos {
bars[i] = fooToBar(foo)
}
return bars
}
#22 不明确 slice 的 empty
和 nil
func main() {
var s []string
log(1, s)
s = []string(nil)
log(2, s)
s = []string{}
log(3, s)
s = make([]string, 0)
log(4, s)
}
func log(i int, s []string) {
fmt.Printf("%d: empty=%t\tnil=%t\n", i, len(s) == 0, s == nil)
}
1: empty=true nil=true
2: empty=true nil=true
3: empty=true nil=false
4: empty=true nil=false
注意:JSON 数据具有 null 和空数组及对象的概念。
json marshel 会根据传入切片是否为 null
有所区别。可能会影响业务序列化/反序列化过程。
{"ID":"foo","Operations":null}
{"ID":"bar","Operations":[]}
另外,reflect.DeepEqual
对两者 slice 也是为不相等的。
#23 判断 slice 是否为空不正确
nil 切片为 len
做了适配。
正确的方式是使用 len()
是否为0判断。
- 当切片为
nil
,len(xxx) == 0
- 切片不为
nil
但为空时,len(xxx) == 0
func main() { var s1 []string if len(s1) == 0 { println("s1 is empty") } s2 := []string(nil) if len(s2) == 0 { println("s2 is empty") } }
output:
s1 is empty
s2 is empty
#26 slice 内存泄露
考虑以下代码
func consumeMessages() {
for {
msg := receiveMessage()
// Do something with msg
storeMessageType(getMessageType(msg))
}
}
func getMessageType(msg []byte) []byte {
return msg[:5]
}
如果 receiveMessage()
返回了数量 1000 的 msg 切片回来,由于 slice 后的数组和 GC 机制,getMessageType
取前 5 个元素的操作,导致近千个元素的内存暂时无法回收。会导致潜在的高内存使用问题。
同样地,对于结构体切片,也会导致潜在的高内存使用。
#27 低效的 map 初始化
map
是 Go 语言的一个内置类型。map
的实现原理如下:
- 有一个
array
桶作为 hash table - 通过 hash(key) 来找到具体的 key-value bucket 数据结构指针
- 一个 bucket 填满后,会链接下一个 bucket
array
初始化时大小为4
,遇到一下条件,会翻倍- bucket 内元素平均大小超过
load factor = 6.5
(在后续可能会修改,该数字也是 Go 内部经验调优得到) - bucket 满的个数超过一定规模
- bucket 内元素平均大小超过
因此各个操作最坏的时间复杂度为
insert
:O(n)
,n 为元素总个数read/update/delete
:O(p)
,p 为一个 hash 指针对应 buckets 总的元素个数 (可能有多个 buckets)
因此,我们初始化一个已知大规模的 map,可以用 make
:
m := make(map[string]int, 1_000_000)
值得指出的是,make
参数中的 10000000
并不是限制了最大个数或指定初始的元素个数,是为了更好的初始化数据结构。我们仍然可以插入超过 10000000
个元素。
Extra:
为什么因子是 6.5 呢? 可看看煎鱼大大的博客
为什么 Go 的负载因子是 6.5?
#28 map 内存泄露
#27 中我们提到了 map 的扩容原理。但是 map 是不能缩容的。
因此一个巨大的 map 清除元素后,仍然会消耗较多的内存。
-
如果我们要累计大量数据的 map,可以通过定时复制到新的 map 达到变相缩容的效果。
-
可以通过强行指定 value 为指针类型节省 map 内存。
-
key 或者 value 如果大于 128 bytes,数据结构中会自动使用指针。
#29 错误地比较量值
==
有诸多限制。
Bool
Numerics
: int, float. complex typesStrings
Channel
: 比较两个channel
是不是在同一次调用创建的,或者是不是都为nil
Interfaces
: 比较两者是不是有一致的动态类型和动态值,或者是不是都为nil
Pointers
: 比较两者是不是统一地址空间,或者同为nil
Structs/arrays
: 比较两者是否相同元素组成。- 结构体内部成员必须都是可以比较的
因此像如下带有 slice
的结构体会被认为是不可比较的。
type customer struct {
id string
operations []float64
}
func main() {
cust1 := customer{id: "x", operations: []float64{1.}}
cust2 := customer{id: "x", operations: []float64{1.}}
fmt.Println(cust1 == cust2)
}
invalid operation:
cust1 == cust2 (struct containing []float64 cannot be compared)
可以使用反射来比较 reflect.DeepEqual
。但是效率很低,一般不在运行环境中使用,在 unit test 环境中使用得比较多。注意,其有较为详细的比较规则,使用之前要详细阅读文档。
如果需要比较结构体,可以自己实现一个 Equal()
方法,逐个元素比较。