面试官:说下Golang Slice的底层实现,泪崩了!
1、Go 语言当中数组和切片的区别是什么?
数组:
数组固定长度数组长度是数组类型的一部分,所以[3]int 和[4]int 是两种不同 的数组类型数组需要指定大小,不指定也会根据处初始化对的自动推算出大 小,不可改变数组是通过值传递的
切片: 切片可以改变长度切片是轻量级的数据结构,三个属性,指针,长度,容量不 需要指定大小切片是地址传递(引用传递)可以通过数组来初始化,也可以通 过内置函数 make()来初始化,初始化的时候 len=cap,然后进行扩容。
2、Go 语言当中值传递和地址传递(引用传递)如何运用?有什么区别?
- 值传递只会把参数的值复制一份放进对应的函数,两个变量的地址不同, 不可相互修改。 
- 地址传递(引用传递)会将变量本身传入对应的函数,在函数中可以对该变 量进行值内容的修改。 
3、Go 语言当中数组和切片在传递的时候的区别是什么?
- 数组是值传递 
- 切片是引用传递 
func main() {
    arr := make([]int, 0)
    for i := 0; i < 2000; i++ {
fmt.Println("len 为", len(arr), "cap 为", cap(arr))
        arr = append(arr, i)
    }
}
我们可以看下结果
依次是0,1,2,4,8,16,32,64,128,256,512,1024
但到了 1024 之后,就变成了1024,1280,1696,2304
每次都是扩容了四分之一左右
5、看下面代码的 defer 的执行顺序是什么? defer 的作用和特点是什么?
defer 的作用是:
你只需要在调用普通函数或方法前加上关键字 defer,就完成了 defer 所需要
的语法。当 defer 语句被执行时,跟在 defer 后面的函数会被延迟执行。直到
包含该 defer 语句的函数执行完毕时,defer 后的函数才会被执行,不论包含defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。你可以在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反。
defer 的常用场景:
- defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。 
- 通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资 - 源被释放。 
- 释放资源的defer应该直接跟在请求资源的语句后。 
切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。
切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用 底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一 个只读对象,其工作机制类似数组指针的一种封装。
切片对象非常小,是因为它是只有 3 个字段的数据结构:
- 指向底层数组的指针 
- 切片的长度 
- 切片的容量 
7、Golang Slice 的扩容机制,有什么注意点?
Go 中切片扩容的策略是这样的:
- 首先判断,如果新申请容量大于2倍的旧容量,最终容量就是新申请的容 量 
- 否则判断,如果旧切片的长度小于1024,则最终容量就是旧容量的两倍 
- 否则判断,如果旧切片长度大于等于1024,则最终容量从旧容量开始循环 - 增加原来的 1/4, 直到最终容量大于等于新申请的容量 
- 如果最终容量计算值溢出,则最终容量就是新申请容量 
8、扩容前后的 Slice 是否相同?
情况一:
原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的 数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址 的 Slice。
情况二:
原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区
域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响
原数组。
要复制一个 Slice,最好使用 Copy 函数。
9、Golang 的参数传递、引用类型
Go 语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷 贝的内容有时候是非引用类型(int、string、struct 等这些),这样就在函 数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等 这些),这样就可以修改原内容数据。
Golang 的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除 了申请内存外,还需要初始化相关属性。内置函数 new 计算类型大小,为其分 配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分 配内存和初始化成员结构,返回对象而非指针。
10、Golang Map 底层实现
Golang 中 map 的底层实现是一个散列表,因此实现 map 的过程实际上就是实现 散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫 hmap(a header for a go map),一个叫 bmap(a bucket for a Go map,通常叫其bucket)。
11、Golang Map 如何扩容
装载因子:count/2^B
触发条件:
1. 装填因子是否大于6.5
2. overflow bucket 是否太多
解决方法:
1. 双倍扩容:扩容采取了一种称为“渐进式”地方式,原有的 key 并不会一
次性搬迁完毕,每次最多只会搬迁 2 个 bucket
2. 等量扩容:重新排列,极端情况下,重新排列也解决不了,map成了链表,
性能大大降低,此时哈希种子 hash0 的设置,可以降低此类极端场景的发 生。
12、Golang Map 查找
Go 语言中 map 采用的是哈希查找表,由一个 key 通过哈希函数得到哈希值,64位系统中就生成一个 64bit 的哈希值,由这个哈希值将 key 对应到不同的桶 (bucket)中,当有多个哈希映射到相同的的桶中时,使用链表解决哈希冲 突。key 经过 hash 后共 64 位,根据 hmap 中 B 的值,计算它到底要落在哪个桶 时,桶的数量为 2^B,如 B=5,那么用 64 位最后 5 位表示第几号桶,在用 hash值的高 8 位确定在 bucket 中的存储位置,当前 bmap 中的 bucket 未找到,则查 询对应的 overflow bucket,对应位置有数据则对比完整的哈希值,确定是否 是要查找的数据。
如果两个不同的 key 落在的同一个桶上,hash 冲突使用链表法接近,遍历bucket 中的 key 如果当前处于 map 进行了扩容,处于数据搬移状态,则优先从oldbuckets 查找。
13、介绍一下 Channel
Go 语言中,不要通过共享内存来通信,而要通过通信来实现内存共享。Go 的CSP(Communicating Sequential Process)并发模型,中文可以叫做通信顺序进
程,是通过 goroutine 和 channel 来实现的。
所以 channel 收发遵循先进先出 FIFO,分为有缓存和无缓存,channel 中大致
有 buffer(当缓冲区大小部位 0 时,是个 ring buffer)、sendx 和 recvx 收发
的位置(ring buffer 记录实现)、sendq、recvq 当前 channel 因为缓冲区不足
而阻塞的队列、使用双向链表存储、还有一个 mutex 锁控制并发、其他原属
等。
14、Go 语言的 Channel 特性?
- 给一个 nil channel 发送数据,造成永远阻塞 
- 从一个 nil channel 接收数据,造成永远阻塞 
- 给一个已经关闭的 channel 发送数据,引起 panic 
- 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值 
- 无缓冲的channel是同步的,而有缓冲的channel是非同步的 
- 关闭一个 nil channel 将会发生 panic 
15、Channel 的 ring buffer 实现
channel 中使用了 ring buffer(环形缓冲区) 来缓存写入的数据。ring buffer 有很多好处,而且非常适合用来实现 FIFO 式的固定长度队列。在 channel 中,ring buffer 的实现如下:

hchan 中有两个与 buffer 相关的变量:recvx 和 sendx。其中 sendx 表示buffer 中可写的 index,recvx 表示 buffer 中可读的 index。从 recvx 到sendx 之间的元素,表示已正常存放入 buffer 中的数据。
我们可以直接使用 buf[recvx]来读取到队列的第一个元素,使用 buf[sendx] =
x 来将元素放到队尾。
最后:
点击下方名片链接,关注 「码农编程进阶笔记 」微信公众号,在微信聊天对话框回复「go电子书」「黑马Go语言」或者直接长按左下图海报中的二维码,可获取最新golang电子书和视频资源
