13 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-06-07 14:30 |
基础面试题 🧱
概述
本文件涵盖 Go 语言基础语法的 22 道高频面试题,包括变量、类型、控制流、内存分配等核心概念。每道题都附有思考引导和面试答题技巧。
[!tip] 💡 使用建议 先遮住答案自己思考,再对照解析学习。遇到不确定的题目,标记后在对应原理篇中寻找更深入的解释。
关联笔记
- hzh/GolangStar/Go语言基础/Go语言变量 — 变量声明与零值
- hzh/GolangStar/Go语言基础/Go语言指针 — new/make 区别
- hzh/GolangStar/Go语言基础/Go语言defer — defer 执行顺序
- hzh/GolangStar/Go语言进阶/反射 — 反射机制
- hzh/GolangStar/Go语言原理/逃逸分析 — 栈 vs 堆分配
正文
Q1:与其他语言相比,使用 Go 有什么好处? 🟢简单
[!question] ❓ 思考一下 如果让你向面试官介绍 Go 的四大优势,你会怎么组织语言?试着用自己的话概括。
参考答案
- 语法简洁务实:每个功能设计都以提升开发效率为目标
- 原生并发支持:Goroutine + Channel 实现高效的 GMP 调度模型
- 代码风格统一:单一标准格式(
gofmt),可读性高 - 高效垃圾回收:支持并行 GC,效率优于 Java 或 Python
[!tip] 💡 面试技巧 回答时按照"语法 -> 并发 -> 工具链 -> 运行时"的逻辑展开,显得条理清晰。如果能提到"Go 的设计哲学是少即是多(less is more)",会给面试官留下深刻印象。
[!note] 📝 核心考点 Go 的核心竞争力在于:简洁语法 + 原生并发 + 快速编译 + 高效 GC。面试官希望确认你理解 Go 的设计哲学,而非死记硬背特性。
Q2:什么是协程? 🟢简单
参考答案
协程是用户态轻量级线程,是线程调度的基本单位。通常在函数前加上 go 关键字即可实现并发。一个 Goroutine 以极小的栈启动(2KB 或 4KB),栈空间不足时自动伸缩,因此可以轻松启动成千上万个 Goroutine。
[!warning] ⚠️ 高频陷阱 Goroutine 不是操作系统线程!它是用户态线程,由 Go 运行时(runtime)调度,而不是由操作系统内核调度。这是 Goroutine 轻量级的根本原因。
[!info] 🔗 延伸阅读
- hzh/GolangStar/Go语言进阶/Goroutine — Goroutine 详细讲解
- hzh/GolangStar/Go语言原理/gmp调度原理 — GMP 调度模型
Q3:协程和线程、进程的区别? 🟢简单
参考答案
| 维度 | 进程 | 线程 | 协程(Goroutine) |
|---|---|---|---|
| 调度单位 | 系统资源分配最小单位 | CPU 调度基本单位 | 用户态调度 |
| 内存空间 | 独立内存空间 | 共享所属进程内存 | 独立栈,共享堆 |
| 切换开销 | 大(上下文复杂) | 中等(内核态切换) | 极小(用户态切换) |
| 通信方式 | IPC(管道/消息队列等) | 共享内存 | Channel / 共享变量 |
| 数量级 | 几十个 | 几百个 | 数十万到百万级 |
[!tip] 💡 面试技巧 可以用一句话总结:"进程重、线程中、协程轻。Go 用极小的栈空间(2KB)+ 用户态调度 = 海量并发。"
Q4:Go 中 make 和 new 的区别? 🟢简单
[!question] ❓ 思考一下 如果你要创建一个长度为 5 的 slice,你会用
make还是new?为什么?
参考答案
make 和 new 都是内存分配的内建函数,但使用场景不同:
| make | new | |
|---|---|---|
| 用途 | 初始化并分配内存 | 仅分配内存,不初始化 |
| 适用类型 | 仅限 slice、map、channel |
任何类型 |
| 返回值 | 初始化后的数据结构本身 | 指向零值的指针 |
// make:创建已初始化的 slice
s := make([]int, 5) // [0 0 0 0 0]
// new:分配内存,返回 *int 指针
p := new(int) // *p == 0
[!note] 📝 核心考点
make返回的是初始化后的值(非指针),new返回的是指向零值的指针。这是面试中最常混淆的点之一。
Q5:Go 中数组和切片的区别? 🟡中等
参考答案
| 数组 | 切片(Slice) | |
|---|---|---|
| 长度 | 固定,是类型的一部分 | 可变长 |
| 传递方式 | 值传递 | 引用传递(地址传递) |
| 底层结构 | 连续内存块 | array 指针 + len + cap |
| 初始化 | 必须指定大小或推断 | 可用 make() 或数组截取 |
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
[!warning] ⚠️ 高频陷阱
[3]int和[4]int是两种不同的类型!因为数组的长度是类型的一部分。这意味着你不能将[3]int赋值给[4]int。
Q6:for range 迭代变量的地址会变化吗? 🟡中等
[!question] ❓ 思考一下 在 for range 循环中取迭代变量的地址,存入 slice 后遍历,Go 1.22 前后行为会有什么不同?
参考答案
| Go 版本 | 行为 | 说明 |
|---|---|---|
| Go 1.22 之前 | 地址不变 | 每次迭代复用同一块内存 |
| Go 1.22 及之后 | 地址变化 | 每次迭代生成新的迭代变量 |
// Go 1.22 之前:所有元素的地址相同
// Go 1.22 及之后:每个元素的地址不同
for index, value := range collection {
_ = &value // 地址行为因版本而异
}
[!info] 🔗 延伸阅读 Go 1.22 改变了 for range 的语义,使迭代变量每次迭代都有独立的内存地址,避免了经典的"闭包捕获同一变量"陷阱。
Q7:如何高效地拼接字符串? 🟡中等
[!question] ❓ 思考一下 如果要拼接 10000 个字符串,你会选择哪种方法?为什么不用
+?
参考答案
性能排序:strings.Join ≈ strings.Builder > bytes.Buffer > "+" > fmt.Sprintf
| 方法 | 特点 | 适用场景 |
|---|---|---|
+ |
每次拼接都遍历+开辟新空间 | 少量字符串拼接 |
fmt.Sprintf |
需要反射,有性能损耗 | 需要格式化的场景 |
strings.Builder |
指针+切片,避免拷贝 | 大量字符串拼接推荐 |
bytes.Buffer |
缓冲 byte 类型数据 | 字节级别操作 |
strings.Join |
基于 Builder,可自定义分隔符 | 已知切片长度的场景 |
a := []string{"a", "b", "c"}
// 推荐:strings.Join(已知长度,预分配容量)
ret := strings.Join(a, "")
// 推荐:strings.Builder(未知长度)
var sb strings.Builder
for _, s := range a {
sb.WriteString(s)
}
ret := sb.String()
[!tip] 💡 面试技巧 如果面试官追问"strings.Join 为什么比 + 快",可以回答:"Join 提前计算总长度并一次性分配内存,而 + 每次都要重新分配。"
Q8:defer 的执行顺序和使用场景? 🟡中等
[!question] ❓ 思考一下 如果有 3 条 defer 语句,它们以什么顺序执行?有名返回值和匿名返回值对 defer 有什么影响?
参考答案
执行顺序:后进先出(LIFO),与声明顺序相反。
func test() int {
i := 0
defer func() { fmt.Println("defer1") }() // 最后执行
defer func() { i += 1; fmt.Println("defer2") }() // 最先执行
return i
}
// 输出:defer2 → defer1 → return 0
[!warning] ⚠️ 高频陷阱 有名返回值 vs 匿名返回值:
- 匿名返回值:return 创建临时变量保存结果,defer 修改不影响返回值
- 有名返回值:return 直接写入命名变量,defer 可以修改返回值
func test() (i int) { // 有名返回值
i = 0
defer func() { i += 1 }()
return i // 实际返回 1
}
[!note] 📝 核心考点 defer 的三个关键点:LIFO 顺序、延迟计算(参数在 defer 声明时就求值)、有名返回值可被修改。
Q9:什么是 rune 类型? 🟢简单
参考答案
Go 的字符有两种表示:
| 类型 | 别名 | 含义 | 字节数 |
|---|---|---|---|
byte |
uint8 |
ASCII 码的一个字符 | 1 |
rune |
int32 |
UTF-8 字符(Unicode) | 4 |
str := "hello 你好"
fmt.Println(len(str)) // 12(按字节计算:7个英文+5个汉字×3)
fmt.Println(len([]rune(str))) // 8(按字符计算)
[!tip] 💡 面试技巧 记住关键数字:UTF-8 下一个汉字占 3 个字节,
len()按字节计算,要统计字符数需转为[]rune。
Q10:Go 语言 tag 有什么用? 🟢简单
参考答案
tag 为结构体成员提供元数据属性,常见用途:
| Tag | 作用 | 示例 |
|---|---|---|
json |
JSON 序列化/反序列化字段名 | `json:"user_name"` |
db |
SQLX 数据库字段名 | `db:"id"` |
form |
Gin 框架前端字段映射 | `form:"username"` |
binding |
参数校验规则 | `binding:"required"` |
Q11:go 打印时 %v %+v %#v 的区别? 🟢简单
参考答案
type student struct { id int32; name string }
a := &student{id: 1, name: "微客鸟窝"}
fmt.Printf("%v\n", a) // &{1 微客鸟窝} -- 只输出值
fmt.Printf("%+v\n", a) // &{id:1 name:微客鸟窝} -- 显示字段名
fmt.Printf("%#v\n", a) // &main.student{id:1,name:"微客鸟窝"} -- 完整类型信息
Q12-13:空 struct{} 的空间占用和用途 🟢简单
参考答案
空 struct 不占用任何空间:
unsafe.Sizeof(struct{}{}) // 输出: 0
三大用途:
- 模拟 Set:
map[string]struct{}代替map[string]bool,节省内存 - Channel 信号:
chan struct{}仅用于通知,不传数据 - 仅有方法的类型:如工具类
type Logger struct{}
Q14:init() 函数什么时候执行? 🟡中等
[!question] ❓ 思考一下 main 包可以有 init() 吗?多个包的 init() 执行顺序是什么?
参考答案
执行顺序:import -> const -> var -> init() -> main()
- 在
main()之前执行 - 按依赖关系初始化(无依赖的最先初始化)
- 同一个包内可以有多个
init(),执行顺序不保证 init()无参数、无返回值、不能被调用
[!warning] ⚠️ 高频陷阱 不要在
init()中做耗时操作或依赖外部服务!它应该在编译期就能确定执行逻辑。
Q15-16:接口比较与 nil 问题 🟡中等
参考答案
两个 interface 相等的条件:
- 两者均为 nil
- 类型 T 相同且值 V 相等
两个 nil 可能不相等! 只有类型相同时才相等:
var p *int = nil
var i interface{} = nil
// p == i → false(*int != interface{})
nil interface 的经典陷阱:
var c Coder = (*Gopher)(nil) // 动态类型为 *Gopher,值为 nil
c == nil // false!因为动态类型不为 nil
[!note] 📝 核心考点 interface == nil 的条件是动态类型 AND 动态值都为 nil,缺一不可。这是 Go 面试中出现频率最高的陷阱题之一。
Q17:Go 函数传参是值传递还是引用传递? 🟢简单
参考答案
Go 只有值传递。 无论是值类型、引用类型还是指针类型,传递的都是副本:
- 值类型:复制实际值
- 引用类型(slice/map/chan):复制指向底层数据的指针
- 指针类型:复制指针的值
[!tip] 💡 面试技巧 "引用类型能修改外部数据不是因为引用传递,而是因为复制了指针,新旧变量指向同一块内存。"
Q18:如何判断变量分配在栈上还是堆上? 🟡中等
参考答案
通过逃逸分析判断:
go build -gcflags '-m -m -l' xxx.go
逃逸场景:
- 返回局部变量指针
- 传递给
interface{}参数 - 被闭包引用
- 超过栈大小限制的大对象
Q19:Go 多返回值如何实现? 🟡中等
参考答案
Go 的多返回值通过在调用方栈帧上预留空间实现:
- 编译器计算所有返回值的总大小
- 在 caller 的栈帧上预留连续空间
- callee 执行 return 时将值复制到预留空间
- caller 直接从自己的栈帧读取
[!info] 🔗 延伸阅读
- hzh/GolangStar/Go语言原理/defer原理 — defer 与有名返回值的交互
- hzh/GolangStar/Go面试题库/代码面试题 — 多返回值实战
Q20:Go 中 _ 的作用 🟢简单
参考答案
- 忽略返回值:
x, _ := m["key"] - 匿名导入:
_ "net/http/pprof"(仅执行 init 注册 profiling) - 忽略循环变量:
for _, v := range slice
Q21-22:指针相关概念 🟡中等
参考答案
| 对比项 | 普通指针 (*int) |
unsafe.Pointer |
uintptr |
|---|---|---|---|
| 类型安全 | 是,受编译器检查 | 否,通用指针 | 是(整数类型) |
| GC 跟踪 | 是 | 是 | 否 |
| 指针运算 | 不支持 | 不支持 | 支持(需转回 Pointer) |
| 类比 C | 强类型指针 | void* |
裸整数地址 |
[!warning] ⚠️ 高频陷阱
uintptr只是一个整数,GC 不知道它指向内存,对应内存可能被回收。unsafe.Pointer受 GC 保护。这是最本质的区别。