Files
cs-note/hzh/GolangStar/Go面试题库/基础面试题.md
T

425 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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语言原理/逃逸分析]]