青训营go语言基础总结五:channel| 豆包MarsCode AI 刷题

95 阅读3分钟

什么是Channel

Channel可以看作是一个管道,多个goroutine可以通过它发送和接收数据。Channel的设计初衷是为了让goroutine之间进行安全、高效的通信。

Channel的创建

可以使用make函数来创建一个channel。Channel类型需要指定其传输的数据类型,例如:

ch := make(chan int) // 创建一个传输int类型数据的channel

Channel的基本操作

发送

使用 <- 操作符将数据发送到channel。

ch <- 10 // 将10发送到channel ch

接收

使用 <- 操作符从channel接收数据。

value := <-ch // 从channel ch接收数据并赋值给变量value

关闭

使用 close 函数关闭channel,一个被关闭的channel不能再发送数据,但仍可以接收未被读取的数据。

close(ch)

Channel的方向

Channel可以被指定为只发送或只接收。通过限定channel的方向,可以提高代码的可读性和安全性。

func sendOnly(ch chan<- int) {
    ch <- 10 // 只能发送数据
}

func receiveOnly(ch <-chan int) {
    value := <-ch  // 只能接收数据
}

Buffered Channel(带缓冲的Channel)

Channel可以是带缓冲的,这意味着可以在没有接收者读取的情况下,先行存储一部分数据。make函数创建channel时,可以指定缓冲容量。

ch := make(chan int, 2) // 创建一个容量为2的带缓冲channel
ch <- 1
ch <- 2  // 可以存入两个元素而不会阻塞

Select 语句

select 语句可以让goroutine等待多个channel操作,哪个channel先准备好就执行哪个操作。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "two"
    }()
    
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("Received", msg1)
        case msg2 := <-ch2:
            fmt.Println("Received", msg2)
        }
    }
}

使用Channel的注意事项

  1. 避免死锁:如果channel的接收和发送操作不能配对,程序会陷入死锁。
  2. 避免channel泄漏:打开channel后需要必要的时候关闭,以防资源泄漏。
  3. 数据传递类型一致:确保channel传递的数据类型一致,否则会引发编译错误。

channel底层原理

image.png 在Go语言的runtime实现中,channel是通过hchan结构体表示的.

环形队列(Circular Queue)

对于带缓冲的channel,hchan中包含一个环形队列用于存储暂时未被接收的数据。

  • buf:这个指针指向实际存储数据的数组。
  • dataqsiz:环形队列的容量。
  • sendx:发送数据的索引。
  • recvx:接收数据的索引。

通过这种设计,数据可以高效地在队列中循环使用,无需频繁地移动内存。

创建channel时的策略

  1. 如果是无缓冲channel
    直接为hchan结构体分配内存并返回指针。
  2. 如果是有缓冲channel,但元素不包含指针类型
    一次性为hchan结构体和底层环数组分配连续内存并返回指针(需要连续内存空间)。
  3. 如果是有缓冲channel,且元素包含指针类型
    则分别分配hchan结构体内存和底层环数组的内存并返回指针(可以利用内存碎片)。

向channel中发送数据的流程

主要分为两大块:边界检查和数据发送。

数据发送流程

  1. 如果channel的读等待队列中存在接收者goroutine,则为同步发送:

    • 无缓冲channel:不用经过channel,直接将数据发送给第一个等待接收的goroutine,并将其唤醒等待调度。
    • 有缓冲channel,但元素个数为0:不用经过channel(假装经过channel)直接将数据发送给第一个等待接收的goroutine,并将其唤醒等待调度。
  2. 如果channel的读等待队列中不存在接收者goroutine

    • 如果底层环数组未满,则把发送者携带的数据从队尾写入,此为异步发送。
    • 如果底层环数组已满或者是无缓冲channel,则将当前goroutine加入写等待队列,并将其挂起,等待被唤醒,此为阻塞发送。