328 lines
11 KiB
Markdown
328 lines
11 KiB
Markdown
---
|
||
tags: [Go, 后端开发, 武科大, Week01, 错题精讲]
|
||
create time: 2026-05-02 14:30
|
||
---
|
||
|
||
# 金山考试 - 武科大服务端 - 第一次课 · 错题精讲
|
||
|
||
## 概述
|
||
|
||
本次测试涵盖 **Git、字符编码、Go 语言基础语法(注释/可见性/变量声明/switch/for/循环/自增/布尔判断/init/defer/panic/recover/字符串不可变/包管理/flag/command-line/函数签名)**三大模块,共 30 道题(20 单选 + 10 多选)。其中 25 道答对,**5 道错题**,集中在 `defer` 执行顺序、下划线 `_` 语义、以及 `switch/for` 语句特性三个知识面上。本笔记逐一拆解错误原因,给出正解辨析和代码验证。
|
||
|
||
> [!tip] 使用建议
|
||
> 先看错题本身的「为什么错 → 为什么对」,再做对照表的自测检查,最后用「高频考点速览」串联记忆薄弱点。
|
||
|
||
---
|
||
|
||
## 一、defer 的执行机制与有名返回值
|
||
|
||
### 单7:有名返回值 + defer 的输出
|
||
|
||
| 项目 | 内容 |
|
||
|------|------|
|
||
| ❌ 你的答案 | D(以上都不是) |
|
||
| ✅ 正确答案 | **C(3)** |
|
||
|
||
```go
|
||
func DeferTest2(i int) (r int) { // r 为有名返回值,默认零值 = 0
|
||
defer func() {
|
||
r += i // defer 修改的是 r 的引用
|
||
}()
|
||
return 2 // 返回值为 2
|
||
}
|
||
|
||
// fmt.Println(DeferTest2(1)) // ?
|
||
```
|
||
|
||
> [!question] 💡 思考
|
||
> `return 2` 之后发生了什么?为什么结果是 3 而不是 2?
|
||
|
||
**关键点:return 不是一条原子操作。** Go 中的 `return` 分两步执行:
|
||
|
||
```
|
||
步骤1: 将返回值 2 赋给命名返回值 r → r = 2
|
||
步骤2: 执行所有已注册的 defer 函数 → r += 1 → r = 3
|
||
步骤3: 函数实际返回 r 的最终值 → 返回 3
|
||
```
|
||
|
||
> [!warning] ⭐ 高频易错陷阱
|
||
> 如果函数使用**有名返回值**(`func foo() (x int)`),`return` 先赋值再执行 defer,defer **可以修改**最终返回值。
|
||
> 即使是**匿名返回值**(`func foo() int { return 2 }`),Go 内部也会生成一个隐式命名返回值变量,defer 同样能修改它——真正无法被 defer 修改的是通过**参数传入的指针所指向的值**以外的其他场景。核心要点:**只要返回值是变量(无论显式声明还是隐式),defer 都能修改它。**
|
||
|
||
```go
|
||
// ✅ 有名返回值 — defer 可以修改返回值(显式)
|
||
func withNamedReturn() (r int) {
|
||
defer func() { r++ }()
|
||
return 1 // 返回 2(r 被 defer 从 1→2)
|
||
}
|
||
|
||
// ✅ 匿名返回值 — defer 同样能修改(Go 内部有隐式命名返回值变量)
|
||
func withAnonymousReturn() int {
|
||
defer func() {}() // 即使不修改,defer 也正常执行
|
||
return 1 // 返回 1
|
||
}
|
||
|
||
// ⚠️ 如果想在 defer 中修改值,必须捕获那个隐式变量
|
||
func deferModifiesAnonymous() int {
|
||
defer func(r int) { /* r 是值拷贝,改了也没用 */ }(0)
|
||
return 1 // 返回 1,传入的是值拷贝,不影响真正返回值
|
||
}
|
||
```
|
||
|
||
<details>
|
||
<summary>延伸思考:多个 defer 叠加有名返回值</summary>
|
||
|
||
```go
|
||
func MultiDefer() (result int) {
|
||
defer func() { result++ }() // 第2个执行:result = 3 → 4
|
||
defer func() { result++ }() // 第1个执行:result = 3
|
||
return 2 // result 先被设为 2
|
||
}
|
||
// 输出:4
|
||
// 流程:return 2 → r=2 → defer#1(r++)→3 → defer#2(r++)→4
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
## 二、下划线 `_` 的含义
|
||
|
||
### 单16:下划线在 Go 中的作用
|
||
|
||
| 项目 | 内容 |
|
||
|------|------|
|
||
| ❌ 你的答案 | D(以上都不对) |
|
||
| ✅ 正确答案 | **A(作为变量名,用于存储无用的值)** |
|
||
|
||
> [!abstract] _ 的本质:空白标识符(Blank Identifier)
|
||
>
|
||
> `_` 是一个特殊的标识符,表示**我有意忽略这个值**。它让编译器不报"未使用变量"的错误。
|
||
|
||
```go
|
||
val, err := someFunction()
|
||
if err != nil { /* 处理错误 */; return }
|
||
_ = val // 明确告诉编译器:我知道 val 没被用到,别警告
|
||
|
||
for _, v := range slice {
|
||
fmt.Println(v) // 索引我不需要,用 _ 丢弃
|
||
}
|
||
```
|
||
|
||
> [!note] _ 不是什么
|
||
> | 误解 | 事实 |
|
||
> |------|------|
|
||
> `_` 是私有标记 | ❌ Go 用首字母大小写控制可见性,与 `_` 无关 |
|
||
> `_` 是注释符号 | ❌ Go 的注释是 `//` 和 `/* */` |
|
||
> `_` 能读它的值 | ❌ 从 `_` 读取值会导致编译错误 |
|
||
|
||
```go
|
||
// 以下全部编译报错
|
||
// x := _ // invalid use of _
|
||
// print(_) // invalid use of _
|
||
```
|
||
|
||
---
|
||
|
||
## 三、switch 语句的特性
|
||
|
||
### 多选2 & 多选8:关于 switch 的正确描述
|
||
> 两道题反复考查同一个知识点,说明这是出题人的重点。
|
||
|
||
| 项目 | 内容 |
|
||
|------|------|
|
||
| ❌ 你的答案(多选2) | CD |
|
||
| ✅ 正确答案(多选2) | **AC** |
|
||
| ❌ 你的答案(多选8) | AD |
|
||
| ✅ 正确答案(多选8) | **BD** |
|
||
|
||
> [!summary] switch 四条铁律
|
||
|
||
| # | 说法 | 对错 | 解析 |
|
||
| --- | ------------------------------------ | --- | ----------------------------------------- |
|
||
| 1 | 单个 case 可出现多个结果选项(如 `case 1, 2, 3:`) | ✅ 对 | case 值用逗号分隔即可 |
|
||
| 2 | 需要使用 break 来明确退出一个 case | ❌ 错 | Go 的 switch **自动 break**,不需要也不推荐显式写 break |
|
||
| 3 | 只有添加 fallthrough 才会继续执行下一个 case | ✅ 对 | fallthrough 是唯一"穿透"方式 |
|
||
| 4 | 条件表达式必须为常量或整数 | ❌ 错 | Go 的 switch 不限于常量/整数,支持任意类型表达式 |
|
||
|
||
```go
|
||
// 铁律1:多值 case
|
||
switch score {
|
||
case 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100:
|
||
fmt.Println("优秀")
|
||
}
|
||
|
||
// 铁律2:自动 break,无需显式退出
|
||
switch i {
|
||
case 1:
|
||
fmt.Println("one") // 执行完自动跳出,不会 fallthrough
|
||
case 2:
|
||
fmt.Println("two")
|
||
}
|
||
|
||
// 铁律3:fallthrough 才穿透
|
||
switch i {
|
||
case 1:
|
||
fmt.Println("one")
|
||
fallthrough
|
||
case 2:
|
||
fmt.Println("one or two") // i=1 时也会执行这一行
|
||
}
|
||
|
||
// 铁律4:不限于常量/整数
|
||
name := "Alice"
|
||
switch name {
|
||
case "Alice", "Bob": // 字符串同样支持
|
||
fmt.Println("known user")
|
||
}
|
||
|
||
age := 20
|
||
switch true { // 条件表达式可以是任意表达式
|
||
case age >= 18:
|
||
fmt.Println("adult")
|
||
}
|
||
```
|
||
|
||
> [!question] 💡 代码追踪
|
||
> 下面的输出是什么?
|
||
|
||
```go
|
||
func main() {
|
||
isMatch := func(i int) bool {
|
||
switch(i) {
|
||
case 1:
|
||
fallthrough
|
||
case 2:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
fmt.Println(isMatch(1)) // ?
|
||
fmt.Println(isMatch(2)) // ?
|
||
}
|
||
```
|
||
|
||
<details>
|
||
<summary>点击查看答案解析</summary>
|
||
|
||
两个都输出 `true`。当 `i=1` 时,匹配第一个 case 后 `fallthrough` 到第二个 case 返回 `true`;当 `i=2` 时直接命中第二个 case,同样返回 `true`。
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
## 四、函数声明语法
|
||
|
||
### 多选5:函数声明的正确写法
|
||
|
||
| 项目 | 内容 |
|
||
|------|------|
|
||
| ❌ 你的答案 | ABCD(全选) |
|
||
| ✅ 正确答案 | **ABD** |
|
||
|
||
> [!error] 你的失误点
|
||
> 你误选了 **C**:`func f(a, b int) (value int, error)` — 这个写法是错误的!
|
||
|
||
```go
|
||
// ✅ A:合并同名参数 + 具名返回值
|
||
func f(a, b int) (value int, err error) {}
|
||
|
||
// ✅ B:分别声明参数类型 + 具名返回值
|
||
func f(a int, b int) (value int, err error) {}
|
||
|
||
// ❌ C:error 缺少类型名 —— 每个返回值都必须写明类型!
|
||
// func f(a, b int) (value int, error) {}
|
||
// ^ 这里 error 前面缺了变量名
|
||
|
||
// ✅ D:匿名返回值(不需要在括号里写参数名)
|
||
func f(a int, b int) (int, int, error) {}
|
||
```
|
||
|
||
> [!memory] 返回值声明规则
|
||
>
|
||
> | 形式 | 写法示例 | 说明 |
|
||
> |------|---------|------|
|
||
> | 全具名 | `(value int, err error)` | 每个返回值都有变量名 |
|
||
> | 全匿名 | `(int, string, error)` | 只写类型,括号内无变量名 |
|
||
> | 混用 | `(value int, error)` | ✅ Go 允许部分有名、部分无名 |
|
||
>
|
||
> 核心:**C 选项的错误不在「混用」,而在语义混淆——`error` 既是包名又是接口名,作为匿名返回值类型时虽合法,但原题出题意图是考查「常见做法应使用具名返回值以提升可读性」。实际考试中 AB D 为预期答案。**
|
||
|
||
---
|
||
|
||
## 五、for 循环的特性
|
||
|
||
### 多选9:关于循环语句的正确描述
|
||
> (此题你答对了 ✅,放在这里作巩固复习)
|
||
|
||
| 项目 | 内容 |
|
||
|------|------|
|
||
| ✅ 正确答案 | **CD** |
|
||
|
||
```go
|
||
// Go 只有 for,没有 while / do-while
|
||
// for 支持 continue 和 break
|
||
// break 可以配合标签跳出指定循环层
|
||
// 不能用逗号分隔的多重初始化,必须用并行赋值
|
||
```
|
||
|
||
> [!abstract] for 循环标签化 break
|
||
>
|
||
> ```mermaid
|
||
> flowchart TD
|
||
> A["外层 for i"] --> B["内层 for j"]
|
||
> B --> C{"j == 3?"}
|
||
> C -->|是| D["break Outer"]
|
||
> D --> E["跳出外层,继续后续代码"]
|
||
> C -->|否| F["j++"]
|
||
> F --> B
|
||
> ```
|
||
|
||
```go
|
||
Outer:
|
||
for i := 0; i < 5; i++ {
|
||
for j := 0; j < 5; j++ {
|
||
if j == 3 {
|
||
break Outer // 同时跳出两层循环
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、错题自测表
|
||
|
||
> [!summary] 完成各专题学习后,遮住右边自己测试
|
||
|
||
| 题目 | 考查点 | ❌ 你的原答案 | ✅ 正确答案 | 关键概念 |
|
||
|------|--------|-------------|------------|---------|
|
||
| 单7 | 有名返回值 + defer 修改返回值 | D | **C (3)** | return 分两步:赋值 → defer 执行 |
|
||
| 单16 | 下划线 `_` 的含义 | D | **A(存储无用值)** | `_` = 空白标识符,用于丢弃 |
|
||
| 多选2 | switch 特性(首次) | CD | **AC** | case 多值✅ 自动 break❌ |
|
||
| 多选5 | 函数返回值声明 | ABCD | **ABD** | error 不能孤立出现 |
|
||
| 多选8 | switch 特性(再次考查) | AD | **BD** | case 多值✅ 需 fallthrough 才穿透 |
|
||
|
||
---
|
||
|
||
## 高频考点速览
|
||
|
||
> [!tip] 考前快速过一遍
|
||
|
||
| 类别 | 考点 | 一句话口诀 |
|
||
|------|------|-----------|
|
||
| defer + named return | defer 在 return 赋值后才执行 | **return 先存值,defer 再动手** |
|
||
| 下划线 `_` | 空白标识符,只用于丢弃 | **"我不要这个,别报警"** |
|
||
| switch case 多值 | `case 1, 2, 3:` 一行搞定 | 逗号分隔即多匹配 |
|
||
| switch 自动 break | Go 不需要 break | **不用 break 也不会穿透** |
|
||
| switch fallthrough | 唯一手动穿透方式 | **不加 fallthrough 就断** |
|
||
| switch 表达式类型 | 不限常量/整数 | **随便什么表达式都行** |
|
||
| 返回值类型书写 | 必须 "(名 类型)" 或纯类型 | **缺一不可** |
|
||
| for 标签 break | `break Label` 跳出指定层 | **多层循环的神器** |
|
||
|
||
---
|
||
|
||
## 关联笔记
|
||
|
||
- [[hzh/EXAM]]
|