Week 02: 控制流、函数与错误处理
掌握 Go 的 for/if/switch 控制流, 函数定义与多返回值, 以及 Go 独特的错误处理模式.
1. 控制流概述
Go 的控制流语句极为简洁:
- 只有
for一种循环 (没有 while, do-while) if和switch都可以包含初始化语句- 没有三元运算符
? :
2. for 循环
2.1 标准 for 循环
for i := 0; i < 10; i++ {
fmt.Println(i)
}三部分组成:
- 初始化语句
i := 0: 循环开始前执行一次 - 条件表达式
i < 10: 每次迭代前检查 - 后置语句
i++: 每次迭代后执行
2.2 while 风格
省略初始化和后置语句:
n := 0
for n < 5 {
fmt.Println(n)
n++
}2.3 无限循环
for {
fmt.Println("无限循环")
// 使用 break 退出
if shouldStop() {
break
}
}2.4 range 遍历
range 用于遍历数组、切片、映射、字符串和通道.
// 遍历切片
nums := []int{1, 2, 3, 4, 5}
for index, value := range nums {
fmt.Printf("索引: %d, 值: %d\n", index, value)
}
// 只需要值
for _, value := range nums {
fmt.Println(value)
}
// 只需要索引
for index := range nums {
fmt.Println(index)
}
// 遍历映射
m := map[string]int{"a": 1, "b": 2}
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// 遍历字符串 (按 rune)
for i, r := range "你好" {
fmt.Printf("%d: %c\n", i, r)
}
// 输出:
// 0: 你
// 3: 好 (注意索引是字节偏移, 不是字符索引)2.5 break 和 continue
break: 跳出当前循环continue: 跳过本次迭代, 进入下一次
for i := 0; i < 10; i++ {
if i == 5 {
break // 完全退出循环
}
if i%2 == 0 {
continue // 跳过偶数
}
fmt.Println(i) // 只打印 1, 3
}2.6 标签 (Label)
用于在嵌套循环中跳出外层:
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // 跳出外层循环
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}3. if 语句
3.1 基本形式
x := 10
if x > 0 {
fmt.Println("正数")
} else if x < 0 {
fmt.Println("负数")
} else {
fmt.Println("零")
}注意: { 必须与 if 在同一行 (Go 的分号插入规则).
3.2 带初始化语句
Go 允许在 if 条件前添加一个初始化语句:
if err := doSomething(); err != nil {
fmt.Println("错误:", err)
return
}
// err 在此不可见 (作用域仅限 if 块)这种模式在错误处理中非常常见, 保持了代码的紧凑性.
3.3 条件表达式必须是布尔值
// 错误! Go 不支持隐式转换
if 1 { // 编译错误
}
// 正确
if x != 0 {
}4. switch 语句
4.1 表达式 switch
day := "Monday"
switch day {
case "Monday":
fmt.Println("周一")
case "Tuesday", "Wednesday": // 多值匹配
fmt.Println("周二或周三")
default:
fmt.Println("其他")
}与 C/Java 的区别:
- 不需要 break: 默认只执行匹配的 case, 不会 fallthrough.
- case 可以是表达式: 不限于常量.
4.2 无表达式 switch
等价于 switch true:
score := 85
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
case score >= 60:
fmt.Println("C")
default:
fmt.Println("F")
}4.3 fallthrough
强制执行下一个 case (即使不匹配):
n := 1
switch n {
case 1:
fmt.Println("一")
fallthrough
case 2:
fmt.Println("二")
}
// 输出:
// 一
// 二4.4 类型 switch
用于判断接口变量的实际类型:
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("整数: %d\n", v)
case string:
fmt.Printf("字符串: %s\n", v)
case bool:
fmt.Printf("布尔: %t\n", v)
default:
fmt.Printf("未知类型: %T\n", v)
}
}
describe(42) // 整数: 42
describe("hello") // 字符串: hello
describe(true) // 布尔: true5. 函数
5.1 函数定义
func add(a int, b int) int {
return a + b
}
// 参数类型相同时可简写
func add(a, b int) int {
return a + b
}5.2 多返回值
Go 函数可以返回多个值, 这在错误处理中非常关键:
func divide(a, b int) (int, int) {
return a / b, a % b
}
quotient, remainder := divide(17, 5)
fmt.Printf("%d / %d = %d 余 %d\n", 17, 5, quotient, remainder)
// 17 / 5 = 3 余 25.3 命名返回值
可以为返回值命名, 它们会被初始化为零值:
func divide(a, b int) (quotient, remainder int) {
quotient = a / b
remainder = a % b
return // 裸返回, 返回命名变量的当前值
}注意: 裸返回在短函数中可以增加可读性, 但在长函数中应避免使用.
5.4 可变参数
使用 ... 声明可变参数:
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6
sum(1, 2, 3, 4, 5) // 15
// 传入切片需要展开
nums := []int{1, 2, 3}
sum(nums...) // 65.5 函数作为值
Go 中函数是一等公民, 可以赋值给变量:
add := func(a, b int) int {
return a + b
}
result := add(1, 2) // 3
// 函数作为参数
func apply(fn func(int, int) int, a, b int) int {
return fn(a, b)
}
apply(add, 3, 4) // 75.6 闭包 (Closure)
闭包是一个函数值, 它引用了其外部作用域的变量:
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3闭包的内存模型: 当内部函数引用外部变量时, 该变量会逃逸到堆上, 其生命周期延长至闭包不再被引用.
// 闭包实际上是一个结构体
// type closureFunc struct {
// code *func() // 函数代码
// count *int // 捕获的变量 (在堆上)
// }6. defer 语句
6.1 基本用法
defer 语句将函数调用推迟到当前函数返回前执行:
func main() {
fmt.Println("开始")
defer fmt.Println("结束") // 延迟执行
fmt.Println("处理中")
}
// 输出:
// 开始
// 处理中
// 结束6.2 常见用途
资源清理:
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // 确保文件关闭
// 读取文件...
return nil
}解锁:
mu.Lock()
defer mu.Unlock()
// 临界区代码...记录耗时:
func process() {
start := time.Now()
defer func() {
fmt.Printf("耗时: %v\n", time.Since(start))
}()
// 处理逻辑...
}6.3 执行顺序 (LIFO)
多个 defer 按后进先出 (Last In, First Out) 顺序执行:
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
// 输出:
// 3
// 2
// 16.4 参数求值时机
defer 函数的参数在 defer 语句执行时求值, 而不是在函数返回时:
func main() {
i := 0
defer fmt.Println(i) // 参数 i 在此时求值 (0)
i++
}
// 输出: 0 (不是 1)如果需要捕获最终值, 使用闭包:
func main() {
i := 0
defer func() {
fmt.Println(i) // 闭包捕获 i 的引用
}()
i++
}
// 输出: 16.5 defer 与 return 的交互
命名返回值可以被 defer 修改:
func test() (result int) {
defer func() {
result++ // 修改返回值
}()
return 10 // result = 10
}
// 返回 11执行顺序:
result = 10(return 赋值)result++(defer 执行)RET(真正返回)
7. 错误处理
7.1 error 接口
Go 使用 error 接口表示错误:
type error interface {
Error() string
}7.2 创建错误
import "errors"
// 方式 1: errors.New
err := errors.New("something went wrong")
// 方式 2: fmt.Errorf (格式化)
err := fmt.Errorf("user %s not found", username)7.3 错误处理模式
标准模式: 函数返回值的最后一个是 error, 调用后立即检查.
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 0)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("结果:", result)7.4 哨兵错误 (Sentinel Errors)
预定义的错误值, 用于精确匹配:
var ErrNotFound = errors.New("not found")
var ErrPermissionDenied = errors.New("permission denied")
func findUser(id int) (*User, error) {
// ...
return nil, ErrNotFound
}
user, err := findUser(123)
if err == ErrNotFound {
fmt.Println("用户不存在")
}7.5 自定义错误类型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Message: "cannot be negative",
}
}
return nil
}7.6 错误包装 (Go 1.13+)
使用 %w 包装错误, 保留错误链:
func readConfig() error {
f, err := os.Open("config.yaml")
if err != nil {
return fmt.Errorf("open config: %w", err)
}
defer f.Close()
// ...
}errors.Is: 检查错误链中是否包含特定错误:
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在")
}errors.As: 从错误链中提取特定类型:
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("验证错误: %s\n", valErr.Field)
}8. panic 和 recover
8.1 panic
panic 用于不可恢复的错误, 会导致程序崩溃:
func mustOpen(path string) *os.File {
f, err := os.Open(path)
if err != nil {
panic(err) // 程序终止
}
return f
}何时使用 panic:
- 启动时配置错误 (无法继续运行)
- 编程错误 (如数组越界, 这会自动 panic)
- 库函数的 "Must" 变体 (如
regexp.MustCompile)
8.2 recover
recover 用于捕获 panic, 必须在 defer 函数中调用:
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
panic("something bad happened")
}
// 输出: Recovered from panic: something bad happened
func main() {
safeCall()
fmt.Println("程序继续运行")
}8.3 使用原则
- 库代码应避免 panic: 返回 error 让调用者决定如何处理.
- recover 仅用于边界: 如 HTTP handler 顶层, 防止单个请求崩溃导致整个服务宕机.
- 不要用 panic 做常规错误处理: panic/recover 的性能开销比 error 大得多.
9. 错误处理进阶模式
9.1 自定义错误类型层次
在生产代码中, 常需要对错误进行分类以便统一处理:
type ErrCode string
const (
ErrCodeNotFound ErrCode = "NOT_FOUND"
ErrCodePermission ErrCode = "PERMISSION_DENIED"
ErrCodeValidation ErrCode = "VALIDATION_FAILED"
)
type AppError struct {
Code ErrCode
Message string
Cause error
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Cause
}
// 构造函数
func NewNotFoundError(resource string, cause error) *AppError {
return &AppError{
Code: ErrCodeNotFound,
Message: fmt.Sprintf("%s not found", resource),
Cause: cause,
}
}9.2 在 HTTP Handler 中统一处理错误
func handleError(w http.ResponseWriter, err error) {
var appErr *AppError
if errors.As(err, &appErr) {
switch appErr.Code {
case ErrCodeNotFound:
http.Error(w, appErr.Message, http.StatusNotFound)
case ErrCodePermission:
http.Error(w, appErr.Message, http.StatusForbidden)
default:
http.Error(w, appErr.Message, http.StatusBadRequest)
}
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
}9.3 多错误聚合 (Go 1.20+)
errors.Join 用于将多个错误合并为一个:
func validateUser(u User) error {
var errs []error
if u.Name == "" {
errs = append(errs, errors.New("name is required"))
}
if u.Age < 0 {
errs = append(errs, errors.New("age must be positive"))
}
if len(u.Email) == 0 {
errs = append(errs, errors.New("email is required"))
}
if len(errs) > 0 {
return errors.Join(errs...) // 合并所有错误
}
return nil
}
// errors.Is 可以检测 Join 中的任意错误
err := validateUser(User{})
if errors.Is(err, ErrEmptyName) {
// 处理
}9.4 错误处理原则
| 原则 | 说明 |
|---|---|
| 只处理一次 | 错误要么返回给调用者, 要么记录日志, 不要两者都做 |
| 添加上下文 | 使用 fmt.Errorf("operation: %w", err) 包装错误 |
| 类型化错误 | 生产代码使用自定义错误类型, 便于分类处理 |
| 保留错误链 | 使用 %w 而非 %v 以保留 errors.Is/As 能力 |
10. 练习
10.1 FizzBuzz
编写程序, 打印 1 到 100. 对于 3 的倍数打印 "Fizz", 5 的倍数打印 "Buzz", 同时是 3 和 5 的倍数打印 "FizzBuzz".
func main() {
for i := 1; i <= 100; i++ {
switch {
case i%15 == 0:
fmt.Println("FizzBuzz")
case i%3 == 0:
fmt.Println("Fizz")
case i%5 == 0:
fmt.Println("Buzz")
default:
fmt.Println(i)
}
}
}10.2 递归阶乘
使用递归实现阶乘函数.
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}10.3 安全除法
实现一个不会 panic 的除法函数.
func safeDivide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}10.4 带重试的函数
实现一个函数, 在失败时重试最多 N 次.
func retry(n int, fn func() error) error {
var err error
for i := 0; i < n; i++ {
err = fn()
if err == nil {
return nil
}
fmt.Printf("尝试 %d 失败: %v\n", i+1, err)
}
return fmt.Errorf("after %d attempts: %w", n, err)
}11. 思考题
- Go 为什么只有
for一种循环? defer的 LIFO 顺序有什么实际意义?- 为什么 Go 选择返回
error而不是使用异常? errors.Is和==比较有什么区别?- 闭包的变量逃逸对性能有什么影响?
12. 本周小结
- 控制流: for (唯一循环), if (可带初始化), switch (不需要 break).
- 函数: 多返回值, 命名返回值, 可变参数, 函数作为值, 闭包.
- defer: 延迟执行, LIFO 顺序, 用于资源清理.
- 错误处理: error 接口, errors.New, fmt.Errorf, 错误包装 (%w), errors.Is/As.
- panic/recover: 用于不可恢复错误, 仅在边界处使用.
控制流和函数是编程的基础, 而 Go 独特的错误处理模式是理解 Go 代码风格的关键. 下周我们将学习复合类型.