Go语言并发处理

并发是 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 核心特点
  1. 通道是类型安全的:声明通道时必须指定传递的数据类型,只能传递对应类型的数据;
  2. 通道是阻塞特性 的(核心):
    • 向一个通道发送数据 ch <- data 时,如果通道满了,发送方 goroutine 会被阻塞,直到有 goroutine 从通道取走数据;
    • 从一个通道接收数据 data := <-ch 时,如果通道空了,接收方 goroutine 会被阻塞,直到有 goroutine 向通道发送数据;
    • 这种阻塞是 runtime 实现的,无额外开销,是 goroutine 协作的核心原理;
  3. 通道可以手动关闭,关闭后无法再发送数据,只能接收剩余数据;
  4. 通道天然解决「数据竞争」: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 := <-chokfalse 表示通道已关闭且无数据;
  • 遍历通道:推荐用 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 核心方法
  1. wg.Add(n):设置需要等待的 goroutine 数量,n 是 goroutine 的个数;
  2. wg.Done():每个 goroutine 执行完成后调用,相当于 wg.Add(-1),表示完成一个;
  3. 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 就绪;
  • 适用场景:同时处理多个通道、设置通道超时、非阻塞读写通道等。

总结

  1. Go 并发的核心思想:不要通过共享内存来通信,要通过通信来共享内存;
  2. Go 并发的两大基石:goroutine(轻量级协程,并发执行体) + channel(通道,协程间通信方式);
  3. Goroutine:轻量(2KB 栈)、高效、百万级创建,runtime 调度,主协程退出则程序结束;
  4. Channel:类型安全、阻塞特性,分无缓冲(同步)和有缓冲(异步),是 goroutine 的安全通信方式;
  5. 等待 goroutine 完成:用 sync.WaitGroup禁止用 time.Sleep
  6. Go 并发的核心优势:相比传统多线程,资源占用更少、调度更快、代码更简洁、数据更安全;
  7. 进阶兜底:必须共享变量时用 sync.Mutex,多路通道控制用 select
相关推荐
小旭95272 小时前
【Java 基础】泛型<T>
java·开发语言·intellij-idea
Tony Bai2 小时前
AI 时代,Go 语言会“失宠”还是“封神”?—— GopherCon 2025 圆桌深度复盘
开发语言·人工智能·后端·golang
寻星探路2 小时前
【全景指南】JavaEE 深度解析:从 Jakarta EE 演进、B/S 架构到 SSM 框架群实战
java·开发语言·人工智能·spring boot·ai·架构·java-ee
tc&2 小时前
新虚拟机安装 Go 环境:问题总结与解决方案
开发语言·后端·golang
小镇学者2 小时前
【python】python虚拟环境与pycharmIDE配置
开发语言·python
哈哈不让取名字11 小时前
基于C++的爬虫框架
开发语言·c++·算法
花间相见11 小时前
【JAVA开发】—— Nginx服务器
java·开发语言·nginx
扶苏-su11 小时前
Java---Properties 类
java·开发语言
橙子家12 小时前
WebAPI 项目通过 CI/CD 自动化部署到 Linux 服务器(docker-compose)
后端