
本文详解 go 语言中 channel 使用时的一个典型错误:发送端正确传递字符串,但接收端误将接收到的单个字符串当作字符切片遍历,导致输出每个字符而非完整字符串,并提供基于 sync.waitgroup 的优雅解决方案。
本文详解 go 语言中 channel 使用时的一个典型错误:发送端正确传递字符串,但接收端误将接收到的单个字符串当作字符切片遍历,导致输出每个字符而非完整字符串,并提供基于 sync.waitgroup 的优雅解决方案。
在 Go 中,chan string 用于在 goroutine 间安全传递字符串值。问题代码看似逻辑清晰:启动 5 个 goroutine,每个调用 doStuff("helloooo", c) 向 channel 发送一次 "helloooo";但接收端却只读取一个值 results := <-c,并错误地对这个字符串执行 for _, r := range results —— 这并非遍历 channel 中的多个消息,而是遍历字符串 "helloooo" 的每个 Unicode 码点(rune),因此打印出 h、e、l、l、o、o、o、o 共 8 行,而非预期的 5 次 "helloooo"。
✅ 正确做法:从 channel 中循环接收所有值
要获取所有发送到 channel 的字符串,应使用 for range 直接遍历 channel:
for s := range c {
fmt.Println(s)
}但注意:for range c 会持续阻塞,直到 channel 被关闭。若不显式关闭,程序将因所有 goroutine 休眠而触发 fatal deadlock:
fatal error: all goroutines are asleep - deadlock!
✅ 推荐方案:用 sync.WaitGroup 协调 goroutine 完成后关闭 channel
这是生产环境中最健壮、无竞态、无超时依赖的方式:
package main
import (
"fmt"
"sync"
)
func doStuff(s string, ch chan string) {
ch <- s
wg.Done() // 标记当前 goroutine 已完成发送
}
var wg sync.WaitGroup
func main() {
c := make(chan string)
// 启动 5 个 goroutine 并注册 WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go doStuff("helloooo", c)
}
// 启动协程等待全部发送完成,然后关闭 channel
go func() {
wg.Wait()
close(c)
}()
// 安全遍历已关闭的 channel
for s := range c {
fmt.Println(s)
}
}✅ 输出结果:
helloooo helloooo helloooo helloooo helloooo
⚠️ 注意事项与最佳实践
- 不要用 len(results) 判断 channel 长度:<-c 只接收一个值,len(results) 返回的是该字符串的字节数(或 rune 数),与 channel 容量/待接收消息数完全无关。
- 避免硬编码数组控制循环次数:如原代码中的 [5]int{1,2,3,4,5},直接用 for i := 0; i < 5; i++ 更简洁、语义清晰。
- channel 关闭权责明确:通常由发送方(或协调者)负责关闭,接收方绝不应关闭 channel;且 channel 只能关闭一次,重复关闭 panic。
- 若需限制并发或收集结果:可配合 buffered channel 或 []string 切片 + sync.Mutex,但本例中无缓冲 channel + WaitGroup 已足够简洁高效。
掌握 channel 的生命周期管理(发送、接收、关闭)和 range 的语义(遍历 channel ≠ 遍历字符串),是写出可靠并发 Go 程序的关键基础。

















