1. defer执行顺序
按照先进后出的顺序执行
2. for range 遍历采用值拷贝
遍历过程中第二参数作为一个临时变量是被重复使用的,即只创建一次,如果读取后需要用到这个值,需要考虑是否会影响到后续使用
3. 闭包
4. 组合继承
5. select随机性
select会随机选择一个可用通用做收发操作
6. map线程安全
使用锁
7. 逃逸分析
Go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析,当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。
8. 什么是内存逃逸,在什么情况下发生,原理是什么?
9. gc
v1.1 STW
v1.3 Mark STW, Sweep 并行
v1.5 三色标记法
v1.8 hybrid write barrier(混合写屏障:优化STW)
10. 简述一下golang的协程调度原理
11. golang的runtime机制
12. 引用
根据地址获得在这个地址上的值是解引用 符号是*
获取某个变量的地址是取引用 符号是&
13. go中用for遍历多次执行goroutine会存在什么问题?怎么改进?
可能导致goroutine读取的都是被遍历对象的最后一个值
可以在循环体定义一个变量存储,变量在循环体内并不共享
也可以把range中的变量当作闭包的参数传入go关键字修饰的闭包函数中
14. 如果要在每个goroutine中都获取返回值(捕获参数),有哪些方案?(全局参数、channel,闭包)
15. gRPC用的什么协议?TCP三次握手?四次挥手?FIN-WAIT-2是什么时候的?
16. 了解GMP模型吗,介绍一下
17. context包的用途
18. set实现
/**
* @Author: SolitudeAlma
* @Date: 2022 2022/7/28 16:08
*/
package stl
type exits struct{}
type Set struct {
m map[interface{}]exits
}
func NewSet(items ...interface{}) *Set {
s := &Set{
m: make(map[interface{}]exits),
}
s.Add(items)
return s
}
func (s *Set) Add(items ...interface{}) {
for _, item := range items {
s.m[item] = exits{}
}
}
func (s *Set) Remove(item interface{}) {
delete(s.m, item)
}
func (s *Set) Contains(item interface{}) bool {
_, ok := s.m[item]
return ok
}
func (s *Set) Size() int {
return len(s.m)
}
func (s *Set) Clear() {
s.m = make(map[interface{}]exits)
}
// InterSet 取交集
func (s *Set) InterSet(anotherSet *Set) *Set {
res := NewSet()
for ele := range s.m {
if _, ok := anotherSet.m[ele]; ok {
res.Add(ele)
}
}
return res
}
func (s *Set) DiffSet(anotherSet *Set) *Set {
res := NewSet()
for ele := range s.m {
if _, ok := anotherSet.m[ele]; !ok {
res.Add(ele)
}
}
return res
}
19. make和new的区别
相同点:都是用来对类型的初始化
不同点:1. make是对slice、map、chan这三个引用类型初始化,并返回类型的引用
new是对基本数据类型和struct这些类型初始化,将其初始化为对应类型零值,并返回指向该内存的指针
2.
20. Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量
Golang中Goroutine 可以通过 Channel 进行安全读写共享变量,还可以通过原子性操作进行.
21. 无缓冲Chan的发送和接收是否同步
ch := make(chan int) 无缓冲的channel由于没有缓冲发送和接收需要同步.
ch := make(chan int, 2) 有缓冲channel不要求发送和接收操作同步.
– channel无缓冲时,无缓冲chan是指在接收前没有能力保存任何值的通道(阻塞)。
这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的goroutine阻塞等待。
– channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
22. Golang的内存模型中为什么小对象多了会造成GC压力
通常小对象过多会导致GC三色法消耗过多的CPU。优化思路是,减少对象分配.
23. Go中对 GC 的触发时机存在两种形式
主动触发(手动触发),通过调用runtime.GC来触发GC,此调用阻塞式地等待当前GC运行完毕.
被动触发,分为两种方式:
a. 使用系统监控,当超过两分钟没有产生任何GC时,强制触发GC.
b. 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,当前内存分配达到一定比例则触发.
24. Go函数返回局部变量的指针是否安全
在 Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上
25. sync.Pool、sync.WaitGroup、sync.Cond
26. map 中删除一个 key,它的内存会释放么?
27. slices能作为map类型的key吗
在golang规范中,可比较的类型都可以作为map key;这个问题又延伸到在:golang规范中,哪些数据类型可以比较?
不能作为map key 的类型包括:
slice
map
function
golang 哪些类型可以作为map key
28. 协程之间的通信方式
channel,全局变量 + mutex
29. map原理
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
// buckets 指向的数据的结构
type bmap struct {
tophash [bucketCnt]uint8
}
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap
nextOverflow *bmap
}
hmap结构字段意思:
* count
表示当前哈希表中的元素数量
* flags
代表当前 map 的状态(是否处于正在写入的状态等)
* B
表示当前哈希表持有的 buckets
数量,但是因为哈希表中桶的数量都是 2
的倍数,所以该字段会存储对数,也就是 len(buckets) == 2^B
(最多可以放 loadFactor * 2^B 个元素即 6.5*2^B,再多就要 hashGrow 了)
* hash0
是哈希的种子,它能为哈希函数的结果引入随机性,这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入
* noverflow
noverflow是溢出桶的数量,当B<16时,为精确值,当B>=16时,为估计值
* oldbuckets
是哈希在扩容时用于保存之前 buckets
的字段,它的大小是当前 buckets
的一半(双倍扩容),因为扩容的时候它才有大小,而buckets
是会扩大两倍。等量扩容两者是相等的
* extra 用于保存溢出桶的地址
bmap结构字段意思:
* tophash 存储了键的哈希的高 8 位,通过比较不同键的哈希的高 8 位可以减少访问键值对次数以提高性能
在运行期间,runtime.bmap
结构体其实不止包含 tophash
s 字段,因为哈希表中可能存储不同类型的键值对,而且 Go 语言也不支持泛型,所以键值对占据的内存空间大小只能在编译时进行推导。runtime.bmap 中的其他字段在运行时也都是通过计算内存地址的方式访问的,所以它的定义中就不包含这些字段,不过我们能根据编译期间的 cmd/compile/internal/gc.bmap
函数重建它的结构:
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
随着哈希表存储的数据逐渐增多,我们会扩容哈希表或者使用额外的桶存储溢出的数据,不会让单个桶中的数据超过8个,不过溢出桶只是临时的解决方案,创建过多的溢出桶最终也会导致哈希的扩容。
hash = hashfunc(key)
bucketIndex = hash % array_size
bmap
就是我们常说的“桶”,桶里面会最多装8个key,用 hashfunc
结果的后 B
位确定桶的编号,这些key之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果是“一类”的。在桶内,又会根据key计算出来的hash值的高8位来决定key到底落入桶内的哪个位置(一个桶内最多有8个位置)。桶在存储的tophash字段后,会存储key数组和value数组。
装载因子 := 元素数量 ÷ 桶数量
装载因子已经超过 6.5或者哈希使用了太多溢出桶会触发哈希的扩容
等量扩容:
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
if B > 15 {
B = 15
}
// noverflow 是个 uint 16,因此限制在 uint16 内比对。
return noverflow >= uint16(1)<<(B&15)
}
过多的定义是超过了常规桶的个数。考虑一种情况:通过反复的 添加/删除 少量数据,map 可以渐渐积累许多的溢出桶,由于元素不多,可能根本不会达到负载因子的阈值。此时申请的溢出桶并不会被释放 map 的内存会慢慢积累泄漏,二是 key/value
的分布可能非常松散,存在大量的的碎片空间(即 emptyOne) 导致查找插入效率都极为低下,因此当检测到溢出桶过多时候,会进行等量扩容,等于是做了一次碎片整理,另外扩容还会清掉多余的溢出桶。
- 溢出的桶太多
- 等量扩容创建的新桶数量只是和旧桶一样 大量的put、delete操作导致bucket比较稀疏,bucket的数量剧增
- 会重新排列,极端情况下,重新排列也解决不了,map成了链表,性能大大降低,此时哈希种子
hash0
的设置,可以降低此类极端场景的发生。
双倍扩容:
func overLoadFactor(count int, B uint8) bool {
// count 是本次新增元素后的元素数量。
// bucketShift(B) 就是 1<<B,即桶的个数,注意一点,这个地方没有将溢出桶数量算进来。
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
bucketCnt
上面说内存结构时候提到过,当前版本为常量 8,是单个桶的元素个数,因此第一个条件是 要求元素总数超过一个桶,否则前面提到的 makemap_small
就很尴尬了。
第二个条件里有 2
个与 负载因子
相关的常量:loadFactorNum
和 loadFactorDen
,loadFactorNum/loadFactorDen
的结果就是触发扩容的阈值负载因子,当前版本中为 13/2 = 6.5
。第二个条件的不等式稍微变换一下,就是在判断当前负载因子是否超过了 6.5
这个阈值。
为什么负载因子阈值选择了 6.5?
在runtime/map.go
文件顶部的注释,开发者给出了实验数据,感兴趣的去阅读下。
- 装载因子过大,直接翻倍,B+1;扩容也不是申请一块内存,立马开始拷贝,每一次访问旧的 buckets 时,就迁移一部分,直到完成,旧 bucket 被 GC 回收。
对map数据进行操作时不可取地址
* 因为随着map元素的增长,map底层重新分配空间会导致之前的地址无效。
查找
1. 根据key
计算出哈希值
2. 根据哈希值低B
位确定所在 bucket
3. 根据哈希值高 8
位确定在 bucket
中的存储位置
4. 当前 bucket
未找到则查找对应的 overflow bucket
。
5. 对应位置有数据则对比完整的哈希值,确定是否是要查找的数据
6. 如果当前 map
进行了扩容,处于数据搬移状态,则优先从 oldbuckets 查找。
插入
1. 根据 key 计算出哈希值
2. 根据哈希值低位确定所在 bucket
3. 根据哈希值高 8 位确定在 bucket 中的存储位置
4. 查找该 key 是否存在,已存在则更新,不存在则插入
map 无序
map 的本质是散列表,而 map 的增长扩容会导致重新进行散列,这就可能使 map 的遍历结果在扩容前后变得不可靠, Go 设计者为了让大家不依赖遍历的顺序,故意在实现 map 遍历时加入了随机数, 让每次遍历的起点–即起始 bucket 的位置不一样,即不让遍历都从 bucket0 开始, 所以即使未扩容时我们遍历出来的 map 也总是无序的。
30. 什么是协程(Goroutine)
协程是用户态轻量级线程,它是线程调度的基本单位。通常在函数前加上go关键字就能实现并发。一个Goroutine会以一个很小的栈启动2KB或4KB,当遇到栈空间不足时,栈会自动伸缩, 因此可以轻易实现成千上万个goroutine同时启动。
31. 什么是 rune 类型
ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的128个字符,为了表示世界上大部分的文字系统,发明了 Unicode, 它是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。Unicode也是4字节
Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。
32. Go 语言中如何表示枚举值(enums)?
在常量中用iota可以表示枚举。iota从0开始。
const (
B = 1 << (10 * iota)
KiB
MiB
GiB
TiB
PiB
EiB
)
33. init() 函数是什么时候执行的?
简答: 在main函数之前执行。
详细:init()函数是go初始化的一部分,由runtime初始化每个导入的包,初始化不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。
每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的init()函数。同一个包,甚至是同一个源文件可以有多个init()函数。init()函数没有入参和返回值,不能被其他函数调用,同一个包内多个init()函数的执行顺序不作保证。
执行顺序:import –> const –> var –>init()–>main() 和递归一致,先import所有包,然后从最后一个被引入的包开始初始化常量、变量、执行init函数
一个文件可以有多个init()函数!
34. 如何知道一个对象是分配在栈上还是堆上?
Go和C++不同,Go局部变量会进行逃逸分析。如果变量离开作用域后没有被引用,则优先分配到栈上,否则分配到堆上。那么如何判断是否发生了逃逸呢?
go build -gcflags '-m -m -l' xxx.go.
关于逃逸的可能情况:变量大小不确定,变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针。
35. slice扩容机制
-
append单个元素,或者append少量的多个元素,这里的少量指double之后的容量能容纳,这样就会走以下扩容流程,不足1024,双倍扩容,超过1024的,1.25倍扩容。
-
若是append多个元素,且double后的容量不能容纳,直接使用预估的容量
slice扩容
36. 为什么有协程泄露(Goroutine Leak)
协程泄漏是指协程创建之后没有得到释放。主要原因有:
缺少接收器,导致发送阻塞
缺少发送器,导致接收阻塞
死锁。多个协程由于竞争资源导致死锁。
WaitGroup Add()和Done()不相等,前者更大。
37. 读写channel应该先关哪个?
应该写channel先关。因为对于已经关闭的channel只能读,不能写。
38. 对已经关闭的chan进行读写会怎么样?
读已经关闭的chan能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。
- 如果chan关闭前,buffer内有元素还未读,会正确读到chan内的值,且返回的第二个bool值(是否读成功)为true。
- 如果chan关闭前,buffer内有元素已经被读完,chan内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个bool值一直为false。
写已经关闭的chan会panic。
39. 说说context包的作用?你用过哪些,原理知道吗?
context
可以用来在goroutine
之间传递上下文信息,相同的context
可以传递给运行在不同goroutine
中的函数,上下文对于多个goroutine
同时使用是安全的,context
包定义了上下文类型,可以使用background
、TODO
创建一个上下文,在函数调用链之间传播context
,也可以使用WithDeadline
、WithTimeout
、WithCancel
或WithValue
创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context
的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。
关于context
原理,可以参看:小白也能看懂的context包详解:从入门到精通
40. 关于数组、切片的一些补充
数组的长度不同,它们并不相等 可以用反射验证
用数组或者切片截取一段得到的切片它们是共享底层数组的,修改切片会影响原数组或切片