Golang深入解析:Channel使用方法与底层机制揭秘‌

目录

  1. 引言
  2. Channel 基本概念与创建
    2.1. Channel 定义与作用
    2.2. 无缓冲 Channel 与缓冲 Channel
    2.3. 单向 Channel(方向限制)
  3. Channel 的基本使用
    3.1. 发送与接收操作(<-
    3.2. 关闭 Channel(close)与检测关闭状态
    3.3. for…range 遍历 Channel
    3.4. select 多路复用
  4. Channel 在并发协程同步中的应用
    4.1. 管道(Pipeline)模式示例
    4.2. 多生产者/多消费者案例
    4.3. 结合 sync.WaitGroup 做任务调度
  5. 底层机制揭秘:Go 运行时如何实现 Channel
    5.1. Go 运行时中的 hchan 结构体
    5.2. 环形队列与缓存布局
    5.3. 发送/接收时的阻塞与唤醒
    5.4. select 的实现原理
  6. 内存模型与 Channel 安全性
    6.1. 内存屏障与可见性
    6.2. Channel 数据在内存中的位置
    6.3. 避免死锁与 Goroutine 泄露
  7. 性能与优化建议
    7.1. 选择合适的缓冲大小
    7.2. 减少争用与热点 Channel
    7.3. 逃逸分析与 GC 影响
  8. 总结

1. 引言

在 Go 语言中,Channel 是连接多个 Goroutine 的核心并发抽象,用于在不同 Goroutine 之间安全地传递数据。相比于锁(sync.Mutex)或原子操作,Channel 更加“Go idiomatic”,可用来实现管道式并发、生产者-消费者、任务调度等模式。本文将从使用方法入手,配合大量代码示例Mermaid 图解,并深入剖析 Go 运行时中 Channel 的底层实现。希望你在阅读本文后,能够对 Channel 的设计初衷与实际应用有一个全面的理解,并学会在项目中高效地使用 Channel。


2. Channel 基本概念与创建

2.1 Channel 定义与作用

在 Go 中,Channel 可以看作一个类型化的队列,它的两种主要操作是“发送”(chan <- value)和“接收”(value := <- chan)。Channel 内部会管理一个FIFO 队列,以及等待在此队列上的 Goroutine 列表。Channel 既可用于在 Goroutine 之间传递数据,也可用于同步——当没有缓冲空间可用时,发送会阻塞;当没有值可读时,接收会阻塞。

// 定义一个只能传递 int 的 Channel
var ch chan int

// 使用 make 创建一个无缓冲的 int 通道
ch = make(chan int)

// 或者一行完成
ch := make(chan int)
  • make(chan T) 返回一个 chan T 类型的 Channel;
  • 无缓冲意味着每次发送操作必须等待某个 Goroutine 来接收,才算完成;
  • 缓冲 Channel允许在缓冲区未满的情况下发送而不阻塞。

2.2 无缓冲 Channel 与缓冲 Channel

2.2.1 无缓冲 Channel

ch := make(chan string) // 无缓冲

go func() {
    ch <- "hello" // 这里将会阻塞,直到有接收方
    fmt.Println("发送完成")
}()

time.Sleep(time.Second)
msg := <-ch // 接收后,发送方解除阻塞
fmt.Println("接收到:", msg)
  1. 发送方 ch <- "hello" 会阻塞,直到另一 Goroutine 执行 <-ch
  2. 接收后才会解除阻塞并打印 “发送完成”。

2.2.2 缓冲 Channel

ch := make(chan string, 2) // 缓冲大小 2

ch <- "first"  // 不阻塞,缓冲区 now: ["first"]
ch <- "second" // 不阻塞,缓冲区 now: ["first", "second"]
// ch <- "third" // 如果再发送会阻塞,因为缓冲已满

fmt.Println(<-ch) // 取出 "first",缓冲区 now: ["second"]
fmt.Println(<-ch) // 取出 "second",缓冲区 now: []
  • 缓冲为 2 时,最多可以先发送两次数据而不阻塞;
  • 若尝试第三次发送,则会阻塞直到有接收方读取。

Mermaid 图解:无缓冲 vs 缓冲 Channel

flowchart LR
    subgraph 无缓冲 Channel
        S1[发送: ch <- "a"] --阻塞--> WaitRecv1[等待接收]
        WaitRecv1 --> R1[接收: <-ch] --> Unblock1[发送解除阻塞]
    end

    subgraph 缓冲 Channel(容量2)
        S2[发送: ch <- "x"] --> Buffer["缓冲[\"x\"]"]
        S3[发送: ch <- "y"] --> Buffer["缓冲[\"x\",\"y\"]"]
        S4[发送: ch <- "z"] --阻塞--> WaitSpace[等待缓冲空间]
        R2[接收: <-ch] --> Buffer["缓冲[\"y\"]"] --> Unblock2[解除阻塞 S4]
    end

2.3 单向 Channel(方向限制)

为了增强代码可读性并避免误用,可以声明只发送只接收的 Channel 类型:

func producer(sendOnly chan<- int) {
    sendOnly <- 42
}

func consumer(recvOnly <-chan int) {
    val := <-recvOnly
    fmt.Println("消费:", val)
}

func main() {
    ch := make(chan int)
    go producer(ch) // 传入只发送类型
    go consumer(ch) // 传入只接收类型
}
  • chan<- T 表示只能发送的 Channel;
  • <-chan T 表示只能接收的 Channel。
单向 Channel 在封装时非常有用,可以在 API 层保证调用者只能做指定方向操作。

3. Channel 的基本使用

3.1 发送与接收操作(<-

  • 发送ch <- value
  • 接收value := <-chvalue, ok := <-ch(检测是否关闭)
  • 双向阻塞模型:当无缓冲且无人接收时,发送会阻塞;当缓冲区满时,缓冲 Channel 的发送也会阻塞。
  • 当 Channel 关闭后,接收仍可继续,但读到的值为类型零值,并且 ok == false
ch := make(chan int, 1)
ch <- 100
close(ch)

if v, ok := <-ch; ok {
    fmt.Println("接收到:", v)
} else {
    fmt.Println("Channel 已关闭,读到零值:", v) // v == 0
}

3.2 关闭 Channel(close)与检测关闭状态

  • close(ch) 会关闭 Channel,使所有挂起的发送者直接 panic,所有接收者可读取完缓冲后得到“零值 + ok=false”。
  • 关闭后,不能再次发送,否则会 panic;但是可以继续读取剩余缓冲区的数据。
ch := make(chan string, 2)
ch <- "A"
ch <- "B"
close(ch)

// 读取剩余
for i := 0; i < 3; i++ {
    v, ok := <-ch
    fmt.Println("读到:", v, "ok?", ok)
}

输出:

读到: A ok? true
读到: B ok? true
读到:  ok? false

3.3 for…range 遍历 Channel

使用 for v := range ch 可以简洁地读取直到 Channel 关闭:

ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)

for v := range ch {
    fmt.Println("Range 收到:", v)
}
  • range 会在读取到所有值且 Channel 关闭后退出;
  • 不能在 range 循环内部再 close(ch),否则会 panic。

3.4 select 多路复用

select 语句可以同时等待多个 Channel 的发送或接收事件,随机选择一个可用分支执行:

ch1 := make(chan string)
ch2 := make(chan string)

go func() {
    time.Sleep(100 * time.Millisecond)
    ch1 <- "消息来自 ch1"
}()
go func() {
    time.Sleep(200 * time.Millisecond)
    ch2 <- "消息来自 ch2"
}()

for i := 0; i < 2; i++ {
    select {
    case msg1 := <-ch1:
        fmt.Println("收到:", msg1)
    case msg2 := <-ch2:
        fmt.Println("收到:", msg2)
    }
}
default 分支时:若所有分支均阻塞,则 select 会阻塞;
default 分支时:如果没有分支就绪,则执行 default 分支,不阻塞。
select {
case v := <-ch:
    fmt.Println("收到:", v)
default:
    fmt.Println("无数据,走 default 分支")
}
select 还可与 time.Aftertime.Tick 组合,实现超时或定时功能:
select {
case v := <-ch:
    fmt.Println("收到:", v)
case <-time.After(time.Second):
    fmt.Println("等待超时")
}

4. Channel 在并发协程同步中的应用

4.1 管道(Pipeline)模式示例

Pipeline 将复杂操作拆解成多个阶段,每个阶段由若干 Goroutine 从上一个阶段的 Channel 中读取数据、处理后写入下一个阶段的 Channel。

package main

import (
    "fmt"
    "strconv"
    "sync"
)

// 第一阶段:生成字符串数字
func gen(nums []int) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for _, n := range nums {
            out <- strconv.Itoa(n)
        }
    }()
    return out
}

// 第二阶段:将字符串转回整数
func strToInt(in <-chan string) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for s := range in {
            num, _ := strconv.Atoi(s)
            out <- num
        }
    }()
    return out
}

// 第三阶段:计算平方
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * n
        }
    }()
    return out
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    p1 := gen(nums)
    p2 := strToInt(p1)
    p3 := square(p2)

    for result := range p3 {
        fmt.Println("Pipeline 结果:", result)
    }
}

Mermaid 图解:Pipeline 并发流程

flowchart LR
    subgraph Stage1[阶段1: 数字 → 字符串]
        G1[Generator] --> ch1[string chan]
    end
    subgraph Stage2[阶段2: 字符串 → 整数]
        G2[Converter] <-- ch1[string chan]
        G2 --> ch2[int chan]
    end
    subgraph Stage3[阶段3: 平方计算]
        G3[Squarer] <-- ch2[int chan]
        G3 --> ch3[int chan]
    end
    subgraph Output[输出]
        Out[Print] <-- ch3[int chan]
    end

4.2 多生产者/多消费者案例

package main

import (
    "fmt"
    "sync"
    "time"
)

func producer(id int, tasks chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 5; i++ {
        tasks <- id*10 + i
        time.Sleep(100 * time.Millisecond)
    }
}

func consumer(id int, tasks <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for n := range tasks {
        fmt.Printf("Consumer %d 处理任务 %d\n", id, n)
        time.Sleep(200 * time.Millisecond)
    }
}

func main() {
    tasks := make(chan int, 10)
    var wg sync.WaitGroup

    // 启动 2 个生产者
    for i := 1; i <= 2; i++ {
        wg.Add(1)
        go producer(i, tasks, &wg)
    }

    // 启动 3 个消费者
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go consumer(i, tasks, &wg)
    }

    wg.Wait()
    close(tasks) // 当所有生产者完成后,再关闭 Channel
    // 再次等待消费者退出
    var wg2 sync.WaitGroup
    wg2.Add(3)
    for i := 1; i <= 3; i++ {
        go func(id int) {
            defer wg2.Done()
            for n := range tasks {
                fmt.Printf("最后 Consumer %d 处理剩余任务 %d\n", id, n)
            }
        }(i)
    }
    wg2.Wait()
    fmt.Println("所有任务完成")
}
  1. 两个生产者并发往 tasks Channel 写入任务;
  2. 三个消费者并发读取并处理;
  3. 当生产者 wg.Wait() 完成后,关闭 tasks
  4. 消费者遍历完 Channel 后退出。

4.3 结合 sync.WaitGroup 做任务调度

当既要等待生产者完成,又要等待所有消费者处理完毕时,可用两个 WaitGroup:一个用于生产者,一个用于消费者。

package main

import (
    "fmt"
    "sync"
)

func main() {
    tasks := make(chan int, 5)
    var prodWg sync.WaitGroup
    var consWg sync.WaitGroup

    // 启动生产者
    prodWg.Add(1)
    go func() {
        defer prodWg.Done()
        for i := 1; i <= 10; i++ {
            tasks <- i
        }
        close(tasks)
    }()

    // 启动 3 个消费者
    for i := 1; i <= 3; i++ {
        consWg.Add(1)
        go func(id int) {
            defer consWg.Done()
            for n := range tasks {
                fmt.Printf("Consumer %d 处理任务 %d\n", id, n)
            }
        }(i)
    }

    // 等待生产者结束
    prodWg.Wait()
    // 等待所有消费者结束
    consWg.Wait()
    fmt.Println("所有生产者和消费者都完成")
}

5. 底层机制揭秘:Go 运行时如何实现 Channel

要真正理解 Channel,必须结合 Go 运行时源码(src/runtime)中的实现。Channel 在底层由一个名为 hchan 的结构体表示,并结合环形队列(ring buffer)等待队列,来实现线程安全的发送、接收和唤醒逻辑。

5.1 Go 运行时中的 hchan 结构体

src/runtime/chan.go 中可见:

// hchan 是 Go 运行时内部的 Channel 结构体
type hchan struct {
    qcount   uint             // 缓冲区中实际元素个数
    dataqsiz uint             // 缓冲区大小(capacity)
    buf      unsafe.Pointer   // 指向循环队列底层数组
    elemsize uint16           // 单个元素大小
    closed   uint32           // 是否关闭标志

    sendx   uint             // 下一个发送的索引
    recvx   uint             // 下一个接收的索引
    recvq   waitq            // 接收队列,存放等待接收的 goroutine
    sendq   waitq            // 发送队列,存放等待发送的 goroutine
    lock    hchanLock        // 保护 hchan 结构的锁(SpinLock)
}
// waitq 是用于存储等待 goroutine 的队列
type waitq struct {
    first *sudog
    last  *sudog
}
// sudog 为等待的 Goroutine 创建的结构
type sudog struct {
    g      *g      // 对应的 goroutine
    next   *sudog  // 下一个等待节点
    elem   unsafe.Pointer // 指向发送或接收的数据指针
    // ... 省略其他字段
}

关键字段解析:

  • buf:缓冲区指针,指向一个底层连续内存区域,大小为 dataqsiz * elemsize
  • sendx/recvx:循环队列的写入和读取索引(mod dataqsiz);
  • qcount:当前缓冲中元素数目;
  • sendq/recvq:分别维护着阻塞等待的发送者和接收者的队列(当缓冲满或空时进入对应等待队列);
  • closed:原子标志,标记 Channel 是否已被关闭。

5.2 环形队列与缓存布局

假设创建了一个缓冲大小为 n 的 Channel,Go 会在堆上分配一个连续内存区域来存储 n 个元素,sendxrecvx 均从 0 开始。每次发送时:

  1. 地址计算:buf + (sendx * elemsize) 存储数据;
  2. sendx = (sendx + 1) % dataqsiz
  3. qcount++

接收时:

  1. 取出 buf + (recvx * elemsize) 的数据;
  2. recvx = (recvx + 1) % dataqsiz
  3. qcount--

Mermaid 图解:Channel 内部环形缓冲布局

flowchart TB
    subgraph Channel hchan.buf
        direction LR
        Slot0[(slot 0)] --> Slot1[(slot 1)] --> Slot2[(slot 2)] --> Slot3[(slot 3)] --> ... --> SlotN[(slot n-1)]
        SlotN ---┐
                 └→(循环)
    end
    SendX("sendx") --> Slot1        %% 举例 sendx=1 存放下一个值
    RecvX("recvx") --> Slot0        %% 举例 recvx=0 读取下一个值
    QCount("qcount = 1")         %% 当前环形队列中已有1个元素

5.3 发送/接收时的阻塞与唤醒

5.3.1 发送过程(chan.send

src/runtime/chan.go 中,chanrecvchansend 是关键函数。简化逻辑如下:

func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
    lock(&c.lock)
    // 如果 Channel 已关闭,panic
    if c.closed != 0 {
        unlock(&c.lock)
        panic("send on closed channel")
    }
    // 如果有等待接收者,则直接唤醒一个 receiver,不走缓冲
    if c.recvq.first != nil {
        sg := dequeue(&c.recvq)    // 从 recvq 取出等待的 sudog
        copyData(sg.elem, ep, c.elemsize) // 直接将数据复制给接收者
        gwake(sg.g, true)          // 唤醒那个 Goroutine
        unlock(&c.lock)
        return true
    }
    // 否则,如果缓冲尚有剩余空间,就直接写入环形队列
    if c.qcount < c.dataqsiz {
        writeToBuf(c, ep)
        c.qcount++
        unlock(&c.lock)
        return true
    }
    // 缓冲已满
    if !block {
        unlock(&c.lock)
        return false   // 非阻塞模式,直接返回
    }
    // 阻塞模式:将当前 Goroutine 包装成 sudog,加入 sendq 等待队列
    sg := acquireSudog()
    sg.elem = ep
    sg.arg = nil // optional
    enqueue(&c.sendq, sg)
    goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 2) 
    // 直到被唤醒才会返回
    return true
}
  • 如果 recvq(等待接收的队列)不为空,表明有 Goroutine 在接收,那么发送方可以直接把数据复制给接收方,二者同步完成,无需先写缓冲。
  • 否则,如果缓冲未满,则先写入环形缓冲队列;
  • 如果缓冲已满且是阻塞模式,发送方会被加入 sendq,并由 goparkunlock 挂起,直到被接收方唤醒;
  • goparkunlock 会释放 c.lock,并让当前 Goroutine 阻塞在“等待被唤醒”状态中。

5.3.2 接收过程(chan.recv

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) bool {
    lock(&c.lock)
    // 如果缓冲中有数据,则直接读取
    if c.qcount > 0 {
        readFromBuf(c, ep)
        c.qcount--
        // 如果有等待发送的 Goroutine,将一个发送者唤醒并放入缓冲
        if c.sendq.first != nil {
            sg := dequeue(&c.sendq)
            writeToBuf(c, sg.elem)
            c.qcount++
            gwake(sg.g, true)
        }
        unlock(&c.lock)
        return true
    }
    // 如果缓冲为空但 sendq 有等待发送者
    if c.sendq.first != nil {
        sg := dequeue(&c.sendq)
        copyData(ep, sg.elem, c.elemsize)  // 直接拿到发送者的数据
        gwake(sg.g, true)                   // 唤醒发送者
        unlock(&c.lock)
        return true
    }
    // 缓冲为空且无发送等待 => 要阻塞或关闭处理
    if c.closed != 0 {
        // 关闭后返回零值,ok=false
        zeroValue(ep)
        unlock(&c.lock)
        return false
    }
    if !block {
        unlock(&c.lock)
        return false
    }
    // 阻塞模式:加入接收等待队列
    sg := acquireSudog()
    sg.elem = ep
    enqueue(&c.recvq, sg)
    goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 2)
    // 唤醒后,数据已被发送者复制到 ep
    return true
}
  • 如果缓冲中有数据,就立刻读取,同时如果有等待发送的 Goroutine,就把一个唤醒,将其数据放入环形缓冲;
  • 如果缓冲为空但有等待的发送者,则会直接从发送者的 sudog 里获取数据,无需经过缓冲;
  • 否则,如果 Channel 关闭,则返回“零值 + ok=false”;
  • 若阻塞模式,则加入 recvq 队列,挂起当前 Goroutine,等待发送方唤醒。

5.4 select 的实现原理

select 在 Go 运行时中非常复杂,位于 src/runtime/select.go。简化流程:

  1. 构建 selOrder 数组:将每个 case 分支随机排序,保证公平性;
  2. 遍历所有分支,尝试非阻塞地进行发送接收操作(调用 chanrecv1/chansend1);

    • 如果某个分支成功立即执行并返回;
  3. 如果所有分支均无法立即执行且有 default 分支,则执行 default
  4. 否则,将当前 Goroutine 打包成 sudog,挂入所有有可能阻塞的 Channel 对应的等待队列(sendqrecvq);
  5. 调用 gopark 挂起当前 Goroutine,直至某个对端操作唤醒;
  6. 被唤醒后,从 sel 对象中读取哪个分支触发,并执行对应逻辑。

Mermaid 图解:select 基本执行流程

flowchart TD
    subgraph Begin[select 开始]
        A[构建 selOrder(随机序)] --> B[尝试逐个 case 非阻塞 send/recv]
        B -->|某个 case 可立即执行| C[执行该 case, 返回]
        B -->|都不能执行且有 default| D[执行 default, 返回]
        B -->|都不能执行且无 default| E[挂起]
        E --> F[等待 Wakeup]
        F --> G[找到已就绪的 case 并执行]
    end

6. 内存模型与 Channel 安全性

6.1 内存屏障与可见性

  • Go 的 内存模型 保证:在 Channel 发送(ch <- v)和接收(v := <-ch)操作之间,有同步点,确保发送方对 v 的写操作对接收方可见。
  • 也就是说,若 Goroutine A 对某个共享变量 x 先修改,然后 Ach <- x,再由 Goroutine B 进行 <-ch 并读取到对应值,则 B 会看到 x 的更新。
  • 这种“通信顺序比关系”由 Go 内存模型保证。

6.2 Channel 数据在内存中的位置

  • hchan.buf 在堆上分配一个底层数组,大小为 dataqsiz * elemsize
  • 每次发送会将拷贝的方式将数据写入到该缓冲区,相当于在内存中执行 memmove(buf+offset, &value, elemsize)
  • 接收时再将缓冲区的数据拷贝到接收方栈上或堆上;
  • 因此,Channel 中传递的是值拷贝(对于引用类型,拷贝是指拷贝指针本身,而不深度拷贝底层结构)。

6.3 避免死锁与 Goroutine 泄露

常见坑点:

  1. 双向阻塞ch := make(chan int),在没有任何接收方的情况下直接 ch <- 1 会永久阻塞;
  2. 未及时关闭 Channel:若生产者不 close(ch),则消费者的 for v := range ch 会永久阻塞;
  3. select 分支都阻塞:若 select 分支里都尝试从一个空 Channel 接收,且无 default,将导致永久阻塞;
  4. 忘记释放挂起的 Goroutine:如在超时情况下需要强制关闭 Channel 或通过 Context 取消。

常见解决之道:

  • 确保对单向通讯的 Channel 仅存在一端 close
  • 使用带缓冲 Channel 在必要时减少阻塞;
  • select 中加入 defaultcase <-ctx.Done() 做超时/取消处理;
  • 谨慎设计管道阶段,保证终止条件可达。

7. 性能与优化建议

7.1 选择合适的缓冲大小

  • 无缓冲 Channel 适合严格同步场景,但会显著增加 Goroutine 切换和上下文切换成本;
  • 缓冲 Channel 可减少阻塞,但过大会增大内存占用,且环形缓冲每次写/读都需要 memmove,当 elemsize 较大时也会带来开销;
  • 一般根据生产者和消费者的速度差异来调优缓冲大小:如果生产速度快于消费速度,可适当增大;否则保持为 1 或较小值。

7.2 减少争用与热点 Channel

  • 同一个 Channel 同时存在大量读写请求 时,内部会存在高频率的 mutex SpinLock 争用;
  • 若出现瓶颈,可考虑:

    1. 分片 Channel:如 []chan T,将不同数据分配到不同 Channel 以降低竞争;
    2. 避免过度 select:当多个 Goroutine 都在对多个 Channel 做 select,会造成 runtime 大量遍历 scase,也会带来开销。

7.3 逃逸分析与 GC 影响

  • Channel 传递大量大对象 时,底层会分配堆空间来存储数据,可能加重 GC 负担;
  • 通过 逃逸分析 工具 (go build -gcflags="-m") 检查是否因 Channel 传参导致大量逃逸;
  • 若发现性能问题,可将大数组或大结构体改为传递指针,或者使用 sync.Pool 缓存对象,减少内存分配。

8. 总结

本文从Channel 基本使用并发模式实践、到底层 hchan 结构与 select 实现,详细剖析了 Go 语言中 Channel 的各个层面:

  1. Channel 的创建与类型:无缓冲/缓冲、单向/双向;
  2. 发送/接收/关闭:阻塞模型、ok 返回值、for…range 遍历、select 多路复用;
  3. 并发协程同步模式:Pipeline、Worker Pool、多生产者/消费者、结合 sync.WaitGroup
  4. Go 运行时内部实现hchan、环形缓冲、等待队列、gopark 挂起与唤醒;
  5. 性能与内存模型:同步顺序关系、逃逸分析、含缓冲 Channel 与锁竞争。

理解了 Channel 的底层机制后,才能在实际项目中游刃有余地使用它,既能确保高性能,也能避免常见死锁场景与资源泄露。希望本文的代码示例Mermaid 图解能帮助你更快上手、深入理解 Go 并发核心 —— Channel。

最后修改于:2025年06月04日 11:05

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日