并发编程是构建高性能服务必不可少的技能,而Go语言在这方面的设计尤其出色。这套入门教程会带你过一遍Debian系统下用Golang写并发程序的完整流程,包括环境搭建、核心并发单位、同步机制,以及几种很实用的并发模式。

1. 准备工作:安装Golang
在Debian上装Golang,最省心的方式就是通过包管理器直接搞定,命令序列非常简单:
# 更新软件包索引
sudo apt update
# 安装Golang(默认会拉取最新稳定版)
sudo apt install golang-go
# 验证安装(输出版本号就说明装好了)
go version
就这么三步,Golang环境就准备妥当了。
2. 基础概念:Goroutines与Channels
2.1 Goroutines(轻量级线程)
Goroutines是Go并发模型的灵魂所在,用起来非常直接——只要在函数调用前面加一个go关键字,它就会在一个独立的轻量级线程里跑起来,开发者完全不用操心线程的创建和销毁。来看个直观的例子:
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Printf("Number: %d\n", i)
time.Sleep(1 * time.Second) // 模拟耗时操作
}
}
func main() {
go printNumbers() // 启动goroutine
// 主goroutine继续执行
for i := 1; i <= 5; i++ {
fmt.Printf("Main: %d\n", i)
time.Sleep(1 * time.Second)
}
}
运行这段代码,主goroutine和printNumbers goroutine会交替输出内容,你看到的数字打印顺序是穿插的,这就是并发执行的效果。
2.2 Channels(协程间通信)
Channel的设计理念很优雅——不用共享内存来通信,而是通过通信来共享内存。它天然解决了竞态条件的问题。看看下面的例子:
package main
import "fmt"
func worker(ch chan<- int) {
for i := 1; i <= 5; i++ {
ch <- i // 发送数据到channel
}
close(ch) // 关闭channel(通知接收方没有更多数据了)
}
func main() {
ch := make(chan int) // 创建一个无缓冲channel
go worker(ch) // 启动worker goroutine
// 从channel接收数据(这里会阻塞,直到有数据进来)
for num := range ch {
fmt.Printf("Received: %d\n", num)
}
}
几个要点记住就好:ch := make(chan int)是创建无缓冲channel;close(ch)用于关闭,这样接收方才知道数据发完了;for num := range ch会一直循环接收,直到channel被关闭。
3. 同步工具:sync包
3.1 WaitGroup(等待一组goroutines完成)
sync.WaitGroup像是一个计数器,记录着还有多少个goroutine没跑完。主goroutine调用Wait(),等计数器归零了再继续往下走,避免主程序提前退出。用法非常直白:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 函数结束时告诉WaitGroup:我干完了
fmt.Printf("Worker %d started\n", id)
time.Sleep(2 * time.Second)
fmt.Printf("Worker %d finished\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 计数器加1
go worker(i, &wg) // 启动goroutine,注意传指针
}
wg.Wait() // 阻塞,直到所有worker都Done
fmt.Println("All workers finished")
}
核心逻辑就是:Add增加计数,Done减少计数,Wait等计数归零。三个函数配合好,goroutine的同步就稳了。
3.2 Mutex(互斥锁,解决竞态条件)
当多个goroutine同时修改同一个变量,就会出现数据争用——谁也说不准最终结果是什么。sync.Mutex就是来解决这个问题的,它保证同一时刻只有一个goroutine能进入临界区:
package main
import (
"fmt"
"sync"
)
var (
counter int
mutex sync.Mutex // 定义互斥锁
)
func increment() {
mutex.Lock() // 加锁,如果被锁住了就阻塞等待
counter++ // 安全修改共享变量
mutex.Unlock() // 解锁
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Printf("Final counter: %d\n", counter) // 输出一定是1000
}
没有Mutex的话,1000个goroutine并发做counter++,跑出来的结果大概率小于1000。用Lock和Unlock把修改变量的操作包起来,数据一致性就有保障了。
4. 常见并发模式
4.1 Worker Pool(限制并发数量)
任务特别多的时候,一股脑启动成千上万个goroutine容易把系统资源吃光。Worker Pool模式就是固定开几个worker,通过channel来分发任务,既控制了并发上限,又不耽误吞吐量:
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}
func main() {
const numJobs = 5
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// 先拉起3个worker等着
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// 往jobs channel里塞5个任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // 关掉jobs,通知worker没有新任务了
wg.Wait() // 等所有worker干完活
close(results) // 关掉results
// 收集并打印结果
for result := range results {
fmt.Printf("Result: %d\n", result)
}
}
这里numWorkers控制了并发数量,jobs channel负责分发任务,results channel收集处理结果,结构清晰、逻辑明了。
4.2 Pipeline(流水线模式)
Pipeline模式把数据处理流程拆成多个阶段,每个阶段由一个独立的goroutine负责,前一个阶段的输出作为下一个阶段的输入。这样每个组件都可以独立开发、测试、复用:
package main
import "fmt"
// generate生成0~9的整数
func generate() <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
return ch
}
// square计算输入整数的平方
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for i := range in {
out <- i * i
}
close(out)
}()
return out
}
func main() {
// 构建pipeline:generate -> square
for num := range square(generate()) {
fmt.Println(num) // 输出0,1,4,9,...,81
}
}
这种模式的好处是每个函数只做一件事,组合起来就能完成复杂的处理流程。可读性和可维护性都非常好。
5. 注意事项
- 避免竞态条件:凡是多个goroutine共享的资源,一定要用
sync.Mutex或sync.RWMutex保护起来。 - 防止死锁:发送和接收必须成对出现,channel的读写要匹配好,否则goroutines会互相卡住。
- 合理使用缓冲channel:无缓冲channel(
make(chan T))天然是同步机制,发送方和接收方必须同时就绪。缓冲channel(make(chan T, size))给了队列缓冲空间,可以平滑吞吐量,但缓冲大小要根据实际场景来设定,不是越大越好。
这套入门教程把Debian下Golang并发编程的核心知识点走了一遍。要想进一步深入,可以去看官方文档中select语句、context包这些高级特性,能让并发程序更加健壮和灵活。
