3

Go tips-笔记: 数据类型 17-29 mistakes

 1 year ago
source link: https://weedge.github.io/post/notions/go-tips/go-tips-03-data-types/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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,100],len(s2)=2,cap(s2)=2
// s3=[2 100],len(s3)=1,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反射并为此付出代价,也可以使用自定义实现和库。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK