Skip to the content.

在开发中,通过日志发现一个协程之间会相互访问同一个变量的问题,然而代码本身并没有做这个设计。经过调试,发现是对Go语言For-range循环内存机制理解的不到位。记录下来,以免再踩这个小坑。

发生的错误

简单的说,我在开发的代码目标是通过一个通过消息队列,一次收到N条消息,然后分别通过协成处理完成消费。怎么接收消息和处理代码不是本文的重点。因此,我们也可以把这个过程,抽象成下面的类似代码:

func consumeIntSlice(intSlice []int) {
	for index, obj := range intSlice {
		fmt.Printf("In For-range: index=%d, pointer=%p, value=%v\n", index, &obj, obj)
		go doSomething(obj)
	}
}

func doSomething(v int) {
	// do something to consume message
	fmt.Printf("In doSomething: pointer=%p, value=%v\n", &v, v)
}

这段代码执行过程是正常的,不会产生其他问题。但是,由于某些原因,我需要将doSomething方法的入参改为指针类型,代码变为:

func doSomething(v *int) {
	// do something to consume message
	fmt.Printf("In doSomething: pointer=%p, value=%v\n", v, *v)
}

相应的,调用方法也变为:

func consumeIntSlice(intSlice []int) {
	for index, obj := range intSlice {
		fmt.Printf("In For-range: index=%d, pointer=%p, value=%v\n", index, &obj, obj)
		go doSomething(&obj)
	}
}

这时,问题出现了,几个doSomething协程使用的变量值,相互有重复的,有时甚至所有协程的入参变量值都相同。打印结果如下:

In For-range: index=0, pointer=0xc0000ae008, value=1
In For-range: index=1, pointer=0xc0000ae008, value=2
In For-range: index=2, pointer=0xc0000ae008, value=3
In For-range: index=3, pointer=0xc0000ae008, value=4
In For-range: index=4, pointer=0xc0000ae008, value=5
In doSomething: pointer=0xc0000ae008, value=5
In doSomething: pointer=0xc0000ae008, value=5
In doSomething: pointer=0xc0000ae008, value=4
In doSomething: pointer=0xc0000ae008, value=5
In doSomething: pointer=0xc0000ae008, value=5

为暴露问题,我们已经加了对变量的打印,可以看到在for-range循环中,变量obj的内存都是相同的。当启动doSomething协程,如果传入的参数是变量的指针,那么在协程处理 的过程中,变量值已经被外部改变了。造成协程内,使用的变量值不可预期。

问题的解决

通过调试可以发现,问题出现的原因是:obj是在For-range循环开始时定义的,内存也是在循环开始时分配的,而不是每个循环过程都重新分配一次,For-range每次循环只改变变量值,而不重新分配变量。

因此,如果我们传入的是地址,则这个地址所对应的变量,可能被每次循环赋值而覆盖。

要解决也很简单,调用方法变为:

func consumeIntSlice(intSlice []int) {
	for index, obj := range intSlice {
		fmt.Printf("In For-range: index=%d, pointer=%p, value=%v\n", index, &obj, obj)
		param := obj
		go doSomething(&param)
	}
}

在协程启动之前给每个协程一个独立的变量,就可以解决这个问题。修改后的运行结果:

In For-range: index=0, pointer=0xc0000ae008, value=1
In For-range: index=1, pointer=0xc0000ae008, value=2
In For-range: index=2, pointer=0xc0000ae008, value=3
In For-range: index=3, pointer=0xc0000ae008, value=4
In For-range: index=4, pointer=0xc0000ae008, value=5
In doSomething: pointer=0xc0000ae030, value=5
In doSomething: pointer=0xc0000ae010, value=1
In doSomething: pointer=0xc0000ae018, value=2
In doSomething: pointer=0xc0000ae020, value=3
In doSomething: pointer=0xc0000ae028, value=4