并发是 Go 语言最核心、最亮眼的特性,Go 从语言层面原生支持并发,不像 Java/Python 等语言需要依赖线程库、协程框架,它的并发设计简洁、轻量、高效,也是 Go 能在后端开发 / 云原生领域脱颖而出的核心原因。
一、Go 并发的核心思想:不要通过共享内存来通信,要通过通信来共享内存
这是 Go 语言之父 Rob Pike 提出的并发黄金法则,也是 Go 并发设计的底层逻辑:
- 传统语言(Java/C++)并发:多线程抢占共享内存 ,通过
锁(synchronized/lock)保证数据安全,容易出现死锁、竞态、锁争用等问题; - Go 语言并发:goroutine 之间不推荐共享内存 ,而是通过专门的
channel管道传递数据,把数据所有权从一个 goroutine 转移到另一个,天然避免数据竞争,代码更安全、简洁。
二、Go 并发的两大基石
Go 的并发能力完全依赖两个核心组件实现,二者相辅相成,缺一不可:goroutine(协程) + channel(通道)
1. Goroutine (轻量级协程) - Go 并发的执行体
(1)什么是 Goroutine
goroutine 是 Go 语言内置的、轻量级的执行单元 ,也叫「用户级线程 / 协程」,它由 Go 运行时(runtime) 调度,而不是操作系统内核调度。
(2)Goroutine 核心特点
- 极致轻量 :一个 goroutine 的初始栈空间仅 2KB ,并且栈空间可以动态扩容 / 缩容(最大几 MB),完全由 runtime 自动管理;
- 资源占用极少 :一台普通服务器上,轻松创建 10 万、甚至百万级别的 goroutine,不会有内存压力;而操作系统线程(thread)的栈空间默认是 1MB,创建几千个线程就会内存溢出;
- 调度效率极高 :Go 的 M/P/G 调度模型,能把 goroutine 高效映射到操作系统线程,调度切换的开销只有线程的几百分之一;
- goroutine 是并发执行:多个 goroutine 在同一个 Go 程序中,宏观上 "同时运行",微观上由 runtime 做时间片轮转调度。
(3)如何创建 Goroutine
语法超级简单:在函数调用前加一个 go 关键字 即可,这个函数就会开启一个新的 goroutine 执行。
Go
package main
import "fmt"
// 定义一个普通函数
func printNum(name string) {
for i := 1; i <= 3; i++ {
fmt.Printf("协程[%s]:输出数字 %d\n", name, i)
}
}
func main() {
// 1. 开启第一个goroutine执行printNum
go printNum("goroutine-1")
// 2. 开启第二个goroutine执行printNum
go printNum("goroutine-2")
// 注意:主goroutine执行完会直接退出,导致子goroutine来不及执行
// 这里简单休眠1秒,等子goroutine执行完毕(仅演示,生产不用这个方式)
fmt.Println("主协程开始等待...")
import "time" // 实际代码要把import写在顶部
time.Sleep(time.Second)
fmt.Println("主协程执行结束")
}
执行结果(顺序不固定,因为 goroutine 并发调度):
主协程开始等待...
协程[goroutine-1]:输出数字 1
协程[goroutine-2]:输出数字 1
协程[goroutine-1]:输出数字 2
协程[goroutine-2]:输出数字 2
协程[goroutine-1]:输出数字 3
协程[goroutine-2]:输出数字 3
主协程执行结束
(4)补充:主 Goroutine
- 每个 Go 程序启动后,会自动创建一个主 goroutine ,执行
main()函数; - 所有子 goroutine 由主 goroutine(或其他子 goroutine)创建;
- 主 goroutine 退出,整个程序立即结束,所有子 goroutine 都会被强制终止(不管是否执行完)。
2. Channel (通道) - Go 并发的通信方式
(1)什么是 Channel
channel 是 Go 语言提供的原生同步通信机制 ,可以理解为「goroutine 之间的管道」。它的核心作用:让多个 goroutine 之间安全的传递数据、实现同步 / 协作 ,完全契合 Go 的并发思想:通过通信来共享内存。
(2)Channel 核心特点
- 通道是类型安全的:声明通道时必须指定传递的数据类型,只能传递对应类型的数据;
- 通道是阻塞特性 的(核心):
- 向一个通道发送数据
ch <- data时,如果通道满了,发送方 goroutine 会被阻塞,直到有 goroutine 从通道取走数据; - 从一个通道接收数据
data := <-ch时,如果通道空了,接收方 goroutine 会被阻塞,直到有 goroutine 向通道发送数据; - 这种阻塞是 runtime 实现的,无额外开销,是 goroutine 协作的核心原理;
- 向一个通道发送数据
- 通道可以手动关闭,关闭后无法再发送数据,只能接收剩余数据;
- 通道天然解决「数据竞争」:goroutine 之间不用共享变量,而是通过通道传递数据,数据在 goroutine 之间 "流转" 而非 "共享"。
(3)如何创建 Channel
语法:通道变量 := make(chan 数据类型, [缓冲区大小])
Channel 分为两种:无缓冲通道 和 有缓冲通道,通过「缓冲区大小」区分,默认是 0(无缓冲)。
① 无缓冲通道 (同步通道)
缓冲区大小为 0,声明时可以省略第二个参数:make(chan int)
- 特性:发送和接收必须同时就绪,否则会阻塞;
- 本质:实现 goroutine 之间的严格同步,发送方发数据的瞬间,必须有接收方在取数据,相当于 "一手交钱、一手交货";
- 适用场景:goroutine 之间的强同步协作。
Go
package main
import "fmt"
func sendData(ch chan int) {
fmt.Println("准备发送数据:100")
ch <- 100 // 发送数据,无缓冲通道会阻塞,直到有goroutine接收
fmt.Println("数据发送成功")
}
func main() {
// 创建无缓冲int类型通道
ch := make(chan int)
// 开启goroutine发送数据
go sendData(ch)
// 主goroutine接收数据,此时sendData的阻塞才会解除
data := <-ch
fmt.Printf("主协程接收数据:%d\n", data)
}
② 有缓冲通道 (异步通道)
缓冲区大小为 >0 的整数,比如 make(chan int, 3) 表示能存放 3 个 int 数据的通道
- 特性:发送数据时,只要缓冲区没满 ,发送方不会阻塞;只有缓冲区满了,发送方才会阻塞;接收数据时,只有缓冲区空了,接收方才会阻塞;
- 本质:实现 goroutine 之间的异步通信,可以暂存数据,不用严格同步;
- 适用场景:需要缓存数据、削峰填谷的并发场景。
Go
package main
import "fmt"
func main() {
// 创建有缓冲通道,缓冲区大小为2
ch := make(chan string, 2)
// 发送数据,缓冲区未满,不会阻塞
ch <- "hello"
ch <- "goroutine"
fmt.Println("两个数据发送成功,缓冲区已满")
// 接收一个数据,缓冲区剩余1个
fmt.Println("接收数据:", <-ch)
// 再发送一个数据,缓冲区未满,不会阻塞
ch <- "channel"
// 遍历接收所有数据
for i := 0; i < 2; i++ {
fmt.Println("接收数据:", <-ch)
}
}
执行结果:
两个数据发送成功,
缓冲区已满
接收数据: hello
接收数据: goroutine
接收数据: channel
(4)Channel 的关闭与遍历
- 关闭通道:
close(ch),只能由发送方关闭,接收方关闭会 panic; - 判断通道是否关闭:接收数据时可以用
data, ok := <-ch,ok为false表示通道已关闭且无数据; - 遍历通道:推荐用
for range ch,会自动遍历通道的所有数据,通道关闭后自动退出循环。
Go
package main
import "fmt"
func sendData(ch chan int) {
for i := 1; i <= 3; i++ {
ch <- i // 发送数据
}
close(ch) // 发送完成,关闭通道
fmt.Println("通道已关闭")
}
func main() {
ch := make(chan int, 2)
go sendData(ch)
// for range 遍历通道,自动处理关闭
for data := range ch {
fmt.Printf("接收数据:%d\n", data)
}
fmt.Println("遍历结束")
}
三、Go 并发的补充核心知识点
1. WaitGroup - 等待一组 Goroutine 执行完成
前面的例子中,我们用 time.Sleep 等待 goroutine 执行完毕,这是极不推荐的写法(睡眠时间无法精准控制)。
Go 的 sync 包提供了 WaitGroup,专门用来等待一组 goroutine 全部执行完成后,再继续执行主 goroutine,是生产环境中最常用的 goroutine 等待方式。
WaitGroup 核心方法
wg.Add(n):设置需要等待的 goroutine 数量,n是 goroutine 的个数;wg.Done():每个 goroutine 执行完成后调用,相当于wg.Add(-1),表示完成一个;wg.Wait():主 goroutine 调用,阻塞直到所有 goroutine 都调用了wg.Done()(计数归 0)。
示例
Go
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup // 声明WaitGroup
func task(name string) {
defer wg.Done() // 函数执行完自动调用Done,防止遗漏
for i := 1; i <= 2; i++ {
fmt.Printf("任务[%s]执行:%d\n", name, i)
}
}
func main() {
taskNum := 3
wg.Add(taskNum) // 需要等待3个goroutine
// 开启3个goroutine
go task("goroutine-1")
go task("goroutine-2")
go task("goroutine-3")
wg.Wait() // 阻塞等待所有任务完成
fmt.Println("所有goroutine执行完毕,主协程退出")
}
2. Go 并发 vs 传统多线程并发(核心优势总结)
很多人会问:Java 也有线程、Python 也有协程,Go 的并发到底好在哪?这里做一个清晰的对比,也是 Go 的核心竞争力:

核心结论 :Go 的并发不是 "锦上添花",而是语言层面的降维打击,它让并发编程的门槛大幅降低,性能大幅提升,不用关心线程池、锁机制,就能写出高效、安全的并发代码。
四、Go 并发的进阶内容(拓展,按需学习)
这里补充两个进阶知识点,是 Go 并发的完整体系:
1. Mutex (互斥锁) - 共享内存的兜底方案
Go 的设计思想是「通信优先于共享」,但极少数场景 下,goroutine 之间必须共享变量(比如全局计数器),此时可以用 sync.Mutex 互斥锁保证数据安全。
mutex.Lock():加锁,同一时间只有一个 goroutine 能获取锁;mutex.Unlock():解锁,释放锁资源;- 注意:尽量少用锁,锁会带来竞态、死锁风险,能用 channel 解决的问题,坚决不用锁。
2. Select - 多路复用通道
select 是 Go 的关键字,专门用来同时监听多个 channel 的读写操作,实现多路并发控制,类似 Linux 的 IO 多路复用。
- 特性:select 会随机选择一个就绪的 case 执行;如果所有 case 都阻塞,会阻塞到有 case 就绪;
- 适用场景:同时处理多个通道、设置通道超时、非阻塞读写通道等。
总结
- Go 并发的核心思想:不要通过共享内存来通信,要通过通信来共享内存;
- Go 并发的两大基石:goroutine(轻量级协程,并发执行体) + channel(通道,协程间通信方式);
- Goroutine:轻量(2KB 栈)、高效、百万级创建,runtime 调度,主协程退出则程序结束;
- Channel:类型安全、阻塞特性,分无缓冲(同步)和有缓冲(异步),是 goroutine 的安全通信方式;
- 等待 goroutine 完成:用
sync.WaitGroup,禁止用 time.Sleep; - Go 并发的核心优势:相比传统多线程,资源占用更少、调度更快、代码更简洁、数据更安全;
- 进阶兜底:必须共享变量时用
sync.Mutex,多路通道控制用select。