Golang中通道和可变参数编译问题解析

Golang中通道和可变参数编译问题解析 大家好,

我是Go语言的新手,正在学习goroutine相关的内容,我想创建一个可变参数的通道合并器。这里有一个在线编译器 A Tour of Go,以下是我编写的代码:

package main

import "fmt"


func generate(v int) <- chan int {
	ch := make(chan int, 1)
	go func(){
		for i := v; ; i++ {
			ch <- i
		}
	}()
	
	return ch
}

func merge(v ... <-chan int ) <- chan int {
	ch := make( chan int, 1 )
	for  z := range v {
		go func() { for { ch <- <-z } }()
	}
	return ch
}

func main() {
	ch := merge( generate(1000), generate(100), generate(100000) )
	
	for i := 0; i < 10; i++ {
	fmt.Println(<- ch)
	}

}

我得到的响应是:

# example
./prog.go:20:27: invalid operation: <-z (receive from non-chan type int)

为什么会这样?难道Go不应该识别出我在for循环中遍历的是通道吗?

谢谢!


更多关于Golang中通道和可变参数编译问题解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

完美!很高兴听到这个消息。

更多关于Golang中通道和可变参数编译问题解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


谢谢!这个方法有效!因此,我建议将这个例子放入 Golang 的教程中。

所以我的建议是将这个例子放在Go语言的教程中。

我不认识官方Go语言教程的维护者,但我写了一篇关于这个问题的短文以供将来参考。 🙂

这是经典的循环内 goroutine 闭包问题。这已经是众所周知且文档中已有说明的内容,无需再添加更多。

func main() {
    for i := 0; i < 5; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
    time.Sleep(time.Second)
}

你好 @aleksanderacai

我看到了一个经典的注意事项(我猜几乎每个人都会遇到这个问题):

merge 函数内部的 goroutine 是一个闭包,因此 goroutine 可以访问外部变量。

然而,在循环中启动的 goroutine 可能直到循环结束后才有机会运行。

当这些 goroutine 最终开始运行时,它们都会获取到相同的 z 值,这个值是 range 遍历 v 的最终值。

→ 始终通过参数将循环数据传递给 goroutine 闭包。这样每个 goroutine 都会接收到相应循环迭代的数据。

		go func(c <-chan int) { for { ch <- <-c } }(z)

Playground: The Go Playground

嗯,我本以为这就是解决方案,因为我发现有人在使用可变参数通道进行迭代。

package main

import "fmt"
import "time"
import "math/rand"

func generate(v int) <- chan int {
	ch := make(chan int, 1)
	go func(){
		for i := v; ; i++ {
			ch <- i
			 time.Sleep(time.Duration(rand.Intn(100))*time.Millisecond)
		}
	}()
	
	return ch
}

func merge(v ... <-chan int ) <- chan int {
	ch := make( chan int, 1 )
	for  i, z := range v {
		fmt.Printf("Merging %v %s\n", i, z)
		go func() { for { ch <- <-z } }()
	}
	return ch
}

func main() {
	ch := merge( generate(1000), generate(100), generate(100000) )
	
	for i := 0; i < 10; i++ {
	fmt.Println(<- ch)
	}

}

但随后发现只有最后一个通道在主导,前两个通道被忽略了。

你的代码问题在于 range 返回的是索引而不是通道本身。在 Go 中,range 作用于切片时,第一个返回值是索引(int 类型),第二个返回值才是元素值。由于你只接收了一个变量 z,它实际接收到的是索引(int 类型),而不是通道,因此 <-z 会报错。

以下是修正后的代码:

package main

import "fmt"

func generate(v int) <-chan int {
	ch := make(chan int, 1)
	go func() {
		for i := v; ; i++ {
			ch <- i
		}
	}()
	return ch
}

func merge(v ...<-chan int) <-chan int {
	ch := make(chan int, 1)
	for _, c := range v { // 使用下划线忽略索引,c 接收通道元素
		go func(c <-chan int) {
			for {
				ch <- <-c
			}
		}(c) // 将通道 c 作为参数传入闭包,避免闭包捕获循环变量的问题
	}
	return ch
}

func main() {
	ch := merge(generate(1000), generate(100), generate(100000))
	for i := 0; i < 10; i++ {
		fmt.Println(<-ch)
	}
}

关键修改:

  1. for z := range v 改为 for _, c := range v,确保 c 是通道元素。
  2. 在闭包中显式传入通道参数 c,避免 goroutine 捕获循环变量时可能出现的竞态问题(所有 goroutine 可能共享同一个 c 的最终值)。
  3. 调整了代码格式以提高可读性。

现在代码可以正确编译并运行,从三个生成器通道中合并输出数据。

回到顶部