Go面试八股(持续更新)
slice和array
type slice struct {
ptr unsafe.Pointer // 指向底层数组第 0 个元素
len int // 当前切片长度(可读写的元素个数)
cap int // 从 ptr 开始到底层数组末尾的元素个数
}slice底层:一个结构体,ptr/len/cap三个字段,ptr指向底层数组,len代表长度,cap代表容量。
slice传递机制:slice类型在传递时本质上是slice结构体值传递,ptr指向底层数组;在传参到函数内部时,若内部的slice发生了重新分配,则内部的slice.ptr和外部slice.ptr会指向不同的底层数组(俗称“脱钩”),若没有发生重新分配,则指向了同一个,此时对内部的slice进行修改,可以修改外部。只读或者修改元素场景下,可以直接把slice传入函数进行修改,简单高效。如果是可能扩容或者需要更改header的函数,必须用变量来接收返回值。
array:一块连续定长的内存,紧密排列布局。
slice和array的区别:
slice长度可变,非类型[]T,array不行且是类型(比如[4]T和[5]T是两个类型)
array变量本身代表整段数据,slice变量本身就是个24bytes的结构体,数据在ptr指向的内存堆栈;
array传参时深拷贝整个对象,slice传参只传结构体
array不可扩容,slice可扩容
array无元数据,slice有len和cap字段
arr[:]得到切片,slice不能直接退化成数组
slice重新分配:之所以append要接收,是因为append后可能会触发slice重新分配导致函数内的slice变了外部没有变,因此需要slc = append(slc, e)
slice扩容机制:slice 扩容 = 值拷贝 + 新数组 + 改 header
先计算新slice的容量,若newLen < cap,复用原数组,不会搬家,只改len字段
若新长度对现有容量不够,则按照1024为分界进行不同规则扩容,
oldLen < 1024 -> newCap = 2 * oldCap;oldLen > 1024 -> newCap = 1.25 * oldCap(小于1k翻倍,大于1k加四分之一)然后把
newCap按元素大小对齐到内存分配器的 size class,最终向上取整到某个桶大小(方便 GC/分配器复用)。
copy:是 Go 提供的纯内存级拷贝工具,功能单一,但坑点集中。记住三句话:“只拷 min 长,不扩 dst 容;值拷一层浅,指针仍共享;返回实际数,安全又透明。”
对基本类型(
int,float,struct值)→ 真正深拷贝到 dst 的新内存。对指针或含指针的元素 → 只拷指针值,指向的对象仍共享(浅拷贝)。
整段复制用
memmove,底层保证重叠区域安全。
func copy(dst, src []T) intslice传参的内存逃逸问题:再大slice传参也只是24B的结构体,与容量和长度无关,读/改元素不会逃逸;存全局/channel/闭包函数/返回必然会逃逸到堆;触发append且需要扩容时,growslice调用mallocgc新数组会直接生在堆上
决定因素:数据是否活过函数周期,是则逃逸
存全局逃逸到堆
var g []int // 全局根 func f() { s := make([]int, 1024) // 只在 f 里用? g = s // 赋给全局 → f 返回后仍需存活 }传入channel逃逸到堆,否则接收方会读到悬空内存
ch := make(chan []int, 1) func producer() { buf := make([]byte, 4096) ch <- buf // buf 被 send 出去 }存闭包会逃逸到堆
func f() { data := make([]int, 1024) go func() { fmt.Println(data[0]) // 协程里用 data }() }作为返回值带出会逃逸到堆
func create() []int { s := make([]int, 1024) return s // 返回 slice }
完全释放:
s=nil整切片置零;s=s[:0:0]len=cap=0赋0,ptr=nil,失去引用被回收,ptr=nil的slice可以用append并发安全:
只读:完全安全,Go 的内存模型保证:只要没有任何写操作,任意数量的 goroutine 并发读同一块内存都不会产生 data race。
一读一写,100%出现竞态,slice 的底层数组就是普通内存,读写都不带同步;哪怕只改一个元素,也会对读端产生未定义结果(读到旧值、半写值、甚至 crash),解决思路:锁sync.Mutex, syncRWMutex, 原子操作sync/atomic, 排队处理goroutine, 复制一份快照读。
map
底层实现hmap+bmap(bucket)
type hmap struct {
count int // 当前元素个数
B uint8 // 桶的数量的对数,比如 B=5,表示 2^5=32 个桶
buckets unsafe.Pointer // 指向桶数组的指针
oldbuckets unsafe.Pointer // 扩容时指向旧桶数组(增量扩容)
extra *mapextra // 溢出桶链表(拉链法)
}type bmap struct {
tophash [8]uint8 // 每个 key 的高 8 位哈希值,用于快速比较
keys [8]keytype
values [8]valuetype
overflow uintptr // 指向下一个溢出桶(拉链法)
}
// Go 源码里 bmap 是伪结构体,真实布局是 连续内存,keys 和 values 是分开存的,为了内存对齐。锁
sync.Mutex
双态锁:Lock/Unlock
只有Lock() Unlock(),谁拿谁独占,释放后才能被其他Lock()继续执行,否则会阻塞
sync.RWMutex
三态锁:无人/多读/单写
有RLock() RUnlock() Lock() Unlock(),写锁要等读锁释放