@@ -740,91 +740,93 @@ Go 如何复制map
740
740
testShareMap()
741
741
}
742
742
743
- 循环变量与闭包问题 ⭐ ️
743
+ 循环变量与闭包问题 ⚠ ️
744
744
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
745
745
go 的循环变量是 per-loop 的而不是 per-iteration 绑定的,这个特性导致了很多隐蔽反直觉并且难以排查的bug。
746
746
go 官方目前仍在讨论是否要改善这个问题:https://github.com/golang/go/discussions/56010 。
747
- 举一些常见例子防止踩坑:
747
+
748
+ 举三个常见例子防止踩坑:
748
749
749
750
.. code-block :: go
750
751
751
- package main
752
+ package main
752
753
753
- import (
754
- "fmt"
755
- "time"
756
- )
754
+ import (
755
+ "fmt"
756
+ "time"
757
+ )
758
+
759
+ // 示例1:goroutine 直接使用闭包变量
760
+ func testForLoopGoroutine() {
761
+ data := []string{"one", "two", "three"}
762
+ for _, v := range data {
763
+ go func() {
764
+ fmt.Println(v) // 最终打印的都是一样的值,和期望不符
765
+ }()
766
+ }
767
+ // output: three three three
768
+ }
769
+
770
+ // 两种方式解决:1.使用一个临时变量
771
+ func testForLoopGoroutine1() {
772
+ data := []string{"one", "two", "three"}
773
+ for _, v := range data {
774
+ v := v // 使用一个临时变量 v
775
+ go func() {
776
+ fmt.Println(v)
777
+ }()
778
+ }
779
+ // one two three (may wrong order)
780
+ }
781
+
782
+ // 方法2:传给匿名goroutine参数
783
+ func testForLoopGoroutine2() {
784
+ data := []string{"one", "two", "three"}
785
+ for _, v := range data {
786
+ go func(in string) {
787
+ fmt.Println(in)
788
+ }(v)
789
+ }
790
+ // one two three (may wrong order)
791
+ }
757
792
758
- // 闭包问题
759
- func testClosure() {
760
- data := []string{"one", "two", "three"}
761
- for _, v := range data {
762
- go func() {
763
- fmt.Println(v)
764
- }()
765
- }
766
- time.Sleep(1 * time.Second) // not good, just for demo
767
- // three three three // 打印的都是最后一个值
768
- }
793
+ // 示例2:对循环变量取地址
794
+ type MyStruct struct{ i int }
769
795
770
- // 两种方式解决:1.使用一个块临时变量
771
- func testClosure1() {
772
- data := []string{"one", "two", "three"}
773
- for _, v := range data {
774
- vcopy := v // 使用一个临时变量,经常可以看到 v := v 这种写法
775
- go func() {
776
- fmt.Println(vcopy)
777
- }()
778
- }
779
- time.Sleep(1 * time.Second) // not good, just for demo
780
- // one two three (may wrong order)
781
- }
796
+ func testForLoopPointer() {
797
+ values := []MyStruct{MyStruct{1}, MyStruct{2}, MyStruct{3}}
798
+ output := []*MyStruct{}
782
799
783
- // 方法2:使用传给匿名goroutine参数,个人推荐使用这种方式
784
- func testClosure2() {
785
- data := []string{"one", "two", "three"}
786
- for _, v := range data {
787
- go func(in string) {
788
- fmt.Println(in)
789
- }(v)
790
- }
791
- time.Sleep(1 * time.Second) // not good, just for demo
792
- // one two three (may wrong order)
793
- }
800
+ for _, v := range values {
801
+ // v := v // 加上这一行才能打印1,2,3
802
+ output = append(output, &v) // 如果不用临时变量,始终取到的是同一个地址
803
+ }
794
804
795
- // 指针接收者调用场景
796
- type field struct {
797
- name string
798
- }
805
+ for _, o := range output {
806
+ fmt.Println(o.i) // 都打印3
807
+ }
808
+ }
799
809
800
- func (p *field) print() {
801
- fmt.Println(p.name)
802
- }
810
+ // 示例3:接收者指针
811
+ type field struct{ name string }
803
812
804
- func testField() {
805
- data := []field{{"one"}, {"two"}, {"three"}}
806
- for _, v := range data {
807
- // v := v // NOTE:直接这样就可以解决,
808
- // 或者使用 struct 指针。 []*field 初始化
809
- go v.print() // print three three three
810
- }
811
- time.Sleep(1 * time.Second)
812
- }
813
+ func (p *field) print() { // 接收者是指针
814
+ fmt.Println(p.name)
815
+ }
813
816
814
- // 结构体示例
815
- values := []MyStruct{MyStruct{1}, MyStruct{2}, MyStruct{3}}
816
- output := []*MyStruct{}
817
- for _, v := range values {
818
- // v := v
819
- output = append(output, &v) // 不要直接引用一个循环变量的地址
820
- }
821
- fmt.Println("output: ", output) // 打印相同的地址
817
+ func testForLoopPointerReceiver() {
818
+ data := []field{{"one"}, {"two"}, {"three"}} // 改成 []*field 可以修复
819
+ for _, v := range data {
820
+ // v := v // NOTE:直接这样就可以解决。或者使用 struct 指针,[]*field 初始化
821
+ go v.print() // print three three three
822
+ }
823
+ }
822
824
823
- 总结一下 :
825
+ 总结一下会出bug的常见情况 :
824
826
825
- - 不要直接对循环变量取地址
826
- - 不要在 goroutine 中直接使用循环变量
827
- - 如果循环变量 (非指针)调用函数是一个指针接收者,调用值需要拷贝一个临时变量再调用
827
+ - 在 goroutine 中直接使用循环变量(示例1)
828
+ - 循环中直接对循环变量取地址(示例2)
829
+ - 循环变量 (非指针)调用函数是一个指针接收者,调用却直接传入循环变量值(示例3)
828
830
829
831
解决方式:
830
832
0 commit comments