Golang协程并发编程面试题实战:Channel与WaitGroup协同技巧
本文整理Golang协程(goroutine)高频面试题,涵盖交替打印序列、N个协程协作以及多文件并发合并等典型场景。重点演示如何利用channel实现无竞争通信,结合sync.WaitGroup确保任务完成信号的正确传递,并提供优雅退出的最佳实践方案。
结果或目标:交替打印奇数和偶数
在并发编程中,控制多个协程的执行顺序是基础且核心的考点。以下两种方法利用不同特性的 Channel 实现奇偶数的有序输出。
第1步:使用无缓冲 channel 实现严格交替
目的:利用无缓冲 Channel 发送与接收必须同时就绪的特性,强制两个 Goroutine 同步。
- 入口:定义一个无缓冲的 Channel
ch := make(chan struct{})。 - 动作:
- 协程 A 负责打印奇数:发送信号后阻塞,等待对方消费。
- 协程 B 负责打印偶数:接收信号后阻塞,打印完成后发信号唤醒对方。
- 关键参数:需使用
sync.WaitGroup等待两个子协程结束。 - 完成状态:控制台按顺序打印 1-100。
package main
import (
"fmt"
"sync"
)
func PrintOddAndEven1() {
// 注意:指针传递以便在主函数外保持引用有效,实际应用中也可直接初始化
var wg sync.WaitGroup
wg.Add(2)
ch := make(chan struct{}) // 无缓冲channel
defer close(ch)
maxVal := 100
go func() {
defer wg.Done()
for i := 1; i <= maxVal; i++ {
ch <- struct{}{}
if i%2 == 1 { // 奇数
fmt.Printf("the odd is %d\n", i)
}
}
}()
go func() {
defer wg.Done()
for i := 1; i <= maxVal; i++ {
<-ch // 从管道当中读取一个数据
if i%2 == 0 { // 偶数
fmt.Printf("the even is %d\n", i)
}
}
}()
wg.Wait()
}
func main() {
PrintOddAndEven1()
fmt.Println("over")
}第2步:使用有缓冲 channel 优化起始信号
目的:通过预写信号到缓冲区,精确控制首个执行的协程,并通过双向通知维持节奏。
- 准备条件:创建两个容量为 1 的 Channel:
oddChan和eveChan。 - 启动策略:预先向
oddChan写入一个空结构体,确保第一个轮到的是奇数协程。 - 动作逻辑:
- 奇数协程读取
oddChan,打印后向eveChan发送信号。 - 偶数协程读取
eveChan,打印后向oddChan发送信号。
- 奇数协程读取
func PrintOddAndEven2() {
var wg sync.WaitGroup
wg.Add(2)
oddChan := make(chan struct{}, 1)
eveChan := make(chan struct{}, 1)
defer close(oddChan)
defer close(eveChan)
// 预先写入,确保先由奇数协程获取控制权
oddChan <- struct{}{}
maxVal := 20
go func() { // 奇数协程
defer wg.Done()
for i := 1; i <= maxVal; i += 2 {
<-oddChan
fmt.Printf("the odd print %d\n", i)
eveChan <- struct{}{} // 通知偶数协程
}
}()
go func() {
defer wg.Done()
for i := 2; i <= maxVal; i += 2 {
<-eveChan
fmt.Printf("the even print %d\n", i)
oddChan <- struct{}{} // 通知奇数协程
}
}()
wg.Wait()
}真实操作顺序:N个协程打印1到maxVal
当协程数量不固定时,需要扩展通信模型。本例演示如何让 N 个协程轮流打印 1 到 MaxVal 之间的数字。
第1步:构建环形通信链
设计思路:将多个 Channel 组织成数组或切片,形成闭环。当前协程执行完后,激活下一个索引对应的 Channel。
- 入口:定义 Channel 切片
chanArr,容量设为 1。 - 参数配置:设置协程数
N=3,总数值maxVal=100。 - 启动流程:仅向
chanArr[0]预写入信号,启动所有协程。
第2步:内部循环与轮换逻辑
动作:每个协程持续监听属于自己的 Channel,读取信号后判断是否越界。
- 计数管理:使用外部共享变量
num追踪下一个应响应的协程索引。 - 边界处理:若
num达到N-1,下一轮重置为 0。 - 唤醒:执行完毕后,向
chanArr[num]发送信号。
第3步:资源清理防泄漏
风险提醒:若不及时关闭 Channel,未执行的协程将永久阻塞导致资源泄漏。
- 退出判定:当某个协程发现值已超过
maxVal,向exitChan发送信号。 - 主协程动作:等待
exitChan接收信号后,遍历关闭所有chanArr[i]。
package main
import (
"fmt"
"sync"
)
func main() {
maxVal := 100
res := 0
N := 3
exitChan := make(chan struct{})
chanArr := make([]chan struct{}, N)
for i := 0; i < N; i++ {
chanArr[i] = make(chan struct{}, 1)
}
num := 0
chanArr[0] <- struct{}{}
for i := 0; i < N; i++ {
go func(i int) {
for {
<-chanArr[i]
if res >= maxVal {
exitChan <- struct{}{}
break
}
fmt.Printf("第%d个协程打印%d\n", i, res)
if num == N-1 {
num = 0
} else {
num++
}
res++
chanArr[num] <- struct{}{}
}
}(i)
}
<-exitChan
// 关闭所有管道,防止协程泄漏
for i := 0; i < N; i++ {
close(chanArr[i])
}
}替代方案与变体:字符数字交替与字符串分割
此类问题本质相同,核心在于“互斥锁”或“信号量”思想的具象化应用。
第1步:双通道交替打印字母与数字
场景:输出格式如 12AB34CD...。即每输出两个数字,输出两个字母。
- 实现方式:可使用有缓冲 Channel 预置信号,或使用无缓冲 Channel 进行一对一握手。
- 无缓冲写法要点:数字协程发送信号 → 打印 → 暂停;字母协程接收信号 → 打印 → 回复信号。
func main() {
// 使用无缓冲 Channel 示例
numChan := make(chan struct{})
defer close(numChan)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for num := 1; num <= 26; num++ {
numChan <- struct{}{}
fmt.Printf("%d", num)
}
}()
go func() {
defer wg.Done()
for ch := 'A'; ch <= 'Z'; ch++ {
<-numChan
fmt.Printf("%s", string(ch))
}
}()
wg.Wait()
}第2步:字符串的协程拆分打印
场景:给定字符串 str,两个协程按索引奇偶性分别打印字符,最终还原完整字符串。
- 关键点:两个协程都遍历整个字符串长度,但通过 Channel 控制谁真正执行 Print 操作。
- 同步机制:类似于奇偶数打印,通过 Channel 交换执行权。
func main() {
chChan := make(chan struct{})
defer close(chChan)
var wg sync.WaitGroup
wg.Add(2)
str := "hello world"
N := len(str)
go func() {
defer wg.Done()
for i := 0; i < N; i++ {
chChan <- struct{}{}
if i%2 == 0 {
fmt.Print(string(str[i]))
}
}
}()
go func() {
defer wg.Done()
for i := 0; i < N; i++ {
<-chChan
if i%2 == 1 {
fmt.Print(string(str[i]))
}
}
}()
wg.Wait()
}复杂并发模型:三个协程循环打印ABC
这是 N 个协程问题的特殊化案例,强调严格的顺序依赖 A→B→C→A。
第1步:构建三角形通信回路
准备条件:创建三个缓冲 Channel:Achan, Bchan, Cchan。
- 初始状态:向
Achan发送信号,指定 A 最先执行。 - 逻辑流向:
- Goroutine A 读取
Achan,打印 A,向Bchan发送信号。 - Goroutine B 读取
Bchan,打印 B,向Cchan发送信号。 - Goroutine C 读取
Cchan,打印 C,向Achan发送信号。
- Goroutine A 读取
第2步:安全退出与防泄漏处理
风险提醒:直接关闭 Channel 会立即解除其他协程的阻塞,可能导致恐慌或逻辑错乱。
- 退出信号:引入专用
exitChan。当计数器超过限制时,协程向exitChan发送信号后break退出循环。 - 资源释放:主协程收到退出信号后,再依次关闭三个业务 Channel,确保所有阻塞的协程能接收到
closed信号而安全退出。
package main
import (
"fmt"
"sync"
)
func main() {
Achan := make(chan struct{}, 1)
Bchan := make(chan struct{}, 1)
Cchan := make(chan struct{}, 1)
defer close(Achan)
defer close(Bchan)
defer close(Cchan)
Achan <- struct{}{}
counter := 0
maxVal := 10
exitChan := make(chan struct{})
go func() {
for {
<-Achan
if counter >= maxVal { exitChan <- struct{}{}; break }
fmt.Printf("A ")
counter++
Bchan <- struct{}{}
}
}()
go func() {
for {
<-Bchan
if counter >= maxVal { exitChan <- struct{}{}; break }
fmt.Printf("B ")
counter++
Cchan <- struct{}{}
}
}()
go func() {
for {
<-Cchan
if counter >= maxVal { exitChan <- struct{}{}; break }
fmt.Printf("C ")
counter++
Achan <- struct{}{}
}
}()
<-exitChan
}实战应用:并发合并多个文件
将文件 I/O 操作与协程结合,利用 Channel 作为生产者-消费者模式的缓冲池,实现高效并发。
第1步:定义全局流通道与读写分离
架构:
- Producer (Reader):多个 Goroutine 并行读取不同文件,将内容写入
fileChan(带缓冲)。 - Consumer (Writer):单个 Goroutine 从
fileChan拉取数据进行写入,确保文件写入顺序可控或去重。
第2步:优雅退出的协调机制
操作步骤:
- 使用
sync.WaitGroup跟踪所有 Reader 的状态。 - 所有 Reader 结束后,主协程关闭
fileChan。 - Writer 通过
range fileChan自动感知管道关闭,逐行写完剩余数据后关闭自身通道writeFish。 - 主协程等待
writeFish信号,确保合并完全结束。
package main
import (
"bufio"
"fmt"
"io"
"os"
"strconv"
"sync"
)
var fileChan = make(chan string, 10000)
var writeFinish = make(chan struct{})
var wg sync.WaitGroup
func readFile(fileName string) {
fin, err := os.Open(fileName)
if err != nil {
fmt.Println(err.Error())
return
}
defer fin.Close()
defer wg.Done()
reader := bufio.NewReader(fin)
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF && len(line) > 0 {
fileChan <- line
}
break
}
if line != "\r\n" {
fileChan <- line
}
}
}
func writeFile(fileName string) {
fout, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
fmt.Println(err)
return
}
defer fout.Close()
defer func() { close(writeFinish) }()
writer := bufio.NewWriter(fout)
for line := range fileChan {
if line != "\r\n" {
writer.WriteString(line)
}
}
writer.Flush()
}
func main() {
wg.Add(3)
for i := 1; i <= 3; i++ {
fileName := "Dir/" + strconv.Itoa(i)
go readFile(fileName)
}
go writeFile("Dir/merge")
wg.Wait()
close(fileChan)
<-writeFinish
}流水线模式:Channel练习
展示经典的 Pipeline 模式:生成数据 → 处理数据 → 消费数据。适用于数据处理管线。
- 步骤:
f1协程生成数字并推入ch1,结束后关闭ch1。f2协程从ch1取值计算平方,推入ch2,处理完关闭ch2。main协程遍历ch2打印结果,配合WaitGroup等待子协程。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(ch1 chan int) {
defer wg.Done()
for i := 0; i < 50; i++ {
ch1 <- i
}
close(ch1)
}
func f2(ch2 chan int, ch1 chan int) {
defer wg.Done()
defer close(ch2)
for x := range ch1 {
ch2 <- x * x
}
}
func main() {
wg.Add(2)
a := make(chan int, 50)
b := make(chan int, 50)
go f1(a)
go f2(b, a)
wg.Wait()
for x := range b {
fmt.Println(x)
}
}以上就是Golang协程常见面试题详细内容的,更多关于Golang并发编程的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













