Go 语言使用 goroutine 运行闭包的“坑”

简介: Go 语言使用 goroutine 运行闭包的“坑”

介绍

在 Go 语言中,函数支持匿名函数,闭包就是一种特殊的匿名函数,它可以用于访问函数体外部的变量。

需要注意的是,在 for ... range ... 中,使用 goroutine 执行闭包时,经常会掉“坑”。

因为匿名函数可以访问函数体外部的变量,而 for ... range ... 返回的 val 的值是引用的同一个内存地址的数据,所以匿名函数访问的函数体外部的 val 值是循环中最后输出的一个值。

“踩坑”示例代码

func main() {
 done := make(chan bool)
 values := []string{"a", "b", "c"}
 for _, v := range values {
  go func() {
   fmt.Println(v)
   done <- true
  }()
 }
 // wait for all goroutines to complete before exiting
 for _ = range values {
  <-done
 }
}

输出结果:

c
c
c

阅读上面这段代码,在 for ... range ... 中,使用 goroutine 执行闭包,打印切片中的元素,实际输出结果不是我们期望得到的输出结果。

这是因为循环的每次迭代都使用相同的变量 v 实例,因此每个闭包共享该单个变量。我们可以在示例代码中简单修改,同时输出变量 v 的内存地址和值。

fmt.Println(v) 修改为 fmt.Printf("val=%s pointer=%p\n", v, &v)

修改后的输出结果:

val=c pointer=0xc000010200
val=c pointer=0xc000010200
val=c pointer=0xc000010200

我们可以在输出结果中发现,打印变量 v 的内存地址都是 0xc000010200

当闭包运行时,它会在执行 fmt.Println 时打印变量 v 的值,但 v 的值可能在 goroutine 启动后已被修改。感兴趣的读者朋友们可以使用 go vet 检查。

怎么避免“踩坑”呢?

一种方法是将变量作为参数传递给闭包:

func main() {
 done := make(chan bool)
 values := []string{"a", "b", "c"}
 for _, v := range values {
  go func(param string) {
   // fmt.Println(v)
   fmt.Printf("val=%s pointer=%p\n", param, &param)
   done <- true
  }(v)
 }
 // wait for all goroutines to complete before exiting
 for _ = range values {
  <-done
 }
}

输出结果:

val=c pointer=0xc000010200
val=a pointer=0xc00009a000
val=b pointer=0xc0000a4000

阅读上面这段代码,通过将变量 v 的值作为参数传递给闭包,然后,该值作为形参 param 的值,在函数体内部被访问。

另外一种方法是创建一个新变量:

func main() {
 done := make(chan bool)
 values := []string{"a", "b", "c"}
 for _, v := range values {
  param := v
  go func() {
   // fmt.Println(v)
   fmt.Printf("val=%s pointer=%p\n", param, &param)
   done <- true
  }()
 }
 // wait for all goroutines to complete before exiting
 for _ = range values {
  <-done
 }
}

输出结果:

val=c pointer=0xc000082200
val=a pointer=0xc0000821e0
val=b pointer=0xc0000821f0

通过输出结果可以发现,该种方式也可以达到我们期望的结果。

03

总结

本文我们介绍了在 for ... range ... 中,Go 语言在每次迭代时,没有定义一个新变量,导致使用 goroutine 运行闭包时,经常会掉“坑”。

我们给出避免“踩坑”的两种方法,其中,第二种方法更简单。

推荐阅读:

参考资料:

  1. https://go.dev/tour/moretypes/25
  2. https://gobyexample.com/closures
  3. https://pkg.go.dev/cmd/vet
  4. https://go.dev/doc/faq#closures_and_goroutines
  5. https://go.dev/doc/effective_go#goroutines
目录
相关文章
|
3天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
16 2
|
4天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
2天前
|
Go
go语言中的 跳转语句
【11月更文挑战第4天】
9 4
|
1天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
11 1
|
2天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
9 2
|
2天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
10 2
|
3天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
|
7天前
|
JavaScript Java Go
探索Go语言在微服务架构中的优势
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出。本文将深入探讨Go语言在构建微服务时的性能优势,包括其在内存管理、网络编程、并发模型以及工具链支持方面的特点。通过对比其他流行语言,我们将揭示Go语言如何成为微服务架构中的一股清流。
|
6天前
|
Ubuntu 编译器 Linux
go语言中SQLite3驱动安装
【11月更文挑战第2天】
28 7