Skip to content

Commit 9535377

Browse files
committed
go: per-loop vs per-iteration demo
1 parent 51e3318 commit 9535377

File tree

1 file changed

+71
-69
lines changed

1 file changed

+71
-69
lines changed

go-note/tricks.rst

Lines changed: 71 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -740,91 +740,93 @@ Go 如何复制map
740740
testShareMap()
741741
}
742742
743-
循环变量与闭包问题
743+
循环变量与闭包问题
744744
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
745745
go 的循环变量是 per-loop 的而不是 per-iteration 绑定的,这个特性导致了很多隐蔽反直觉并且难以排查的bug。
746746
go 官方目前仍在讨论是否要改善这个问题:https://github.com/golang/go/discussions/56010 。
747-
举一些常见例子防止踩坑:
747+
748+
举三个常见例子防止踩坑:
748749

749750
.. code-block:: go
750751
751-
package main
752+
package main
752753
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+
}
757792
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 }
769795
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{}
782799
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+
}
794804
795-
// 指针接收者调用场景
796-
type field struct {
797-
name string
798-
}
805+
for _, o := range output {
806+
fmt.Println(o.i) // 都打印3
807+
}
808+
}
799809
800-
func (p *field) print() {
801-
fmt.Println(p.name)
802-
}
810+
// 示例3:接收者指针
811+
type field struct{ name string }
803812
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+
}
813816
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+
}
822824
823-
总结一下
825+
总结一下会出bug的常见情况
824826

825-
- 不要直接对循环变量取地址
826-
- 不要在 goroutine 中直接使用循环变量
827-
- 如果循环变量(非指针)调用函数是一个指针接收者,调用值需要拷贝一个临时变量再调用
827+
- 在 goroutine 中直接使用循环变量(示例1)
828+
- 循环中直接对循环变量取地址(示例2)
829+
- 循环变量(非指针)调用函数是一个指针接收者,调用却直接传入循环变量值(示例3)
828830

829831
解决方式:
830832

0 commit comments

Comments
 (0)