Golang基础笔记十之goroutine和channel

本文首发于公众号:Hunter后端

原文链接:Golang基础笔记十之goroutine和channel

这一篇介绍 Golang 里的 goroutine 和 channel 通道。

以下是本篇笔记目录:

  1. goroutine
  2. channel
  3. goroutine 与 channel 的使用

1、goroutine

goroutine 是一种轻量级线程(用户态线程),由 Go 运行时管理而非操作系统,它是 Go 并发模型的核心,能高效处理大量并发任务。

1. goroutine 的使用

goroutine 的使用非常简单,直接使用 go + 函数 即可启动一个 goroutine:

go 复制代码
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")
}

2. 匿名函数使用 goroutine

go 复制代码
func main() {
    go func() {
        fmt.Println("msg from goroutine")
    }()
    time.Sleep(1 * time.Second)
}

而如果 goroutine 运行的函数有返回值,想要获取函数的返回值应该如何操作呢?

或者当我们使用 goroutine 的时候,如何使主 goroutine 等待并发的 goroutine 执行完毕再接着往后执行呢?

其中一个方法就是使用 channel 来获取返回值,以及使用 channel 的阻塞状态来等待并发的 goroutine 执行完毕。

2、channel

channel,即通道,可用于在 goroutine 之间传递数据和同步状态。

1. channel 的声明与创建

channel 是强类型的,每个 channel 只能传递一种类型的数据。

1) 无缓冲通道

比如我们声明一个传递 int 类型的 channel:

go 复制代码
var ch chan int

或者直接创建一个传递 int 数据的通道:

go 复制代码
ch := make(chan int)
2) 有缓冲通道

在创建 channel 的时候,如果不指定容量,那么则称其为无缓冲通道,如果指定了容量,则为有缓冲通道,比如下面创建一个容量为 3 的通道:

go 复制代码
ch := make(chan int, 3)

2. channel 的操作

发送数据

向一个 channel 发送数据的操作如下:

go 复制代码
ch <- 21

接收数据

从一个 channel 中接收数据的操作如下:

go 复制代码
x := <-ch

或者仅仅是接收数据但不使用,可以直接丢弃:

go 复制代码
<-ch

使用 range 遍历 channel

也可以使用 range 的方式遍历从 channel 中接收数据,但是需要在通道关闭后:

go 复制代码
for x := range ch {
    fmt.Println(x)
}

关闭 channel

关闭一个 channel 的操作如下:

go 复制代码
close(ch)

3、goroutine 与 channel 的使用

下面介绍几种 channel 在使用中的特殊情况。

1. 阻塞情况

对于 channel 的使用,如果使用不慎,有可能会造成阻塞,以下是几种阻塞的情况

1) 无缓冲通道

对于无缓冲通道而言,发送和接收的操作必须同时发生,否则会进入阻塞状态。

go 复制代码
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)
}

在上面的操作中,最后输出的结果如下:

go 复制代码
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 发送过来,才接着往后执行。

而如果我们将等待的地方放在接收前:

go 复制代码
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)
}

最后输出的信息如下:

go 复制代码
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

可以看到发送的地方也同时发生了阻塞。

通过这两个示例,可以证实前面的观点,即 对于无缓冲通道而言,发送和接收的操作必须同时发生,否则会进入阻塞状态。

2) 有缓冲通道

对于有缓冲通道来说,阻塞的情况会发生在向已满的通道里发送数据,或者从空的通道里接收数据,下面是各自的示例:

向已满的通道里发送数据

当我们向已满的通道里发送数据,会发生阻塞,下面是代码示例:

go 复制代码
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"))
    }

}

可以看到输出的信息如下:

go 复制代码
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 里消费数据,第四条数据才往里写入。

从空通道里接收数据

如果从空通道里接收数据,也会发生阻塞,下面是代码示例:

go 复制代码
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"))
    }
}

其输出信息如下:

go 复制代码
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 秒才能开始接收数据,在这之前一直是处于阻塞状态。

2. 通道的关闭

通道在关闭后,不可以再向其中发送数据,但是还可以从中接收数据。

go 复制代码
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)
}

3. 发送和接收通道的类型

前面我们将通道作为参数传递给函数,其类型仅仅是通道类型,如果我们想要在代码层面使其更严谨,比如某个函数中只允许发送或者接收数据,我们在其类型进行更严谨的声明。

比如发送通道我们可以对其声明为 chan<-,接收通道可以对其声明为 <-chan,下面是代码示例:

go 复制代码
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 之间的一些并发控制与锁相关的一些概念我们将在之后的笔记中进行更详细的介绍。

相关推荐
XHunter2 天前
Golang基础笔记九之方法与接口
golang基础笔记
XHunter19 天前
Golang基础笔记三之数组和切片
golang基础笔记
XHunter19 天前
Golang基础笔记二之字符串及其操作
golang基础笔记