笔记

17.与八进制文字混淆

Go 可以处理二进制、十六进制、虚数和八进制数。八进制数字以 0 开头。但是,为了提高可读性并避免未来代码阅读器可能犯的错误,请使用前缀明确表示八进制数字0o

18.忽略整数溢出

在处理大数字或进行转换成小数字,进行运算时,可能会出现溢出。溢出会产生一些bug, 所以需要再一些可能溢出的场景中增加检测判断是否溢出,溢出则直接panic,或者进行错误返回

19.不理解浮点数

float64类型为例。math.SmallestNonzeroFloat64float64最小值)和math.MaxFloat64(最大值)之间存在无限多个实数值float64。但是该float64类型有有限位数:64。因为不可能将无限值放入有限空间,所以必须使用近似值,因此可能会失去精度。同样的逻辑适用于类型float32。所以在使用==运算符比较两个浮点数可能会导致不准确,浮点计算的结果取决于实际的处理器。最多处理器有一个浮点单元 (FPU) 来处理此类计算。不能保证在一台机器上执行的结果在另一台具有不同 FPU 的机器上是相同的。testify测试( https://github.com/stretchr/testify ) 有一个InDelta功能断言两个值在彼此给定的误差范围内。

Gofloat32float64是近似值。必须牢记一些规则:

  • 比较两个浮点数时,检查它们的差异是否在可接受的范围内。
  • 执行加法或减法时,将具有相似数量级的运算分组以获得更好的准确性。
  • 为了保证准确性,如果一系列运算需要加、减、乘或除,请先执行乘除运算。

20.不了解切片长度和容量

平常代码中经常用到是的slice, 和数组不是一个概念,这个是在Go中最容易犯得错误,需要了解slice结构,对于只读操作,可以在slice结构指向的数组进行复用,而无需重新分配空间(注意复用后是否需要回收);但是在append操作时,需要考虑什么时候扩容,重新分配了数组空间;书中有个错误地方,也是很多文章介绍slice grow时不准确的地方,在1.18版本之后,slicegrow方法有所改进,具体查看 https://github.com/golang/go/blob/release-branch.go1.18/src/runtime/slice.go growslice函数;

21.切片初始化效率低下

切片在初始化时,尽量分配好容量,如果经常append操作,对于未初始化容量的slice, 在append 进行slicegrow扩容操作时,会分配一个临时的内存空间,导致 GC 需要付出额外的努力来清理所有这些临时分配的内存空间。也尽量初始化slice长度大小,这样直接可以用数组下标进行操作,效率更高,对于性能有要求的场景选择后者;

22.对 nil 和空切片感到困惑

比如 这个在开发api返回json数据时经常会遇到,一般情况会返回一个空切片,防止调用端未判断nil而导致panic;具体根据上下文初始化:

  • var s []string如果不确定最终长度并且切片可以为空
  • []string(nil)作为创建 nil 和空切片的语法糖
  • make([]string, length)如果未来的长度已知

23.没有正确检查切片是否为空

不管是空切片还是nil, 通过检查长度len(slice)是最好的选择, 都是0

24.没有正确使用切片copy

其实就是了解copy函数,复制到目标切片的元素数量为源切片和目标切片长度最小值min(len(dst),len(src)),而且不能混淆参数来,copy(dst,src []Type),还有一种替换方案:dst := append([]int(nil), src...)

25.使用切片附加的意外副作用

s1 := []int{1, 2, 3}
s2 := s1[1:2]
s3 := append(s2, 10)
// s1=[1 2 10],len(s1)=3,cap(s1)=3
// s2=[2],len(s2)=1,cap(s2)=2
// s3=[2 10],len(s3)=2,cap(s3)=2

这里和第20个一样的道理,理解切片的接口,以及什么时候扩容,不扩容时,共享底层数组,打印数据有长度大小决定,如果在容量范围内想要获取溢出的数据,也是可以做到的,需要引入不安全的指针操作,比如想越界获取s2[1]的值,如下 :

bp := (*[3]uintptr)(unsafe.Pointer(&s2))
h := [3]uintptr{bp[0], bp[1] + 1, bp[2]}
ss2 := *(*[]int64)(unsafe.Pointer(&h))
ss2[1] = 100
// s1=[1 2 100],len(s1)=3,cap(s1)=3
// s2=[2],len(s2)=1,cap(s2)=2
// s3=[2 100],len(s3)=2,cap(s3)=2

如果不想改变s1和s2指向的数组数据,可以进行copy操作,然后在append数据给s3

s1 := []int{1, 2, 3}
s2 := s1[1:2]
s3 := make([]int,2)
copy(s3,s2)
// s3 := append([]int(nil),s2...)
s3 = append(s3, 10)
// s1=[1 2 3],len(s1)=3,cap(s1)=3
// s2=[2],len(s2)=1,cap(s2)=2
// s3=[2 10],len(s3)=2,cap(s3)=2

完整切片表达式: s[low:high:max]。max没有赋值默认为原始数据容量大小,如果max值超过原始数据容量则会在运行时panic;生成的切片的容量等于max - low,长度等于high-low,数据范围在[low,high) 左闭右开区间进行切片。

所以使用切片时,可能会面临导致意想不到的副作用的情况。如果生成的切片的长度小于其容量,append则可以改变原始切片。如果想限制可能的副作用的范围,可以使用切片copy或完整的切片表达式,这会阻止进行复制。

26.切片和内存泄漏 (重要)

切片中导致内存泄露的原因是,切片一直在复用,未被gc释放,每次重新使用又会重新分配一次内存空间,一直重复分配,导致内存泄露,比如,在网络请求中,消息数据大于32KB,需要对网络的消息协议进行解包之后处理保存数据, 首先从网络中获取消息,然后从消息中获取协议头,通过切片方式复用对应消息,下次从网络请求中获取消息数据,又重新从堆上分配了新的内存空间,以前复用的空间还未释放,最终导致内存泄露;解决这个问题的方案,从消息中获取协议头,只copy协议头部分数据进行处理,处理完,由gc来释放内存空间;

根据经验,请记住对大切片或数组进行切片可能会导致潜在的高内存消耗。剩余的空间不会被 GC 回收,尽管只使用了几个元素,但可以保留一个大的后备数组。copy切片是防止这种情况的解决方案。通过runtime.ReadMemStats 函数可以获取运行时内存alloctor的统计信息MemStats,

func printAlloc() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("%d KB\\n", m.Alloc/1024)
}

介绍了两个潜在的内存泄漏问题:

第一个是关于对现有切片或数组进行切片以保留容量;如果处理大型切片并将它们重新切片以仅保留一小部分,则大量内存将保持分配状态但未使用。

第二个问题是,当对指针或具有指针字段的结构使用切片操作时,需要知道 GC 不会回收这些元素。

在这种情况下,两个选项是执行复制或将剩余元素字段显式标记为nil

27.低效的map初始化

在使用slice切片时,如果预先知道要添加到切片中的元素数量,就可以使用给定的大小或容量对其进行初始化。这避免了必须不断重复昂贵的切片增长操作。这个想法与map类似。这样可以尽量减少map的扩容,重新分配新的空间,以及扩容后的rehash平衡所有元素。

28.map和内存泄漏

向map中添加n个元素,然后删除所有元素,意味着在内存中保留相同数量的桶。因为 Go map 的大小只会增加,所以它的内存消耗也会增加。没有自动策略来缩小它。如果这导致高内存消耗,可以尝试不同的方法,例如强制 Go 重新创建map(这种方式不太可取,在复制之后和下一次垃圾回收之前,可能会在短时间内消耗当前内存的两倍) 或 val存放指向数据的指针;

tips: 需要了解map的结构,以及map中的key (可比较类型)

29.错误地比较值

具体可比较的类型见官方最新文档: (重要)

The Go Programming Language Specification - The Go Programming Language

考虑到文档中这些行为,如果必须比较两个切片、两个map或两个包含不可比较类型的结构,有哪些选择?如果坚持使用标准库,一个选择是使用运行时反射reflect包中的reflect.DeepEqual方法, 但是由性能损失,一般不用于生产环境,主要用于单元测试返回的值是否是预期值,还有类似的三方库用于测试时的期望值比较,比如go-cmp ( https://github.com/google/go-cmp ) 或testify( https://github.com/stretchr/testify );如果性能在运行时至关重要,那么实施自定义方法可能是最佳解决方案;

概括

  • 阅读现有代码时,请记住以 0 开头的整数文字是八进制数。此外,为了提高可读性,通过在八进制整数前加上0o.
  • 因为整数上溢和下溢在 Go 中是静默处理的,所以可以实现自己的函数来捕获它们。
  • 在给定的误差范围内进行浮点比较可以确保代码是可移植的。
  • 执行加法或减法时,将具有相似数量级的运算分组以提高准确性。此外,在加减法之前执行乘法和除法。
  • 了解切片长度和容量之间的差异应该是 Go 开发人员核心知识的一部分。切片长度是切片中可用元素的数量,而切片容量是后备数组中元素的数量。
  • 创建切片时,如果其长度已知,则使用给定的长度或容量对其进行初始化。这减少了分配的数量并提高了性能。map的逻辑相同,需要初始化它们的大小。
  • append如果两个不同的函数使用由同一数组支持的切片,则使用复制或完整切片表达式是一种防止产生冲突的方法。但是,如果想缩小一个大切片,只有切片copy可以防止内存泄漏。
  • 要使用内置函数将一个切片复制到另一个切片copy,请记住复制的元素数对应于两个切片长度之间的最小值。
  • 使用指针切片或具有指针字段的结构,可以通过标记nil为切片操作排除的元素来避免内存泄漏。
  • 为了防止常见的混淆,例如在使用encoding/jsonorreflect包时,需要了解 nil 和空切片之间的区别。两者都是零长度、零容量的切片,但只有 nil 切片不需要分配。
  • 要检查切片是否不包含任何元素,请检查其长度。无论切片是否为nil空,此检查都有效。map也是如此。
  • 要设计明确的 API,不应该区分 nil 和空切片。
  • map可以在内存中增长,但永远不会缩小。因此,如果它导致一些内存问题,可以尝试不同的选项,例如强制 Go 重新创建map或使用指针。
  • 要在 Go 中比较类型,如果两种类型是可比较的,则可以使用==!=运算符:布尔值、数字、string、指针、channel和由可比较类型组成的结构体。否则,可以使用reflect.DeepEqual反射并为此付出代价,也可以使用自定义实现和库。