使用通道时Goroutine的执行顺序问题及代码异常分析
问题描述
使用通道时Goroutine的执行顺序是怎样的?原本认为对通道进行读写操作会阻塞当前Goroutine,但测试代码却不符合这一规则:
func main() { ch := make(chan int) go sum(ch, 3) fmt.Println("Write number: 10") ch <- 10 fmt.Println("Write number: 20") ch <- 20 fmt.Println("Write number: 30") ch <- 30 fmt.Println("Finish main") } func sum(ch chan int, len int) { fmt.Println("Func 'sum' start") sum := 0 for i := 0; i < len; i++ { fmt.Println("For start") num := <-ch fmt.Printf("Read from ch: %d\n", num) sum += num fmt.Println("For finish") } fmt.Printf("Sum: %d\n", sum) }
预期执行流程是主Goroutine每次向通道写入数据后会阻塞,直到sum Goroutine读取数据后才继续执行,输出为交替的写入提示与读取操作日志。但实际输出中主Goroutine连续写入了20和30才被读取。此外,当将sum的第二个参数改为2时,主Goroutine写入第三个值后无读取者,却未触发报错,这是为什么?
问题解答
1. 为什么主Goroutine会出现“连续写入”的现象?
你使用的是无缓冲通道,理论上每次发送操作ch <- x都会阻塞当前Goroutine,直到有其他Goroutine完成接收操作<-ch。实际出现“连续写入”的视觉效果,主要有两个原因:
- Go调度器的时机不确定性:主Goroutine在被唤醒后,会快速执行后续的打印语句(比如
fmt.Println("Write number:20")),之后才进入下一次发送阻塞。而sum Goroutine的调度唤醒可能存在延迟,导致它的打印输出和主Goroutine的打印输出交错,看起来像是主Goroutine连续完成了写入。 - 标准输出的缓冲特性:Go的
fmt.Println输出是带缓冲的,多个打印语句的内容可能被暂时缓冲,之后一次性输出,这也会造成“连续写入”的错觉。
本质上无缓冲通道的阻塞规则没有被打破,只是执行流程的可视化输出因为调度和缓冲出现了偏差。
2. 写入无接收者的通道为何未触发报错?
当sum的参数改为2时,sum Goroutine只执行2次接收操作就会退出,此时通道不再有活跃的接收者。但主Goroutine写入第三个值后没有触发panic,是因为主Goroutine会在写入后立刻执行完剩余代码并退出——Go程序的生命周期由主Goroutine主导,主Goroutine退出时,所有其他Goroutine会被强制终止,程序直接结束,主Goroutine还没来得及因为“发送到无接收者的通道”而触发panic。
如果想验证这个panic,可以在主Goroutine写入第三个值后添加等待逻辑,比如:
ch <- 30 time.Sleep(time.Second) // 让主Goroutine等待足够时间 fmt.Println("Finish main")
此时主Goroutine会在发送操作上持续阻塞,因为没有接收者,最终会触发panic。
内容的提问来源于stack exchange,提问作者Phoen1x

