时间:2025-07-09 21:54
人气:
作者:admin
本文首发于公众号:Hunter后端
这一篇介绍 Golang 里的 goroutine 和 channel 通道。
以下是本篇笔记目录:
goroutine 是一种轻量级线程(用户态线程),由 Go 运行时管理而非操作系统,它是 Go 并发模型的核心,能高效处理大量并发任务。
goroutine 的使用非常简单,直接使用 go + 函数 即可启动一个 goroutine:
package main
import (
"fmt"
"time"
)
func PrintGoroutineInfo() {
fmt.Println("msg from goroutine")
}
func main() {
go PrintGoroutineInfo()
time.Sleep(1 * time.Millisecond)
fmt.Println("msg from main")
}
func main() {
go func() {
fmt.Println("msg from goroutine")
}()
time.Sleep(1 * time.Second)
}
而如果 goroutine 运行的函数有返回值,想要获取函数的返回值应该如何操作呢?
或者当我们使用 goroutine 的时候,如何使主 goroutine 等待并发的 goroutine 执行完毕再接着往后执行呢?
其中一个方法就是使用 channel 来获取返回值,以及使用 channel 的阻塞状态来等待并发的 goroutine 执行完毕。
channel,即通道,可用于在 goroutine 之间传递数据和同步状态。
channel 是强类型的,每个 channel 只能传递一种类型的数据。
比如我们声明一个传递 int 类型的 channel:
var ch chan int
或者直接创建一个传递 int 数据的通道:
ch := make(chan int)
在创建 channel 的时候,如果不指定容量,那么则称其为无缓冲通道,如果指定了容量,则为有缓冲通道,比如下面创建一个容量为 3 的通道:
ch := make(chan int, 3)
发送数据
向一个 channel 发送数据的操作如下:
ch <- 21
接收数据
从一个 channel 中接收数据的操作如下:
x := <-ch
或者仅仅是接收数据但不使用,可以直接丢弃:
<-ch
使用 range 遍历 channel
也可以使用 range 的方式遍历从 channel 中接收数据,但是需要在通道关闭后:
for x := range ch {
fmt.Println(x)
}
关闭 channel
关闭一个 channel 的操作如下:
close(ch)
下面介绍几种 channel 在使用中的特殊情况。
对于 channel 的使用,如果使用不慎,有可能会造成阻塞,以下是几种阻塞的情况
对于无缓冲通道而言,发送和接收的操作必须同时发生,否则会进入阻塞状态。
func CapZeroChannel(ch chan int) {
time.Sleep(5 * time.Second)
ch <- 1
fmt.Println("inner func, send msg:", time.Now().Format("2006-01-02 15:04:05"))
}
func main() {
ch := make(chan int)
go CapZeroChannel(ch)
fmt.Println("before func:", time.Now().Format("2006-01-02 15:04:05"))
x := <-ch
fmt.Println("after func:", time.Now().Format("2006-01-02 15:04:05"))
fmt.Println(x)
}
在上面的操作中,最后输出的结果如下:
before func: 2025-06-28 23:33:03
inner func, send msg: 2025-06-28 23:33:08
after func: 2025-06-28 23:33:08
可以看到,在并发的 CapZeroChannel() 函数中,发送数据前等待了 5 秒钟,同时在主 goroutine,也就是 main 函数中,通道接收值的地方发生了阻塞,直到发送方把数据通过 channel 发送过来,才接着往后执行。
而如果我们将等待的地方放在接收前:
func CapZeroChannel(ch chan int) {
ch <- 1
fmt.Println("inner func, send msg:", time.Now().Format("2006-01-02 15:04:05"))
}
func main() {
ch := make(chan int)
go CapZeroChannel(ch)
fmt.Println("before func:", time.Now().Format("2006-01-02 15:04:05"))
time.Sleep(5 * time.Second)
x := <-ch
fmt.Println("after func:", time.Now().Format("2006-01-02 15:04:05"))
fmt.Println(x)
}
最后输出的信息如下:
before func: 2025-06-28 23:37:32
after func: 2025-06-28 23:37:37
inner func, send msg: 2025-06-28 23:37:37
可以看到发送的地方也同时发生了阻塞。
通过这两个示例,可以证实前面的观点,即 对于无缓冲通道而言,发送和接收的操作必须同时发生,否则会进入阻塞状态。
对于有缓冲通道来说,阻塞的情况会发生在向已满的通道里发送数据,或者从空的通道里接收数据,下面是各自的示例:
向已满的通道里发送数据
当我们向已满的通道里发送数据,会发生阻塞,下面是代码示例:
func SendMsgToChannel(ch chan int) {
ch <- 1
fmt.Println("send msg to channel: ", 1, " at: ", time.Now().Format("2006-01-02 15:04:05"))
ch <- 2
fmt.Println("send msg to channel: ", 2, " at: ", time.Now().Format("2006-01-02 15:04:05"))
ch <- 3
fmt.Println("send msg to channel: ", 3, " at: ", time.Now().Format("2006-01-02 15:04:05"))
ch <- 4
fmt.Println("send msg to channel: ", 4, " at: ", time.Now().Format("2006-01-02 15:04:05"))
close(ch)
}
func main() {
ch := make(chan int, 3)
go SendMsgToChannel(ch)
time.Sleep(5 * time.Second)
for x := range ch {
fmt.Println("receive msg from channel: ", x, " at: ", time.Now().Format("2006-01-02 15:04:05"))
}
}
可以看到输出的信息如下:
send msg to channel: 1 at: 2025-06-29 00:36:24
send msg to channel: 2 at: 2025-06-29 00:36:24
send msg to channel: 3 at: 2025-06-29 00:36:24
receive msg from channel: 1 at: 2025-06-29 00:36:29
receive msg from channel: 2 at: 2025-06-29 00:36:29
receive msg from channel: 3 at: 2025-06-29 00:36:29
receive msg from channel: 4 at: 2025-06-29 00:36:29
send msg to channel: 4 at: 2025-06-29 00:36:29
前面向通道里发送三条数据,把有缓冲通道占满了,但是在接收前 sleep 了五秒钟,所以没有及时从通道里接收数据,当向通道里发送第四条数据的时候就会发生阻塞。
五秒钟后,主 goroutine 开始从 channel 里消费数据,第四条数据才往里写入。
从空通道里接收数据
如果从空通道里接收数据,也会发生阻塞,下面是代码示例:
func SendMsgToChannel(ch chan int) {
time.Sleep(3 * time.Second)
ch <- 1
fmt.Println("send msg to channel: ", 1, " at: ", time.Now().Format("2006-01-02 15:04:05"))
ch <- 2
fmt.Println("send msg to channel: ", 2, " at: ", time.Now().Format("2006-01-02 15:04:05"))
ch <- 3
fmt.Println("send msg to channel: ", 3, " at: ", time.Now().Format("2006-01-02 15:04:05"))
ch <- 4
fmt.Println("send msg to channel: ", 4, " at: ", time.Now().Format("2006-01-02 15:04:05"))
close(ch)
}
func main() {
ch := make(chan int, 3)
go SendMsgToChannel(ch)
fmt.Println("start receive msg from channel at: ", time.Now().Format("2006-01-02 15:04:05"))
for x := range ch {
fmt.Println("receive msg from channel: ", x, " at: ", time.Now().Format("2006-01-02 15:04:05"))
}
}
其输出信息如下:
start receive msg from channel at: 2025-06-29 00:41:24
receive msg from channel: 1 at: 2025-06-29 00:41:27
send msg to channel: 1 at: 2025-06-29 00:41:27
send msg to channel: 2 at: 2025-06-29 00:41:27
send msg to channel: 3 at: 2025-06-29 00:41:27
send msg to channel: 4 at: 2025-06-29 00:41:27
receive msg from channel: 2 at: 2025-06-29 00:41:27
receive msg from channel: 3 at: 2025-06-29 00:41:27
receive msg from channel: 4 at: 2025-06-29 00:41:27
可以看到,在发送信息前 sleep 了 3 秒,因此接收方也等待了 3 秒才能开始接收数据,在这之前一直是处于阻塞状态。
通道在关闭后,不可以再向其中发送数据,但是还可以从中接收数据。
func PrintGoroutineInfo(ch chan int) {
fmt.Println("msg from goroutine")
time.Sleep(1 * time.Second)
ch <- 2
close(ch)
}
func main() {
ch := make(chan int)
go PrintGoroutineInfo(ch)
x := <-ch
fmt.Println(x)
}
前面我们将通道作为参数传递给函数,其类型仅仅是通道类型,如果我们想要在代码层面使其更严谨,比如某个函数中只允许发送或者接收数据,我们在其类型进行更严谨的声明。
比如发送通道我们可以对其声明为 chan<-,接收通道可以对其声明为 <-chan,下面是代码示例:
func SendMsg(ch chan<- int) {
ch <- 4
fmt.Println("send msg: ")
}
func ReceiveMsg(ch <-chan int) {
x := <-ch
fmt.Println("receive msg: ", x)
}
func main() {
ch := make(chan int, 3)
go SendMsg(ch)
go ReceiveMsg(ch)
time.Sleep(1 * time.Second)
fmt.Println("end")
}
在这篇笔记中,我们介绍 goroutine 和 channel 在 Golang 中的使用,比如如何使用 goroutine 开启一个并发操作,如何使用 channel 在 goroutine 间进行通信。
但是关于 goroutine 之间的一些并发控制与锁相关的一些概念我们将在之后的笔记中进行更详细的介绍。