Files
cs-note/hzh/GolangStar/Go面试题库/代码面试题.md
T

10 KiB
Raw Blame History

tags, create time
tags create time
go
golang
interview
code-questions
2026-06-07 14:30

编程算法面试题 💻

概述

本文件涵盖 Go 并发编程场景下的 9 道代码实现题,涉及协程同步、通道协作、并发控制等实战场景。这些题目在面试中通常要求"现场手写代码",考察对 Channel、Mutex、WaitGroup 的综合运用能力。

[!tip] 💡 使用建议 每道题先自己思考解题思路,再对照答案学习。重点理解"为什么用这种方式"而非死记代码。

关联笔记

正文

Q1:100 个协程顺序打印 1-1000 🟡中等

[!question] ❓ 思考一下 100 个协程同时运行,如何让它们按 1-1000 的顺序依次打印?你会用什么机制来协调它们的执行顺序?

参考答案

核心思路:用 map 为每个协程分配独立的 channel,主循环按序发送信号到对应协程的 channel,通过阻塞保证顺序。

func main() {
    s := make(chan struct{})                    // 控制打印顺序的信号
    m := make(map[int]chan int, 100)            // 每个协程的入口 channel

    for i := 1; i <= 100; i++ {
        m[i] = make(chan int)
        go func(id int) {
            for {
                num := <-m[id]                  // 等待属于自己的数字
                fmt.Println(num)
                s <- struct{}{}                 // 打印完成,通知主循环
            }
        }(i)
    }

    for i := 1; i <= 1000; i++ {
        id := i % 100
        if id == 0 { id = 100 }
        m[id] <- i                              // 发送给对应协程
        <-s                                     // 阻塞等待该协程打印完毕
    }
}

[!note] 📝 解析思路

  • map + channel 组合:每个协程独占一个 channel,主循环精确投递
  • 信号量 s:每次只发一个数,收到打印完成的信号才发下一个,保证严格顺序
  • 面试官可能会追问:"如果不用 map 怎么做?"→ 可以用 100 个独立声明的 channel 或用 sync.Mutex 保护共享输出

Q2:三个 goroutine 交替打印 abc 10 次 🟡中等

参考答案

核心思路:三个 channel 串联成环形,a -> b -> c -> a。

func main() {
    ch1 := make(chan struct{}) // a → b
    ch2 := make(chan struct{}) // b → c
    ch3 := make(chan struct{}) // c → a
    var wg sync.WaitGroup
    wg.Add(3)

    // 打印 a
    go func() {
        defer wg.Done()
        for i := 0; i < 10; i++ {
            <-ch1
            fmt.Print("a")
            ch2 <- struct{}{}
        }
        <-ch1 // 消费最后一次信号
    }()

    // 打印 b
    go func() {
        defer wg.Done()
        for i := 0; i < 10; i++ {
            <-ch2
            fmt.Print("b")
            ch3 <- struct{}{}
        }
    }()

    // 打印 c
    go func() {
        defer wg.Done()
        for i := 0; i < 10; i++ {
            <-ch3
            fmt.Print("c")
            ch1 <- struct{}{}
        }
    }()

    ch1 <- struct{}{} // 启动信号
    wg.Wait()
    close(ch1)
    close(ch2)
    close(ch3)
    fmt.Println("end")
}

[!warning] ⚠️ 高频陷阱 第 10 次打印 c 后会给 ch1 发信号,但 a 的循环已经结束了。需要额外消费一次 ch1 防止阻塞。

[!tip] 💡 面试技巧 回答时可以画图展示三个 channel 的环形关系,然后说明"每次只有收到信号的 goroutine 才能执行,执行完给下一个发信号"。


Q3:不超过 10 个 goroutine 不重复打印 slice 中的 100 个元素 🟡中等

参考答案

方案一:有缓冲 channel 控制并发度(无序)

ch := make(chan struct{}, 10) // 容量 10 = 最大并发数
for i := 0; i < 100; i++ {
    wg.Add(1)
    ch <- struct{}{}           // 写满 10 个后阻塞
    go func(idx int) {
        defer wg.Done()
        fmt.Printf("%d\n", ss[idx])
        <-ch                   // 打印完释放一个槽位
    }(i)
}

方案二:固定 10 个 goroutine 顺序打印

hashMap := make(map[int]chan int)  // 10 个 channel 对应 10 个 goroutine
sort := make(chan struct{})         // 控制顺序

for i := 0; i < 10; i++ {
    hashMap[i] = make(chan int)
    go func(idx int) {
        for val := range hashMap[idx] {
            fmt.Printf("go %d: %d\n", idx, val)
            sort <- struct{}{}      // 打印完通知
        }
    }(i)
}

for _, v := range ss {
    id := v % 10
    hashMap[id] <- v
    <-sort                           // 等当前打印完再发下一个
}

[!info] 🔗 延伸阅读

  • 方案一适合不需要顺序的场景(吞吐量优先)
  • 方案二适合需要顺序的场景(可控性优先)

Q4:两个协程交替打印奇偶数 🟢简单

参考答案

chan1 := make(chan struct{})

// 偶数协程
go func() {
    for i := 0; i < 10; i++ {
        chan1 <- struct{}{}           // 发出信号
        if i%2 == 0 {
            fmt.Println("偶数:", i)
        }
    }
}()

// 奇数协程
go func() {
    for i := 0; i < 10; i++ {
        <-chan1                        // 等待信号
        if i%2 == 1 {
            fmt.Println("奇数:", i)
        }
    }
}()

Q5:用单个 channel 实现 0,1 交替打印 🟢简单

参考答案

msg := make(chan struct{})

go func() {
    for {
        <-msg                         // 接收信号
        fmt.Println("0")
        msg <- struct{}{}             // 回传信号
    }
}()

go func() {
    for {
        <-msg                         // 接收信号
        fmt.Println("1")
        msg <- struct{}{}             // 回传信号
    }
}()

msg <- struct{}{}                     // 初始信号

[!tip] 💡 面试技巧 "用一个 channel 作为信号灯,0 和 1 各持一半——谁拿到灯谁就打印,打印完把灯放回 channel。"


Q6:sync.Cond 实现多生产者多消费者 🟡中等

参考答案

var cond sync.Cond
cond.L = new(sync.Mutex)
msgCh := make(chan int, 5)

// 生产者:缓冲区满时等待
producer := func(ctx context.Context, out chan<- int, idx int) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            cond.Broadcast()
            return
        default:
            cond.L.Lock()
            for len(msgCh) == 5 {
                cond.Wait()  // 缓冲区满,等待
            }
            out <- rand.Intn(500)
            cond.Signal()     // 唤醒一个消费者
            cond.L.Unlock()
        }
    }
}

// 消费者:缓冲区空时等待
consumer := func(ctx context.Context, in <-chan int, idx int) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            for len(msgCh) > 0 { /* 继续消费 */ }
            return
        default:
            cond.L.Lock()
            for len(msgCh) == 0 {
                cond.Wait()   // 缓冲区空,等待
            }
            num := <-in
            cond.Signal()     // 唤醒一个生产者
            cond.L.Unlock()
        }
    }
}

[!note] 📝 解析思路 sync.Cond 的核心是:Lock + Wait/Signal/Broadcast。Wait 会释放锁并挂起 goroutine,Signal 唤醒一个等待者,Broadcast 唤醒全部。适合"条件满足才执行"的场景。


Q7:1000 个并发控制 + 1 秒超时 🟡中等

参考答案

tasks := make(chan int, 1000)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
    wg.Add(1)
    tasks <- i
    go func(id int) {
        defer wg.Done()
        select {
        case <-ctx.Done():
            return              // 超时退出
        default:
            fmt.Printf("goroutine %d\n", id)
        }
    }(i)
}

<-ctx.Done()
fmt.Println("exec done")
close(tasks)
wg.Wait()

[!warning] ⚠️ 高频陷阱 这段代码的缺陷是 1000 个 goroutine 几乎同时启动,1 秒内可能来不及全部执行完。实际场景中应限制并发数量(如用 semaphore channel)。

[!tip] 💡 面试技巧 如果面试官指出问题,可以回答:"可以用有缓冲 channel 做信号量控制并发数:sem := make(chan struct{}, 10),获取/释放 sem 来控制最多 10 个并发。"


Q8:两个 Goroutine 交替打印字母与数字 a1b2c3 🟡中等

参考答案

numCh := make(chan struct{}) // 通知打印数字
strCh := make(chan struct{}) // 通知打印字母
var wg sync.WaitGroup
wg.Add(2)

// 打印字符 a-z
go func() {
    defer wg.Done()
    for i := 'a'; i <= 'z'; i++ {
        fmt.Print(string(i))
        numCh <- struct{}{}   // 通知打印数字
        <-strCh               // 等待对方完成
    }
}()

// 打印数字 1-26
go func() {
    defer wg.Done()
    for i := 1; i <= 26; i++ {
        <-numCh               // 等待对方完成
        fmt.Print(i)
        strCh <- struct{}{}   // 通知打印字母
    }
}()

wg.Wait()

Q9:限制 10 个 goroutine 执行,执行完一个放一个新进来 🟢简单

参考答案

ch := make(chan struct{}, 10) // 容量 10 = 并发上限
var wg sync.WaitGroup

for i := 0; i < 20; i++ {
    wg.Add(1)
    ch <- struct{}{}           // 写满 10 个后自动阻塞
    go func(id int) {
        defer wg.Done()
        fmt.Printf("id: %d\n", id)
        <-ch                   // 执行完释放一个槽位
    }(i)
}
wg.Wait()

[!tip] 💡 面试技巧 这是最经典的并发控制模式——用有缓冲 channel 做信号量(Semaphore)。面试中如果只写出这个,说明掌握了基础;如果能补充"用 context 加超时"或"用 errgroup 处理错误",则是加分项。

关联笔记