一、语言基础与特性
- Go 语言的特性/优势是什么?
- 简洁的语法,学习成本低。
- 强大的并发编程(goroutine, channel)。
- 垃圾回收(GC)。
- 编译型语言,性能高,部署简单(单一二进制文件)。
- 丰富的标准库。
- 静态类型,强类型。
- 内置工具链(go fmt, go test, go mod 等)。
- GOPATH 与 Go Modules 的区别?
- GOPATH 是旧的项目管理和依赖方式,所有项目代码必须放在 GOPATH/src 下。(已基本废弃)
- Go Modules(Go 1.11 引入,1.16 默认启用)是现代的依赖管理工具,允许项目放在任何位置,通过 go.mod 和 go.sum 文件管理依赖和版本。现在必须掌握 Modules。
- 值类型和引用类型有哪些?
- 值类型:基本数据类型(int, float, bool, string),数组(array),结构体(struct)。传递时进行拷贝
- 引用类型:切片(slice),映射(map),通道(channel),指针(pointer),函数(func),接口(interface)。传递时传递的是底层数据的引用。
- 数组(array) 和 切片(slice) 的区别?
- 数组:长度固定,是值类型。[3]int 和 [5]int 是不同的类型。
- 切片:动态数组,长度可变,是引用类型。底层引用一个数组。由 ptr(指向底层数组的指针)、len(长度)、cap(容量)三部分组成。
- 切片的扩容机制?
- 当容量不足时,会创建一个新的更大的底层数组,将旧数据拷贝过去。
- 大致规则(旧版本):容量 < 1024 时,翻倍扩容;>= 1024 时,每次增加 25%。注意:Go 1.18 及以后,扩容策略有调整,系数更平滑,但核心思想不变。面试能说出“根据容量决定增长系数”即可。
- make 和 new 的区别?
- new(T):为类型 T 分配零值内存,返回一个 指针 *T。适用于所有类型。
- make(T, args):仅用于创建 slice, map, channel 这三种引用类型,返回的是 初始化后的 T 类型(不是指针) ,因为它需要初始化内部数据结构。
- defer 的执行顺序和特性?
- 多个 defer 按 后进先出(LIFO栈) 的顺序执行。
- defer 语句中的参数(非闭包情况)在 defer 声明时就会求值并固定。
- 常用于资源释放(关闭文件、解锁)、捕获 panic 等。
- 函数传参是值传递还是引用传递?
- Go 语言只有值传递。但是当传递引用类型(slice, map, channel)或指针时,拷贝的是这个“引用”或“指针”本身,所以函数内部可以通过它修改指向的数据。
二、并发编程(重中之重!)
- Goroutine 和线程的区别?
- 内存占用:Goroutine 初始栈很小(KB 级),可动态扩缩;线程栈固定(MB 级)。
- 调度:Goroutine 由 Go 运行时(runtime)在用户态调度,上下文切换成本极低;线程由操作系统内核调度,成本高。
- 关系:M:N 模型,多个 Goroutine 复用在少量操作系统线程上。
- Channel 的特性?有缓冲和无缓冲 Channel 的区别?
- 用于 Goroutine 间的通信和同步。
- 无缓冲 Channel :发送和接收必须同时准备好,否则会阻塞。是同步的。
- 有缓冲 Channel :缓冲区未满时可发送不阻塞,缓冲区非空时可接收不阻塞。是异步的。
- select 的用途和特性?
- 用于监听多个 channel 的发送/接收操作。
- 当多个 case 同时就绪时,随机选择 一个执行。
- 常用 default 分支实现非阻塞的收发。
- 同步原语:sync.Mutex (互斥锁) 、sync.RWMutex (读写锁) 、sync.WaitGroup、sync.Once、sync.Map 的适用场景?
- Mutex:保护临界区,互斥访问
- RWMutex:读多写少的场景,允许多个读锁。
- WaitGroup:等待一组 Goroutine 完成。
- Once:确保某个操作只执行一次(如单例初始化)。
- sync.Map:适用于读多写少且 key 相对稳定的并发 map,比 map+Mutex/RWMutex 在特定场景下性能更好。
- 如何优雅地关闭 Channel?或如何避免对已关闭 Channel 的操作?
- 原则:只由发送方关闭 Channel,且只关闭一次。
- 接收方可通过 val, ok := <-ch 的 ok 值判断 Channel 是否已关闭。
- 可以使用 context 包来协调多个 Goroutine 的取消和超时。
- 什么是 Context?它的作用是什么?
- 用于在 API 边界间传递截止时间(deadline) 、取消信号(cancellation) 和请求域的值。
- 核心方法:WithCancel, WithTimeout, WithDeadline, WithValue。
- 常用于控制 Goroutine 的生命周期,实现超时和取消。
- Goroutine 泄露是什么?如何避免?
- Goroutine 启动后永远无法退出,导致内存和资源无法释放。
- 避免方法:使用 context 控制退出;确保 channel 的发送/接收能正常结束;使用 WaitGroup 等待所有 Goroutine 完成。
三、 内存管理与GC
- Go 的垃圾回收(GC)原理?三色标记法是什么?
- Go 使用 并发标记-清扫(Concurrent Mark-Sweep) 算法。
- 三色标记法:
- 白色:未被引用的对象(待回收)。
- 灰色:存活对象,但其引用的对象还未被扫描。
- 黑色:存活对象,且其引用的对象都已被扫描。
- 过程:从根对象(全局变量、栈等)出发,将可达对象标记为黑色,最后回收白色对象。整个过程与用户程序并发执行,但有短暂的 STW(Stop-The-World)阶段。
- 如何优化GC性能?
- 核心是减少堆上的内存分配。
- 手段:使用对象池(sync.Pool);尽量使用值类型而非指针;复用 slice([:0] 清空);避免在循环中创建临时对象等。
- 逃逸分析是什么?
- 编译器在编译阶段决定一个变量应该分配在栈上还是堆上。
- 逃逸到堆:如果变量的生命周期超出了函数范围(如被返回、被全局引用、被闭包捕获等),它就会“逃逸”到堆上分配,由 GC 管理。
- 查看命令:go build -gcflags="-m"。
四、高级特性与底层原理
- Interface 的底层实现(iface 和 eface)
- iface:包含方法的接口。数据结构包含 tab(指向类型和方法信息的指针)和 data(指向实际数据的指针)。
- eface:空接口(interface{})。数据结构包含 _type(指向类型信息)和 data。
- 接口的动态类型和动态值:接口变量可以持有任何实现了该接口的具体类型的值。
- 反射(reflect)的原理和注意事项
- 通过 reflect.TypeOf 和 reflect.ValueOf 获取类型和值信息。
- 反射性能较低,应谨慎使用。
- 常用于序列化/反序列化、框架等需要运行时类型操作的场景。
- Map 的底层原理?遍历顺序是随机的吗?
- 底层是哈希表,使用拉链法解决哈希冲突(现在新版使用桶数组+溢出桶)。
- 是的,遍历顺序是随机的。这是语言设计者故意为之,以防止开发者依赖不稳定的遍历顺序。
- GPM 调度模型?
- G:Goroutine。
- P:Processor,逻辑处理器,管理一组 Goroutine 队列(本地队列和全局队列)。
- M:Machine,操作系统线程,真正执行计算的部分。
- 调度流程:P 从队列中取出 G 交给 M 执行。当 G 发生系统调用阻塞时,P 会与 M 解绑,寻找空闲的 M 或创建新的 M 来执行其他 G,以充分利用 CPU。