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.Mutexatomic):

    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)。
    • 常用函数: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 是否已关闭 (okfalse 表示已关闭且无缓冲值可读)。
    • 对于多个接收者,通常是发送者负责关闭。
  • 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.gogo 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 包中常用同步原语的核心用法,是面试中考察并发基础的常见切入点。

#面试问题记录##面经##go##mygo#
全部评论

相关推荐

没有offer的瓦学弟:我去!这么晚还有HC?大佬,牛
投递字节跳动等公司7个岗位 腾讯求职进展汇总
点赞 评论 收藏
分享
评论
1
8
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务