Golang 并发编程高频面试题 Top 10 💻
Golang 并发编程高频面试题 Top 10 💻
记录下,部分是面试中没回答出来的
1. Goroutine 是什么?它和线程有什么区别?
-
解读: 这是考察对 Go 并发核心概念的理解。
-
思路:
- 解释 Goroutine 是 Go 语言实现的轻量级并发执行单元。
- 对比线程:Goroutine 由 Go 运行时管理,切换成本远低于内核线程;Goroutine 初始栈空间小(通常2KB),可按需增长;更多 Goroutine 可以运行在更少的线程上(M:N 模型)。
-
Code
package main import ( "fmt" "time" ) func say(s string) { for i := 0; i < 3; i++ { fmt.Println(s) time.Sleep(100 * time.Millisecond) } } func main() { go say("Hello") // 启动一个新的 Goroutine say("World") // 当前 Goroutine 执行 // 注意:实际项目中需要机制等待 Goroutine 完成,例如 sync.WaitGroup }
2. Channel 是什么?有什么用途?
-
解读: 考察对 Go 主要通信机制的理解。
-
思路:
- Channel 是 Goroutine 之间通信的管道,遵循“不要通过共享内存来通信,而要通过通信来共享内存”的哲学。
- 类型化:每个 channel 只能传递特定类型的数据。
- 主要用途:数据传递、Goroutine 同步、信号通知。
- 分类:无缓冲 Channel(同步,发送和接收必须同时准备好)、有缓冲 Channel(异步,有一定容量)。
-
Code (示例):
package main import "fmt" func main() { // 无缓冲 Channel ch1 := make(chan int) go func() { ch1 <- 1 // 发送,会阻塞直到有接收者 fmt.Println("Sent to ch1") }() val1 := <-ch1 // 接收,会阻塞直到有发送者 fmt.Println("Received from ch1:", val1) // 有缓冲 Channel ch2 := make(chan string, 2) ch2 <- "first" ch2 <- "second" // ch2 <- "third" // 如果没有接收者,这里会阻塞或 panic (如果 channel 已关闭) fmt.Println("Received from ch2:", <-ch2) fmt.Println("Received from ch2:", <-ch2) }
3. sync.WaitGroup
的用途和使用场景?
-
解读: 考察并发同步原语的使用。
-
思路:
sync.WaitGroup
用于等待一组 Goroutine 完成执行。- 主要方法:
Add(delta int)
增加计数器,Done()
减少计数器(通常在 Goroutine 结束时用defer
调用),Wait()
阻塞直到计数器归零。 - 场景:主 Goroutine 需要等待多个子 Goroutine 完成某些任务后再继续执行。
-
Code:
package main import ( "fmt" "sync" "time" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // 减少计数器 fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup numWorkers := 3 for i := 1; i <= numWorkers; i++ { wg.Add(1) // 增加计数器 go worker(i, &wg) } wg.Wait() // 等待所有 worker 完成 fmt.Println("All workers completed.") }
4. select
语句的作用是什么?
-
解读: 考察多路复用 Channel 操作的能力。
-
思路:
select
语句用于在多个 Channel 操作中进行选择。- 行为类似
switch
,但其case
是 Channel 的发送或接收操作。 - 如果多个
case
同时就绪,select
会随机选择一个执行。 - 可以有
default
子句,当所有case
都不就绪时执行default
(实现非阻塞操作)。
-
Code:
package main import ( "fmt" "time" ) func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { time.Sleep(1 * time.Second) ch1 <- "from ch1" }() go func() { time.Sleep(2 * time.Second) ch2 <- "from ch2" }() for i := 0; i < 2; i++ { // 等待两个消息 select { case msg1 := <-ch1: fmt.Println("Received:", msg1) case msg2 := <-ch2: fmt.Println("Received:", msg2) case <-time.After(3 * time.Second): // 超时处理 fmt.Println("Timeout: No message received after 3 seconds.") return // default: // 如果希望是非阻塞的,可以添加 default // fmt.Println("No activity") // time.Sleep(100 * time.Millisecond) } } }
5. 如何实现一个 Goroutine 安全的计数器?
-
解读: 考察并发访问共享资源时的同步问题。
-
思路:
- 直接并发修改共享变量会导致竞态条件。
- 方法1:使用
sync.Mutex
(互斥锁) 保护临界区。 - 方法2:使用
sync/atomic
包提供的原子操作。 - 方法3 (不常用于简单计数器,但可提及):通过 Channel 将所有修改操作串行化到单个 Goroutine 处理。
-
Code (使用
sync.Mutex
和atomic
):package main import ( "fmt" "sync" "sync/atomic" ) // 使用 Mutex type SafeCounterMutex struct { mu sync.Mutex count int } func (c *SafeCounterMutex) Inc() { c.mu.Lock() c.count++ c.mu.Unlock() } func (c *SafeCounterMutex) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.count } // 使用 atomic type SafeCounterAtomic struct { count int64 // atomic 操作通常针对 int64/uint64 } func (c *SafeCounterAtomic) Inc() { atomic.AddInt64(&c.count, 1) } func (c *SafeCounterAtomic) Value() int64 { return atomic.LoadInt64(&c.count) } func main() { var wg sync.WaitGroup counterMutex := &SafeCounterMutex{} counterAtomic := &SafeCounterAtomic{} numIncrements := 1000 // 测试 Mutex 版本 for i := 0; i < numIncrements; i++ { wg.Add(1) go func() { defer wg.Done() counterMutex.Inc() }() } wg.Wait() fmt.Println("Mutex Counter:", counterMutex.Value()) // 测试 Atomic 版本 for i := 0; i < numIncrements; i++ { wg.Add(1) go func() { defer wg.Done() counterAtomic.Inc() }() } wg.Wait() fmt.Println("Atomic Counter:", counterAtomic.Value()) }
6. 解释一下 Go 的 context
包的作用。
-
解读: 考察对 Goroutine 生命周期管理、取消和超时的理解。
-
思路:
context.Context
用于在 API 边界和 Goroutine 之间传递请求范围的值、取消信号和截止日期。- 主要功能:
- 取消 (Cancellation): 父 Context 被取消时,其派生的所有子 Context 也会被取消。Goroutine 可以监听
Context.Done()
Channel 来优雅退出。 - 超时 (Timeout/Deadline): 可以设置 Context 在特定时间点或一段时间后自动取消。
- 值传递 (Value Passing): 可以在 Context 中携带请求范围的数据(不推荐用于传递可选参数,主要用于传递请求特定信息,如用户ID、Trace ID)。
- 取消 (Cancellation): 父 Context 被取消时,其派生的所有子 Context 也会被取消。Goroutine 可以监听
- 常用函数:
context.Background()
,context.TODO()
,context.WithCancel()
,context.WithTimeout()
,context.WithDeadline()
,context.WithValue()
。
-
Code:
package main import ( "context" "fmt" "time" ) func operation(ctx context.Context, duration time.Duration) { select { case <-time.After(duration): fmt.Println("Operation completed") case <-ctx.Done(): fmt.Println("Operation cancelled:", ctx.Err()) } } func main() { // 1. WithCancel ctxCancel, cancel := context.WithCancel(context.Background()) go operation(ctxCancel, 5*time.Second) time.Sleep(1 * time.Second) cancel() // 取消操作 time.Sleep(100 * time.Millisecond) // 给 operation goroutine 一点时间响应取消 fmt.Println("---") // 2. WithTimeout ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 1*time.Second) defer cancelTimeout() // 推荐调用 cancel 释放资源,即使超时会自动触发 go operation(ctxTimeout, 2*time.Second) // 这个操作会因为超时而被取消 time.Sleep(2 * time.Second) // 等待超时或操作完成 fmt.Println("---") // 3. WithValue type key string const requestIDKey key = "requestID" ctxValue := context.WithValue(context.Background(), requestIDKey, "12345-abc") processRequest(ctxValue) } func processRequest(ctx context.Context) { type key string const requestIDKey key = "requestID" if id, ok := ctx.Value(requestIDKey).(string); ok { fmt.Println("Processing request with ID:", id) } else { fmt.Println("No request ID found in context") } }
7. 如何优雅地关闭一个 Channel 并通知所有接收者?
-
解读: 考察 Channel 的关闭机制和广播信号。
-
思路:
- 通过
close(ch)
关闭 Channel。 - 关闭后,不能再向 Channel 发送数据(会 panic)。
- 接收者可以从已关闭的 Channel 接收数据,会接收到已缓冲的值,然后是对应类型的零值。
- 使用
value, ok := <-ch
语法判断 Channel 是否已关闭 (ok
为false
表示已关闭且无缓冲值可读)。 - 对于多个接收者,通常是发送者负责关闭。
- 通过
-
Code:
package main import ( "fmt" "sync" "time" ) func workerReceiver(id int, ch <-chan int, wg *sync.WaitGroup) { defer wg.Done() for { val, ok := <-ch if !ok { fmt.Printf("Worker %d: Channel closed, exiting.\n", id) return } fmt.Printf("Worker %d received: %d\n", id, val) time.Sleep(50 * time.Millisecond) } } func main() { ch := make(chan int, 3) var wg sync.WaitGroup numReceivers := 3 for i := 0; i < numReceivers; i++ { wg.Add(1) go workerReceiver(i, ch, &wg) } // 发送数据 for i := 1; i <= 5; i++ { ch <- i fmt.Printf("Sent: %d\n", i) time.Sleep(100 * time.Millisecond) } fmt.Println("Closing channel...") close(ch) // 关闭 channel wg.Wait() // 等待所有接收者退出 fmt.Println("All receivers finished.") }
8. 什么是竞态条件 (Race Condition)?如何检测和避免?
-
解读: 并发编程的核心问题之一。
-
思路:
- 定义: 多个 Goroutine 并发访问和修改共享资源,且最终结果依赖于这些操作的执行顺序时,就会发生竞态条件。
- 检测: Go 提供了内置的竞态检测器。编译时使用
-race
标志 (go run -race main.go
或go test -race
)。 - 避免:
- 互斥锁 (
sync.Mutex
,sync.RWMutex
): 保护对共享资源的访问。 - 原子操作 (
sync/atomic
): 对基本数据类型进行原子读写。 - Channel: 通过通信共享数据,将对共享资源的访问限制在单个 Goroutine 中(串行化)。
- 不可变数据: 如果共享数据是不可变的,则不会发生竞态。
- 互斥锁 (
-
Code (演示竞态和使用 Mutex 修复):
package main import ( "fmt" "sync" ) func main() { var counter int // 共享资源 var wg sync.WaitGroup var mu sync.Mutex // 用于修复竞态 numGoroutines := 100 incrementsPerGoroutine := 100 // 模拟竞态条件 (如果不加锁) // 编译和运行时加上 -race 标志可以检测到 for i := 0; i < numGoroutines; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < incrementsPerGoroutine; j++ { // counter++ // 存在竞态条件 // 使用 Mutex 修复 mu.Lock() counter++ mu.Unlock() } }() } wg.Wait() fmt.Printf("Expected counter: %d\n", numGoroutines*incrementsPerGoroutine) fmt.Printf("Actual counter: %d\n", counter) // 如果不加锁且 -race 检测,会报告竞态。 // 加锁后,结果符合预期。 }
9. sync.Once
的使用场景是什么?
-
解读: 考察如何确保某个操作只执行一次。
-
思路:
sync.Once
是一个对象,它包含一个Do(f func())
方法。Do
方法会确保传入的函数f
在所有 Goroutine 中只被执行一次,即使Do
被多次调用。- 场景:单例模式的初始化、全局资源的惰性初始化等。
-
Code:
package main import ( "fmt" "sync" "time" ) var once sync.Once var instance *Singleton type Singleton struct { name string } func GetInstance() *Singleton { once.Do(func() { fmt.Println("Initializing Singleton...") instance = &Singleton{name: "The One and Only"} time.Sleep(500 * time.Millisecond) // 模拟初始化耗时 fmt.Println("Singleton initialized.") }) return instance } func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(id int) { defer wg.Done() s := GetInstance() fmt.Printf("Goroutine %d got instance: %s\n", id, s.name) }(i) } wg.Wait() // 再次获取,不会重复初始化 s1 := GetInstance() s2 := GetInstance() fmt.Println("s1 and s2 are the same instance:", s1 == s2) }
10. 实现一个生产者-消费者模型。
-
解读: 综合考察 Channel、Goroutine 和同步。
-
思路:
- 生产者 (Producer): 生成数据并将其发送到 Channel。
- 消费者 (Consumer): 从 Channel 接收数据并处理。
- Channel: 作为生产者和消费者之间的缓冲区。
- 同步:
- 生产者生产完毕后,需要通知消费者(例如关闭 Channel)。
- 主 Goroutine 可能需要等待生产者和消费者都完成。
-
Code:
package main import ( "fmt" "math/rand" "sync" "time" ) const ( numProducers = 2 numConsumers = 3 numItems = 10 // 每个生产者生产的条目数 bufferSize = 5 // Channel 缓冲区大小 ) func producer(id int, dataChan chan<- int, wg *sync.WaitGroup) { defer wg.Done() for i := 0; i < numItems; i++ { item := rand.Intn(100) // 生成随机数据 dataChan <- item fmt.Printf("Producer %d sent: %d\n", id, item) time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond) } fmt.Printf("Producer %d finished.\n", id) } func consumer(id int, dataChan <-chan int, wg *sync.WaitGroup) { defer wg.Done() for item := range dataChan { // range 会在 channel 关闭后自动退出循环 fmt.Printf("Consumer %d received: %d\n", id, item) time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) } fmt.Printf("Consumer %d finished because channel closed.\n", id) } func main() { rand.Seed(time.Now().UnixNano()) dataChan := make(chan int, bufferSize) var wgProducers sync.WaitGroup var wgConsumers sync.WaitGroup // 启动生产者 for i := 0; i < numProducers; i++ { wgProducers.Add(1) go producer(i, dataChan, &wgProducers) } // 启动消费者 for i := 0; i < numConsumers; i++ { wgConsumers.Add(1) go consumer(i, dataChan, &wgConsumers) } // 等待所有生产者完成 wgProducers.Wait() fmt.Println("All producers finished. Closing data channel.") close(dataChan) // 关闭 channel,通知消费者没有更多数据了 // 等待所有消费者完成 wgConsumers.Wait() fmt.Println("All consumers finished. Program exiting.") }
这些题目覆盖了 Goroutine、Channel、以及 sync
包中常用同步原语的核心用法,是面试中考察并发基础的常见切入点。