作者: kan 发布于 2025-12-18 15:08 公开分享

一、语言基础与特性

  1. Go 语言的特性/优势是什么?
    • 简洁的语法,学习成本低。
    • 强大的并发编程(goroutine, channel)。
    • 垃圾回收(GC)。
    • 编译型语言,性能高,部署简单(单一二进制文件)。
    • 丰富的标准库。
    • 静态类型,强类型。
    • 内置工具链(go fmt, go test, go mod 等)。
  2. GOPATHGo Modules 的区别?
    • GOPATH 是旧的项目管理和依赖方式,所有项目代码必须放在 GOPATH/src 下。(已基本废弃)
    • Go Modules(Go 1.11 引入,1.16 默认启用)是现代的依赖管理工具,允许项目放在任何位置,通过 go.mod 和 go.sum 文件管理依赖和版本。现在必须掌握 Modules。
  3. 值类型和引用类型有哪些?
    • 值类型:基本数据类型(int, float, bool, string),数组(array),结构体(struct)。传递时进行拷贝
    • 引用类型:切片(slice),映射(map),通道(channel),指针(pointer),函数(func),接口(interface)。传递时传递的是底层数据的引用
  4. 数组(array)切片(slice) 的区别?
    • 数组:长度固定,是值类型。[3]int 和 [5]int 是不同的类型。
    • 切片:动态数组,长度可变,是引用类型。底层引用一个数组。由 ptr(指向底层数组的指针)、len(长度)、cap(容量)三部分组成。
  5. 切片的扩容机制?
    • 当容量不足时,会创建一个新的更大的底层数组,将旧数据拷贝过去。
    • 大致规则(旧版本):容量 < 1024 时,翻倍扩容;>= 1024 时,每次增加 25%。注意:Go 1.18 及以后,扩容策略有调整,系数更平滑,但核心思想不变。面试能说出“根据容量决定增长系数”即可。
  6. make 和 new 的区别?
    • new(T):为类型 T 分配零值内存,返回一个 指针 *T。适用于所有类型。
    • make(T, args):仅用于创建 slice, map, channel 这三种引用类型,返回的是 初始化后的 T 类型(不是指针) ,因为它需要初始化内部数据结构。
  7. defer 的执行顺序和特性?
    • 多个 defer 按 后进先出(LIFO栈) 的顺序执行。
    • defer 语句中的参数(非闭包情况)在 defer 声明时就会求值并固定。
    • 常用于资源释放(关闭文件、解锁)、捕获 panic 等。
  8. 函数传参是值传递还是引用传递?
    • Go 语言只有值传递。但是当传递引用类型(slice, map, channel)或指针时,拷贝的是这个“引用”或“指针”本身,所以函数内部可以通过它修改指向的数据。

二、并发编程(重中之重!)

  1. Goroutine 和线程的区别?
    • 内存占用:Goroutine 初始栈很小(KB 级),可动态扩缩;线程栈固定(MB 级)。
    • 调度:Goroutine 由 Go 运行时(runtime)在用户态调度,上下文切换成本极低;线程由操作系统内核调度,成本高。
    • 关系:M:N 模型,多个 Goroutine 复用在少量操作系统线程上。
  2. Channel 的特性?有缓冲和无缓冲 Channel 的区别?
    • 用于 Goroutine 间的通信和同步。
    • 无缓冲 Channel :发送和接收必须同时准备好,否则会阻塞。是同步的。
    • 有缓冲 Channel :缓冲区未满时可发送不阻塞,缓冲区非空时可接收不阻塞。是异步的。
  3. select 的用途和特性?
    • 用于监听多个 channel 的发送/接收操作。
    • 当多个 case 同时就绪时,随机选择 一个执行。
    • 常用 default 分支实现非阻塞的收发。
  4. 同步原语:sync.Mutex (互斥锁) 、sync.RWMutex (读写锁) 、sync.WaitGroup、sync.Once、sync.Map 的适用场景?
    • Mutex:保护临界区,互斥访问
    • RWMutex:读多写少的场景,允许多个读锁。
    • WaitGroup:等待一组 Goroutine 完成。
    • Once:确保某个操作只执行一次(如单例初始化)。
    • sync.Map:适用于读多写少且 key 相对稳定的并发 map,比 map+Mutex/RWMutex 在特定场景下性能更好。
  5. 如何优雅地关闭 Channel?或如何避免对已关闭 Channel 的操作?
    • 原则:只由发送方关闭 Channel,且只关闭一次。
    • 接收方可通过 val, ok := <-ch 的 ok 值判断 Channel 是否已关闭。
    • 可以使用 context 包来协调多个 Goroutine 的取消和超时。
  6. 什么是 Context?它的作用是什么?
    • 用于在 API 边界间传递截止时间(deadline)取消信号(cancellation)请求域的值
    • 核心方法:WithCancel, WithTimeout, WithDeadline, WithValue。
    • 常用于控制 Goroutine 的生命周期,实现超时和取消。
  7. Goroutine 泄露是什么?如何避免?
    • Goroutine 启动后永远无法退出,导致内存和资源无法释放。
    • 避免方法:使用 context 控制退出;确保 channel 的发送/接收能正常结束;使用 WaitGroup 等待所有 Goroutine 完成。

三、 内存管理与GC

  1. Go 的垃圾回收(GC)原理?三色标记法是什么?
    • Go 使用 并发标记-清扫(Concurrent Mark-Sweep) 算法。
    • 三色标记法
      • 白色:未被引用的对象(待回收)。
      • 灰色:存活对象,但其引用的对象还未被扫描。
      • 黑色:存活对象,且其引用的对象都已被扫描。
    • 过程:从根对象(全局变量、栈等)出发,将可达对象标记为黑色,最后回收白色对象。整个过程与用户程序并发执行,但有短暂的 STW(Stop-The-World)阶段。
  2. 如何优化GC性能?
    • 核心是减少堆上的内存分配。
    • 手段:使用对象池(sync.Pool);尽量使用值类型而非指针;复用 slice([:0] 清空);避免在循环中创建临时对象等。
  3. 逃逸分析是什么?
    • 编译器在编译阶段决定一个变量应该分配在栈上还是堆上。
    • 逃逸到堆:如果变量的生命周期超出了函数范围(如被返回、被全局引用、被闭包捕获等),它就会“逃逸”到堆上分配,由 GC 管理。
    • 查看命令:go build -gcflags="-m"。

四、高级特性与底层原理

  1. Interface 的底层实现(iface 和 eface)
    • iface:包含方法的接口。数据结构包含 tab(指向类型和方法信息的指针)和 data(指向实际数据的指针)。
    • eface:空接口(interface{})。数据结构包含 _type(指向类型信息)和 data。
    • 接口的动态类型和动态值:接口变量可以持有任何实现了该接口的具体类型的值。
  2. 反射(reflect)的原理和注意事项
    • 通过 reflect.TypeOf 和 reflect.ValueOf 获取类型和值信息。
    • 反射性能较低,应谨慎使用。
    • 常用于序列化/反序列化、框架等需要运行时类型操作的场景。
  3. Map 的底层原理?遍历顺序是随机的吗?
    • 底层是哈希表,使用拉链法解决哈希冲突(现在新版使用桶数组+溢出桶)。
    • 是的,遍历顺序是随机的。这是语言设计者故意为之,以防止开发者依赖不稳定的遍历顺序。
  4. GPM 调度模型?
    • G:Goroutine。
    • P:Processor,逻辑处理器,管理一组 Goroutine 队列(本地队列和全局队列)。
    • M:Machine,操作系统线程,真正执行计算的部分。
    • 调度流程:P 从队列中取出 G 交给 M 执行。当 G 发生系统调用阻塞时,P 会与 M 解绑,寻找空闲的 M 或创建新的 M 来执行其他 G,以充分利用 CPU。