Go面试八股(持续更新)

远野千束技术2025/10/2216 minutes130104257 chars

Go面试八股(持续更新)

slice和array

type slice struct {
    ptr unsafe.Pointer // 指向底层数组第 0 个元素
    len int            // 当前切片长度(可读写的元素个数)
    cap int            // 从 ptr 开始到底层数组末尾的元素个数
}

  1. slice底层:一个结构体,ptr/len/cap三个字段,ptr指向底层数组,len代表长度,cap代表容量。

  2. slice传递机制:slice类型在传递时本质上是slice结构体值传递,ptr指向底层数组;在传参到函数内部时,若内部的slice发生了重新分配,则内部的slice.ptr和外部slice.ptr会指向不同的底层数组(俗称“脱钩”),若没有发生重新分配,则指向了同一个,此时对内部的slice进行修改,可以修改外部。只读或者修改元素场景下,可以直接把slice传入函数进行修改,简单高效。如果是可能扩容或者需要更改header的函数,必须用变量来接收返回值。

  3. array:一块连续定长的内存,紧密排列布局。

  4. slice和array的区别

    1. slice长度可变,非类型[]T,array不行且是类型(比如[4]T和[5]T是两个类型)

    2. array变量本身代表整段数据,slice变量本身就是个24bytes的结构体,数据在ptr指向的内存堆栈;

    3. array传参时深拷贝整个对象,slice传参只传结构体

    4. array不可扩容,slice可扩容

    5. array无元数据,slice有len和cap字段

    6. arr[:]得到切片,slice不能直接退化成数组

  5. slice重新分配:之所以append要接收,是因为append后可能会触发slice重新分配导致函数内的slice变了外部没有变,因此需要slc = append(slc, e)

  6. slice扩容机制:slice 扩容 = 值拷贝 + 新数组 + 改 header

    1. 先计算新slice的容量,若newLen < cap,复用原数组,不会搬家,只改len字段

    2. 若新长度对现有容量不够,则按照1024为分界进行不同规则扩容,oldLen < 1024 -> newCap = 2 * oldCapoldLen > 1024 -> newCap = 1.25 * oldCap(小于1k翻倍,大于1k加四分之一)

    3. 然后把 newCap 按元素大小对齐到内存分配器的 size class,最终向上取整到某个桶大小(方便 GC/分配器复用)。

  7. copy:是 Go 提供的纯内存级拷贝工具,功能单一,但坑点集中。记住三句话:“只拷 min 长,不扩 dst 容;值拷一层浅,指针仍共享;返回实际数,安全又透明。”

    1. 对基本类型(int, float, struct 值)→ 真正深拷贝到 dst 的新内存。

    2. 对指针或含指针的元素 → 只拷指针值,指向的对象仍共享(浅拷贝)。

    3. 整段复制用 memmove ,底层保证重叠区域安全。

func copy(dst, src []T) int
  1. slice传参的内存逃逸问题:再大slice传参也只是24B的结构体,与容量和长度无关,读/改元素不会逃逸;存全局/channel/闭包函数/返回必然会逃逸到堆;触发append且需要扩容时,growslice调用mallocgc新数组会直接生在堆上

    1.   决定因素:数据是否活过函数周期,是则逃逸

    2. 存全局逃逸到堆

    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
    }
  1. 完全释放s=nil整切片置零;s=s[:0:0]len=cap=0赋0,ptr=nil,失去引用被回收,ptr=nil的slice可以用append

  2. 并发安全

    1. 只读:完全安全,Go 的内存模型保证:只要没有任何写操作,任意数量的 goroutine 并发读同一块内存都不会产生 data race。

    2. 一读一写,100%出现竞态,slice 的底层数组就是普通内存,读写都不带同步;哪怕只改一个元素,也会对读端产生未定义结果(读到旧值、半写值、甚至 crash),解决思路:锁sync.Mutex, syncRWMutex, 原子操作sync/atomic, 排队处理goroutine, 复制一份快照读。

map

  1. 底层实现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(),写锁要等读锁释放

Copyright:Go面试八股(持续更新)

Posted at
10/22/2025

Unless stated otherwise, articles are licensed under CC BY-NC-SA 4.0. Please credit the source.

1 users liked
Comment (0)

Load more

关于我
S

SnowyKami

A full-stack developer.

Tags cloud
HomePage.already_in_love_n_day
Misskey
Go面试八股(持续更新) - Snowykami's Blog