Go面试:select是随机的还是顺序的?

本文深入探讨了Go语言中select语句的工作原理,包括其在阻塞和非阻塞状态下的行为,以及如何通过default分支控制流程。通过具体示例代码验证了select在多个channel准备就绪时的随机选择特性。
  1. select语句不使用default分支时,处于阻塞状态直到其中一个channel的收/发操作准备就绪(或者channel关闭或者缓冲区有值),如果同时有多个channel的收/发操作准备就绪(或者channel关闭)则随机选择其中一个。
  2. select语句使用default分支时,处于非阻塞状态,从所有准备就绪(或者channel关闭或者缓冲区有值)的channel中随机选择其中一个,如果没有则执行default分支。

严格来讲,select的实现包括两种顺序:一是:轮询顺序(poll order),二是加锁/解锁顺序(lock order)。个人理解,这道题的本意应该是在问轮询顺序。

轮询顺序的实现: 首先将所有case分支进行随机排序,然后按照这个随机顺序来遍历case分支,选择第一个符合条件(就绪或关闭或缓冲区有值)的channel即返回不再遍历后面的case分支。

runtime/select.go对应源码节选:

调用fastrandn来随机排序pollorder

	// generate permuted order
	for i := 1; i < ncases; i++ {
		j := fastrandn(uint32(i + 1))
		pollorder[i] = pollorder[j]
		pollorder[j] = uint16(i)
	}


loop:
	// pass 1 - look for something already waiting
	var dfli int
	var dfl *scase
	var casi int
	var cas *scase
	var recvOK bool
	for i := 0; i < ncases; i++ {
		casi = int(pollorder[i])
		cas = &scases[casi]
		c = cas.c

		switch cas.kind {
		case caseNil:
			continue

		case caseRecv:
			sg = c.sendq.dequeue()
			if sg != nil {
				goto recv
			}
			if c.qcount > 0 {
				goto bufrecv
			}
			if c.closed != 0 {
				goto rclose
			}

		case caseSend:
			if raceenabled {
				racereadpc(c.raceaddr(), cas.pc, chansendpc)
			}
			if c.closed != 0 {
				goto sclose
			}
			sg = c.recvq.dequeue()
			if sg != nil {
				goto send
			}
			if c.qcount < c.dataqsiz {
				goto bufsend
			}

		case caseDefault:
			dfli = casi
			dfl = cas
		}
	}

	if dfl != nil {
		selunlock(scases, lockorder)
		casi = dfli
		cas = dfl
		goto retc
	}

不使用default(阻塞)验证如下: 

select_block.go

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	var cnt [2]int
	for i := 0; i < 1000; i++ {
		go func(c1 chan int) {
			c1 <- 0
		}(ch1)

		go func(c2 chan int) {
			c2 <- 1
		}(ch2)

		//等10毫秒,确保两个channel都已准备就绪
		time.Sleep(10 * time.Millisecond)

		var index int

		select {
		case index = <-ch1:
			cnt[index]++
		case index = <-ch2:
			cnt[index]++
		}
	}
	fmt.Printf("cnt=%v\n", cnt)
}

 连续执行多次,可以看到每次数字都不同,但比例接近于1 :1而且总和是1000,说明在同时接收到两个channel的返回时select等概率随机选择其中一个,而将另一个舍弃。

[root@dev tutorial]# go run select_block.go
cnt=[507 493]
[root@dev tutorial]# go run select_block.go
cnt=[496 504]
[root@dev tutorial]# go run select_block.go
cnt=[492 508]
[root@dev tutorial]# go run select_block.go
cnt=[493 507]
[root@dev tutorial]# go run select_block.go
cnt=[499 501]
[root@dev tutorial]# go run select_block.go
cnt=[505 495]

使用default分支(非阻塞)的验证:

select_nonblock.go

package main

import (
	"fmt"
	"time"
)

func main() {
	var cnt [2]int
	for i := 0; i < 1000; i++ {
		timer1 := time.NewTimer(5 * time.Millisecond)
		defer timer1.Stop()

		timer2 := time.NewTimer(5 * time.Millisecond)
		defer timer2.Stop()

		//等10毫秒,确保两个channel都已准备就绪
		time.Sleep(10 * time.Millisecond)

		select {
		case <-timer1.C:
			cnt[0]++
		case <-timer2.C:
			cnt[1]++
		default:
		}
	}

	fmt.Printf("cnt=%v\n", cnt)

}

输出:

[root@dev tutorial]# go run select_nonblock.go
cnt=[498 502]
[root@dev tutorial]# go run select_nonblock.go
cnt=[544 456]
[root@dev tutorial]# go run select_nonblock.go
cnt=[520 480]

相关文章:

《Go语言:为什么要使用上下文(context)而不是计时器(timer)加通道(channel)的方式来控制协程》

《Go语言:select的典型应用场景之IO超时控制的示例(阻塞方式)》

《Go语言:select的典型应用场景之IO超时控制的示例(采用default,非阻塞方式)》

<think>好的,用户让我介绍一下Golang的面试相关内容。首先,我需要确定用户的需求是什么。他们可能正在准备Golang的面试,想了解常见的面试问题、重点考察的知识点,以及如何有效准备。接下来,我要考虑用户可能的背景,可能是有一定Go语言基础的程序员,准备应聘中级或高级职位。需要涵盖基础语法、并发模型、内存管理、标准库、性能优化等方面。 首先,结构要清晰,分点说明。常见的面试问题部分需要涵盖语言特性、并发模型、内存管理、标准库、性能优化等。每个大点下再细分小点,比如并发模型里的Goroutine、Channel、Context、同步机制等。需要确保内容全面,但不过于冗长。 然后,需要提供准备建议,比如理解核心概念、动手实践、阅读源码、模拟面试等。这部分要实用,能够给用户实际的帮助。同时,要注意使用中文,并且所有行内公式用$...$,但考虑到Golang面试可能涉及数学的地方不多,不过如果有算法复杂度的问题,可能需要提到Big O表示法,比如$O(n)$,这时候要正确应用格式。 还要注意避免使用Markdown,保持自然的口语化表达,但结构不能乱。需要检查是否有需要单独成段的公式,如果有的话用$$...$$,不过Golang面试内容可能较少涉及复杂公式,暂时不需要。 另外,用户可能对实际面试中的高频问题感兴趣,比如GMP模型、defer执行顺序、slice和map的底层实现等。这些需要详细说明,并给出示例代码,但代码部分可能需要用文字描述,因为用户没有特别要求代码块。不过根据系统指令,没有提到代码格式的问题,所以可能需要用自然语言描述。 还要考虑错误处理和接口的实现,这些是Go的重要特性,面试中常被问到。比如error处理机制、接口的动态类型和静态类型区别,以及空接口的应用场景。 在并发模型部分,要解释清楚Goroutine和线程的区别,Channel的缓冲和非缓冲区别,以及如何避免死锁。Context的使用场景也很重要,比如超时控制、取消信号传递等。 内存管理方面,逃逸分析和GC原理是关键点。需要解释堆栈分配的区别,以及GC的三色标记法流程。 标准库部分,常见的有http、json、testing等包,可以举例说明如何使用,比如实现HTTP服务器或处理JSON数据。 性能优化需要提到pprof工具的使用,如何分析CPU和内存性能,以及避免内存泄漏的方法,比如defer的使用注意事项。 在准备建议中,强调动手写代码的重要性,比如实现并发模型或数据结构,阅读标准库源码来深入理解实现细节。模拟面试和算法题练习也是必要的,尤其是LeetCode上的Go题目。 最后,检查是否有遗漏的重要知识点,比如Go的模块管理、依赖管理工具go mod,以及错误处理的最佳实践。可能还需要提到测试和基准测试的编写,因为良好的测试习惯是开发中的重要部分。 总结下来,需要组织一个结构清晰、覆盖全面、实用性强且符合用户需求的回答,确保内容正确可靠,帮助用户有效准备Golang面试。</think>### Golang面试核心要点解析 #### 一、高频考察方向 1. **语言特性** - **并发模型**:GMP调度模型(Goroutine调度、M:N线程模型)、Channel底层结构(环形队列+互斥锁)、`select`非阻塞机制 - **内存管理**:逃逸分析原则、GC三色标记法(标记阶段需STW停顿约$1\text{ms}$)、内存对齐规则(如结构体字段排序优化) - **数据结构**:Slice扩容机制(容量<1024时翻倍,>1024时1.25倍)、Map的桶结构(拉链法+溢出桶) 2. **典型代码题** ```go // 高频考题示例:交替打印数字 func main() { ch := make(chan struct{}) go func() { for i := 1; i <= 10; i += 2 { <-ch fmt.Print(i) ch <- struct{}{} } }() go func() { for i := 2; i <= 10; i += 2 { ch <- struct{}{} <-ch fmt.Print(i) } }() time.Sleep(time.Second) } ``` #### 二、进阶考察点 1. **运行时机制** - **调度器**:P的数量由`GOMAXPROCS`控制(默认CPU核数)、work-stealing调度算法 - **内存分配**:mcache/mcentral/mheap三级分配体系,微小对象(<16B)使用固定大小span 2. **标准库实现** - **HTTP Server**:每个连接创建goroutine处理、`ResponseWriter`接口实现原理 - **Context**:树形结构传播、`WithCancel`内部使用通道通知取消 #### 三、性能优化实践 1. **诊断工具链** - `pprof`火焰图分析:通过`go tool pprof -http=:8080 profile.out`定位热点 - 竞争检测:编译时添加`-race`参数,运行时检测数据竞争 2. **优化案例** - 减少堆分配:通过`sync.Pool`复用对象(如JSON解析时的临时缓冲区) - 并发控制:使用`errgroup.Group`实现goroutine错误传播和上下文取消 #### 四、架构设计问题 1. **微服务场景** - 连接池管理:数据库连接池参数设置(`MaxOpenConns`/`MaxIdleConns`) - 熔断降级:集成Hystrix模式或自定义滑动窗口统计 2. **分布式系统** - 一致性实现:基于Raft协议的状态机复制(如Etcd实现原理) - 分布式锁:结合Redis的`SETNX`或Etcd的租约机制 #### 五、深度原理问题 1. **调度器细节** - 系统调用处理:当G进行阻塞调用时,M会与P解绑 - 抢占式调度:基于信号(SIGURG)的协作式抢占,1.14+版本实现 2. **GC演进** - 历史版本对比:Go1.3标记清除法→1.5并发标记→1.8混合写屏障 - 调优参数:`GOGC`控制GC触发阈值(默认100%堆增长) #### 六、面试准备策略 1. **知识体系构建** - 深入阅读《Go语言设计与实现》《Concurrency in Go》 - 研究标准库源码(如`sync.Map`的无锁设计) 2. **实战训练** - 手写生产者-消费者模型(带优雅退出机制) - 实现简易RPC框架(包含编解码、服务发现、负载均衡) 3. **模拟面试** - 常见陷阱题: ```go // 以下代码输出什么? func main() { var m = map[int]bool{1: true, 2: false} delete(m, 2) for v := range m { fmt.Print(v) } } ``` 答案:输出`1`(遍历map时元素顺序随机) 通过系统化的知识梳理(建议制作包含20+核心知识点的思维导图)、300+行典型代码编写、重点源码分析(如调度器源码`runtime/proc.go`),可显著提升面试通过率。建议每周进行2次模拟面试,重点关注代码实现和系统设计环节。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值