Files
cs-note/hzh/EXAM/Week02.md
T
2026-05-24 11:42:38 +08:00

487 lines
16 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, 后端开发, 武科大, Week02, 错题精讲]
create time: 2026-05-02 14:30
---
# 金山考试 - 武科大服务端 - 第二次课 · 错题精讲
## 概述
本次测试涵盖 **切片与数组、map、结构体与方法、接口、JSON、标准库(fmt/os/time)、内存分配 new vs make、并发(channel/sync)** 八大模块,共 37 道题(21 单选 + 3 多选)。其中 **36 道答对,1 道错题**,集中在 `map` 的引用语义与 `json.Unmarshal` 参数传递规则上。本笔记以错题为切入点,逐层展开相关知识。
> [!tip] 使用建议
> 先看错题本身的「为什么错 → 为什么对」,再顺着带出的知识点体系理解关联概念,最后用「高频考点速览」串联记忆薄弱点。
---
## 一、map 的引用语义与 json.Unmarshal 必须传地址
### 单6:关于 map 的正确说法
| 项目 | 内容 |
|------|------|
| ❌ 你的答案 | B |
| ✅ 正确答案 | **A** |
```go
// 【题目】关于 map,下面说法正确的是?
// A. map 反序列化时 json.Unmarshal() 的入参必须为 map 的地址 ✅
// B. 在函数调用中传递 map,则子函数中对 map 元素的增加不会导致父函数中 map 的修改 ❌
// C. 不能使用内置函数 delete() 删除 map 的元素 ❌
// D. 以上都不对 ❌
```
> [!error] 你的误区根源
> 你选了 **B**,认为「map 传进去后子函数的增删改不会影响外部」。这是把 map 当成了**值类型**。实际上 map 是 Go 中的**引用类型**——它的底层是一个指向哈希表的指针(runtime hmap),无论怎么传,大家都指向同一个数据源。
### 为什么 A 对:Unmarshal 必须传地址
```go
m := make(map[string]int)
// ❌ 错误:传值,内部修改只在局部生效
// json.Unmarshal([]byte(`{"a":1}`), m) // panic: assignment to entry in nil map
// ✅ 正确:传地址 &m,Unmarshal 才能往里写
json.Unmarshal([]byte(`{"a":1,"b":2}`), &m)
fmt.Println(m) // map[a:1 b:2]
```
> [!abstract] Unmarshal 的设计原理
```mermaid
flowchart TD
A["json.Unmarshal data"] --> B{入参是 *any}
B -->|"传的是 &m"| C["*map 解引用 → 拿到原始 map 指针"]
C --> D["向原始 map 写入 key-value ✅"]
B -->|"传的是 m(值)"| E["m 本身是指针,传给 any 后丢失"]
E --> F["无法获取原始引用,写入失败或无效果 ❌"]
style C fill:#9f9
style F fill:#f99
```
核心逻辑:`json.Unmarshal` 的签名是 `func Unmarshal(data []byte, v any)`,第二个参数接收 `any` 类型。当传入 `&m` 时,内部通过反射检查到这是一个指向 map 的指针,于是能直接向原 map 写入数据。如果传 `m`,由于 map 本身就是引用类型但不满足 `Settable` 条件(reflect 层面 value 不可取址),写入会静默失败或直接 panic。
### 为什么 B 错:map 是引用类型,子函数可以修改父函数
```go
func addElement(m map[string]int) {
m["newKey"] = 999 // ✅ 直接往原 map 里添加
}
func main() {
m := make(map[string]int)
m["a"] = 1
addElement(m)
fmt.Println(m) // map[a:1 newKey:999] ← 父函数可见!
}
```
> [!memory] map 不是"副本"
> map 变量本质上就是一个指针(准确说是 runtime hmap 的引用)。赋值、传参都是共享同一个底层数据结构。
### 对比验证:切片 vs array 的行为差异
> [!note] 这个对比帮你彻底分清引用类型和值类型
>
> ```go
> // --- slice:引用类型,子函数内增删改对外可见 ---
> func modifySlice(s []int) {
> s[0] = 99 // 修改元素 → 外可见 ✅
> s = append(s, 100) // append 可能换底层数组 → 外不可见(取决于是否扩容)
> }
>
> // --- array:值类型,子函数拿到完整副本 ---
> func modifyArray(a [3]int) {
> a[0] = 99 // 只改了副本 → 外不可见 ❌
> }
>
> func main() {
> arr := [3]int{1, 2, 3}
> modifyArray(arr)
> fmt.Println(arr) // [1 2 3] ← 没变
>
> sl := []int{1, 2, 3}
> modifySlice(sl)
> fmt.Println(sl) // [99 2 3] ← 变了
> }
> ```
### C 选项为什么错:delete 可以正常删除
```go
m := map[string]int{"a": 1, "b": 2, "c": 3}
delete(m, "b") // ✅ 成功删除
fmt.Println(m) // map[a:1 c:3]
delete(m, "z") // ✅ key 不存在也没事,不 panic
```
---
## 二、由 map 带出:切片的引用语义
### 单4 & 多选3:切片与数组的核心区别
既然上面提到了切片和 map 一样是引用类型,这里一并梳理清楚。
> [!summary] slice header 结构
```mermaid
flowchart LR
A["slice header"] --> B["pointer → 底层数组首元素"]
A --> C["len → 当前长度"]
A --> D["cap → 容量"]
E["底层数组 [10 3 4 5 ...]"] -.->|"pointer 指向"| B
style A fill:#e1d5e
```
```go
// 单选题4:切片操作改变底层数组
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // slice = [2, 3, 4], 底层共享 arr[1..4]
slice[0] = 10 // slice[0] → arr[1]
fmt.Println(arr) // [1 10 3 4 5] ✅ 选 B
```
> [!question] 💡 思考
> 以下代码输出什么?为什么和上一题结果不同?
```go
src := []int{1, 2, 3} // cap = 3
dst := append(src, 4) // cap 不够,翻倍分配到新底层数组
src[0] = 99 // 只改 src 的底层
fmt.Println(dst[0]) // ? → 1(因为 dst 已脱离共享)
```
### 多选题3:数组和切片的四组说法全辨析
> [!error] 这道题你全选了 ABCD —— 恭喜答对了,巩固一下记忆:
| 说法 | 对错 | 解析 |
|------|------|------|
| A. 数组定义时必须指定长度,且长度是类型的一部分 | ✅ | `[5]int` 和 `[6]int` 是**不同类型** |
| B. 切片定义不需要指定长度,长度运行时动态变化 | ✅ | `[]int` 没有固定大小 |
| C. 数组作为函数参数传的是副本 | ✅ | 整个数组会被拷贝,代价大 |
| D. 切片传参传的是 header 副本,但对内容的修改反映到原始切片 | ✅ | 拷贝的是 pointer/len/cap,不改这三个就行 |
---
## 三、声明方式:new vs make
### 单选题1 & 单选题14
> [!abstract] new 和 make 是 Go 新手最容易混淆的概念之一
```go
// make — 仅用于 slice / map / channel,返回类型本身
s := make([]int, 0, 10) // 创建一个 len=0, cap=10 的整型切片 ✅ 单选题2 答案
m := make(map[string]int) // 初始化一个可用的 map
ch := make(chan int, 5) // 创建缓冲大小为 5 的 channel
// new — 适用于任何类型,返回 *T(指向零值的指针)
p := new(int) // *int,p 指向 0
per := new(Person) // *Person,per 指向 Person{} 零值实例 ✅ 单选题14 答案
```
> [!warning] ⭐ 单选题1的经典陷阱
> `new([]int)` **不是**一个切片,而是一个 `*[]int`(指向 nil 切片的指针)!所以单选题1 选 D 是错误的声明方式。
| 维度 | make(T, args) | new(T) |
|------|---------------|--------|
| 返回类型 | T 本身 | *T |
| 适用类型 | 仅限 slice/map/channel | 任意类型 |
| 初始化 | 完成内部数据结构分配 | 仅分配零值内存 |
---
## 四、结构体与方法
### 单选题7:字段访问语法
> [!note] Go 中没有 `->` 运算符
```go
type Person struct {
Name string
Age int
}
p := Person{Name: "Alice", Age: 20}
p.Name // ✅ 值访问
pp := &p
pp.Name // ✅ 指针也直接用 .(编译器自动解引用)
(*pp).Name // ✅ 手动解引用,等价于上一行
// pp->Name // ❌ Go 不支持这种语法
```
### 单选题8 & 单选题20 & 单选题21:值接收者 vs 指针接收者
> [!memory] 三条铁律
>
> 1. **方法需要修改对象 → 必须用指针接收者**
> 2. **结构体很大 → 优先用指针接收者避免拷贝开销**
> 3. **同一类型的部分方法用了指针接收者 → 其余方法也应统一用指针**
```go
// 单选题21 原代码解析
type Person struct {
Name string
Age int
}
// 值接收者 — p 是副本,修改不影响原对象
func (p Person) ChangeName(newName string) {
p.Name = newName // 只改了副本
}
func main() {
p := Person{Name: "Alice"}
p.ChangeName("Bob")
fmt.Println(p.Name) // Alice ❌ 没有被修改(选 B)
}
// 如果用指针接收者,就会修改原对象
func (p *Person) SetName(newName string) {
p.Name = newName // 修改原对象
}
func main() {
p := Person{Name: "Alice"}
p.SetName("Bob") // 编译器自动转 (&p).SetName("Bob")
fmt.Println(p.Name) // Bob ✅
}
```
> [!question] 💡 单选题8 为什么选 C 是错的?
> C 说「值接收者在传递参数时效率更高」——这不一定!对于小结构体差别不大,但对于大结构体,值拷贝反而更慢。**值接收者的真正用途是保证方法不修改对象**,而非性能优化。因此 C 是错误的说法。
---
## 五、接口实现
### 单选题9:如何实现一个接口
> [!abstract] 鸭子类型(Duck Typing)
>
> "如果一个东西走起来像鸭子、叫起来像鸭子,那它就是鸭子。"
```go
type Writer interface {
Write(data []byte) (n int, err error)
}
// 只需实现接口中的所有方法,无需显式声明 implements
type MyWriter struct { buf []byte }
func (w MyWriter) Write(data []byte) (n int, err error) {
w.buf = append(w.buf, data...)
return len(data), nil
}
var _ Writer = MyWriter{} // 编译期校验:MyWriter 确实实现了 Writer
```
> [!memory] 一句话口诀
> **"只需要声明并实现接口中的所有方法 —— 编译器自动完成适配。"**
>
> B 错在「部分实现」,A 不完整(还需要「实现」),D 错在接口不能被继承(Go 没有继承概念)。
---
## 六、标准库用法
### 单选题10:JSON 处理
```go
import "encoding/json"
data := map[string]int{"a": 1, "b": 2}
bytes, err := json.Marshal(data) // 序列化
var result map[string]int
err = json.Unmarshal(bytes, &result) // 反序列化(注意 &result)
```
> [!tip] 优先使用 `encoding/json` 标准库即可,除非有特殊性能需求才考虑 jsoniter 等第三方包。单选题10 选 A。
### 单选题11:格式化输出
```go
fmt.Print("hello") // 无换行
fmt.Println("hello") // 自动换行
fmt.Printf("%d %s", 42, "hi") // 格式化输出 ✅ 单选题11 答案
// fmt.Format() 不存在
```
### 单选题12:读取文件
```go
// ✅ 现代写法(Go 1.16+)
data, err := os.ReadFile("config.json")
// ⚠️ ioutil 已在 Go 1.16 标记废弃
// data, err := ioutil.ReadFile("config.json") // 旧写法
```
### 单选题13:时间处理
```go
import "time"
now := time.Now() // 当前时间
formatted := now.Format("2006-01-02 15:04:05") // 格式化
```
> [!note] Go 的时间格式固定使用 reference time:`2006-01-02 15:04:05`(即 2006-1-2 15:04:05 MST)
---
## 七、并发编程
### 单选题15:无缓冲 channel 的特点
> [!abstract] 无缓冲 channel = 同步通信
```go
ch := make(chan int) // 无缓冲
go func() { ch <- 42 }() // 启动发送方
x := <-ch // 接收方等待 → 收发同时发生 ✅ 单选题15 答案
// 如果只有发送方在运行,<-ch 会永久阻塞(goroutine leak)
```
> [!warning] ⭐ 核心概念
> 无缓冲 channel 的发送和接收**必须同时就绪**,缺一不可。这是一种 goroutine 之间的握手同步机制。
### 单选题16:Select 语句
```go
select {
case msg := <-ch1:
fmt.Println(msg)
case ch2 <- data:
fmt.Println("sent")
default:
fmt.Println("none ready") // 都不阻塞时的兜底
}
```
> [!memory] select = switch for channels
> 监听多个 channel 操作,哪个准备好了执行哪个;都不就绪且有 default 则立即走 default。单选题16 选 C(监听多个 Channel 操作)。
### 单选题17:sync.WaitGroup 确保 goroutine 同步
```go
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done() // 退出时计数器 -1
doWork(id)
}(i)
}
wg.Wait() // 阻塞直到所有 goroutine 完成 ✅ 单选题17 答案
```
> [!note] Mutex 更适合保护临界区,Atomic 适合简单计数,Context 适合取消信号传递。WaitGroup 是唯一专门用于「等一组 goroutine 完成」的原语。
### 单选题19:RWMutex vs Mutex
```mermaid
flowchart LR
subgraph Mutex["sync.Mutex"]
M1["同一时刻只能有\n一个 goroutine\n进入临界区"]
end
subgraph RWMutex["sync.RWMutex"]
R1["多个读锁\n可同时持有"]
R2["或一个写锁\n独占访问"]
end
Mutex -->|"读少写多时更高效"| W1
RWMutex -->|"读多写少时优势明显"| W2
style R1 fill:#9f9
```
> [!memory] 一句话区分
> **Mutex 一人进出,RWMutex 多人同读一人独写。**
---
## 八、基础语法补遗
### 单选题18:取地址运算符
```go
x := 42
px := &x // & 获取地址 → *int
fmt.Println(*px) // * 解引用 → 42
// 结构体同理
p := Person{Name: "Alice"}
pp := &p
pp.Name // Go 自动解引用(等价于 (*p).Name)
```
> [!memory] Go 中关于地址的两个符号
>
> | 符号 | 名称 | 作用 |
> |------|------|------|
> | `&` | 取地址符 | `px := &x` |
> | `*` | 解引用 / 指针类型前缀 | `val := *px`, `var px *int` |
### 多选题1:通过指针访问成员变量的方式
> [!abstract] 多选题1 全辨析
| 选项 | 代码 | 对错 | 解析 |
|------|------|------|------|
| A | `p.name` | ✅ | Go 的语法糖,自动解引用 |
| B | `(&p).name` | ❌ | `p` 已经是变量名,再加 `&` 变成指向指针的指针 |
| C | `(*p).name` | ✅ | 先解引用拿到结构体,再访问字段 |
| D | `p->name` | ❌ | Go 没有 `->` 运算符 |
> [!note] 注意区分:如果是 `p` 已经是 `*Person` 指针类型,则 `p.name` 和 `(*p).name` 都合法。`&p` 会得到 `**Person`,多了一层无意义间接。
---
## 错题自测表
> [!summary] 遮住右侧,自己检验是否真正理解了
| 题目 | 考查点 | ❌ 你的原答案 | ✅ 正确答案 | 关键概念 |
|------|--------|-------------|------------|---------|
| 单6 | map 的引用语义 + json.Unmarshal 参数 | B | **A** | Unmarshal 需传地址;map 是引用类型 |
---
## 高频考点速览
> [!tip] 考前快速过一遍
| 类别 | 考点 | 一句话口诀 |
|------|------|-----------|
| make vs new | make 初始化引用类型,new 分配零值内存 | **"make 建结构,new 清空白"** |
| slice 共享底层 | 修改切片元素 = 修改原数组对应位置 | **"切片是视图,底层是一家的"** |
| map 引用语义 | 传 map 不拷贝数据,内外同步可见 | **"map 传指针,改哪都看见"** |
| json.Unmarshal 参数 | 必须传 `&mapVar` 才能写入 | **"没地址写不进"** |
| 结构体字段访问 | Go 统一用 `.` | **Go 里没有 `->`** |
| 值 vs 指针接收者 | 要修改对象必须用指针 | **要改就用指针** |
| 接口隐式实现 | 实现全部方法即满足 | **鸭子类型,不用 declare** |
| 无缓冲 channel | 收发必须同时就绪 | **"等你,你等我"** |
| select | 多路复用 channel 操作 | **"谁先好谁先上"** |
| WaitGroup | 等待一组 goroutine 完成 | **加 1 → 起任务 → Done → Wait** |
| RWMutex | 读共享、写独占 | **多人同读,一人独写** |
| 取地址运算符 | `&` 取地址,`*` 解引用 | **"& 取 *" |
| ioutil 废弃 | 被 os/io 包取代 | **Go 1.16+ 别再用 ioutil** |
---
## 关联笔记
- [[hzh/EXAM]]