Golang中向goroutine传递指针变量导致值为nil的问题

Golang中向goroutine传递指针变量导致值为nil的问题 我在Revel框架中遇到了以下问题: 在我的revel应用中,我使用了revel模块。其中一个模块包含一个函数

func addToBlocklist(r *revel.Request, claims map[string]interface{})

该函数通过以下方式调用

go addToBlocklist(c.Request, c.Args[jwt.TokenClaimsKey].(map[string]interface{}))

c的类型为

type JwtAuth struct {
	*revel.Controller
}

reve.Controller的Request类型为*revel.Request,与addToBlocklist函数的预期类型一致。 在将c.Request传递给addToBlocklist之前,当我执行fmt.Println(c.Request)时,输出为&{0xc0003079b0 0xc0003280b0 text/html html <nil> GET 127.0.0.1:35376 0.0.0.0:9001 /logout map[] <nil> 0xc0003265b0} 而在addToBlocklist函数开头执行fmt.Println®时,输出为&{<nil> 0xc0003280b0 <nil> <nil> map[] <nil> 0xc0003265b0}

这是在docker机器上使用golang:1.14.6-alpine镜像运行revel测试时的结果。

当我在本地使用go 1.14.6 windows环境运行时,addToBlocklist函数中的r值能够正常传递。


更多关于Golang中向goroutine传递指针变量导致值为nil的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

通过添加 defer fmt.Println(c) 发现,调用 addToBlocklist 的函数在 addToBlocklist 运行之前就结束了。而且 Controller 似乎在 addToBlocklist 运行之前就被置为 nil 了。

RefreshToken:  &{0xc00030b980 0xc000328080 text/html html   <nil> GET 127.0.0.1:35546 0.0.0.0:9001 /refresh-token map[] <nil> 0xc0002e4c30}
End RefreshToken:  &{0xc00030b980 0xc000328080 text/html html   <nil> GET 127.0.0.1:35546 0.0.0.0:9001 /refresh-token map[] <nil> 0xc0002e4c30}
addToBlockList:  &{<nil> 0xc000328080     <nil>    <nil> map[] <nil> 0xc0002e4c30}

感谢 @petrus 的提示。

更多关于Golang中向goroutine传递指针变量导致值为nil的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


goroutine 的参数会被求值,并且该 goroutine 会被发送到调度器。之后,goroutine 被调度并执行。在此期间,指针参数所引用的项发生了改变。

package main

import (
	"fmt"
	"time"
)

func main() {
	a := 42
	fmt.Println(a)
	go func(a *int) { fmt.Println(*a) }(&a)
	a = 7
	time.Sleep(10 * time.Millisecond)
}

https://play.golang.org/p/RBw64XoJPAN

42
7

你也可能遇到竞态条件。在这种情况下,结果是未定义的。

$ go run -race racer.go
42
==================
WARNING: DATA RACE
Read at 0x00c00013a010 by goroutine 7:
  main.main.func1()
      /home/petrus/racer.go:11 +0x3e

Previous write at 0x00c00013a010 by main goroutine:
  main.main()
      /home/petrus/racer.go:12 +0x112

Goroutine 7 (running) created at:
  main.main()
      /home/petrus/racer.go:11 +0x104
==================
7
Found 1 data race(s)
exit status 66
$

请提供一个能最小化复现你问题的代码示例。

这是一个典型的goroutine数据竞争问题。在Go中,当你在goroutine中传递指针时,如果原始数据在goroutine执行前被修改或释放,就会导致指针指向无效内存。

在你的代码中,c.Request是一个指针,当goroutine启动时,它捕获的是当前c.Request的引用。但在Revel框架中,控制器和请求对象有特定的生命周期管理,可能在goroutine执行时,原始的c.Request已经被释放或重用。

以下是问题重现和解决方案:

// 问题代码 - 存在数据竞争
go addToBlocklist(c.Request, c.Args[jwt.TokenClaimsKey].(map[string]interface{}))

// 解决方案1:传递值副本
func addToBlocklist(r revel.Request, claims map[string]interface{}) {
    // 使用值而不是指针
}

// 调用时
go addToBlocklist(*c.Request, c.Args[jwt.TokenClaimsKey].(map[string]interface{}))

// 解决方案2:在goroutine启动前复制数据
func addToBlocklist(r *revel.Request, claims map[string]interface{}) {
    // 函数保持不变
}

// 调用时传递请求的深拷贝
reqCopy := &revel.Request{
    In:            c.Request.In,
    Method:        c.Request.Method,
    // 复制所有需要的字段
}
go addToBlocklist(reqCopy, c.Args[jwt.TokenClaimsKey].(map[string]interface{}))

// 解决方案3:使用闭包捕获当前值
go func(req *revel.Request, claims map[string]interface{}) {
    addToBlocklist(req, claims)
}(c.Request, c.Args[jwt.TokenClaimsKey].(map[string]interface{}))

// 解决方案4:使用通道同步
type BlocklistTask struct {
    Request *revel.Request
    Claims  map[string]interface{}
}

blocklistChan := make(chan BlocklistTask, 100)

// 启动worker goroutine
go func() {
    for task := range blocklistChan {
        addToBlocklist(task.Request, task.Claims)
    }
}()

// 发送任务
blocklistChan <- BlocklistTask{
    Request: c.Request,
    Claims:  c.Args[jwt.TokenClaimsKey].(map[string]interface{}),
}

在Revel框架中,控制器和请求对象通常在请求处理结束后被回收。当你在goroutine中异步处理时,主goroutine可能已经完成了请求处理并释放了相关资源。这就是为什么在Docker环境中会出现nil指针,而在本地Windows环境中可能因为不同的调度时机而暂时正常工作。

推荐使用解决方案4的通道模式,因为它提供了更好的并发控制和资源管理。如果addToBlocklist操作不是关键路径,也可以考虑使用解决方案1的值传递方式。

回到顶部