Skip to content

Channel

Channel 管道,这是 Go 的并发世界里至关重要的一块,它允许不同 goroutines 之间的数据交换和同步。

  • 管道中的数据流动方式与队列一样,即先进先出。

核心用法是下面两段:

go
make(chan 元素类型, 缓冲大小) // channel 的类型决定了他所能传递的数据类型,第二个参数是可选的,用于指定 channel 的缓冲大小。

// 例如:
make(chan int, 100) // 有缓冲的channel,可以暂存100个值,满了就阻塞发送方的goroutine。
make(chan string) // 无缓冲的channel,只能暂存一个值,满了就会阻塞发送方的goroutine。
go
// 箭头方向即为数据流的方向,只有<- ,没有->
ch <- x  // 发送数据
x = <-ch // 接收数据并赋值给x
<-ch     // 接收数据并丢弃

管道类型

就是 chan ,但直接声明 var ch chan int 这种是不能直接使用的,只有 make 初始化的管道才可以正常使用。

管道操作

创建

make 一般有三个参数,用于创建 slice、map、channel,但创建管道时最多只会用到两个参数。

go
intCh := make(chan int)
// 缓冲区大小为1的管道
strCh := make(chan string, 1)

关闭

有一个内置函数:func close(c chan<- Type)

示例:

go
func main() {
	intCh := make(chan int)
	// do something
	close(intCh)
}

有时候使用 defer 关闭管道更好:

go
func main() {
	// 创建有缓冲管道
	ch := make(chan int, 5)
	// 创建两个无缓冲管道
	chW := make(chan struct{})
	chR := make(chan struct{})
    // 在 main 结束后关闭管道
	defer func() {
		close(ch)
		close(chW)
		close(chR)
	}()
}

读写

略一部分(上方有)

go
ints, ok := <-intCh

依旧可使用ok判断是否读取成功。

无缓冲

部分略(上方有)

无缓冲管道不应该同步的使用,正确来说应该开启一个新的协程来发送数据:

go
func main() {
	// 创建无缓冲管道
	ch := make(chan int)
	defer close(ch)
	go func() {
		// 写入数据
		ch <- 123
	}()
	// 读取数据
	n := <-ch
	fmt.Println(n)
}

有缓冲

尽管channel具备缓冲,但依旧非常危险,一旦缓冲满了,就会阻塞发送方,而缓冲空了则会阻塞接收方。

缓冲只是缓解短时突发的手段,不是同步或无限吞吐的解决方案。

因此在并发设计中必须谨慎使用并配合其他控制手段,比如增加接收方数量或提高单个接收方的处理能力、增加限流手段等等。

访问管道信息

len(ch) 可知正在缓冲的数据数量,用 cap(ch) 可知管道缓冲区大小。

go
func main() {
   // 创建并发送数据
   ch := make(chan int, 5)
   ch <- 1
   ch <- 2
   ch <- 3
   // 打印len和cap
   fmt.Println(len(ch), cap(ch))
}
/* 输出
3 5
*/

小技巧

等待子协程

不只有缓冲满了才能阻塞发送方,管道内没有数据也可以阻塞接收方

利用这一条件,可实现主协程等待子协程执行完毕的场景。

go
func main() {
	// 创建一个无缓冲管道
	ch := make(chan struct{})
	defer close(ch)
	go func() {
		fmt.Println(2)
		time.Sleep(1 * time.Second)
		// 写入数据
		ch <- struct{}{}
	}()
	// 没有数据无法读取,阻塞,等待数据
	<-ch
	fmt.Println(1)
}

使用range等待

range在go里面是一个通用的迭代器。

对于channel而言,range可以不断地接收管道里面的数据,同时会阻塞当前协程,直到管道被关闭。

<-ch更优雅的写法:

go
// 更优雅的等待:range 会在 channel 被关闭后退出
for range ch {
    // 不需要对 v 做任何处理——这里只是等待信号
}

// 如果要提取数据
for v := range ch {
    fmt.Println(v)
}

仅通知

若一个管道仅用于通知单个消息,可以传递struct{}{}空数据。

go
ch <- struct{}{}
go
func main() {
	defer func() {
		close(ch)
		close(chW)
		close(chR)
	}()
	// 负责写
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
			fmt.Println("写入", i)
		}
		chW <- struct{}{}
	}()
	// 负责读
	go func() {
		for i := 0; i < 10; i++ {
            // 每次读取数据都需要花费1毫秒
			time.Sleep(time.Millisecond)
			fmt.Println("读取", <-ch)
		}
		chR <- struct{}{}
	}()
	fmt.Println("写入完毕", <-chW)
	fmt.Println("读取完毕", <-chR)
}

一些注意点

  • 管道最好要由发送方关闭,因为大多数情况下接收方只知道接收数据,并不知道该在什么时候关闭管道。

Select

select语句可以让goroutine同时等待多个通信操作。

select会阻塞,直到其中一个case可以运行,若多个case已就绪,他会随机选一个执行。

go
select {
case n, ok := <-chA:
    fmt.Println(n, ok)
case n, ok := <-chB:
    fmt.Println(n, ok)
case n, ok := <-chC:
    fmt.Println(n, ok)
default:
    fmt.Println("所有管道都不可用")
    // 没有管道可用时,使用 default 分支可以使协程不阻塞(但也使程序跳过了接收数据的步骤。。)。
}