练习
学完本章你应该掌握
- 能把普通失败设计成
error返回值,并在调用方先判断err再决定继续、提示、返回还是中断当前流程。 - 能使用
fmt.Errorf("...: %w", err)为底层错误补充上下文,让调用方知道错误发生在哪一层。 - 能区分哪些场景应该返回
error,哪些场景更适合使用panic表示程序无法继续执行的严重问题。 - 能使用
defer + recover()在合适的边界兜住 panic,并把崩溃改造成日志、提示或错误结果。 - 能理解 recover 只对当前 goroutine 生效,不会回到 panic 发生点继续执行,也不会替你跨 goroutine 捕获异常。
- 能把本章知识自然放进前面学过的函数、结构体、切片、map、输入校验、goroutine 和 channel 场景里。
简单
第 1 题:编写返回 error 的分数解析函数
编写一个 Go 程序,完成下面要求:
- 编写函数
parseScore(text string) (int, error) - 使用
strconv.Atoi把字符串分数转成整数 - 如果转换失败,直接把错误返回给调用方
- 如果分数不在
0 ~ 100之间,返回错误score out of range - 在
main中至少测试两次: 一次传入"88"一次传入"abc" - 调用方必须先判断
err
查看参考答案
| |
一种可能的输出结果:
| |
说明:这道题的重点不是 Atoi 本身,而是让你把“失败也算结果的一部分”真正写进函数签名里。
第 2 题:补全错误包装语句
补全下面代码中的空白,让底层错误在向上返回时带上 load age 这层上下文:
| |
查看参考答案
应补全为:
| |
完整函数如下:
| |
说明:%w 的作用是把底层错误包进新的错误里,这样调用方既能看到当前这层上下文,也不会丢失原始错误信息。
第 3 题:修正把普通输入错误写成 panic 的程序
下面程序把普通用户输入错误直接写成了 panic。
请改正代码,让它在输入有误时给出提示并结束当前流程,而不是直接崩溃程序。
| |
查看参考答案
修正后的代码如下:
| |
关键点:
- 用户输入错误属于普通业务失败,优先返回提示或结束当前流程,不要动不动就
panic panic更适合“程序根本无法继续”的场景,而不是“这次输入不合法”
第 4 题:编写必须加载配置函数
编写一个 Go 程序,完成下面要求:
- 定义结构体
Config,字段为DBURL string - 编写函数
mustLoadConfig(cfg Config) - 如果
cfg.DBURL == "",触发panic("missing DB_URL") - 在
main中故意传入一个空配置,观察程序中断
关键输出应类似这样:
| |
查看参考答案
| |
说明:这里的配置缺失会导致程序启动后根本无法继续工作,所以比起返回普通 error,更适合用 panic 直接中断。
第 5 题:使用 recover 让主流程继续
编写一个 Go 程序,完成下面要求:
- 编写函数
safeRun() - 在
safeRun中先输出safeRun start - 然后触发
panic("lesson broken") - 使用
defer + recover()捕获这次 panic,并输出recovered: lesson broken - 在
main中调用safeRun()后,再输出main continues
查看参考答案
| |
输出结果:
| |
说明:所谓“恢复”,不是回到 panic 那一行后面继续执行,而是把原本要继续向上传播的 panic 截住,让外层流程还能往下走。
一般
第 6 题:封装资料卡生成函数并向上抛错
编写一个 Go 程序,完成下面要求:
- 编写函数
parseAge(text string) (int, error) - 在这个函数里使用
strconv.Atoi - 如果转换失败,返回
fmt.Errorf("parse age: %w", err) - 再编写函数
buildProfile(name, ageText string) (string, error) - 如果
name == "",返回错误name is empty - 如果年龄解析失败,继续向上包装成
fmt.Errorf("build profile: %w", err) - 成功时返回:
| |
- 在
main中至少测试一次错误场景
查看参考答案
| |
输出结果:
| |
说明:这道题真正要练的是“每一层只补充自己知道的上下文”,而不是一出错就把底层细节丢掉。
第 7 题:修正放错位置的 recover
下面程序想捕获 panic,但现在写法不对。
请改正代码,让它最终输出:
| |
| |
查看参考答案
修正后的代码如下:
| |
原因说明:
recover()必须在defer调用的函数中直接使用才有效- 原程序把
recover()放在普通流程里,而且panic发生后那几行本来就不会继续执行
第 8 题:把 panic 转成 error 返回
编写一个 Go 程序,完成下面要求:
- 编写函数
runJob(name string) (err error) - 如果
name == "",触发panic("job name empty") - 使用
defer + recover()捕获这次 panic - 捕获后把它转换成错误返回给调用方,例如:
| |
- 如果
name不为空,输出run ok: 任务名 - 在
main中至少测试一次正常场景和一次失败场景
查看参考答案
| |
一种可能的输出结果:
| |
说明:这类写法常见于程序边界层。内部出现了 panic,但边界层不想让整个程序直接崩掉,而是希望把它整理成一个可处理的错误结果。
第 9 题:判断哪些场景该用 error,哪些该用 panic
下面四种情况里,哪些更适合返回 error,哪些更适合使用 panic?请分别写出判断,并简要说明原因。
| |
查看参考答案
一种合理的判断如下:
| |
原因说明:
A属于普通业务输入错误,应该交给调用方提示用户或重新输入B属于启动阶段的致命配置问题,程序无法继续正常工作,更适合直接中断C是可预期的业务失败或超时结果,通常应作为error或失败状态返回D更像代码 bug 或运行时严重错误,这类问题通常会触发 panic,而不是正常业务分支
第 10 题:为高阶执行器加上恢复保护
编写一个 Go 程序,完成下面要求:
- 编写函数
executeSafely(fn func()) error - 在函数内部执行传入的
fn - 如果
fn发生 panic,使用recover捕获,并返回:
| |
- 在
main中至少测试两次: 一次传入正常函数 一次传入会panic("bad task")的函数
查看参考答案
| |
一种可能的输出结果:
| |
说明:这道题顺手复习了上一章的高阶函数。executeSafely 接收的不是普通数据,而是一个“待执行的行为”。
进阶
下面四题会把异常处理和前面学过的结构体、函数、切片、goroutine、channel 串起来,重点考察你能不能在更真实一点的程序边界里区分 error、panic 和 recover。
第 11 题:解析成绩表并补充学生上下文
编写一个 Go 程序,完成下面要求:
- 定义结构体
StudentInput,字段包括:Name stringScoreText string - 编写函数
parseOneScore(text string) (int, error) - 再编写函数
buildScoreMap(inputs []StudentInput) (map[string]int, error) buildScoreMap需要遍历输入切片,把"文本分数"转成真正的整数分数- 如果某个学生解析失败,要返回带学生名上下文的错误,例如:
| |
- 成功时返回
map[string]int
测试数据可以使用:
| |
查看参考答案
| |
说明:这道题把结构体、切片、map、函数和错误包装放到了一起。真正要练的是“错误不只要往上抛,还要带着足够的定位信息一起抛”。
第 12 题:在 goroutine 中各自 recover 任务 panic
编写一个 Go 程序,完成下面要求:
- 定义结构体
Task,字段包括:ID intName string - 编写函数
worker(task Task, resultCh chan string, wg *sync.WaitGroup) - 在
worker中使用defer + recover() - 如果
task.Name == "",触发panic("task name empty") - 如果没有 panic,就发送:
| |
- 如果发生 panic,就发送:
| |
- 在
main中准备 3 个任务,其中 1 个任务名为空 - 使用 goroutine 启动所有任务
- 使用
WaitGroup等待全部完成后关闭resultCh - 使用
for range输出所有结果
说明:输出顺序不固定。
查看参考答案
| |
说明:这道题最关键的点不是 WaitGroup 或 channel,而是体会“每个 goroutine 要自己 recover 自己的 panic”。
第 13 题:修正试图在 main 中 recover 子 goroutine 的代码
下面程序想在 main 中统一 recover 一个子 goroutine 里的 panic,但它现在做不到。
请改正代码,让它稳定输出:
| |
| |
查看参考答案
修正后的代码如下:
| |
原因说明:
main里的recover只能处理main这个 goroutine 自己的 panic- 子 goroutine 里的 panic,必须在那个 goroutine 自己的
defer里恢复 - 这里顺手把
time.Sleep换成了WaitGroup,等待方式更稳定
第 14 题:实现一个并发批改中心
请完成一个综合练习:
- 定义结构体
CheckTask,字段包括:Index intName stringScoreText string - 定义结构体
CheckResult,字段包括:Index intText string - 编写函数
parseScore(text string) (int, error)使用strconv.Atoi如果解析失败,返回fmt.Errorf("parse score: %w", err)如果分数不在0 ~ 100范围内,返回score out of range - 编写函数
levelOf(score int) string按A / B / C / D返回等级 - 编写 goroutine 函数
checkTask(task CheckTask, resultCh chan CheckResult, wg *sync.WaitGroup)在里面使用defer + recover()如果task.Name == "",触发panic("missing student name")如果文本分数解析失败,发送失败结果 如果成功,发送成功结果 - 在
main中准备下面这组任务:
| |
- 为每个任务启动一个 goroutine
- 使用
channel + WaitGroup收集结果 - 最后按
Index的原始顺序输出结果
期望输出类似这样:
| |
查看参考答案
| |
说明:
- 解析失败属于普通业务错误,所以这里用
error返回 - 学生名缺失被题目约定成不可继续的严重问题,所以这里用
panic checkTask再用recover把单个任务的崩溃兜住,避免整个批改中心一起退出- 这道题把
01-12单元里学过的结构体、切片、函数、判断、goroutine、channel 和异常处理真正串起来了