《100 Go Mistakes and How to Avoid Them》之数据类型篇
礼物说3.1 创建令人困惑的八进制字面量
八进制字面量很容易会产生误解。文章举了个例子:
:= 100 + 010
sum .Println(sum) fmt
第一眼看去结果应该是110
,但其实结果是108
,因为字面量的int以0
开头表示了八进制。更多的是用在一些特定场景,比如在file, err := os.OpenFile("foo", os.O_RDONLY, 0644)
中可以代表权限。如果想要区分这种误解,可以用0o644
去代替,效果一样,可以看起来更清晰。二进制(0b / 0B),十六进制(0x / 0X)也类似.另外我们也可以使用_
去做分隔符增加可读性。比如1亿:1_000_000_000.
3.2 忽略整形溢出
关于int的溢出问题,比较常见,不再赘述。
3.3 不理解浮点数
浮点数储存的都是一个近似值,float32(单精度)和float64(双精度)遵循了IEEE-754
标准,分为了3块:
1.标记(sign) 表示正负
2.指数(exponent) float32(8位) float64(11位)
3.尾数(mantissa) float32(23位) float64(52位)
公式为:sign * 2^exponent * mantissa
所以 直接用 ==
来比较两个浮点数是不准的.可取的一个方案是类似testify中的InDelta
函数比较差值。另外 浮点数中 有几个特殊的值。
1.Positive infinite (math.IsInf) 正无穷
2.Negative infinite (math.IsInf) 负无穷
3.NaN (Not-a-Number) (math.IsNaN) 表示无法表示的结果.
// IsNaN reports whether f is an IEEE 754 “not-a-number” value.
func IsNaN(f float64) (is bool) {
// IEEE 754 says that only NaNs satisfy f != f.
// To avoid the floating-point hardware, could use:
// x := Float64bits(f);
// return uint32(x>>shift)&mask == mask && x != uvinf && x != uvneginf
return f != f
}
关于如何近可能的保持精度,提供了两个例子
func f1(n int) float64 {
:= 10_000.
result for i := 0; i < n; i++ {
+= 1.0001
result }
return result
}
func f2(n int) float64 {
:= 0.
result for i := 0; i < n; i++ {
+= 1.0001
result }
return result + 10_000.
}
n | Exact result | f1 | f2 |
---|---|---|---|
10 | 10010.001 | 10010.099999999993 | 10010.001 |
1k | 11000.1 | 11000.099999999293 | 11000.099999999982 |
1m | 1.0101e+06 | 1.0100999999761417e+06 | 1.0100999999766762e+06 |
精度的丢失是会叠加的,当我们有一系列的加减法时,我们可以把数据级相近的操作放一起。
另一个例子就是比较a × (b + c)
和a×b+a×c
:= 100000.001
a := 1.0001
b := 1.0002
c .Println(a * (b + c))
fmt.Println(a*b + a*c)
fmt200030.00200030004
200030.0020003
关于浮点数的计算我们可以总结三个点:
比较两个浮点数,我们可以检查他们的差值在一个可控范围内。
当执行批量加减法时,量级相近的操作分组可以让结果更精确。
当执行加减乘除时,先乘除在加减可以让结果更精确。
3.4 不理解切片的length
以及capacity
关于slice length
&capacity
如何使用,以及扩容方式。 length
表示当前使用的长度,capacity
表示备用的长度。如果两者相等再append
就会触发扩容。
3.5 无效的切片初始化
make([]Bar, 0, n)
提前分配len和cap可以避免内存申请
func convert(foos []Foo) []Bar {
:= len(foos)
n := make([]Bar, 0, n)
bars for _, foo := range foos {
= append(bars, fooToBar(foo))
bars }
return bars
}
func convert(foos []Foo) []Bar {
:= len(foos)
n := make([]Bar, n)
bars for i, foo := range foos {
[i] = fooToBar(foo)
bars}
return bars
}
以上两个方式区别是 方法二 会快4%,因为没有重复调用内置函数append
,而第一种方式更有可读性。
3.6 不清楚 nil
vs empty slices
nil slice 没有任何的内存分配。在 encoding/json 包中有做区分,nil slice 的 marshal结果是null
,empty slice的是[]
.并且两者无法相等。关于什么时机使用:
var s []string
不确定最终长度并且slice可以为空时
s = []string(nil)
作为语法糖创建一个nil slice
s = []string{}
在有初始化元素的时候使用。
s = make([]string, 0)
长度已知
3.7 不恰当的判断empty slices
直接判断len == 0 覆盖了nil和empty两种场景
3.8 错误使用切片copy
函数
关于copy
内置函数常见的错误.
:= []int{0, 1, 2}
src var dst []int
copy(dst, src)
.Println(dst) fmt
上面的打印会是[],因为dst的长度为0.正确的方式是dst := make([]int, len(src))
. 另外一种常见的写法是dst := append([]int(nil), src...)
.
3.9 切片append
边界问题
:= []int{1, 2, 3}
s1 := s1[1:2]
s2 := append(s2, 10) s3
s3的append会影响s1的第三个值,因为他们底层的data指向的同一块连续内存,可以使用copy函数去创建一个副本再去编辑。
3.10 切片内存泄漏
func consumeMessages() {
for {
:= receiveMessage()
msg // Do something with msg
(getMessageType(msg))
storeMessageType}
}
func getMessageType(msg []byte) []byte {
return msg[:5]
}
以上的这个case会出现内存泄漏,msg无法被GC掉因为切片的引用。
有种方式Full slice expressions
func getMessageType(msg []byte) []byte {
return msg[:5:5]
}
三参数切片,最后一个参数表示limited capacity
append的时候如果遇到full capacity就会重新分配,但是并不影响他内存泄漏。所以最好的方式还是copy创建副本。
3.11 无效的Map初始化
初始化的时候指定大小,关于map内部结构,可以查看之前关于map内部实现的文章.
3.12 Map的内存泄露
:= 1_000_000
n := make(map[int][128]byte)
m ()
printAllocfor i := 0; i < n; i++ {
[i] = randBytes()
m}
()
printAllocfor i := 0; i < n; i++ {
delete(m, i)
}
.GC()
runtime()
printAlloc.KeepAlive(m) runtime
结果是 :
0 MB
461 MB
293 MB
map的bucket是无法收缩的,移除map里的元素并不能影响已经存在的bucket。可行的方案一是创建一个新的map,复制所有元素然后覆盖map。另一种方案是改成map[int]*[128]byte
,这样就能用一个指针去替代原来的值。不过这样的方式应该会使GC的时候扫描map,影响GC效率。
3.13 错误的值比较
slices & maps 是无法通过==
比较的。我们可以看下哪些是可比较的:
Booleans 比较值是否相等
Numerics (int, float, and complex types) 比较值是否相等
Strings 比较内容
Channels 比较是否是同一个make创建的,或者是否都是nil
Interfaces 比较是否有相同的类型和值 或者是否都是nil
Pointers 比较是否都指向同一个值 或者是否都是nil
Structs and arrays 比较组成的简单类型
如果我们想要比较slices & maps 我么可以借助reflect.DeepEqual
.该函数回去递归遍历values。
NOTE reflect.DeepEqual has a specific behavior depending on the type we provide. Before using it, read the documentation carefully.
reflect.DeepEqual
比==
慢100倍。最好不要在runtime的时候使用,在测试用例中使用。如果追求性能,我们可以自己实现比较函数,通过遍历比较,也会比reflect.DeepEqual
快很多。或者我们也可以看看三方库如何实现go-cmp or testify. 另外我们必须记住一些标准库中的比较函数 比如bytes.Compare
。