十天前,Go语言更新至1.22版本,带来了一些与我们日常工作密切相关的新特性。本期内容将被纳入我的高级付费课程中,供大家深入学习。我已经将我的环境更新到了Go 1.22,最新的版本。让我们来探讨几个主要的更新。
首先,我们来讨论与for ~~range~~循环相关的改进。在1.2版本之前,遍历切片或映射时,变量V实际上在每次迭代中复用相同的内存空间。这意味着,如果你在循环中启动了多个协程,由于协程的启动可能比循环本身慢,所有协程可能都会引用到循环的最后一个元素。在Go 1.22版本中,这一行为得到了改进:每次迭代时,V都会分配新的内存,确保每个协程能够正确地引用到各自的迭代变量。
其次,新版本简化了范围的遍历写法,例如遍历一个0到3的��间,现在可以直接写成更简洁的形式,输出0到2,使得Go语言的这部分用法与其他语言更为接近,使用起来更加方便。
接下来,关于随机数的生成,Go 1.2引入了一些变化。旧版本中使用的一些方法在新版本中已不再支持,标识符从小写n变为了大写N,反映了API的变动。这些改变不仅涉及方法名称的变化,也包括了性能的提升,新算法使得随机数生成速度大大加快。
再看切片的拼接,新版本提供了一种更为简洁的方式来合并多个切片。在旧版本中,你需要逐一通过append函数将切片合并。而现在,一行代码就可以完成多个切片的合并操作,极大地简化了代码。
最后,我们讨论了HTTP路由的改进。Go 1.22版本丰富了标准库中的路由功能,支持RESTful风格的URL定义。这意味着,你可以在URL中使用花括号定义变量,然后在请求处理函数中获取这些变量的值,从而使得路由处理更加灵活和强大。
通过实际演示,我们可以看到这些变化带来的实际效果,无论是在性能提升还是在编码便利性上,Go 1.22版本都带来了显著的改进。以上就是本次分享的主要内容,希望能够帮助大家更好地理解和应用Go语言的最新版本。
大家好,随着过年的临近,我们今天将录制一节特别课程,探讨自从版本1.18起就开始的一个连续更新过程。值得注意的是,这个更新过程似乎永远不会结束,只要有新的更新发布,我们就会继续介绍。目前,我们已经进展到了1.22版本,从1.18开始,期间引入了许多新特性。特别是最新的1.22版本,它在我录制此视频时刚刚发布。通常情况下,我们不建议在版本号以.0结尾时立即在生产环境中使用新版本,更稳妥的做法是等到.3或.4版本再进行升级。当然,对于开发环境,使用最新的.0版本进行开发是完全可行的。
如果你目前的项目还在使用1.21或更早的版本,建议先进行适应性测试,而不是急于升级。今天的课程将讨论一个所谓的“修复”——虽然这不完全是一个修复,因为之前的行为本质上是一个特性,我们在之前的课程中已经讨论过循环遍历时产生的指针问题。在1.22版本之后,这个问题被“修复”,或者说,官方认为旧的实现方式可能会导致代码中出现问题,因此进行了调整。
接下来,我会通过代码示例演示。首先,让我们回顾在1.21版本中的代码表现。我们之前录制的视频中提到,例如,有一个数组或切片,然后我们创建了一个map或另一个切片,里面存储的是指针。常规写法可能会导致一些误解,尤其是当我们遍历这些集合时。例如,我们尝试打印出数组中的每个元素的指针,但在1.21或更早的版本中,这会导致错误,因为遍历过程中使用的是副本,这意味着所有元素的指针都会指向同一个地址。
现在,让我们看看1.22版本的代码表现。在Go 1.22之后,每次循环迭代时都会创建一个新的变量实例,因此不会出现之前版本中的问题。这意味着,即使我们在循环中处理指针,每个元素的地址也会是唯一的,从而避免了旧版本中存在的问题。
此外,我们还将讨论循环和迭代的新方式,这在以前的Go版本中是不可能的,比如直接遍历一个数字范围。虽然在1.22版本中这种写法是可行的,但我们的IDE可能还不支持最新语法,显示黄色波浪线。不过,实际运行时不会有问题。
在总结今天的课程时,我们提到,即使旧版本中的代码可能需要某些方式去“修复”所谓的问题,这些代码在1.22版本中仍然是兼容的。这也意味着,如果你将1.22版本的代码放到1.21或更早的版本中,可能会遇到兼容性问题。
今天的课时就到这里。如果有任何问题,欢迎在群里提问。
比较值得关注的 不再共享同一个for 循环迭代器的变量
之前这是一个经典错误,也曾因为认识不到位,在大大小小的公司,引发过很多严重bug(想起来某年中秋节前夜,排查到凌晨3点的经历)
解决办法也很经典—引入一个中间变量
之前的 关于range二三事–著名的golang for range的坑 曾有记述
即在go 1.22之前,for range 迭代出的值是同一个变量,只分配了一次内存地址~
即
1 | package main |
在1.22之前,输出为
1 | 3 |
1.22及之后:
1 | 2 |
(因为map的乱序,每次输出的顺序可能不一样)
虽然说这个改动规避了大多数情况下可能出现的问题, 但其实也带来一些风险,凡事难免有例外,例如我之前明确知道迭代的变量都是3,是我有意为之的.
升级1.22后,结果不一致了
Go官方提供了一个叫做bisect的工具,同时移除了go vet中loopclosure对于这种情况不再报错(并没有移除,可以通过go tool vet help查看)..但我发现,在go 1.21执行
go vet ./…也不会打印出问题..
其实通过 关于range二三事–著名的golang for range的坑 后面增加的调试信息,在第一个for循环写入时,就已经有了问题…
所以和是不是range有没有range 没有什么关系,更确切说,一直以来有问题的是for,1.22解决的也是针对for,
这样来验证
1 | package main |
对于1.21,输出:
1 | (map[int]*int) (len=3) { |
对于1.22,输出:
1 | (map[int]*int) (len=3) { |
可以在 https://godbolt.org/ 比较下面这段代码在go 1.21和go 1.22下的差异
1 | package main |
1 | 下面这两段汇编有什么区别: 汇编1:main_main_pc0: |
这两段汇编代码都是用于Go语言编译过程中的低级表示,展示了特定函数(在这个案例中是main.main)的机器指令。尽管这两段汇编看起来非常相似,它们在结构、分配的栈空间大小以及一些操作细节上有所不同。下面是一些主要的区别:
栈空间分配:
- 第一段汇编为函数
main.main分配了$124字节的栈空间。 - 第二段汇编为相同的函数分配了$132字节的栈空间。
这表明两个函数版本在局部变量的数量或类型上可能有所不同,导致栈空间需求的变化。
- 第一段汇编为函数
栈帧设置:
- 在两段代码中,栈帧的设置(
MOVW.W R14, -128(R13)与MOVW.W R14, -136(R13))略有不同,这反映了不同的栈空间分配。
- 在两段代码中,栈帧的设置(
函数调用和操作:
- 虽然大部分操作看起来相似,但是两者在调用的内置函数(如
runtime.duffzero、runtime.newobject等)和执行的具体操作上有细微差别。例如,第二段代码中使用了runtime.rand32而不是第一段中的runtime.fastrand,这可能指示对随机数生成逻辑的不同处理。
- 虽然大部分操作看起来相似,但是两者在调用的内置函数(如
局部变量处理:
- 在对局部变量的处理上,两段代码使用了不同的临时变量(如
main..autotmp_3-104(SP)与main..autotmp_5-32(SP)),这可能指示了不同的局部变量布局或优化级别。
- 在对局部变量的处理上,两段代码使用了不同的临时变量(如
条件分支和循环:
- 两段代码在条件分支和循环逻辑的实现上略有不同,这可以从跳转标签(如
main_main_pc196与main_main_pc248)和条件跳转指令(如BLS、BGT)的使用上看出。
- 两段代码在条件分支和循环逻辑的实现上略有不同,这可以从跳转标签(如
垃圾回收和写屏障:
- 两段代码都涉及到垃圾回收和写屏障的调用,但是它们的具体调用点和上下文可能有所不同,这反映了不同的内存管理策略或编译器优化。
总的来说,尽管这两段汇编代码在高层面上执行相似的功能,它们在细节上的差异可能源于不同的编译器版本、编译器优化选项、源代码级别的差异,或者是不同的运行时行为优化。理解这些差异需要深入到Go语言的编译器实现和目标平台的汇编语言特性中。
局部变量有什么区别?
从给定的汇编代码段中直接比较局部变量的差异是一项挑战,因为汇编语言展示的是编译后的结果,而不直接展示高级语言中的变量名或类型。然而,通过观察栈空间的使用和操作,我们可以推断一些关于局部变量的差异:
栈空间分配差异:第一段代码为局部变量分配了$124字节,而第二段代码分配了$132字节。这表明第二段代码可能定义了更多的局部变量或者有更大的局部变量。
临时变量的使用:在两段代码中,通过
SP(栈指针)的不同偏移量引用的临时变量不同(例如,main..autotmp_3-104(SP)与main..autotmp_5-32(SP)),这暗示了不同的局部变量或临时数据结构被用于计算或状态保持。局部变量的初始化:两段代码都使用
DUFFZERO或类似方法对局部变量进行初始化,但是具体的初始化目标(如main..autotmp_4-76(SP)与main..autotmp_6-108(SP))和顺序有所不同,这可能反映了不同的局部变量布局或优化策略。局部变量操作:对局部变量的操作,包括赋值和读取,通过不同的汇编指令和寄存器间接表示。比如,两段代码中局部变量的读写操作(如
MOVW指令的使用)涉及不同的寄存器和栈帧位置,表明它们操作的局部变量集可能不同。函数调用中的局部变量传递:通过对函数调用参数的分析(如
CALL runtime.newobject(SB)),我们可以看到不同的局部变量被用作参数传递,这也暗示了局部变量的差异。
综上所述,虽然直接从汇编代码中分辨局部变量的具体差异较为复杂,我们可以推断两段代码在局部变量的数量、类型、初始化以及使用方式上存在差异。这些差异可能源自原始Go代码中的不同局部变量声明和使用模式,或者是编译器在不同优化级别下的不同行为。理解这些差异的确切性质通常需要对应的高级语言代码和对编译器行为的深入了解。
对于
1 | package main |
这段汇编代码是Go语言编译后的产物,针对您之前提供的Go源代码。它展示了main.main函数的具体执行指令,包括内存操作、函数调用和条件分支等。下面是对这段汇编代码的关键部分的解读,以及它们在源代码中的对应关系:
函数入口和栈空间分配:
TEXT main.main(SB), ABIInternal, $124-0定义了main.main函数的入口点,其中$124-0表明为这个函数分配了124字节的栈空间。MOVW 8(g), R1通过全局指针g获取当前goroutine的栈顶地址。CMP R1, R13和BLS main_main_pc244检查是否有足够的栈空间,如果不够则跳转到扩展栈空间的代码。
局部变量和循环:
MOVW $main..autotmp_3-104(SP), R1和随后的DUFFZERO调用初始化局部变量。DUFFZERO用于清零一段内存,这里可能是为映射分配的内存。- 循环体通过对
i的操作实现,MOVW (R2), R0和CMP $0, R0以及跳转指令控制循环迭代。
打印和映射操作:
- 包含
CALL runtime.printlock(SB)、CALL runtime.printstring(SB)、CALL runtime.printpointer(SB)和CALL runtime.printunlock(SB)的序列用于打印字符串”shuang:”和变量i的指针值。 MOVW $type:map[int]*int(SB), R2和随后的CALL runtime.mapassign_fast32(SB)展示了向映射中赋值的过程,其中使用runtime.mapassign_fast32快速赋值函数针对32位键的映射。
- 包含
垃圾回收和写屏障:
MOVW runtime.writeBarrier(SB), R1和CMP $0, R1以及条件跳转和CALL runtime.gcWriteBarrier2(SB)的使用,表明这里处理了垃圾回收的写屏障。写屏障用于在赋值时保持垃圾回收器的正确性。
函数退出:
- 代码末尾的
MOVW.P 128(R13), R15和NOP指令用于恢复栈指针并退出函数。 - 如果栈空间不足,
CALL runtime.morestack_noctxt(SB)用于请求更多的栈空间,然后通过JMP main_main_pc0跳回函数开始重新执行。
- 代码末尾的
这段汇编代码是编译器根据Go源代码生成的,直接反映了源代码的逻辑结构和运行时行为,包括循环、条件分支、函数调用、映射操作和垃圾回收处理。每一条汇编指令都对应源代码中的特定操作或行为,通过这种方式,Go编译器将高级语言转换成机器可以直接执行的指令集。
之所以存在差异,是因为Go 1.22版本开始,for
range语句中声明的循环变量(比如这里的i和v)不再是整个loop一份(loop var per loop),而是每次iteration都会有自己的变量(loop var per-iteration)
Go1.22 新特性:for 循环不再共享循环变量,且支持整数范围
Go 1.22 slices 库的更新:高效拼接、零化处理和越界插入优化
原文链接: https://dashen.tech/2017/02/20/Go-1-22新特性介绍/
版权声明: 转载请注明出处.