使用 Channel 最常见的错误是 panic 和 goroutine 泄漏
March 12, 2025
出现的情况
- close 为 nil 的 chan;
 - send 已经 close 的 chan;
 - close 已经 close 的 chan。
 
Go语言中的Goroutine、Channel和垃圾回收
问题分析
在您提供的process函数中,如果主程序循环阻塞,而process函数不再被引用,我们需要考虑以下几个方面:
- Goroutine的生命周期
 - Channel的阻塞特性
 - Go语言的垃圾回收机制
 
Goroutine的生命周期
Goroutine不会自动结束。它们会在以下情况下终止:
- 当goroutine的函数返回时
 - 当程序退出时
 
在这个例子中,如果timeout发生,主goroutine会返回,但子goroutine会继续运行并尝试向channel发送数据。
Channel的阻塞特性
无缓冲的channel(如本例中的ch)会在发送操作时阻塞,直到有接收者准备好接收数据。在timeout后,没有接收者,所以子goroutine会一直阻塞在ch <- true这一行。
垃圾回收
Go的垃圾回收器无法回收仍在运行的goroutine。即使process函数返回且不再被引用,子goroutine仍然在运行,因此不会被回收。
同样,channel ch 也不会被回收,因为它仍然被子goroutine引用。
结论
- 主程序循环阻塞不会直接导致goroutine或channel被回收。
 - 即使
process函数不再被引用,子goroutine仍然在运行,因此不会被GC回收。 - Channel 
ch也不会被回收,因为它仍然被子goroutine引用。 - 这种情况会导致goroutine泄漏,因为子goroutine会一直存在,无法被回收。
 
改进建议
为了避免goroutine泄漏,可以考虑以下改进:
- 使用context来控制goroutine的生命周期。
 - 在timeout后,主动关闭channel,让子goroutine得知需要退出。
 - 在子goroutine中使用select语句,增加一个退出的case。
 
例如:
func process(ctx context.Context, timeout time.Duration) bool {
    ch := make(chan bool)
    ctx, cancel := context.WithTimeout(ctx, timeout)
    defer cancel()
    go func() {
        // 模拟处理耗时的业务
        select {
        case <-time.After(timeout + time.Second):
            ch <- true
        case <-ctx.Done():
            // context被取消,goroutine退出
        }
        fmt.Println("exit goroutine")
    }()
    select {
    case result := <-ch:
        return result
    case <-ctx.Done():
        return false
    }
}这样可以确保在timeout或主函数返回时,子goroutine能够正确退出,避免资源泄漏。
最后更新于