You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go协程死锁问题:基于select模式的排查与优化方案

问题分析

这段Go代码触发死锁的核心原因有两个:

  1. goOne和goTwo向无缓冲通道发送数据,但main goroutine里的select执行太快,还没等子goroutine把数据发进通道就走到了default分支,没有完成接收操作。
  2. 无缓冲通道的发送操作会一直阻塞,直到有对应的接收操作完成。两个子goroutine卡在ch1 <- "Channel 1"和ch2 <- "Channel 2"处,无法执行wg.Done(),导致main goroutine的wg.Wait()永远等待,最终所有goroutine都进入休眠状态,触发死锁。

另外原代码的select分支还有逻辑错误:比如case <-ch1:已经接收了一次通道值,后面又调用fmt.Println(<-ch1)会再次尝试接收,这会导致额外阻塞(通道里已无数据)。

原代码示例

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    ch1 := make(chan string)
    ch2 := make(chan string)

    wg.Add(2)
    go goOne(&wg, ch1)
    go goTwo(&wg, ch2)

    select {
    case <-ch1:
        fmt.Println(<-ch1)
        close(ch1)

    case <-ch2:
        fmt.Println(<-ch2)
        close(ch2)

    default:
        fmt.Println("Default Case")
    }
    wg.Wait()

}

func goTwo(wg *sync.WaitGroup, ch2 chan string) {
    ch2 <- "Channel 2"
    wg.Done()
}

func goOne(wg *sync.WaitGroup, ch1 chan string) {
    ch1 <- "Channel 1"
    wg.Done()
}

原代码输出

Default Case
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000108270?)
/usr/local/go/src/runtime/sema.go:62 +0x25
sync.(*WaitGroup).Wait(0x4b9778?)
/usr/local/go/src/sync/waitgroup.go:139 +0x52
main.main()
/home/nidhey/Documents/Go_Learning/goroutines/select.go:29 +0x2af

goroutine 6 [chan send]:
main.goOne(0x0?, 0x0?)
/home/nidhey/Documents/Go_Learning/goroutines/select.go:39 +0x28
created by main.main
/home/nidhey/Documents/Go_Learning/goroutines/select.go:14 +0xc5

goroutine 7 [chan send]:
main.goTwo(0x0?, 0x0?)
/home/nidhey/Documents/Go_Learning/goroutines/select.go:33 +0x28
created by main.main
/home/nidhey/Documents/Go_Learning/goroutines/select.go:15 +0x119

修复方案

基于select模式处理通道阻塞,提供三种可行修复方式:

方式一:select循环接收所有通道数据

通过循环select接收两个通道的数据,结合WaitGroup等待子goroutine完成,确保所有数据都被处理:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    ch1 := make(chan string)
    ch2 := make(chan string)

    wg.Add(2)
    go goOne(&wg, ch1)
    go goTwo(&wg, ch2)

    // 用done通道标记所有goroutine完成
    done := make(chan struct{})
    go func() {
        wg.Wait()
        close(done)
        close(ch1)
        close(ch2)
    }()

    // 循环select接收通道数据,直到所有任务完成
    for {
        select {
        case msg, ok := <-ch1:
            if ok {
                fmt.Println(msg)
            }
        case msg, ok := <-ch2:
            if ok {
                fmt.Println(msg)
            }
        case <-done:
            return
        }
    }
}

func goTwo(wg *sync.WaitGroup, ch2 chan string) {
    ch2 <- "Channel 2"
    wg.Done()
}

func goOne(wg *sync.WaitGroup, ch1 chan string) {
    ch1 <- "Channel 1"
    wg.Done()
}

方式二:去掉default分支,让select等待通道数据

原代码的default分支是提前跳过接收的关键,去掉后select会阻塞直到某个通道有数据可读,同时修正重复接收的问题:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    ch1 := make(chan string)
    ch2 := make(chan string)

    wg.Add(2)
    go goOne(&wg, ch1)
    go goTwo(&wg, ch2)

    // 等待第一个到达的通道数据并打印
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case msg := <-ch2:
        fmt.Println(msg)
    }

    // 启动goroutine接收剩余通道数据,避免子goroutine阻塞
    go func() { <-ch1 }()
    go func() { <-ch2 }()

    wg.Wait()
}

func goTwo(wg *sync.WaitGroup, ch2 chan string) {
    ch2 <- "Channel 2"
    wg.Done()
}

func goOne(wg *sync.WaitGroup, ch1 chan string) {
    ch1 <- "Channel 1"
    wg.Done()
}

方式三:使用缓冲通道

如果不需要严格同步,可将通道改为带缓冲的,发送操作不会立即阻塞:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    // 创建带1个缓冲的通道
    ch1 := make(chan string, 1)
    ch2 := make(chan string, 1)

    wg.Add(2)
    go goOne(&wg, ch1)
    go goTwo(&wg, ch2)

    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case msg := <-ch2:
        fmt.Println(msg)
    default:
        fmt.Println("Default Case")
    }

    wg.Wait()
    // 接收剩余数据,避免资源泄漏
    <-ch1
    <-ch2
}

func goTwo(wg *sync.WaitGroup, ch2 chan string) {
    ch2 <- "Channel 2"
    wg.Done()
}

func goOne(wg *sync.WaitGroup, ch1 chan string) {
    ch1 <- "Channel 1"
    wg.Done()
}

内容的提问来源于stack exchange,提问作者Nidhey Indurkar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 10:15:34