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

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

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

以下是本篇笔记目录:

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

1、goroutine

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

1. goroutine 的使用

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")
}

2. 匿名函数使用 goroutine

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:

var ch chan int

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

ch := make(chan int)
2) 有缓冲通道

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

ch := make(chan int, 3)

2. channel 的操作

发送数据

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

ch <- 21

接收数据

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

x := <-ch

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

<-ch

使用 range 遍历 channel

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

for x := range ch {
fmt.Println(x)
}

关闭 channel

关闭一个 channel 的操作如下:

close(ch)

3、goroutine 与 channel 的使用

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

1. 阻塞情况

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

1) 无缓冲通道

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


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

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

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

2) 有缓冲通道

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

向已满的通道里发送数据

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

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

2. 通道的关闭

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

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,下面是代码示例:

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

Golang基础笔记十之goroutine和channel的更多相关文章

  1. golang语言并发与并行——goroutine和channel的详细理解(一)

    如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. ...

  2. golang语言并发与并行——goroutine和channel的详细理解

    如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. ...

  3. golang语言并发与并行——goroutine和channel的详细理解(一) 转发自https://blog.csdn.net/skh2015java/article/details/60330785

    如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. ...

  4. Golang基础笔记

    <基础> Go语言中的3个关键字用于标准的错误处理流程: defer,panic,recover. 定义一个名为f 的匿名函数: Go 不支持继承和重载. Go的goroutine概念:使 ...

  5. C#基础笔记(第十天)

    C#基础笔记(第十天) 1.字段.属性.方法.构造函数字段:存储数据属性:保护字段,对字段的取值和设值进行限定方法:描述对象的行为构造函数:初始化对象(给对象的每个属性依次的赋值)类中成员,如果不加访 ...

  6. shell基础二十篇 一些笔记

    shell基础二十篇 转自 http://bbs.chinaunix.net/thread-452942-1-1.html 研讨:Bash 内建命令 read (read命令更具体的说明见博客收藏的一 ...

  7. Go基础--goroutine和channel

    goroutine 在go语言中,每一个并发的执行单元叫做一个goroutine 这里说到并发,所以先解释一下并发和并行的概念: 并发:逻辑上具备同时处理多个任务的能力 并行:物理上在同一时刻执行多个 ...

  8. Golang学习笔记:channel

    channel channel是goroutine之间的通信机制,它可以让一个goroutine通过它给另一个goroutine发送数据,每个channel在创建的时候必须指定一个类型,指定的类型是任 ...

  9. Golang基础教程

    以下使用goland的IDE演示,包含总计的golang基础功能共20个章节 一.go语言结构: 二.go基础语法: 三.变量 四.常量 五.运算符 六.条件语句 七.循环 八.函数 九.变量作用域 ...

  10. GoLang基础数据类型--->字典(map)详解

    GoLang基础数据类型--->字典(map)详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.   可能大家刚刚接触Golang的小伙伴都会跟我一样,这个map是干嘛的,是 ...

随机推荐

  1. 又一款眼前一亮的Linux终端工具!

    大家好,我是良许. 最近二舅视频刷爆了全网,大家有没去看呢?强烈推荐大家观看一波,也就 11 分钟,保证会触动你的泪点. 不过今天不讲二舅哈,还是来聊聊技术. 今天给大家介绍一款最近发现的功能十分强大 ...

  2. Windows IntelliJ IDEA 快捷键终极大全

    自动代码 常用的有fori/sout/psvm+Tab即可生成循环.System.out.main方法等boilerplate样板代码 . 例如要输入for(User user : users)只需输 ...

  3. jmeter从csv文件中取数据设置变量的方法

    从csv取数据是参数化方法之一 首先,CSV数据文件设置,选择数据文件,点击http请求,右键-添加-配置元件-csv data set config,添加CSV数据文件设置 添加后可对设置名称进行修 ...

  4. [not]exists和[not]in的区别

    前言:近期在处理业务实际问题时发现使用in导致查询速度非常慢,于是查阅资料后发现使用exists能极大缩短查询时间,于是便将此经历记录下来. 数据源: grade表 stu_info表 exists与 ...

  5. SpringBoot Task定时任务

    参数详解 @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) ...

  6. 11.23DP进阶总结

    例.1 Luogu-P1387最大正方形 按如下复杂度来分析 O(\(n^6\)) O(\(n^5\)) O(\(n^3\)) O(\(n^2\log n\)) O(n^2) O(\(n^6\)) 最 ...

  7. C# 线程(四)——Task初始

    一.相关介绍 .NetFramework3.0时代实现,C#中多线程.异步编程最佳实践,特点: 1.所有的Task操作的线程来自线程池,避免了频繁的线程创建及销毁 2.含有丰富的Api,能满足我们在开 ...

  8. 47.3K star!这款开源RAG引擎真香!文档理解+精准检索+可视化干预,一站式搞定!

    嗨,大家好,我是小华同学,关注我们获得"最新.最全.最优质"开源项目和高效工作学习方法 RAGFlow 是基于深度文档理解的开源RAG引擎,通过与LLM结合提供带精准引用的问答能力 ...

  9. 【记录】VScode|两种缩放快捷键的功能和开启方式(Ctrl+/-,Ctrl滚轮)

    1 面板缩放 快捷键:Ctrl+'+'/'-'. 2 滚轮缩放字体 快捷键:Ctrl+滚轮 开启方式:如下图,打开设置,搜索zoom,勾选. 更多快捷键:Ctrl+K Ctrl+S打开快捷键设置(或左 ...

  10. .NET 8 开发的跨平台多商户第三方支付SDK

    前言 快速发展的互联网应用开发中,支付功能已成为各类平台不可或缺的一环.为了帮助大家更高效地接入主流支付渠道,推荐一套基于 .NET 开发的第三方支付 SDK.该 SDK 支持跨平台运行,适用于多种操 ...