Golang协程并发编程面试题实战:Channel与WaitGroup协同技巧

竹里
竹里
发布时间:2026-10-13 | 2人浏览过
原创

本文整理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 发送信号。

第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并发编程的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表