425 lines
13 KiB
Markdown
425 lines
13 KiB
Markdown
---
|
||
tags: [go, golang, interview, basic-questions]
|
||
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 的四大优势,你会怎么组织语言?试着用自己的话概括。
|
||
|
||
## 参考答案
|
||
|
||
1. **语法简洁务实**:每个功能设计都以提升开发效率为目标
|
||
2. **原生并发支持**:Goroutine + Channel 实现高效的 GMP 调度模型
|
||
3. **代码风格统一**:单一标准格式(`gofmt`),可读性高
|
||
4. **高效垃圾回收**:支持并行 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` | 任何类型 |
|
||
| 返回值 | 初始化后的数据结构本身 | 指向零值的指针 |
|
||
|
||
```go
|
||
// 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()` 或数组截取 |
|
||
|
||
```go
|
||
// 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
|
||
// 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,可自定义分隔符 | 已知切片长度的场景 |
|
||
|
||
```go
|
||
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),与声明顺序相反。
|
||
|
||
```go
|
||
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 可以修改返回值
|
||
|
||
```go
|
||
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 |
|
||
|
||
```go
|
||
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 的区别? 🟢简单
|
||
|
||
## 参考答案
|
||
|
||
```go
|
||
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 不占用任何空间:
|
||
|
||
```go
|
||
unsafe.Sizeof(struct{}{}) // 输出: 0
|
||
```
|
||
|
||
**三大用途:**
|
||
1. **模拟 Set**:`map[string]struct{}` 代替 `map[string]bool`,节省内存
|
||
2. **Channel 信号**:`chan struct{}` 仅用于通知,不传数据
|
||
3. **仅有方法的类型**:如工具类 `type Logger struct{}`
|
||
|
||
---
|
||
|
||
### Q14:init() 函数什么时候执行? 🟡中等
|
||
|
||
> [!question] ❓ 思考一下
|
||
> main 包可以有 init() 吗?多个包的 init() 执行顺序是什么?
|
||
|
||
## 参考答案
|
||
|
||
**执行顺序**:import -> const -> var -> `init()` -> `main()`
|
||
|
||
1. 在 `main()` 之前执行
|
||
2. 按依赖关系初始化(无依赖的最先初始化)
|
||
3. 同一个包内可以有多个 `init()`,执行顺序**不保证**
|
||
4. `init()` 无参数、无返回值、不能被调用
|
||
|
||
> [!warning] ⚠️ 高频陷阱
|
||
> 不要在 `init()` 中做耗时操作或依赖外部服务!它应该在编译期就能确定执行逻辑。
|
||
|
||
---
|
||
|
||
### Q15-16:接口比较与 nil 问题 🟡中等
|
||
|
||
## 参考答案
|
||
|
||
**两个 interface 相等的条件:**
|
||
1. 两者均为 nil
|
||
2. 类型 T 相同且值 V 相等
|
||
|
||
**两个 nil 可能不相等!** 只有类型相同时才相等:
|
||
|
||
```go
|
||
var p *int = nil
|
||
var i interface{} = nil
|
||
// p == i → false(*int != interface{})
|
||
```
|
||
|
||
**nil interface 的经典陷阱:**
|
||
|
||
```go
|
||
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:如何判断变量分配在栈上还是堆上? 🟡中等
|
||
|
||
## 参考答案
|
||
|
||
通过**逃逸分析**判断:
|
||
|
||
```bash
|
||
go build -gcflags '-m -m -l' xxx.go
|
||
```
|
||
|
||
**逃逸场景:**
|
||
- 返回局部变量指针
|
||
- 传递给 `interface{}` 参数
|
||
- 被闭包引用
|
||
- 超过栈大小限制的大对象
|
||
|
||
---
|
||
|
||
### Q19:Go 多返回值如何实现? 🟡中等
|
||
|
||
## 参考答案
|
||
|
||
Go 的多返回值通过在**调用方栈帧上预留空间**实现:
|
||
|
||
1. 编译器计算所有返回值的总大小
|
||
2. 在 caller 的栈帧上预留连续空间
|
||
3. callee 执行 return 时将值复制到预留空间
|
||
4. caller 直接从自己的栈帧读取
|
||
|
||
> [!info] 🔗 延伸阅读
|
||
> - [[hzh/GolangStar/Go语言原理/defer原理]] — defer 与有名返回值的交互
|
||
> - [[hzh/GolangStar/Go面试题库/代码面试题]] — 多返回值实战
|
||
|
||
---
|
||
|
||
### Q20:Go 中 `_` 的作用 🟢简单
|
||
|
||
## 参考答案
|
||
|
||
1. **忽略返回值**:`x, _ := m["key"]`
|
||
2. **匿名导入**:`_ "net/http/pprof"`(仅执行 init 注册 profiling)
|
||
3. **忽略循环变量**:`for _, v := range slice`
|
||
|
||
---
|
||
|
||
### Q21-22:指针相关概念 🟡中等
|
||
|
||
## 参考答案
|
||
|
||
| 对比项 | 普通指针 (`*int`) | `unsafe.Pointer` | `uintptr` |
|
||
|--------|------------------|-----------------|-----------|
|
||
| 类型安全 | 是,受编译器检查 | 否,通用指针 | 是(整数类型) |
|
||
| GC 跟踪 | 是 | 是 | **否** |
|
||
| 指针运算 | 不支持 | 不支持 | 支持(需转回 Pointer) |
|
||
| 类比 C | 强类型指针 | `void*` | 裸整数地址 |
|
||
|
||
> [!warning] ⚠️ 高频陷阱
|
||
> `uintptr` 只是一个整数,GC 不知道它指向内存,对应内存可能被回收。`unsafe.Pointer` 受 GC 保护。这是最本质的区别。
|
||
|
||
## 关联笔记
|
||
|
||
- [[hzh/GolangStar/Go语言基础/Go语言变量]]
|
||
- [[hzh/GolangStar/Go语言基础/Go语言指针]]
|
||
- [[hzh/GolangStar/Go语言基础/Go语言defer]]
|
||
- [[hzh/GolangStar/Go语言进阶/反射]]
|
||
- [[hzh/GolangStar/Go语言原理/逃逸分析]]
|