Go 并发编程代码面试题
#golang · #面试题 · #并发 · #goroutine · #channel
以下是 Go 并发编程高频手撕代码题,涵盖协程调度、channel 通信、并发控制等核心模式。
1. N 个协程顺序打印 1~100
题目:启动 N 个协程,按顺序打印 1~100。例如 N=4,协程 0 打印 1,5,9,...,协程 1 打印 2,6,10,...。
方案:按尾数分配 + channel 轮转
go
package main
import (
"fmt"
"sync"
)
func main() {
const (
n = 4 // 协程数
total = 100 // 打印总数
)
chs := make([]chan int, n)
for i := 0; i < n; i++ {
chs[i] = make(chan int, 1)
}
var wg sync.WaitGroup
wg.Add(n)
for i := 0; i < n; i++ {
go func(id int) {
defer wg.Done()
for num := range chs[id] {
if num > total {
// 通知下一个协程退出
next := (id + 1) % n
chs[next] <- num
return
}
fmt.Printf("goroutine %d: %d\n", id, num)
next := (id + 1) % n
chs[next] <- num + 1
}
}(i)
}
// 启动协程 0
chs[0] <- 1
wg.Wait()
}方案二:单纯 channel 令牌传递
go
func printWithToken(n int) {
chs := make([]chan struct{}, n)
for i := range chs {
chs[i] = make(chan struct{})
}
var wg sync.WaitGroup
wg.Add(n)
for i := 0; i < n; i++ {
go func(id int) {
defer wg.Done()
for j := id + 1; j <= 100; j += n {
<-chs[id] // 等待令牌
fmt.Printf("g%d: %d\n", id, j)
chs[(id+1)%n] <- struct{}{} // 传递令牌
}
if id == n-1 {
<-chs[n-1] // 最后一个接收完最后的令牌
}
}(i)
}
chs[0] <- struct{}{} // 给协程 0 发令牌
wg.Wait()
}2. 三个协程交替打印 abc
题目:启动 3 个协程,交替打印 a、b、c,共打印 N 轮(输出 "abcabcabc...")。
方案:channel 令牌传递
go
func printABC(n int) {
chA, chB, chC := make(chan struct{}), make(chan struct{}), make(chan struct{})
done := make(chan struct{})
// 协程 A:打印 a
go func() {
for i := 0; i < n; i++ {
<-chA
fmt.Print("a")
chB <- struct{}{}
}
}()
// 协程 B:打印 b
go func() {
for i := 0; i < n; i++ {
<-chB
fmt.Print("b")
chC <- struct{}{}
}
}()
// 协程 C:打印 c
go func() {
for i := 0; i < n; i++ {
<-chC
fmt.Print("c")
if i == n-1 {
close(done)
return
}
chA <- struct{}{}
}
}()
chA <- struct{}{} // 启动
<-done
fmt.Println()
}
// 输出(n=3):abcabcabc变体:交替打印 0 和 1
go
func print01(n int) {
ch0, ch1 := make(chan struct{}), make(chan struct{})
done := make(chan struct{})
go func() {
for i := 0; i < n; i++ {
<-ch0
fmt.Print("0")
ch1 <- struct{}{}
}
}()
go func() {
for i := 0; i < n; i++ {
<-ch1
fmt.Print("1")
if i == n-1 {
close(done)
return
}
ch0 <- struct{}{}
}
}()
ch0 <- struct{}{}
<-done
fmt.Println()
}
// 输出(n=5):01010101013. 限制协程数量
题目:有 10 万个任务,限制最多 10 个 goroutine 并发执行。
方案一:带缓冲 channel 做信号量
go
func workerPoolSemaphore() {
const (
totalTasks = 100000
maxWorkers = 10
)
var wg sync.WaitGroup
sem := make(chan struct{}, maxWorkers) // 信号量
for i := 0; i < totalTasks; i++ {
wg.Add(1)
sem <- struct{}{} // 获取许可
go func(taskID int) {
defer wg.Done()
defer func() { <-sem }() // 释放许可
// 执行任务
_ = taskID
time.Sleep(10 * time.Millisecond)
}(i)
}
wg.Wait()
close(sem)
}方案二:Worker Pool 模式
go
func workerPoolFixed() {
const (
totalTasks = 100000
maxWorkers = 10
)
tasks := make(chan int, totalTasks)
var wg sync.WaitGroup
// 启动固定数量的 worker
for i := 0; i < maxWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for taskID := range tasks {
_ = taskID
time.Sleep(10 * time.Millisecond)
}
}(i)
}
// 投递任务
for i := 0; i < totalTasks; i++ {
tasks <- i
}
close(tasks)
wg.Wait()
}| 方案 | 优点 | 缺点 |
|---|---|---|
| 信号量 | 简单直接 | 每个任务创建一次 goroutine |
| Worker Pool | goroutine 复用,无创建开销 | 代码稍长 |
4. 生产者消费者模型
方案一:channel 实现(最常用)
go
func producerConsumerChannel() {
const (
numProducers = 3
numConsumers = 2
numItems = 30
)
ch := make(chan int, 5) // 缓冲区
var wg sync.WaitGroup
// 生产者
for i := 0; i < numProducers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < numItems/numProducers; j++ {
item := id*100 + j
ch <- item
fmt.Printf("生产者 %d 生产: %d\n", id, item)
}
}(i)
}
// 消费者
var cg sync.WaitGroup
for i := 0; i < numConsumers; i++ {
cg.Add(1)
go func(id int) {
defer cg.Done()
for item := range ch {
fmt.Printf("消费者 %d 消费: %d\n", id, item)
time.Sleep(10 * time.Millisecond)
}
}(i)
}
wg.Wait()
close(ch) // 生产完关闭 channel
cg.Wait()
}方案二:sync.Cond 实现(多生产者多消费者)
go
type Queue struct {
mu sync.Mutex
cond *sync.Cond
items []int
capacity int
closed bool
}
func NewQueue(capacity int) *Queue {
q := &Queue{capacity: capacity}
q.cond = sync.NewCond(&q.mu)
return q
}
func (q *Queue) Produce(item int) bool {
q.mu.Lock()
defer q.mu.Unlock()
// 队列满时等待
for len(q.items) >= q.capacity && !q.closed {
q.cond.Wait()
}
if q.closed {
return false
}
q.items = append(q.items, item)
fmt.Printf("生产: %d, 队列长度: %d\n", item, len(q.items))
q.cond.Broadcast() // 通知所有等待的消费者
return true
}
func (q *Queue) Consume() (int, bool) {
q.mu.Lock()
defer q.mu.Unlock()
// 队列空时等待
for len(q.items) == 0 && !q.closed {
q.cond.Wait()
}
if q.closed && len(q.items) == 0 {
return 0, false
}
item := q.items[0]
q.items = q.items[1:]
fmt.Printf("消费: %d, 队列长度: %d\n", item, len(q.items))
q.cond.Broadcast() // 通知所有等待的生产者
return item, true
}
func (q *Queue) Close() {
q.mu.Lock()
q.closed = true
q.mu.Unlock()
q.cond.Broadcast() // 唤醒所有等待者
}Cond vs Channel 选型
| 场景 | 推荐 | 原因 |
|---|---|---|
| 简单生产者-消费者 | channel | 代码简洁,Go 原生支持 |
| 需要 Broadcast 通知 | Cond | channel 只能一对一 |
| 条件反复变化 | Cond | channel 关闭后无法复用 |
| 多消费者竞争 | channel | 天然支持,无需额外同步 |
5. 超时控制
题目:1000 个任务,每个任务最长执行 1 秒,超时自动取消。
go
func executeWithTimeout() {
const (
numTasks = 1000
timeout = 1 * time.Second
)
var wg sync.WaitGroup
results := make(chan string, numTasks)
for i := 0; i < numTasks; i++ {
wg.Add(1)
go func(taskID int) {
defer wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resultCh := make(chan string, 1)
// 实际任务逻辑
go func() {
resultCh <- doWork(taskID)
}()
select {
case result := <-resultCh:
results <- fmt.Sprintf("任务 %d 成功: %s", taskID, result)
case <-ctx.Done():
results <- fmt.Sprintf("任务 %d 超时", taskID)
}
}(i)
}
wg.Wait()
close(results)
// 统计结果
var success, timeout int
for r := range results {
if strings.Contains(r, "超时") {
timeout++
} else {
success++
}
}
fmt.Printf("成功: %d, 超时: %d\n", success, timeout)
}
// 模拟可能超时的工作
func doWork(id int) string {
// 模拟不同耗时
sleep := time.Duration(200+rand.Intn(1500)) * time.Millisecond
time.Sleep(sleep)
return fmt.Sprintf("完成(耗时%v)", sleep)
}超时控制核心模式总结
go
// 模式一:context.WithTimeout + select
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
select {
case <-resultCh:
// 正常完成
case <-ctx.Done():
// 超时处理
}
// 模式二:time.After
select {
case <-resultCh:
case <-time.After(1 * time.Second):
// 超时
}
// 模式三:带超时的重试
func retryWithTimeout(fn func() error, maxRetry int, timeout time.Duration) error {
for i := 0; i < maxRetry; i++ {
errCh := make(chan error, 1)
go func() { errCh <- fn() }()
select {
case err := <-errCh:
if err == nil { return nil }
case <-time.After(timeout):
// 超时,重试
}
}
return fmt.Errorf("重试 %d 次后仍失败", maxRetry)
}注意:
time.After在循环中使用会导致内存泄漏(每次创建新的 timer 不会被 GC),推荐在循环外使用time.NewTimer并手动 Reset。
并发编程核心模式速查
| 模式 | 实现 | 适用场景 |
|---|---|---|
| 等待完成 | sync.WaitGroup | 等待一组 goroutine 完成 |
| 限制并发 | 带缓冲 channel 信号量 | 控制并发 goroutine 数量 |
| 生产者消费者 | channel / Cond | 解耦生产和消费速率 |
| 交替执行 | channel 令牌传递 | 顺序控制多个 goroutine |
| 超时控制 | context.WithTimeout + select | 防止 goroutine 泄漏 |
| 保护共享变量 | sync.Mutex / sync.RWMutex | 多个 goroutine 读写同一数据 |
| 一次性初始化 | sync.Once | 单例、延迟加载 |
| 广播通知 | sync.Cond / close(channel) | 一对多事件通知 |
高频数据结构与算法题
10. 接雨水(Trapping Rain Water)
给定
height = [0,1,0,2,1,0,1,3,2,1,2,1],输出能接的雨水量 = 6。
go
// 双指针法 — O(n) 时间,O(1) 空间
func trap(height []int) int {
l, r := 0, len(height)-1
leftMax, rightMax := 0, 0
water := 0
for l < r {
if height[l] < height[r] {
if height[l] >= leftMax {
leftMax = height[l]
} else {
water += leftMax - height[l]
}
l++
} else {
if height[r] >= rightMax {
rightMax = height[r]
} else {
water += rightMax - height[r]
}
r--
}
}
return water
}text
关键洞察: 不是按列算"上面能接多少",而是按较小边界推进:
- 左指针的 leftMax < 右指针的 rightMax → 左指针处的水量取决于 leftMax
- 不需要关心离左指针更远的柱子(反正右边有更大的挡着)11. 岛屿数量(Number of Islands)
go
func numIslands(grid [][]byte) int {
if len(grid) == 0 { return 0 }
count := 0
for i := 0; i < len(grid); i++ {
for j := 0; j < len(grid[0]); j++ {
if grid[i][j] == '1' {
dfs(grid, i, j)
count++
}
}
}
return count
}
func dfs(grid [][]byte, i, j int) {
if i < 0 || i >= len(grid) || j < 0 || j >= len(grid[0]) || grid[i][j] != '1' {
return
}
grid[i][j] = '0' // 原地标记,省 visited 空间
dfs(grid, i+1, j)
dfs(grid, i-1, j)
dfs(grid, i, j+1)
dfs(grid, i, j-1)
}| 技巧 | 说明 |
|---|---|
| 原地标记 | 将 '1' 改为 '0' 替代 visited 矩阵,O(1) 额外空间 |
| DFS vs BFS | DFS 用栈(递归),BFS 用队列,两者时间复杂度相同 |
| 并查集 | 也可以,但面试中 DFS 最直观 |
12. 买卖股票最佳时机(Best Time to Buy and Sell Stock)
go
// 只允许一次交易
func maxProfit(prices []int) int {
if len(prices) == 0 { return 0 }
minPrice := prices[0]
maxProfit := 0
for _, p := range prices[1:] {
if p < minPrice {
minPrice = p
} else if p - minPrice > maxProfit {
maxProfit = p - minPrice
}
}
return maxProfit
}
// 允许多次交易(无冷却期)— 贪心
func maxProfitII(prices []int) int {
profit := 0
for i := 1; i < len(prices); i++ {
if prices[i] > prices[i-1] {
profit += prices[i] - prices[i-1]
}
}
return profit
}
// 最多两次交易 — DP: dp[k][i] = 第 k 次交易在第 i 天的最大收益
func maxProfitIII(prices []int) int {
buy1, sell1 := math.MinInt32, 0
buy2, sell2 := math.MinInt32, 0
for _, p := range prices {
buy1 = max(buy1, -p)
sell1 = max(sell1, buy1+p)
buy2 = max(buy2, sell1-p)
sell2 = max(sell2, buy2+p)
}
return sell2
}mermaid
stateDiagram-v2
[*] --> buy1 : 第一次买入
buy1 --> sell1 : 第一次卖出
sell1 --> buy2 : 第二次买入
buy2 --> sell2 : 第二次卖出
sell2 --> [*]
note left of buy1: buy1 = max(buy1, -price)<br/>在最低点买入
note right of sell1: sell1 = max(sell1, buy1+price)<br/>在买入后最高点卖出13. LRU Cache
go
type LRUCache struct {
cap int
cache map[int]*list.Element
list *list.List
}
type entry struct{ key, value int }
func Constructor(capacity int) LRUCache {
return LRUCache{
cap: capacity,
cache: make(map[int]*list.Element),
list: list.New(),
}
}
func (c *LRUCache) Get(key int) int {
if ele, ok := c.cache[key]; ok {
c.list.MoveToFront(ele)
return ele.Value.(*entry).value
}
return -1
}
func (c *LRUCache) Put(key, value int) {
if ele, ok := c.cache[key]; ok {
ele.Value.(*entry).value = value
c.list.MoveToFront(ele)
return
}
if c.list.Len() >= c.cap {
oldest := c.list.Back()
c.list.Remove(oldest)
delete(c.cache, oldest.Value.(*entry).key)
}
ele := c.list.PushFront(&entry{key, value})
c.cache[key] = ele
}Go 实现 LRU 最佳组合:
container/list(双向链表)+map[int]*list.Element(O(1) 查找)。
14. 二叉树层序遍历
go
func levelOrder(root *TreeNode) [][]int {
if root == nil { return nil }
var res [][]int
queue := []*TreeNode{root}
for len(queue) > 0 {
levelSize := len(queue)
var level []int
for i := 0; i < levelSize; i++ {
node := queue[0]
queue = queue[1:]
level = append(level, node.Val)
if node.Left != nil { queue = append(queue, node.Left) }
if node.Right != nil { queue = append(queue, node.Right) }
}
res = append(res, level)
}
return res
}15. 最长递增子序列(LIS)
go
// O(n²) DP
func lengthOfLIS(nums []int) int {
dp := make([]int, len(nums))
maxLen := 1
for i := range dp { dp[i] = 1 }
for i := 1; i < len(nums); i++ {
for j := 0; j < i; j++ {
if nums[i] > nums[j] {
dp[i] = max(dp[i], dp[j]+1)
}
}
maxLen = max(maxLen, dp[i])
}
return maxLen
}
// O(n log n) 贪心 + 二分
func lengthOfLIS_opt(nums []int) int {
tails := make([]int, 0) // tails[i] = 长度为 i+1 的 IS 的最小结尾值
for _, x := range nums {
idx := sort.SearchInts(tails, x)
if idx == len(tails) {
tails = append(tails, x)
} else {
tails[idx] = x
}
}
return len(tails)
}16. 合并 K 个有序链表
go
// 最小堆解法 — O(N log K)
func mergeKLists(lists []*ListNode) *ListNode {
h := &MinHeap{}
heap.Init(h)
for _, l := range lists {
if l != nil { heap.Push(h, l) }
}
dummy := &ListNode{}
cur := dummy
for h.Len() > 0 {
node := heap.Pop(h).(*ListNode)
cur.Next = node
cur = cur.Next
if node.Next != nil {
heap.Push(h, node.Next)
}
}
return dummy.Next
}
type MinHeap []*ListNode
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i].Val < h[j].Val }
func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x interface{}) { *h = append(*h, x.(*ListNode)) }
func (h *MinHeap) Pop() interface{} {
old := *h; n := len(old); x := old[n-1]; *h = old[:n-1]; return x
}算法复杂度速查 (补充高频题)
| 题目 | 最优时间 | 最优空间 | 核心技巧 |
|---|---|---|---|
| 接雨水 | O(n) | O(1) | 双指针 + 较小边界推进 |
| 岛屿数量 | O(mn) | O(1) | DFS 原地标记 |
| 买卖股票 I | O(n) | O(1) | 维护最低价 |
| 买卖股票 III | O(n) | O(1) | 4 状态 DP |
| LRU Cache | O(1) | O(n) | 哈希 + 双向链表 |
| 层序遍历 | O(n) | O(n) | BFS + 层计数 |
| 最长递增子序列 | O(n log n) | O(n) | 贪心 + 二分 |
| 合并 K 个链表 | O(N log K) | O(K) | 最小堆 |
登录后即可发表评论 👇