channel 是否有缓冲带,其行为会有一些不同。理解这个差异对决定到底应该使不使用缓冲带很有帮助。
理解无缓冲带 channel
使用无缓冲带 channel 进行接收和发送操作时,一定要注意,接收和发送操作不能放在同一个 goroutine 中,否则是没有意义的。
为什么这么说呢?
这是因为无缓冲带 channel 的特点是,同步阻塞。
无缓冲带,可以分为两种情况:
- 从无缓冲带 channel 接收数据时,如果同时没有发送者发送数据,那么则会一直阻塞,直到有发送者出现进行发送。
- 向无缓冲带 channel 发送数据,如果同时没有接收者进行接收,那么则会一直阻塞,直到有接收者出现进行接收。
结合下面这个图进行理解,也就是无缓冲带 channel 只有同时具备接收和发送能力才会继续往下执行。
结合下面的示例代码来理解:
1 | func main() { |
也就是无论 time.Sleep 休息多少秒, goroutine 中的 123 都不会打印出来。因为 goroutine 阻塞在 ch2 <- 123
这行代码了。
那么应该怎么改写, 123 才会打印出来呢?
只需要将发送数据这一行代码移出 goroutine 即可:
1 | ... |
运行上面的示例代码,输出如下:
1 | go |
符合预期。
理解有缓冲带 channel
很多资料或者文章里面会说,有缓冲带 channel 是异步非阻塞的,这个异步到底该如何理解呢?
有缓冲带,可以分为四种情况:
- 如果缓冲带已满,那么对它的所有发送操作都会被阻塞,直到缓冲带中有元素值被接收
- 如果缓冲带已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现
- 如果缓冲带未满,那么对它的所有发送操作都不会阻塞
- 如果缓冲带非空,那么对它的所有接收操作都不会阻塞
上面所说异步非阻塞,就是针对第三四种情况的,而第一二种情况,在缓冲带已满、已空时,就和无缓冲带一样了,因此也会是同步阻塞的。
下面用一段示例代码来加深理解。
还是上面的示例代码,只是初始化 channel 时,使用的是有缓冲带的 channel。
1 | func main() { |
执行一下,输出如下:
1 | go |
为什么现在就打印出 123 来了?
正是因为有缓冲带的存在,有缓冲带 channel 不需要同时具有接收和发送能力,才能继续往下执行,数据的发送者将数据放在缓冲带即可,而无需等待数据的接收者出现。
结合下面这个图进行理解:
在这个过程里面,两个操作不是同步的,因此就说有缓冲带 channel 是异步非阻塞。
这两种类型的缓冲带,各自有各自的特点,有的场景仅适合使用无缓冲带 channel,有的场景则只适合使用有缓冲带 channel,不过,也有两种缓冲带都可以使用的场景。
一个示例
下面这个示例就是两种缓冲带都可以使用的场景。
先来看看示例代码:
1 | func hello(w http.ResponseWriter, req *http.Request) { |
为了方便后面 goroutine 的使用,这里启动了一个 HTTP 服务。
函数 keepPrinting 的作用很简单,在一个不会主动停止的 for 循环中,一直打印变量 i 的值。
运行这段示例代码,会发现终端一直有输出:
1 | ... |
这时,访问一下刚刚启动的 Web 服务:
1 | $ curl -I http://localhost:9999/hello |
怎么状态码是 502,不是 200?
这是因为 main goroutine 一直在 for 循环里面,就没有出来过,所以服务压根就没有启动。
那么有没有什么办法,既可以保证服务正常启动,同时又可以执行 keepPrinting 函数?
答案是有的,那就是“创建”一个 goroutine,由这个 goroutine 去执行 keepPrinting 函数。
因此,上面的示例代码就变成了这样:
1 | func main() { |
运行更新之后的示例代码,会发现终端同样一直有输出:
1 | ... |
访问一下 Web 服务:
1 | $ curl -I http://localhost:9999/hello |
状态码是 200,说明服务已经启动了。
现在虽然达到了上面的预期结果,但是终端输出的内容太多了,导致其他重要的内容容易一下子被“冲走了”。
那么有没有什么办法,可以限制一下输出,只在服务端收到来自客户端的请求时,才会去打印。
当然是可以的,这里就要借助 channel 了。
对上面的示例代码进行改造:
1 |
|
再次执行示例代码,启动 Web 服务:
1 | App runs on port 9999. |
终端访问 Web 服务:
1 | $ curl -I http://localhost:9999/hello |
控制台继续输出如下内容:
1 | 2022/11/25 10:55:56 URL.Path = /hello |
这里这个示例,无论是使用无缓冲 channel还是有缓冲 channel,其效果都是一样的。
- 使用无缓冲 channel 时,对无缓冲 channel 的接收和发送操作是同步的,只有同时具备接收和发送能力才会继续往下执行。
- 使用有缓冲 channel 时,因为在缓冲带已空的情况下,对它的所有接收操作都会被阻塞,因此效果上和使用无缓冲 channel 是一样的。
当没有新的请求进来时,也就是没有对 channel 进行发送操作,goroutine 会一直阻塞在 url := <-ch
对 channel 的接收操作上。
一旦新的请求进来了,也就是对 channel 进行发送操作了,此时 goroutine 通过接收操作拿到了数据,因此继续往下执行。
goroutine 再次重新进入 for 循环,阻塞等待,重复上面的逻辑。