Skip to content

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):0101010101

3. 限制协程数量 ​

题目:有 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 Poolgoroutine 复用,无创建开销代码稍长

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 通知Condchannel 只能一对一
条件反复变化Condchannel 关闭后无法复用
多消费者竞争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 BFSDFS 用栈(递归),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 原地标记
买卖股票 IO(n)O(1)维护最低价
买卖股票 IIIO(n)O(1)4 状态 DP
LRU CacheO(1)O(n)哈希 + 双向链表
层序遍历O(n)O(n)BFS + 层计数
最长递增子序列O(n log n)O(n)贪心 + 二分
合并 K 个链表O(N log K)O(K)最小堆

参考 ​

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记