1。简介在Go语言中,函数支持匿名函数,而闭包是一种特殊的匿名函数,可以用来访问函数体之外的变量。需要注意的是,在for...range...中,使用goroutine执行闭包时,往往会掉进一个“坑”。因为匿名函数可以访问函数体外的变量,而for...range...返回的val的值是引用的同一内存地址的数据,所以匿名函数访问的函数体外的val值是循环中的最后一个值输出的值。2.《踩坑》示例代码funcmain(){done:=make(chanbool)values:=[]string{"a","b","c"}for_,v:=rangevalues{gofunc(){fmt.Println(v)done<-true}()}//等待所有goroutines完成后退出for_=rangevalues{<-done}}outputresult:cccread上面的代码,在Infor...range...中,使用goroutine执行了闭包,打印了切片中的元素,但是实际输出并不是我们预期的。这是因为循环的每次迭代都使用相同的变量v实例,因此每个闭包共享该单个变量。我们可以在示例代码中简单修改一下,同时输出变量v的内存地址和值。将fmt.Println(v)更改为fmt.Printf("val=%spointer=%p\n",v,&v)。修改后的输出结果:val=cpointer=0xc000010200val=cpointer=0xc000010200val=cpointer=0xc000010200我们在输出结果中可以发现打印出的变量v的内存地址为0xc000010200。当闭包运行时,它会在执行fmt.Println时打印变量v的值,但是v的值可能在goroutine启动后被修改了。有兴趣的读者朋友可以用govet查一下。如何避免“踩坑”?一种方法是将变量作为参数传递给闭包:funcmain(){done:=make(chanbool)values:=[]string{"a","b","c"}for_,v:=rangevalues{gofunc(paramstring){//fmt.Println(v)fmt.Printf("val=%spointer=%p\n",param,¶m)done<-true}(v)}//等待所有goroutine完成后再退出for_=rangevalues{<-done}}output:val=cpointer=0xc000010200val=apointer=0xc00009a000val=bpointer=0xc0000a4000将值传递给闭包作为参数,然后在函数体内部访问该值作为形参param的值。另一种方法是创建一个新变量:funcmain(){done:=make(chanbool)values:=[]string{"a","b","c"}for_,v:=rangevalues{param:=vgofunc(){//fmt.Println(v)fmt.Printf("val=%spointer=%p\n",param,¶m)done<-true}()}//等待所有goroutine完成后退出for_=rangevalues{<-done}}输出结果:val=cpointer=0xc000082200val=apointer=0xc0000821e0val=bpointer=0xc0000821f0通过输出结果可以发现,这个方法也可以达到我们想要的结果。3.小结在这篇文章中,我们介绍了在for...range...中,Go语言在每次迭代时并没有定义一个新的变量,这在使用goroutine运行闭包时经常会出现“陷阱”。我们给出两种避免“踩坑”的方法,其中第二种方法比较简单。参考资料:https://go.dev/tour/moretypes/25https://gobyexample.com/closureshttps://pkg.go.dev/cmd/vethttps://go.dev/doc/faq#closures_and_goroutineshttps://go.dev/doc/effective_go#goroutines
