第八天:PHP开发者快速掌握Go并发编程 | Goroutine与Channel从入门到实战
作为PHP开发者,你知道实现并发需要依赖pcntl扩展创建多进程、pthreads扩展实现多线程,或借助Swoole/Workerman框架——步骤繁琐且资源消耗高。而Go内置原生并发支持:Goroutine(协程)是轻量级线程(占用KB级内存),Channel(通道)实现Goroutine间安全通信,sync.WaitGroup实现并发同步,无需第三方扩展即可高效实现并发。今天我们以PHP多进程/线程为参照,快速掌握Go并发编程的核心逻辑。
一、Goroutine(协程):对标PHP多进程/多线程
Goroutine是Go的轻量级执行单元(由Go运行时管理,非操作系统线程),创建成本极低(初始栈仅2KB),单机可轻松创建数万个Goroutine,对标PHP的多进程/线程,但性能和易用性远超后者。
1. Goroutine基础(创建与运行)
创建Goroutine只需在函数调用前加go关键字,对标PHP的pcntl_fork()(多进程)或new Thread()(多线程)。
| 特性 | Go Goroutine | PHP多进程/线程 |
|---|---|---|
| 创建方式 | go 函数名(参数) |
多进程:pcntl_fork();多线程:new Thread() |
| 资源占用 | KB级栈内存,轻量级 | MB级内存(进程)/ 几十KB(线程),重量级 |
| 调度 | Go运行时调度(M:N映射到OS线程) | 操作系统调度 |
基础示例:Go Goroutine vs PHP多进程
// Go:创建Goroutine(并发执行任务)
func task(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("Goroutine %s:执行第%d次\n", name, i+1)
time.Sleep(100 * time.Millisecond) // 模拟耗时操作
}
}
func main() {
// 启动2个Goroutine(并发执行)
go task("A")
go task("B")
// 主线程等待(否则主线程退出,Goroutine也会终止)
time.Sleep(500 * time.Millisecond)
fmt.Println("主线程执行完毕")
}
PHP对应写法(多进程):
// PHP:创建多进程(pcntl_fork)
function task($name) {
for ($i = 0; $i < 3; $i++) {
echo "进程 {$name}:执行第" . ($i+1) . "次\n";
usleep(100000); // 模拟耗时操作(100ms)
}
}
// 启动2个进程
$pid1 = pcntl_fork();
if ($pid1 == 0) {
// 子进程1
task("A");
exit(0);
}
$pid2 = pcntl_fork();
if ($pid2 == 0) {
// 子进程2
task("B");
exit(0);
}
// 主进程等待子进程结束
pcntl_waitpid($pid1, $status);
pcntl_waitpid($pid2, $status);
echo "主进程执行完毕\n";
⚠️ 核心差异:
1. Go主线程退出后,所有Goroutine会立即终止(需手动等待);PHP主进程退出后,子进程可继续运行(需手动管理);
2. Goroutine创建无需额外扩展,PHP多进程需开启pcntl扩展、多线程需开启pthreads扩展;
3. Goroutine间共享进程内存(需同步),PHP多进程间内存隔离(需IPC通信)。
2. sync.WaitGroup:Goroutine同步(对标PHP pcntl_waitpid)
手动time.Sleep等待Goroutine不灵活,Go提供sync.WaitGroup实现“等待所有Goroutine完成”,对标PHP的pcntl_waitpid批量等待子进程。
// Go:WaitGroup同步Goroutine
var wg sync.WaitGroup
func taskWithWait(name string) {
defer wg.Done() // Goroutine完成后,计数-1(必须放在开头)
for i := 0; i < 3; i++ {
fmt.Printf("Goroutine %s:执行第%d次\n", name, i+1)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// 设置等待计数(2个Goroutine)
wg.Add(2)
go taskWithWait("A")
go taskWithWait("B")
// 阻塞等待所有Goroutine完成(计数归0)
wg.Wait()
fmt.Println("所有Goroutine执行完毕")
}
PHP对应写法(批量等待子进程):
// PHP:批量等待子进程
function task($name) {
for ($i = 0; $i < 3; $i++) {
echo "进程 {$name}:执行第" . ($i+1) . "次\n";
usleep(100000);
}
}
$pids = [];
// 启动2个子进程
for ($i = 0; $i < 2; $i++) {
$pid = pcntl_fork();
if ($pid == 0) {
task(chr(65 + $i)); // A/B
exit(0);
}
$pids[] = $pid;
}
// 批量等待所有子进程
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
}
echo "所有子进程执行完毕\n";
二、Channel(通道):Goroutine间通信(对标PHP IPC)
Goroutine间共享内存易引发竞态条件,Go推荐用Channel(类型化通道)实现“通信顺序进程(CSP)”——通过通道安全传递数据,对标PHP的进程间通信(IPC)如管道、消息队列、共享内存。
1. Channel基础(声明与使用)
Channel需指定数据类型(如chan int),支持发送(ch<-值)、接收(<-ch)、关闭(close(ch))操作,分为“无缓冲通道”和“有缓冲通道”。
// Go:无缓冲Channel(同步通信)
func sendData(ch chan string) {
// 发送数据到通道(阻塞,直到有接收方)
ch <- "Hello Goroutine"
close(ch) // 关闭通道(可选,告知接收方无数据)
}
func main() {
// 声明Channel(字符串类型)
ch := make(chan string)
// 启动Goroutine发送数据
go sendData(ch)
// 从通道接收数据(阻塞,直到有数据)
data := <-ch
fmt.Println("接收数据:", data) // 输出:接收数据:Hello Goroutine
// 检查通道是否关闭
data, ok := <-ch
if !ok {
fmt.Println("通道已关闭") // 输出此内容
}
}
PHP对应写法(管道通信):
// PHP:管道实现进程间通信
// 创建管道
$pipe = fopen("php://temp", "r+");
// 子进程发送数据
$pid = pcntl_fork();
if ($pid == 0) {
fwrite($pipe, "Hello Process");
fclose($pipe);
exit(0);
}
// 主进程接收数据
pcntl_waitpid($pid, $status);
rewind($pipe);
$data = fread($pipe, 1024);
echo "接收数据:{$data}\n"; // 输出:接收数据:Hello Process
fclose($pipe);
2. 有缓冲Channel(异步通信)
有缓冲Channel指定容量(如make(chan int, 3)),发送数据时仅当缓冲区满才阻塞,对标PHP的带缓冲消息队列。
// Go:有缓冲Channel
func main() {
// 声明有缓冲Channel(容量2)
ch := make(chan int, 2)
// 发送数据(缓冲区未满,不阻塞)
ch <- 10
ch <- 20
fmt.Println("缓冲区长度:", len(ch)) // 输出2
// 接收数据
fmt.Println(<-ch) // 输出10
fmt.Println(<-ch) // 输出20
close(ch)
}
3. 实战:Channel实现Goroutine任务分发
// Go:Channel分发任务(并发处理任务)
func worker(id int, tasks <-chan int, results chan<- int) {
defer wg.Done()
for task := range tasks { // 遍历通道,直到关闭
fmt.Printf("工作协程%d:处理任务%d\n", id, task)
results <- task * 2 // 处理结果发送到结果通道
time.Sleep(100 * time.Millisecond)
}
}
var wg sync.WaitGroup
func main() {
// 任务通道和结果通道
tasks := make(chan int, 5)
results := make(chan int, 5)
// 启动3个工作协程
wg.Add(3)
for i := 1; i <= 3; i++ {
go worker(i, tasks, results)
}
// 发送5个任务
for i := 1; i <= 5; i++ {
tasks <- i
}
close(tasks) // 关闭任务通道(告知协程无新任务)
// 等待所有协程完成
wg.Wait()
close(results) // 关闭结果通道
// 输出结果
fmt.Println("处理结果:")
for res := range results {
fmt.Println(res)
}
}
💡 核心知识点:
1. Channel是“类型安全”的,只能传递指定类型数据;
2. 无缓冲Channel是“同步通信”(发送/接收阻塞),有缓冲Channel是“异步通信”;
3. 用for range遍历Channel时,需关闭Channel否则会阻塞;
4. Go的select可监听多个Channel,对标PHP的stream_select。
三、实战案例:PHP vs Go 并发处理批量任务
需求:并发处理10个计算任务(计算数字平方),收集所有结果并输出
Go写法(Goroutine+Channel+WaitGroup)
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
// 计算平方的工作协程
func squareWorker(id int, num int, resChan chan<- int) {
defer wg.Done()
time.Sleep(50 * time.Millisecond) // 模拟耗时
res := num * num
fmt.Printf("协程%d:计算%d的平方=%d\n", id, num, res)
resChan <- res
}
func main() {
nums := []int{1,2,3,4,5,6,7,8,9,10}
resChan := make(chan int, len(nums))
// 启动协程处理每个任务
for i, num := range nums {
wg.Add(1)
go squareWorker(i+1, num, resChan)
}
// 等待所有协程完成,关闭结果通道
go func() {
wg.Wait()
close(resChan)
}()
// 收集结果
total := 0
for res := range resChan {
total += res
}
fmt.Printf("所有任务完成,结果总和:%d\n", total) // 输出385
}
PHP写法(多进程处理)
// PHP:多进程处理批量任务
function squareWorker($id, $num, $pipe) {
usleep(50000); // 模拟耗时
$res = $num * $num;
echo "进程{$id}:计算{$num}的平方={$res}\n";
fwrite($pipe, $res . "\n");
exit(0);
}
// 创建管道收集结果
$pipe = fopen("php://temp", "r+");
$pids = [];
$nums = [1,2,3,4,5,6,7,8,9,10];
// 启动子进程处理任务
foreach ($nums as $i => $num) {
$pid = pcntl_fork();
if ($pid == 0) {
squareWorker($i+1, $num, $pipe);
}
$pids[] = $pid;
}
// 等待所有子进程
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
}
// 收集并计算总和
rewind($pipe);
$total = 0;
while (($res = fgets($pipe)) !== false) {
$total += intval(trim($res));
}
fclose($pipe);
echo "所有任务完成,结果总和:{$total}\n"; // 输出385
五、今日小结
今天我们以PHP多进程/线程为参照,掌握了Go并发编程的核心:
- Goroutine是轻量级协程,
go关键字快速创建,对标PHP多进程/线程但更轻量、高效; sync.WaitGroup实现Goroutine同步,对标PHP的pcntl_waitpid批量等待子进程;- Channel是Goroutine间安全通信的核心,分为无缓冲(同步)和有缓冲(异步),对标PHP的IPC通信;
- Go并发遵循“不要通过共享内存通信,要通过通信共享内存”,避免竞态条件;
- Go原生支持并发,无需第三方扩展,开发效率远高于PHP的多进程/线程。
明天我们将学习Go项目实战(模块化、依赖管理、简单Web服务),对比PHP的Composer、Web开发,详解如何从0搭建一个Go基础Web应用,完成从语法到实战的落地。
