再过10分钟你就能了解Go中的Channels(chan)啦

268 阅读5分钟

Channels是一个管道,Go中写作chan,数据沿箭头方向流动,通道必须在使用前make创建,示例代码是对一个切片中的数字求和,将工作分配给两个goroutine。 一旦两个goroutine都完成了它们的计算,它就会计算出最终的结果

package main

import "fmt"

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // send sum to c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // receive from c

	fmt.Println(x, y, x+y)
}

1. 创建

创建能存储3个 int 类型数据的chan

ch:=make(chan int,3)

()

chan 的零值是 nilclose 一个 nil 通道会引发 panic。往 nil 通道写入或从中读取数据会永久阻塞.

2. 读写

  • 写:ch <- x,将 x 发送到 chan 中

  • 读:x = <-ch,从 chan 中接收,保存到 x

  • 读(忽略返回值):<-ch,从 ch 中接收,忽略接收到的结果

  • 读(判断是否是关闭前发送的):x, ok := <-ch,使用了两个返回值接收,第二个返回值表明了接收到的 x 是不是 chan 关闭之前发送进去的,true 就代表是。

3. 缓冲

  1. 无缓冲的 chan 会在发送的时候阻塞,直到有另一个协程从 chan 中获取数据(make时候不传递或者传 0 表示 chan 本身不能存储数据是无缓冲的,go 底层会直接在两个 goroutine 之间传递,而不经过 chan 的复制。).
  2. 有缓冲的 chan 在协程往里面写入数据的时候,可以进行缓冲。在需要读取 chan 的 goroutine 的处理速度比较慢的时候,写入 chan 的 goroutine 也可以持续运行,直到写满 chan 的缓冲区,如果 chan 的数据一直没有被接收,然后满了的时候,往 chan 写入数据的协程依然会陷入阻塞。但这种阻塞状态会在 chan 的数据被读取的时候解除(如果make时候第二个参数大于 0,我们往 chan 写数据的时候,会先复制到 chan 这个数据结构,然后其他的 goroutine 从 chan 中读取数据的时候,chan 会将数据复制到这个 goroutine 中)。

4. len和cap

  1. len:通过 len 我们可以查询一个 chan 的长度,也就是有多少被发送到这个 chan 但是还没有被接收的值。
  2. cap:通过 cap 可以查询一个容道的容量,也就是我们传给 make 函数的第二个参数,它表示 chan 最多可以容纳多少数据。
  3. 如果 channil,那么 lencap 都会返回 0。

5. chan 的方向

  1. chan,没有指定方向,既可以读又可以写。
  2. chan<-,只写 chan,只能往 chan 中写入数据
  3. <-chan,只读 chan,只能从 chan 中读取数据
  4. 无方向的 chan 可以转换为 chan<- 或者 <-chan,但是反过来不行
package main

import "fmt"

var done = make(chan struct{})

// ch 是只写 chan,如果在这个函数里面从 ch 读取数据编译不会通过
func producer(ch chan<- int) {
	for i := 0; i < 3; i++ {
		ch <- i
		fmt.Printf("produce %d\n", i)
	}
	// 发送 3 个数之后,关闭 chan
	close(ch)
}

// ch 是只读 chan,如果在这个函数里往 ch 写入数据编译不会通过
func consumer(ch <-chan int) {
	for {
		i, ok := <-ch
		if !ok {
			// chan 的数据已经被全部接收完,
			// 发送 done 信号
			done <- struct{}{}
			break
		}
		fmt.Printf("consume %d\n", i)
	}
}

func main() {
	nums := make(chan int, 10)
	go producer(nums)
	go consumer(nums)
	// 收到结束信号之后继续往下执行
	<-done
}

6. for...range 语句

chan 读取数据的时候,可能需要用两个值来接收 chan 的返回值,第二个值用来判断接收到的值是否是 chan 关闭之前发送的。

for...range 语法也可以用来从 chan 中读取数据,它会循环,直到 chan 关闭,直接免去了判断的操作

package main

import "fmt"

func main() {
	done := make(chan struct{})

	nums := make(chan int)
	go func() {
		for i := 0; i < 3; i++ {
			fmt.Printf("send %d\n", i)
			nums <- i

		}
		close(nums)
	}()

	go func() {
		// 传统写法
		//for {
		//	num, ok := <-nums
		//	if !ok {
		//		break
		//	}
		//	fmt.Printf("receive %d\n", num)
		//}

		// range 语法糖
		for num := range nums {
			fmt.Printf("receive %d\n", num)
		}
		done <- struct{}{}
	}()

	<-done
}

7. select语句

go 里面有一个关键字 select,可以让我们同时监听几个 chan,在任意一个 chan 有数据的时候,select 里面的 case 块得以执行:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	// ch1 会先收到数据
	go func() {
		time.Sleep(time.Second)
		ch1 <- 1
	}()
	go func() {
		time.Sleep(time.Second * 2)
		ch2 <- 1
	}()

	// select 会阻塞,直到其中某一个分支收到数据
	select {
	case <-ch1:
		// 执行这一行代码
		fmt.Println("from ch1")
	case <-ch2:
		// 这一行不会被执行
		fmt.Println("from ch2")
	}
}

select-case 的用法类似于 switch-case,也有一个 default 语句,在 select 里面

  • 如果 default 之前的 case 都不满足,则执行 default 块的代码。
  • 如果没有 default 语句,则会一直阻塞,直到某一个 case 上面的 chan 返回(有数据、或者 chan 被关闭都会返回)

当然,case 后面可以从 chan 读取数据,也可以往 chan 写数据,比如:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	// 往 nil chan 写入数据会阻塞
	var ch2 chan int

	// ch1 会先收到数据
	go func() {
		time.Sleep(time.Second)
		ch1 <- 1
	}()

	// 会阻塞,直到其中一个 case 返回
	select {
	case <-ch1:
		// 执行这一行代码
		fmt.Println("from ch1")
	case ch2 <- 1: // 永远不会满足,因为 ch2 是 nil
		fmt.Println("from ch2")
	}
}

8. select 常见用法

select 的一种很常见的用法是,等待一个 chan 和一个定时器(实现控制超时的功能),比如:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)

	// ch1 一秒后才收到数据
	go func() {
		time.Sleep(time.Second)
		ch1 <- 1
	}()

	select {
	case <-ch1:
		fmt.Println("from ch1")
	case <-time.After(time.Millisecond * 100):
		// 执行如下代码,因为这个 case 在 100ms 后就返回了
		fmt.Println("from ch2")
	}
}

如果需要控制某些操作的超时时间,那么就可以在时间到了之后,做一些清理操作,然后终止一些工作,最后退出协程。