基础

Go 环境搭建与工具链

  • Go 环境搭建与工具链
    • 安装 Go
      • 官网直接安装

        # macOS(用 Homebrew)
        brew install go
        
        # Linux(手动安装)
        wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
        sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz
        export PATH=$PATH:/usr/local/go/bin
        
        # Windows
        # 直接下载 .msi 安装包:https://go.dev/dl/
        
        # 验证安装
        go version
        # 输出:go version go1.23.0 darwin/arm64
        
      • goenv 环境管理

        # Linux(手动安装)
        wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
        sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz
        export PATH=$PATH:/usr/local/go/bin
        
        # Windows
        # 直接下载 .msi 安装包:https://go.dev/dl/
        
        # 验证安装
        go version
        # 输出:go version go1.23.0 darwin/arm64
        
    • 理解环境变量
      • 常用环境变量

        # 查看所有 Go 环境变量
        go env
        
        # 最重要的几个:
        go env GOROOT   # Go 的安装路径(一般不需要改)
        go env GOPATH   # 工作目录,默认 ~/go
        go env GOMODULE # 模块模式(应该是 "on")
        
      • GOPATH vs Go Modules
        • 早期 Go 用 GOPATH 管理所有的代码,所有项目必须放在 $GOPATH/src 下
        • Go 1.11 引入了 Go Modules,可以在任意目录创建项目
        • 现在 Go Modules 是默认模式,不需要关注 GOPATH 了
        • 只需要确认 GO111MODULE=on
      • 设置常用模块

        # 确保模块模式开启
        go env -w GO111MODULE=on
        
        # 设置国内代理(如果你在中国大陆)
        go env -w GOPROXY=https://goproxy.cn,direct
        
    • VsCode 插件
      • Go 官方插件
      • Go Test Exploerer 可视化运行测试
      • Error Lens 行内显示错误信息
    • 核心命令
      • go mod init <模块名> 初始化一个新模块
      • go run main.go 编译并直接运行(不生成二进制)
      • go build 编译生成可执行文件
      • go fmt ./… 格式化代码
      • go test ./… 运行所有测试
      • go get <包名> 添加依赖到当前模块
      • go mod tidy 清理未使用的依赖
      • go vet ./… 静态分析,检查常用错误
      • go doc <包名>

类型系统与变量

  • 类型系统与变量
    • 基本类型总览
      • Go 是强类型语言,每个类型都有明确的类型
      • 整数
        • int/uint 平台相关(32 位或 64 位) 最常用
        • int8/int16/int32/int64 指定位数的有符号整数
        • uint8(byte)/uint16/uint32/uint64 无符号整数
      • 浮点数
        • float32 单进度浮点数 (7 位有效数字)
        • float64 双进度浮点数 (15 位有效数字)
      • 其他
        • bool true/false
        • string UTF-8 编码的不可变字节序列
        • byte uint8 的别名,表示一个字节
        • rune int32 的别名,表示一个 Unicode 码点
      • byte vs rune
        • byte 是一个字节,用于处理原始字节数据
        • rune 是一个 Unicode 码点,用于处理字符
        • 一个中文字符占 3 个 byte,但只是一个 1 rune
        • 遍历字符串,for range 遍历的是 rune,for i 遍历的是 byte
      s := "Hello你好"
      
      // byte 视角:看到的是字节
      fmt.Println(len(s))        // 11(5个ASCII + 6个中文字节)
      
      // rune 视角:看到的是字符
      fmt.Println(len([]rune(s))) // 7(5个英文字符 + 2个中文字符)
      
      // for range 按 rune 遍历
      for i, ch := range s {
          fmt.Printf("索引=%d 字符=%c\n", i, ch)
      }
      // 索引=0 字符=H
      // 索引=1 字符=e
      // ...
      // 索引=5 字符=你   ← 注意索引跳了
      // 索引=8 字符=好
      
    • 变量声明的四种方式
      • 核心原则:函数内用 :=,函数外用 var
      // ① var + 类型(最完整的写法)
      var name string = "Gopher"
      
      // ② var + 类型推断(省略类型)
      var age = 25  // Go 推断为 int
      
      // ③ 短变量声明(最常用,只能在函数内)
      city := "Tokyo"  // 等价于 var city = "Tokyo"
      
      // ④ 批量声明(常用于包级别变量)
      var (
          host     = "localhost"
          port     = 8080
          debug    = false
      )
      
      • 选用参考
        • 函数内:几乎总是使用 := 短声明,简洁明了
        • 函数外:只能用 var,因为 := 只能在函数内使用
        • 需要显式指定类型时,用 var name Type
        • 声明但是暂不赋值,用var name Type
      • 常见陷阱
        • 短声明的遮蔽
        x := 1
        fmt.Println(x)  // 1
        
        if true {
            x := 2     // 注意:这是一个新的 x,遮蔽了外层的 x!
            fmt.Println(x)  // 2
        }
        fmt.Println(x)  // 1 ← 外层的 x 没有变!
        
        // 正确做法:用 = 而不是 :=
        if true {
            x = 2      // 修改的是外层的 x
        }
        fmt.Println(x)  // 2
        
    • 零值机制
      • Go 没有 undefined、null、None 的概念
      • 每个类型都有一个确定的零值,声明变量时不赋值就是零值
      // 零值是安全的,可以直接使用
      var count int      // 0,可以直接 count++
      var name string    // "",可以直接 name += "Go"
      var ok bool        // false
      
      // 但 nil 类型需要初始化后才能使用!
      var m map[string]int   // nil,直接 m["key"] = 1 会 panic!
      m = make(map[string]int) // 先 make 初始化
      
      var s []int        // nil,但 append 是安全的
      s = append(s, 1)   // ✅ nil 切片可以直接 append
      
    • 常量与 iota 枚举
      • Go 用 const 定义常量
      • Go 没有 enum 关键字,但是提供了 iota 自增器
      • 基本常量
      const Pi = 3.14159
      const AppName = "MyApp"
      
      // 批量声明
      const (
          StatusOK    = 200
          StatusNotFound = 404
          StatusError = 500
      )
      
      • iota Go 的枚举利器
      // iota 在 const 块中从 0 开始,每行自增 1
      type Weekday int
      
      const (
          Sunday    Weekday = iota  // 0
          Monday                    // 1(自动继承 iota 表达式)
          Tuesday                   // 2
          Wednesday                 // 3
          Thursday                  // 4
          Friday                    // 5
          Saturday                  // 6
      )
      
      • iota 高级用法
      // 用法一:跳过某个值
      type Color int
      const (
          _      Color = iota  // 0,用 _ 跳过
          Red                  // 1
          Green                // 2
          Blue                 // 3
      )
      
      // 用法二:位掩码(权限系统常用)
      type Permission uint
      
      const (
          Read    Permission = 1 << iota  // 1  (001)
          Write                           // 2  (010)
          Execute                         // 4  (100)
      )
      // 组合权限
      userPerm := Read | Write  // 3 (011)
      fmt.Println(userPerm & Read != 0)   // true,有读权限
      fmt.Println(userPerm & Execute != 0) // false,无执行权限
      
      // 用法三:文件大小单位
      const (
          _  = iota
          KB = 1 << (10 * iota)  // 1 << 10 = 1024
          MB                      // 1 << 20
          GB                      // 1 << 30
          TB                      // 1 << 40
      )
      
      • iota 的本质
        • 不是一个值,是 const 块的行索引计数器
        • 每次遇到一个新的 const 就重置为 0
        • 同一行的多个 iota 值相同
    • 类型转换

      // ❌ 隐式转换在 Go 中不存在
      var a int = 42
      var b float64 = a      // 编译错误!
      var c int64 = a        // 编译错误!即使都是整数
      
      // ✅ 必须显式转换
      var b float64 = float64(a)  // 42.0
      var c int64 = int64(a)      // 42
      
      // 字符串 ↔ 数字(用 strconv 包)
      import "strconv"
      
      s := strconv.Itoa(42)         // int → string: "42"
      n, err := strconv.Atoi("42")  // string → int: 42
      f, err := strconv.ParseFloat("3.14", 64)  // string → float64
      
      // 字符串 ↔ 字节切片
      bytes := []byte("Hello")     // string → []byte
      str := string(bytes)          // []byte → string
      
      // ⚠️ 精度丢失要注意
      big := int64(1<<62)
      small := int32(big)  // 溢出!结果不可预测
      
      • fmt.Sprintf 万能转字符串
        • 性能不如 strconv,高频调用优先用 strconv

流程控制

  • 流程控制
    • if/else 条件判断
      • 条件不需要括号
      • 允许在条件前加入一个初始化语句
      • 基本语法

        score := 85
        
        if score >= 90 {
            fmt.Println("优秀")
        } else if score >= 60 {
            fmt.Println("及格")
        } else {
            fmt.Println("不及格")
        }
        
        // 注意:
        // 1. 条件不需要括号 (score >= 90) 写成 score >= 90
        // 2. 大括号必须有,即使只有一行
        // 3. else 必须和右大括号同一行
        
      • 前置初始化语句 (GO 特色)

        // 在条件中声明并使用变量
        if err := doSomething(); err != nil {
            fmt.Println("出错了:", err)
            return
        }
        // err 的作用域仅限于 if-else 块内,块外不可见
        
        // 对比:不用前置初始化的写法
        err := doSomething()
        if err != nil {
            fmt.Println("出错了:", err)
            return
        }
        // 这里 err 仍在作用域内,可能造成后续变量名冲突
        
      • 前置初始化的好处
        • 限制了变量的作用域
        • 避免污染外部命名空间
        • 在错误处理中常见
    • for 循环
      • Go 只有 for 一个循环关键字
      • for 有多种形式,能覆盖其他语言的各种循环要求
      • 经典三段式

        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
        
      • 类 while 循环 (只有条件)

        n := 10
        for n > 0 {
            fmt.Println(n)
            n-- 
        }
        
      • 无限循环

        for {
            // 死循环,用 break 退出
            if someCondition() {
                break
            }
        }
        
        // 常用于服务器主循环
        for {
            conn, err := listener.Accept()
            if err != nil {
                continue
            }
            go handleConn(conn)
        }
        
      • for-range 遍历

        // 遍历切片/数组
        nums := []int{10, 20, 30}
        for i, v := range nums {
            fmt.Printf("索引=%d 值=%d\n", i, v)
        }
        
        // 只要索引
        for i := range nums {
            fmt.Println(i)
        }
        
        // 只要值(用 _ 忽略索引)
        for _, v := range nums {
            fmt.Println(v)
        }
        
        // 遍历 map(顺序随机!)
        m := map[string]int{"a": 1, "b": 2}
        for k, v := range m {
            fmt.Printf("%s=%d\n", k, v)
        }
        
        // 遍历字符串(按 rune)
        for i, ch := range "Hello你好" {
            fmt.Printf("%d: %c\n", i, ch)
        }
        
        // 遍历 channel(Day 9 会讲)
        // for msg := range ch { ... }
        
      • 循环复用坑
        • 在 Go 1.22 之前,for-range 的循环变量是复用的,在 go routine 中捕获会出现问题
        // Go 1.22+ 这样写是安全的
        for _, v := range []int{1, 2, 3} {
            go func() {
                fmt.Println(v) // 1.22 前可能全打印 3
            }()
        }
        
        // Go 1.22 之前的兼容写法:
        for _, v := range []int{1, 2, 3} {
            v := v // 显式创建副本
            go func() {
                fmt.Println(v)
            }()
        }
        
    • switch 分支
      • 支持默认 break、多值匹配、任意表达式
      • 基本用法:默认不穿透

        day := 3
        
        switch day {
        case 1:
            fmt.Println("周一")
        case 2:
            fmt.Println("周二")
        case 3:
            fmt.Println("周三")
            // 默认自动 break,不会掉到 case 4
        case 4, 5:  // ✨ 多值匹配
            fmt.Println("周四或周五")
        case 6, 7:
            fmt.Println("周末")
        default:
            fmt.Println("无效")
        }
        
      • 无条件 switch (if-else 链的优雅写法)

        score := 85
        
        // 省略 switch 后的条件,相当于 switch true
        switch {
        case score >= 90:
            fmt.Println("A")
        case score >= 80:
            fmt.Println("B")
        case score >= 60:
            fmt.Println("C")
        default:
            fmt.Println("F")
        }
        
        // 比 if-else 链更清晰
        // 等价于:
        // if score >= 90 { ... } 
        // else if score >= 80 { ... }
        // ...
        
      • 前置初始化语句

        switch os := runtime.GOOS; os {
        case "linux":
            fmt.Println("Linux")
        case "darwin":
            fmt.Println("macOS")
        case "windows":
            fmt.Println("Windows")
        default:
            fmt.Printf("未知系统: %s\n", os)
        }
        
      • fallthrough:显式穿透

        // 如果真的需要穿透到下一个 case,用 fallthrough
        switch n := 1; n {
        case 1:
            fmt.Println("一")
            fallthrough  // 继续执行下一个 case
        case 2:
            fmt.Println("二") // 会被打印
        case 3:
            fmt.Println("三") // 不会打印,fallthrough 只穿透一层
        }
        // 输出:一、二
        
    • break、continue、goto
      • 基础 break 和 continue

        for i := 0; i < 10; i++ {
            if i == 3 {
                continue  // 跳过本次迭代
            }
            if i == 7 {
                break     // 退出循环
            }
            fmt.Println(i)
        }
        // 输出:0 1 2 4 5 6
        
      • 标签:跳出嵌套循环

        // 问题:break 只能跳出一层循环
        // 在嵌套循环中如何跳出外层?
        
        OuterLoop:
        for i := 0; i < 5; i++ {
            for j := 0; j < 5; j++ {
                if i*j > 6 {
                    break OuterLoop  // 直接跳出外层循环
                }
                fmt.Printf("%d*%d ", i, j)
            }
        }
        
        // continue 也支持标签:continue OuterLoop 会回到外层循环的下一次迭代
        
      • goto 存在,但是不建议用
    • defer 延迟执行
      • defer 是 Go 的标志性特性,用来注册「函数退出前必须执行」的逻辑
      • 它彻底解决了资源清理、错误恢复等场景
      • 基本行为:函数返回前执行

        func main() {
            fmt.Println("1. 开始")
            defer fmt.Println("3. 延迟执行")
            fmt.Println("2. 中间")
        }
        // 输出:
        // 1. 开始
        // 2. 中间
        // 3. 延迟执行 ← 在函数返回前执行
        
      • 典型用法:资源清理

        // 文件操作
        func readFile(name string) error {
            f, err := os.Open(name)
            if err != nil {
                return err
            }
            defer f.Close()  // ✨ 立刻注册关闭,无论函数怎么退出都会执行
        
            // 读取文件内容...
            // 即使这里 return 或 panic,f.Close() 都会被执行
            return nil
        }
        
        // 锁
        func updateCounter() {
            mu.Lock()
            defer mu.Unlock()  // ✨ 保证解锁,不会死锁
        
            counter++
            // 即使后续代码 panic,锁也能释放
        }
        
      • 多个 defer: LIFO 栈顺序

        func main() {
            defer fmt.Println("1")
            defer fmt.Println("2")
            defer fmt.Println("3")
            fmt.Println("main")
        }
        // 输出:
        // main
        // 3  ← 后进先出(栈)
        // 2
        // 1
        
      • defer 语句参数
        • defer 语句的参数在注册是就计算好,不是执行时
        func main() {
            x := 10
            defer fmt.Println("defer:", x)  // x 的值 10 被立即捕获
            x = 20
            fmt.Println("main:", x)
        }
        // 输出:
        // main: 20
        // defer: 10   ← 不是 20!
        
        // 如果想用最终值,用闭包:
        func main() {
            x := 10
            defer func() {
                fmt.Println("defer:", x)  // 闭包捕获变量引用
            }()
            x = 20
        }
        // 输出 defer: 20
        

函数

  • 函数深入
    • 函数基础
      • Go 的函数定义和 C 系列语言不一样:关键字 func 在前,返回值类型在后

        // 基本语法:func 函数名(参数列表) 返回值类型 { 函数体 }
        func add(a int, b int) int {
            return a + b
        }
        
        // 参数同类型可以合并声明
        func add2(a, b int) int {
            return a + b
        }
        
        // 无返回值
        func greet(name string) {
            fmt.Println("Hello,", name)
        }
        
        // 无参数也无返回值
        func printVersion() {
            fmt.Println("v1.0.0")
        }
        
      • 函数是一等公民
        • Go 的函数可以赋值给变量、作为参数传递、作为返回值返回
        • 这让 Go 支持函数式编程风格,比如高阶函数、回调、中间件模式等
        // 函数赋值给变量
        var operation func(int, int) int = add
        result := operation(1, 2) // 3
        
        // 函数作为参数
        func apply(nums []int, fn func(int) int) []int {
            result := make([]int, len(nums))
            for i, n := range nums {
                result[i] = fn(n)
            }
            return result
        }
        
        doubled := apply([]int{1, 2, 3}, func(x int) int {
            return x * 2
        }) // [2, 4, 6]
        
    • 多返回值
      • go 原生支持多返回值

        // 返回商和余数
        func divmod(a, b int) (int, int) {
            return a / b, a % b
        }
        
        q, r := divmod(10, 3) // q=3, r=1
        
        // 只想要其中一个,用 _ 忽略
        q, _ := divmod(10, 3) // 只要商
        _, r = divmod(10, 3) // 只要余数
        
      • 几乎所有主流库都用这个特性返回 (result, error)
      • go 的惯用法:(result,error)

        // 几乎所有可能失败的操作都返回 (结果, error)
        func readFile(name string) (string, error) {
            data, err := os.ReadFile(name)
            if err != nil {
                return "", err // 失败时返回零值 + 错误
            }
            return string(data), nil // 成功时返回结果 + nil
        }
        
        // 调用方的标准模式
        content, err := readFile("hello.txt")
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(content)
        
    • 命名返回值
      • go 允许给返回值命名,在返回值多,复杂逻辑时有用

        // 普通多返回值
        func divide1(a, b float64) (float64, error) {
            if b == 0 {
                return 0, errors.New("除数不能为零")
            }
            return a / b, nil
        }
        
        // 命名返回值版本
        func divide2(a, b float64) (result float64, err error) {
            if b == 0 {
                err = errors.New("除数不能为零")
                return // 裸 return,自动返回 result=0, err=上面的值
            }
            result = a / b
            return // 自动返回 result, err
        }
        
      • 命名返回值 + defer

        // 在 defer 中修改返回值
        func doWork() (err error) {
            defer func() {
                if r := recover(); r != nil {
                    err = fmt.Errorf("恢复自 panic: %v", r)
                    // ✨ 这里能修改返回值 err!
                }
            }()
        
            // 可能 panic 的代码
            panic("something bad")
        }
        
        result := doWork() // result 不是 nil,而是包装后的 error
        
      • 什么时候用命名返回值
        • 返回值很多且含义复杂时,命名可以当文档
        • 需要在 defer 中修改返回值时(recover 模式)
    • 可变参数函数
      • 用 …T 表示可变参数,函数内部会收到一个 []T 类型的切片
      • fmt.Println 就是典型的可变参数函数。
      • 示例

        // 求任意个数字的和
        func sum(nums ...int) int {
            total := 0
            for _, n := range nums {
                total += n
            }
            return total
        }
        
        sum()              // 0(传递空切片)
        sum(1, 2, 3)       // 6
        sum(1, 2, 3, 4, 5) // 15
        
        // 把切片展开传递,用 ... 展开
        nums := []int{10, 20, 30}
        sum(nums...) // 60,注意是 nums... 而不是 nums
        
      • 混合使用固定参数和可变参数

        // 可变参数必须是最后一个参数
        func greet(greeting string, names ...string) {
            for _, name := range names {
                fmt.Printf("%s, %s!\n", greeting, name)
            }
        }
        
        greet("Hello", "Alice", "Bob", "Charlie")
        // Hello, Alice!
        // Hello, Bob!
        // Hello, Charlie!
        
    • 闭包与匿名函数
      • 闭包是能访问外部作用域变量的函数
      • Go 的闭包语法简洁,是实现迭代器、中间件、回调等模式的基础
      • 匿名函数

        // 定义后立即调用(IIFE)
        func() {
            fmt.Println("我是匿名函数")
        }()
        
        // 赋值给变量
        add := func(a, b int) int {
            return a + b
        }
        fmt.Println(add(1, 2)) // 3
        
      • 闭包:捕获外部变量

        // 计数器生成器
        func makeCounter() func() int {
            count := 0
            return func() int {
                count++ // ✨ 闭包捕获了外部的 count
                return count
            }
        }
        
        c1 := makeCounter()
        fmt.Println(c1()) // 1
        fmt.Println(c1()) // 2
        fmt.Println(c1()) // 3
        
        // 每个闭包有自己独立的状态
        c2 := makeCounter()
        fmt.Println(c2()) // 1(c2 的 count 是独立的)
        fmt.Println(c1()) // 4(c1 继续累加)
        
      • 闭包实战:装饰器模式

        // 为函数添加日志功能
        func withLogging(name string, fn func(int) int) func(int) int {
            return func(x int) int {
                fmt.Printf("调用 %s(%d)\n", name, x)
                result := fn(x)
                fmt.Printf("%s(%d) = %d\n", name, x, result)
                return result
            }
        }
        
        double := func(x int) int { return x * 2 }
        loggedDouble := withLogging("double", double)
        
        loggedDouble(5)
        // 调用 double(5)
        // double(5) = 10
        
        // 这是 HTTP 中间件的基础思想!
        
      • 闭包的捕获规则
        • 闭包捕获的是引用,不是值
        • 修改外部变量会反映到闭包内
        • 闭包修改也会反映到外部
    • error 作为 返回值
      • Go 把 error 当作普通值处理,而不是特殊的异常机制
      • error 的本质:一个接口

        // error 其实就是标准库定义的一个接口
        // 任何实现了 Error() string 方法的类型都是 error
        type error interface {
            Error() string
        }
        
        // 创建 error 的几种方式:
        // 方式一:errors.New
        import "errors"
        err := errors.New("文件不存在")
        
        // 方式二:fmt.Errorf(支持格式化)
        err = fmt.Errorf("文件 %s 不存在", filename)
        
        // 方式三:自定义类型(后续再深入)
        
      • 标准返回模式

        // ✅ Go 惯用写法:err 作为最后一个返回值
        func findUser(id int) (*User, error) {
            if id < 0 {
                return nil, fmt.Errorf("无效 ID: %d", id)
            }
        
            user, ok := userCache[id]
            if !ok {
                return nil, errors.New("用户不存在")
            }
        
            return user, nil
        }
        
        // 调用方必须检查 err
        user, err := findUser(42)
        if err != nil {
            return err
        }
        // 这里 user 保证不是 nil
        
      • 提前返回:降低嵌套

        // ❌ 过度嵌套(来自 Java/Python 习惯)
        func processDataBad(path string) error {
            data, err := readFile(path)
            if err == nil {
                parsed, err := parse(data)
                if err == nil {
                    result, err := transform(parsed)
                    if err == nil {
                        save(result)
                        return nil
                    }
                    return err
                }
                return err
            }
            return err
        }
        
        // ✅ Go 风格:早期返回,扁平化
        func processData(path string) error {
            data, err := readFile(path)
            if err != nil {
                return err
            }
        
            parsed, err := parse(data)
            if err != nil {
                return err
            }
        
            result, err := transform(parsed)
            if err != nil {
                return err
            }
        
            return save(result)
        }
        

数组、切片与 Map

  • 数组、切片与 Map
    • 数组:固定长度的值类型
      • Go 的数组和 C/Java 的不一样,它是值类型,长度是类型的一部分
      • 实际开发很少使用数组,几乎都使用切片
      • 示例

        // 声明数组:类型 [N]T
        var a [3]int           // [0, 0, 0],零值初始化
        b := [3]int{1, 2, 3}   // 字面量初始化
        c := [...]int{1, 2, 3} // ... 让编译器推断长度
        
        // 长度是类型的一部分!
        var x [3]int
        var y [4]int
        x = y  // ❌ 编译错误:[3]int 和 [4]int 是不同类型
        
        // 访问和修改
        fmt.Println(b[0])      // 1
        b[0] = 100
        fmt.Println(len(b))    // 3
        
        // 遍历
        for i, v := range b {
            fmt.Printf("b[%d] = %d\n", i, v)
        }
        
      • 数组是值类型
        • 把数组赋值给另一个变量,或者传入函数,都会完整拷贝一份
        • 大数组这样性能会很差,实际开发总是使用切片
        a := [3]int{1, 2, 3}
        b := a       // ✨ 完全拷贝!b 和 a 是独立的
        b[0] = 100
        
        fmt.Println(a)  // [1 2 3]  ← a 没变
        fmt.Println(b)  // [100 2 3]
        
        // 传入函数也是拷贝
        func modify(arr [3]int) {
            arr[0] = 999
        }
        modify(a)
        fmt.Println(a)  // [1 2 3]  ← 依然没变!
        
    • 切片:Go 的主力军
      • 切片是 Go 中最常用的数据结构
      • 看起来像动态数组,但底层是对数组的视图
      • 切片的三要素

        // 切片在内存中是一个结构体(概念上):
        // type slice struct {
        //     ptr      *T    // 指向底层数组的指针
        //     len      int   // 当前长度
        //     cap      int   // 容量(底层数组剩余空间)
        // }
        
        s := []int{10, 20, 30}
        fmt.Println(len(s))  // 3(长度)
        fmt.Println(cap(s))  // 3(容量)
        
      • 创建切片的五种方式

        // ① 字面量
        s1 := []int{1, 2, 3}
        
        // ② make:指定长度(和容量)
        s2 := make([]int, 5)       // len=5, cap=5,值全是 0
        s3 := make([]int, 3, 10)   // len=3, cap=10
        
        // ③ nil 切片
        var s4 []int                // nil,len=0, cap=0
        fmt.Println(s4 == nil)      // true
        // 但 nil 切片可以直接 append,不会 panic
        
        // ④ 空切片(和 nil 切片 len/cap 相同但底层不同)
        s5 := []int{}
        fmt.Println(s5 == nil)      // false
        
        // ⑤ 切割已有数组/切片(下一节详讲)
        arr := [5]int{1, 2, 3, 4, 5}
        s6 := arr[1:4]              // [2, 3, 4]
        
      • ni 切片 vs 空切片
        • 两者几乎可以互换
        • 都可以 append,都可以 range
        • JSON 序列化时 nil 变为 null,空切片变为 【】
        • 推荐总是优先使用 nil 切片
    • 切片操作:切割与 append
      • 切割语法 s[low:high:max]

        s := []int{0, 1, 2, 3, 4, 5}
        
        // 基本切割 [low:high]
        s[1:4]      // [1, 2, 3]  ← 左闭右开
        s[:3]       // [0, 1, 2]  省略 low 默认 0
        s[3:]       // [3, 4, 5]  省略 high 默认 len
        s[:]        // [0, 1, 2, 3, 4, 5]  完整拷贝引用
        
        // 三参数切割 [low:high:max] —— 限制容量
        s2 := s[1:4:4]
        fmt.Println(len(s2), cap(s2))  // 3, 3(容量被限制)
        // 不加 :max 的话,cap(s2) 会是 5(底层数组剩余容量)
        
      • append: 扩容机制

        s := []int{1, 2, 3}
        s = append(s, 4)          // [1 2 3 4]
        s = append(s, 5, 6, 7)    // 添加多个
        
        // 合并两个切片
        a := []int{1, 2}
        b := []int{3, 4}
        c := append(a, b...)      // 注意 ...,展开 b
        // c = [1 2 3 4]
        
      • 扩容规则

        // 当 append 超过 cap 时,Go 会分配新的底层数组
        // 扩容规则(Go 1.18+ 简化):
        //   cap < 256:翻倍(1 → 2 → 4 → 8 ...)
        //   cap >= 256:每次增加约 25%
        
        s := make([]int, 0, 2)
        fmt.Println(cap(s))  // 2
        
        s = append(s, 1, 2, 3)
        fmt.Println(cap(s))  // 4(翻倍)
        
        // 扩容是昂贵的!如果知道最终大小,一开始就 make 好
        s := make([]int, 0, 1000)  // ✅ 预分配
        for i := 0; i < 1000; i++ {
            s = append(s, i)        // 不会触发扩容
        }
        
    • 切片陷阱
      • 切割共享底层数组

        original := []int{1, 2, 3, 4, 5}
        sub := original[1:3]        // [2, 3],但底层数组是共享的!
        
        sub[0] = 999
        fmt.Println(original)       // [1 999 3 4 5]  ← 原数组被改了!
        fmt.Println(sub)            // [999 3]
        
        // 安全做法:显式拷贝
        sub := make([]int, 2)
        copy(sub, original[1:3])    // sub 是独立的
        sub[0] = 999
        fmt.Println(original)       // [1 2 3 4 5]  ← 不受影响
        
      • append 可能修改原切片

        original := []int{1, 2, 3, 4, 5}
        sub := original[:3]         // [1 2 3],cap=5
        
        sub = append(sub, 999)      // append 时 cap 够用,复用底层数组!
        fmt.Println(original)       // [1 2 3 999 5]  ← 原数组第 4 位被改了!
        
        // 用三参数切割限制容量,强制 append 时分配新数组
        sub := original[:3:3]       // cap=3
        sub = append(sub, 999)      // 容量不够,分配新数组
        fmt.Println(original)       // [1 2 3 4 5]  ← 原数组安全
        
      • 大切片导致内存泄露

        // 读一个 1GB 大文件,只保留前 100 字节
        func readSmallPart(filename string) []byte {
            big, _ := os.ReadFile(filename)  // 1GB
            return big[:100]  // ⚠️ 返回的切片引用着整个 1GB 数组!
            // 只要返回值存活,1GB 内存就不会被 GC
        }
        
        // 正确做法:主动拷贝脱离引用
        func readSmallPart(filename string) []byte {
            big, _ := os.ReadFile(filename)
            small := make([]byte, 100)
            copy(small, big)
            return small  // ✅ 只引用 100 字节,big 可以被 GC
        }
        
    • 切片常见操作
      • 删除元素

        // Go 没有内置的删除函数,用切片拼接
        s := []int{1, 2, 3, 4, 5}
        i := 2  // 要删除的索引
        
        // 删除索引 i 的元素
        s = append(s[:i], s[i+1:]...)
        // s = [1 2 4 5]
        
        // Go 1.21+ 用 slices.Delete
        import "slices"
        s = slices.Delete(s, i, i+1)
        
      • 插入元素

        s := []int{1, 2, 4, 5}
        i := 2       // 插入位置
        val := 3     // 要插入的值
        
        // 在索引 i 处插入 val
        s = append(s[:i], append([]int{val}, s[i:]...)...)
        // s = [1 2 3 4 5]
        
        // Go 1.21+ 用 slices.Insert(推荐)
        s = slices.Insert(s, i, val)
        
      • 复制、反转、排序

        // 复制
        src := []int{1, 2, 3}
        dst := make([]int, len(src))
        n := copy(dst, src)    // n 是实际复制的元素数
        
        // Go 1.21+ 最简洁
        dst := slices.Clone(src)
        
        // 排序
        import "sort"
        nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
        sort.Ints(nums)         // [1 1 2 3 4 5 6 9]
        sort.Sort(sort.Reverse(sort.IntSlice(nums)))  // 降序
        
        // Go 1.21+ 更简洁
        slices.Sort(nums)
        slices.Reverse(nums)
        
        // 自定义排序
        sort.Slice(nums, func(i, j int) bool {
            return nums[i] > nums[j]  // 降序
        })
        
    • map: 键值对容器
      • Go 的 map 是哈希表实现,支持任何可比较类型作为 key
      • 使用前必须初始化,nil map 不能写入
      • 声明与初始化

        // ❌ 错误:只声明,没初始化
        var m1 map[string]int
        m1["a"] = 1  // panic: assignment to entry in nil map
        
        // ✅ 方式一:make
        m2 := make(map[string]int)
        m2["a"] = 1
        
        // ✅ 方式二:字面量
        m3 := map[string]int{
            "one":   1,
            "two":   2,
            "three": 3,
        }
        
        // ✅ 方式三:make 指定初始容量(性能优化)
        m4 := make(map[string]int, 1000)  // 提前分配空间
        
      • 增删改查

        m := map[string]int{"a": 1, "b": 2}
        
        // 增/改(语法相同)
        m["c"] = 3        // 新增
        m["a"] = 100      // 修改
        
        // 查
        v := m["a"]       // 100
        missing := m["x"] // 0(不存在时返回零值!)
        
        // ✨ 判断 key 是否存在:逗号 ok 惯用法
        v, ok := m["a"]   // v=100, ok=true
        v, ok := m["x"]   // v=0,   ok=false
        if ok {
            fmt.Println("存在")
        }
        
        // 删除(不存在也不会报错)
        delete(m, "a")
        
        // 长度
        fmt.Println(len(m))
        
      • 遍历(顺序随机)

        m := map[string]int{"a": 1, "b": 2, "c": 3}
        
        // 每次遍历顺序都可能不同!
        for k, v := range m {
            fmt.Printf("%s=%d\n", k, v)
        }
        
        // 需要有序遍历:把 key 取出来排序
        keys := make([]string, 0, len(m))
        for k := range m {
            keys = append(keys, k)
        }
        sort.Strings(keys)
        
        for _, k := range keys {
            fmt.Printf("%s=%d\n", k, m[k])
        }
        
    • map 进阶:零值技巧与并发
      • 零值技巧:计数器模式

        // map 访问不存在的 key 返回零值,这特性很好用
        words := []string{"go", "rust", "go", "python", "go", "rust"}
        
        count := make(map[string]int)
        for _, w := range words {
            count[w]++  // ✨ 不存在时 count[w] 是 0,+1 变成 1
        }
        // count = {"go": 3, "rust": 2, "python": 1}
        
        // 分组:map + 切片
        type User struct {
            Name string
            City string
        }
        users := []User{
            {"Alice", "Tokyo"},
            {"Bob", "Tokyo"},
            {"Charlie", "Osaka"},
        }
        
        byCity := make(map[string][]User)
        for _, u := range users {
            byCity[u.City] = append(byCity[u.City], u)
        }
        // byCity["Tokyo"] = [{Alice Tokyo} {Bob Tokyo}]
        
      • map 作为集合

        // Go 没有内置 Set,用 map[T]struct{} 模拟
        set := map[string]struct{}{}
        
        // 添加
        set["apple"] = struct{}{}
        set["banana"] = struct{}{}
        
        // 判断存在
        _, ok := set["apple"]   // true
        
        // 删除
        delete(set, "apple")
        
        // 为什么用 struct{} 而不是 bool?
        // struct{} 不占内存(0 字节),比 bool(1字节)更省空间
        
      • map 不是并发安全的

        // ❌ 多个 goroutine 同时读写 map 会 panic!
        m := make(map[string]int)
        go func() { m["a"] = 1 }()
        go func() { m["b"] = 2 }()
        // 运行时报错:fatal error: concurrent map writes
        
        // ✅ 方式一:sync.Mutex 加锁
        var mu sync.Mutex
        m := make(map[string]int)
        
        go func() {
            mu.Lock()
            defer mu.Unlock()
            m["a"] = 1
        }()
        
        // ✅ 方式二:sync.Map(适合读多写少场景)
        var sm sync.Map
        sm.Store("a", 1)
        v, ok := sm.Load("a")
        sm.Delete("a")
        sm.Range(func(k, v any) bool {
            fmt.Println(k, v)
            return true  // 返回 false 停止遍历
        })
        

结构体与方法

  • 结构体与方法
    • 结构体基础
      • 结构体是 Go 中组织数据的核心方式
      • Go 没有 class,但 struct + 方法已经能表达几乎所有面向对象的特性,而且更简单直接
      • 定义与初始化

        // 定义结构体
        type User struct {
            ID       int
            Name     string
            Email    string
            Active   bool
        }
        
        // 初始化方式一:字段名(推荐,最清晰)
        u1 := User{
            ID:     1,
            Name:   "Alice",
            Email:  "alice@example.com",
            Active: true,
        }
        
        // 初始化方式二:按字段顺序(脆弱,字段增减会出问题)
        u2 := User{2, "Bob", "bob@example.com", false}
        
        // 初始化方式三:零值
        var u3 User  // 所有字段都是零值:{0, "", "", false}
        u3.Name = "Charlie"
        
        // 初始化方式四:new(返回指针)
        u4 := new(User)  // 等价于 &User{}
        u4.Name = "David"
        
      • 访问和修改字段

        u := User{Name: "Alice"}
        
        // 访问字段
        fmt.Println(u.Name)
        
        // 修改字段
        u.Name = "Alicia"
        
        // 指针也用 . 访问(Go 自动解引用)
        p := &u
        p.Name = "Alice 2"  // 等价于 (*p).Name,Go 帮你省了*
        fmt.Println(u.Name)  // Alice 2
        
      • 字段名大小写
        • 首字母大写的字段是导出的(public),可以被包外访问
        • 小写的是未导出的(private),只能包内使用
    • 值类型 vs 指针
      • struct 是值类型。赋值、传参、返回都是完整拷贝

        u1 := User{Name: "Alice"}
        u2 := u1           // ✨ 完全拷贝!u1 和 u2 是两个独立对象
        u2.Name = "Bob"
        
        fmt.Println(u1.Name)  // Alice(没变)
        fmt.Println(u2.Name)  // Bob
        
        // 传入函数也是拷贝
        func modify(u User) {
            u.Name = "Changed"  // 只修改了拷贝
        }
        modify(u1)
        fmt.Println(u1.Name)  // Alice(没变!)
        
        // 想修改原对象?传指针
        func modifyPtr(u *User) {
            u.Name = "Changed"
        }
        modifyPtr(&u1)
        fmt.Println(u1.Name)  // Changed
        
      • 什么时候用指针

        // ✅ 使用指针的场景:
        
        // 1. 需要修改原对象
        func (u *User) SetName(name string) {
            u.Name = name
        }
        
        // 2. 结构体很大,拷贝开销大
        type Config struct {
            // 假设有 50 个字段
        }
        func process(c *Config) { ... }  // 传指针避免拷贝
        
        // 3. 想表达「可能为空」的语义
        func findUser(id int) *User {
            if !found {
                return nil  // 用 nil 表示未找到
            }
            return &user
        }
        
        // ❌ 不用指针的场景:
        // - 小结构体(几个字段的)
        // - 不修改的只读操作
        // - 代表不可变概念(如 time.Time)
        
      • 经验法则
        • 拿不准就用指针
        • Go 代码中 *User 比 User 更常见
        • 但像 time.Time、image.Point 这种小且不可变的类型,用值类型更自然
    • 方法:给类型加行为
      • 方法是绑定到特定类型的函数
      • Go 的方法语法有点特别——接收者写在函数名前面
      • 方法定义语法

        type Rectangle struct {
            Width, Height float64
        }
        
        // 方法定义:func (接收者 类型) 方法名(参数) 返回值
        func (r Rectangle) Area() float64 {
            return r.Width * r.Height
        }
        
        func (r Rectangle) Perimeter() float64 {
            return 2 * (r.Width + r.Height)
        }
        
        // 调用
        rect := Rectangle{Width: 3, Height: 4}
        fmt.Println(rect.Area())       // 12
        fmt.Println(rect.Perimeter())  // 14
        
      • 值接收者 vs 指针接收者

        type Counter struct {
            count int
        }
        
        // ❌ 值接收者:修改不会生效!
        func (c Counter) IncrementWrong() {
            c.count++  // 只修改了副本
        }
        
        // ✅ 指针接收者:修改原对象
        func (c *Counter) Increment() {
            c.count++
        }
        
        // ✅ 只读方法用值接收者
        func (c Counter) Get() int {
            return c.count
        }
        
        c := Counter{}
        c.IncrementWrong()
        fmt.Println(c.Get())  // 0 ← 没变!
        
        c.Increment()
        fmt.Println(c.Get())  // 1 ← 生效了
        c.Increment()
        fmt.Println(c.Get())  // 2
        
      • 选择接受者类型的原则
        • 需要修改对象 -> 必须用指针接收者
        • 结构体很大 -> 用指针避免拷贝
        • 包含 sync.Mutex 等不可拷贝字段 -> 必须用指针
        • 其他情况两者都行,但是一个类型的方法应该统一
        • 要么全值接收者,要么全指针接收者
      • go 的自动转换

        // Go 会在值和指针之间自动转换,调用方法时不用纠结
        
        c := Counter{}   // 值
        c.Increment()    // ✨ Go 自动取地址 (&c).Increment()
        c.Get()          // 值调用值方法,直接
        
        p := &Counter{}  // 指针
        p.Increment()    // 指针调用指针方法,直接
        p.Get()          // ✨ Go 自动解引用 (*p).Get()
        
        // 唯一限制:不能在「不可寻址」的值上调用指针方法
        Counter{}.Increment()  // ❌ 编译错误,字面量不可寻址
        
        // 但可以:
        c := Counter{}
        c.Increment()          // ✅ 变量可寻址
        
    • 给任意类型定义方法
      • 方法不只能定义在 struct 上,任何在当前包定义的类型都可以有方法

        // 给 int 起别名并加方法
        type Celsius float64
        
        func (c Celsius) ToFahrenheit() float64 {
            return float64(c)*9/5 + 32
        }
        
        func (c Celsius) String() string {
            return fmt.Sprintf("%.1f°C", float64(c))
        }
        
        // 给切片起别名并加方法
        type IntSlice []int
        
        func (s IntSlice) Sum() int {
            total := 0
            for _, n := range s {
                total += n
            }
            return total
        }
        
        func (s IntSlice) Max() int {
            if len(s) == 0 {
                return 0
            }
            m := s[0]
            for _, n := range s[1:] {
                if n > m {
                    m = n
                }
            }
            return m
        }
        
        func main() {
            temp := Celsius(25)
            fmt.Println(temp)               // 25.0°C(调用了 String 方法)
            fmt.Println(temp.ToFahrenheit()) // 77
        
            nums := IntSlice{1, 5, 3, 8, 2}
            fmt.Println(nums.Sum())  // 19
            fmt.Println(nums.Max())  // 8
        }
        
      • 只能给本包类型加方法
        • 不能直接给 int、string 等内置类型加方法,也不能给其他包的类型加方法
        • 需要的话就 type MyInt int 定义一个新类型。这个限制防止混乱——每个类型的方法归属清晰
    • 结构体嵌入
      • Go 没有继承,但有「嵌入」
      • 把一个 struct 嵌入到另一个里面,外层可以直接访问内层的字段和方法
      • 这就是 Go 面向对象的核心——组合优于继承
      • 基本嵌入

        type Animal struct {
            Name string
            Age  int
        }
        
        func (a Animal) Describe() string {
            return fmt.Sprintf("%s, %d岁", a.Name, a.Age)
        }
        
        // Dog 嵌入 Animal
        type Dog struct {
            Animal          // ✨ 嵌入字段(没有字段名)
            Breed  string
        }
        
        d := Dog{
            Animal: Animal{Name: "旺财", Age: 3},
            Breed:  "柴犬",
        }
        
        // 直接访问嵌入字段的属性和方法(提升)
        fmt.Println(d.Name)         // "旺财"(不用 d.Animal.Name)
        fmt.Println(d.Describe())   // "旺财, 3岁"(继承了 Animal 的方法)
        fmt.Println(d.Breed)        // "柴犬"
        
        // 也可以显式访问
        fmt.Println(d.Animal.Name)  // 同上
        
      • 方法覆盖

        // Dog 可以定义同名方法「覆盖」Animal 的
        func (d Dog) Describe() string {
            // 可以调用被覆盖的方法
            base := d.Animal.Describe()
            return fmt.Sprintf("%s,品种:%s", base, d.Breed)
        }
        
        d := Dog{Animal: Animal{Name: "旺财", Age: 3}, Breed: "柴犬"}
        fmt.Println(d.Describe())  // "旺财, 3岁,品种:柴犬"
        
      • 嵌入接口

        // 也可以嵌入接口,常见于标准库
        type ReadCloser interface {
            io.Reader    // 嵌入接口
            io.Closer    // 嵌入接口
        }
        // 等价于:
        // type ReadCloser interface {
        //     Read(p []byte) (n int, err error)
        //     Close() error
        // }
        
        // 实战场景:给一个已有类型「增强」
        type LoggedDB struct {
            *sql.DB          // 嵌入指针,继承所有方法
            logger *log.Logger
        }
        
        // 只需要定义想增强的方法
        func (db *LoggedDB) Query(query string, args ...any) (*sql.Rows, error) {
            db.logger.Printf("SQL: %s", query)
            return db.DB.Query(query, args...)
        }
        // 其他方法(Ping、Exec 等)自动继承自 *sql.DB
        
      • 嵌入 vs 继承
        • 嵌入不是继承
        • 嵌入字段是一个真实存在的字段,外层 struct has-a 内层,不是 is-a
        • 你可以嵌入多个类型(多继承的感觉),但本质是组合:Dog 不是 Animal 的子类,而是「包含了一个 Animal」
    • 构造函数模式
      • Go 没有 constructor 关键字
      • 惯例是定义 NewXxx 函数返回初始化好的实例,这样可以封装验证、默认值、依赖注入等逻辑。
      • 示例

        type User struct {
            ID        int
            Name      string
            Email     string
            CreatedAt time.Time
        }
        
        // 标准构造函数:返回值
        func NewUser(name, email string) User {
            return User{
                Name:      name,
                Email:     email,
                CreatedAt: time.Now(),
            }
        }
        
        // 更常见:返回指针 + error
        func NewUser(name, email string) (*User, error) {
            if name == "" {
                return nil, errors.New("name 不能为空")
            }
            if !strings.Contains(email, "@") {
                return nil, fmt.Errorf("无效的 email: %s", email)
            }
            return &User{
                Name:      name,
                Email:     email,
                CreatedAt: time.Now(),
            }, nil
        }
        
        // 使用
        u, err := NewUser("Alice", "alice@example.com")
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(u.Name)
        
      • 函数选项模式

        // 问题:构造函数参数太多怎么办?
        // func NewServer(host string, port int, timeout time.Duration, tls bool, ...) 
        
        // 解决:函数选项模式(Functional Options)
        type Server struct {
            host    string
            port    int
            timeout time.Duration
            tls     bool
        }
        
        // Option 是一个修改 Server 的函数
        type Option func(*Server)
        
        func WithPort(p int) Option {
            return func(s *Server) { s.port = p }
        }
        
        func WithTimeout(t time.Duration) Option {
            return func(s *Server) { s.timeout = t }
        }
        
        func WithTLS() Option {
            return func(s *Server) { s.tls = true }
        }
        
        // 构造函数接受任意个 Option
        func NewServer(host string, opts ...Option) *Server {
            // 默认值
            s := &Server{
                host:    host,
                port:    80,
                timeout: 30 * time.Second,
                tls:     false,
            }
            // 应用选项
            for _, opt := range opts {
                opt(s)
            }
            return s
        }
        
        // 使用:非常灵活
        s1 := NewServer("localhost")
        s2 := NewServer("example.com", WithPort(443), WithTLS())
        s3 := NewServer("api.com", WithTimeout(10*time.Second))
        
      • 函数选项模式的威力
        • 这个模式在 Go 生态广泛使用(gRPC、Kubernetes、各种库的配置都是这个模式)
        • 优势:可选参数不用硬编码、扩展性好(加新选项不破坏现有代码)、可读性强(调用方清楚写出每个选项)
    • 结构体标签
      • Struct Tag 是字段后面的反引号字符串,为字段附加元数据
      • JSON 序列化、数据库映射、表单校验都用它
      • 示例

        type User struct {
            ID       int       `json:"id"`
            Name     string    `json:"name" validate:"required,min=2"`
            Email    string    `json:"email" validate:"required,email"`
            Password string    `json:"-"`                          // - 表示不参与 JSON 序列化
            Age      int       `json:"age,omitempty"`               // omitempty:零值时忽略
            Role     string    `json:"role" db:"user_role"`          // 同一字段可有多个 tag
        }
        
        // JSON 序列化
        u := User{
            ID:    1,
            Name:  "Alice",
            Email: "alice@example.com",
            Password: "secret",
            // Age 是 0,会被忽略
        }
        
        data, _ := json.Marshal(u)
        fmt.Println(string(data))
        // {"id":1,"name":"Alice","email":"alice@example.com","role":""}
        // 注意:password 不在输出中,age 也被忽略
        
      • 常见 Tag 用法一览

        type Article struct {
            // encoding/json
            ID       int       `json:"id"`
            Title    string    `json:"title"`
            Content  string    `json:"content,omitempty"`  // 空时忽略
            Internal string    `json:"-"`                   // 从不序列化
        
            // GORM(Day 17-18)
            CreatedAt time.Time `gorm:"autoCreateTime"`
            Slug      string    `gorm:"uniqueIndex;size:100"`
        
            // validator(参数校验)
            Author   string    `validate:"required,min=2,max=50"`
            Views    int       `validate:"gte=0"`
        
            // 组合使用
            Email    string    `json:"email" validate:"required,email" db:"user_email"`
        }
        
      • tag 只是字符串
        • Tag 本身只是给字段附加的字符串元数据,语言本身不处理
        • 是各个库(encoding/json、gorm、validator…)通过反射读取 tag 并做相应处理
        • tag 的语法完全由使用它的库定义

接口与多态

  • 接口与多态
    • 接口是什么
      • 接口是 Go 最优雅的特性。
      • 它定义了一组方法签名,任何实现了这些方法的类型都「自动」满足这个接口——不需要显式声明 implements。
      • 这叫做「隐式接口实现」,也叫鸭子类型。
      • 接口示例

        // 定义接口:只有方法签名,没有实现
        type Animal interface {
            Sound() string
            Name() string
        }
        
        // Dog 实现了 Animal 接口
        // 注意:不需要写 "implements Animal"!
        type Dog struct{ name string }
        
        func (d Dog) Sound() string { return "汪汪" }
        func (d Dog) Name() string  { return d.name }
        
        // Cat 也实现了 Animal 接口
        type Cat struct{ name string }
        
        func (c Cat) Sound() string { return "喵喵" }
        func (c Cat) Name() string  { return c.name }
        
        // 函数接受接口类型
        func Describe(a Animal) {
            fmt.Printf("%s 说:%s\n", a.Name(), a.Sound())
        }
        
        func main() {
            dog := Dog{name: "旺财"}
            cat := Cat{name: "咪咪"}
        
            Describe(dog)  // 旺财 说:汪汪
            Describe(cat)  // 咪咪 说:喵喵
        
            // 接口变量可以持有任意满足条件的类型
            var a Animal = dog
            a = cat  // 随时切换
        }
        
      • 隐式实现的威力
        • Java/C# 要写 implements Animal,Go 不需要
        • 只要你的类型有对应的方法,它就是那个接口
        • 你可以让一个你不能修改的第三方类型满足你的接口
        • 也可以在不改动现有代码的情况下新定义接口来约束它们
        • 这是 Go 解耦的核心武器
    • 接口值的内部结构
      • 接口值在底层由两部分组成:动态类型(type)和动态值(value)
      • 示例

        // 接口值 = (type, value)
        var a Animal  // (nil, nil),零值
        
        a = Dog{name: "旺财"}
        // a 现在是 (Dog, {name:"旺财"})
        
        // 打印类型和值
        fmt.Printf("类型: %T\n", a)  // main.Dog
        fmt.Printf("值: %v\n", a)    // {旺财}
        
      • nil 接口 vs 含 nil 的接口

        // 这是 Go 最反直觉的陷阱之一!
        
        var d *Dog = nil  // d 是 nil 指针
        
        var a Animal = d  // a 的 type=*Dog, value=nil
                        // a 不是 nil!因为 type 部分有值
        
        fmt.Println(a == nil)   // false ← 让人意外!
        fmt.Println(d == nil)   // true
        
        // 正确判断接口是否有效
        func process(a Animal) {
            // ❌ 错误:a != nil 不代表 a 里的值不是 nil
            if a != nil {
                // 如果 a = (*Dog)(nil),这里调用方法会 panic
            }
        
            // ✅ 正确:用 reflect 或 type switch 检查
            if a == nil {
                return
            }
        }
        
        // 最佳实践:函数返回接口时,失败直接返回 nil
        func findAnimal() Animal {
            var d *Dog = nil
            return d   // ❌ 坑!返回的接口不是 nil
            return nil // ✅ 这才是真正的 nil 接口
        }
        
        • 「含有 nil 指针的接口,不等于 nil 接口。」
        • 一个接口值只有当 type 和 value 都是 nil 时,它才等于 nil
        • 函数返回接口类型时,失败直接 return nil,而不是 return (*ConcreteType)(nil)
    • 类型断言与 type switch
      • 有时候你需要从接口里把具体类型「取出来」
      • Go 提供了两种方式:类型断言和 type switch
      • 类型断言

        var a Animal = Dog{name: "旺财"}
        
        // 方式一:直接断言(不安全,失败会 panic)
        d := a.(Dog)
        fmt.Println(d.name)  // 旺财
        
        // 方式二:逗号 ok(推荐,安全)
        d, ok := a.(Dog)
        if ok {
            fmt.Println("是 Dog:", d.name)
        } else {
            fmt.Println("不是 Dog")
        }
        
        // 断言为接口(检查是否实现了另一个接口)
        type Swimmer interface {
            Swim() string
        }
        
        if s, ok := a.(Swimmer); ok {
            fmt.Println(s.Swim())
        } else {
            fmt.Println("不会游泳")
        }
        
      • type switch

        func describe(i interface{}) {
            switch v := i.(type) {
            case int:
                fmt.Printf("整数: %d,翻倍是 %d\n", v, v*2)
            case string:
                fmt.Printf("字符串: %q,长度 %d\n", v, len(v))
            case bool:
                fmt.Printf("布尔值: %t\n", v)
            case []int:
                fmt.Printf("整数切片,长度 %d\n", len(v))
            case nil:
                fmt.Println("是 nil")
            default:
                fmt.Printf("未知类型: %T\n", v)
            }
        }
        
        describe(42)            // 整数: 42,翻倍是 84
        describe("hello")       // 字符串: "hello",长度 5
        describe(true)          // 布尔值: true
        describe([]int{1,2,3})  // 整数切片,长度 3
        describe(nil)           // 是 nil
        
      • 类型断言 vs 类型转换
        • 类型断言 a.(Dog) 是从接口里取出具体类型
        • 运行时才知道结果,失败会 panic
        • 类型转换 int(3.14) 是编译时已知的类型转换,不会失败(但可能丢精度)
    • 标准库核心接口
      • Go 标准库定义了很多小接口,每个接口只有 1-3 个方法
      • fmt.Stringer:自定义打印格式

        // 标准库定义:
        // type Stringer interface {
        //     String() string
        // }
        
        type Point struct{ X, Y int }
        
        // 实现 Stringer 接口
        func (p Point) String() string {
            return fmt.Sprintf("(%d, %d)", p.X, p.Y)
        }
        
        p := Point{3, 4}
        fmt.Println(p)        // (3, 4)  ← 自动调用 String()
        fmt.Printf("%v\n", p) // (3, 4)
        fmt.Printf("%s\n", p) // (3, 4)
        
      • error:自定义错误类型

        // error 接口只有一个方法:
        // type error interface {
        //     Error() string
        // }
        
        // 自定义错误类型(携带更多信息)
        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 || age > 150 {
                return &ValidationError{
                    Field:   "age",
                    Message: fmt.Sprintf("值 %d 超出合法范围 [0, 150]", age),
                }
            }
            return nil
        }
        
        err := validateAge(-1)
        if err != nil {
            fmt.Println(err)  // 字段 age 验证失败: 值 -1 超出合法范围 [0, 150]
        
            // 用类型断言获取详细信息
            if ve, ok := err.(*ValidationError); ok {
                fmt.Println("出问题的字段:", ve.Field)
            }
        }
        
      • io.Reader 和 io.Writer:I/O 的基石

        // io.Reader:能被读取的任何东西
        // type Reader interface {
        //     Read(p []byte) (n int, err error)
        // }
        
        // io.Writer:能被写入的任何东西
        // type Writer interface {
        //     Write(p []byte) (n int, err error)
        // }
        
        // 这两个接口让函数极其通用:
        func copyData(dst io.Writer, src io.Reader) (int64, error) {
            return io.Copy(dst, src)
        }
        
        // 文件、网络连接、内存 buffer 都实现了这两个接口:
        file, _ := os.Open("input.txt")
        buf := &bytes.Buffer{}
        
        copyData(buf, file)      // 文件 → 内存
        copyData(os.Stdout, buf) // 内存 → 标准输出
        copyData(os.Stdout, strings.NewReader("hello")) // 字符串 → 标准输出
        
        // 自己实现 io.Writer(比如日志收集器)
        type LogWriter struct {
            prefix string
        }
        
        func (w *LogWriter) Write(p []byte) (int, error) {
            fmt.Printf("[%s] %s", w.prefix, p)
            return len(p), nil
        }
        
        lw := &LogWriter{prefix: "INFO"}
        fmt.Fprintf(lw, "服务器启动在端口 %d\n", 8080)
        // [INFO] 服务器启动在端口 8080
        
    • 接口组合
      • 和结构体嵌入一样,接口也可以嵌入其他接口
      • Go 鼓励定义小接口,再通过组合构建大接口
      • 示例

        // 小接口:单一职责
        type Reader interface {
            Read(p []byte) (n int, err error)
        }
        
        type Writer interface {
            Write(p []byte) (n int, err error)
        }
        
        type Closer interface {
            Close() error
        }
        
        // 通过组合构建复合接口
        type ReadWriter interface {
            Reader
            Writer
        }
        
        type ReadWriteCloser interface {
            Reader
            Writer
            Closer
        }
        
        // 实践中的例子
        type Shape interface {
            Area() float64
            Perimeter() float64
        }
        
        type Drawable interface {
            Draw() string
        }
        
        // 组合成「可绘制的形状」
        type DrawableShape interface {
            Shape
            Drawable
        }
        
      • 接口应该越小越好

        // ❌ 大接口:实现困难,测试困难,耦合高
        type UserService interface {
            CreateUser(name, email string) (*User, error)
            GetUser(id int) (*User, error)
            UpdateUser(id int, name string) error
            DeleteUser(id int) error
            ListUsers() ([]*User, error)
            AuthenticateUser(email, password string) (string, error)
            ResetPassword(email string) error
            // ... 还有 20 个方法
        }
        
        // ✅ 小接口:精准,易测试,低耦合
        type UserCreator interface {
            CreateUser(name, email string) (*User, error)
        }
        
        type UserFinder interface {
            GetUser(id int) (*User, error)
        }
        
        type Authenticator interface {
            Authenticate(email, password string) (string, error)
        }
        
        // 函数只声明它真正需要的
        func registerHandler(creator UserCreator) http.HandlerFunc {
            return func(w http.ResponseWriter, r *http.Request) {
                // 只需要 CreateUser,不依赖整个 UserService
            }
        }
        
    • 空接口与 any
      • 空接口 interface{} 没有任何方法要求,所以所有类型都满足它
      • Go 1.18 引入了 any 作为 interface{} 的别名,更简洁
      • 示例

        // interface{} 和 any 完全等价
        var x any = 42
        x = "hello"
        x = []int{1, 2, 3}
        x = struct{ Name string }{"Alice"}
        
        // 常见于需要处理任意类型的场景
        func printAll(values ...any) {
            for _, v := range values {
                fmt.Printf("%T: %v\n", v, v)
            }
        }
        
        printAll(1, "hello", true, 3.14)
        // int: 1
        // string: hello
        // bool: true
        // float64: 3.14
        
        // map 存储任意类型(类似 JSON 对象)
        config := map[string]any{
            "host":    "localhost",
            "port":    8080,
            "debug":   true,
            "timeout": 30.5,
        }
        
      • 使用 any 的代价

        // any 丢失了类型信息,使用时必须断言
        var v any = 42
        
        // ❌ 不能直接运算
        // fmt.Println(v + 1)  // 编译错误
        
        // ✅ 先断言再使用
        if n, ok := v.(int); ok {
            fmt.Println(n + 1)  // 43
        }
        
        // any 的性能也比具体类型差(有装箱开销)
        // 能用泛型(Day 14)就用泛型,不要滥用 any
        
        // 合理使用场景:
        // 1. JSON 解析(结构未知时)
        // 2. 通用容器(在泛型之前的历史代码)
        // 3. fmt.Println 这种需要接受任意值的工具函数
        
      • any 不是银弹
        • any 让你绕过了类型系统,失去了编译器的保护
        • 能用具体类型就用具体类型,能用泛型就用泛型(Go 1.18+),any 是最后的选择
    • 接口定义最佳实践
      • 在使用方定义接口,不在实现方

        // ❌ 在实现包里定义接口(Java 风格)
        // package userservice
        // type UserService interface { ... }
        // type UserServiceImpl struct { ... }
        
        // ✅ Go 风格:在需要的地方定义接口
        // package handler
        
        type userStore interface {  // 小写,包私有
            GetUser(id int) (*User, error)
            CreateUser(name, email string) (*User, error)
        }
        
        type Handler struct {
            store userStore  // 依赖接口,不依赖具体类型
        }
        
        // 任何实现了 GetUser 和 CreateUser 的类型都可以注入
        func NewHandler(store userStore) *Handler {
            return &Handler{store: store}
        }
        
      • 接口让测试更容易

        // 生产代码
        type DBStore struct { db *sql.DB }
        func (s *DBStore) GetUser(id int) (*User, error) { /* 查数据库 */ }
        
        // 测试用 Mock(不需要数据库!)
        type MockStore struct {
            users map[int]*User
        }
        func (m *MockStore) GetUser(id int) (*User, error) {
            u, ok := m.users[id]
            if !ok {
                return nil, errors.New("not found")
            }
            return u, nil
        }
        func (m *MockStore) CreateUser(name, email string) (*User, error) {
            u := &User{ID: len(m.users) + 1, Name: name, Email: email}
            m.users[u.ID] = u
            return u, nil
        }
        
        // 测试
        func TestHandler(t *testing.T) {
            mock := &MockStore{users: map[int]*User{
                1: {ID: 1, Name: "Alice"},
            }}
            h := NewHandler(mock)  // 注入 Mock
            // ... 测试 handler 逻辑,完全不依赖数据库
        }
        
      • 接口是 Go 依赖注入的基础
        • Go 不需要 Spring 这样的 DI 框架
        • 接口 + 构造函数注入就够了
        • 生产环境注入真实实现,测试时注入 Mock
        • 这让代码既解耦又易测试
        • 这是 Go 后端项目的标准架构模式

并发与工程化

  • 并发与工程化
    • Goroutine 入门
      • Goroutine 是什么
        • Goroutine 是 Go 并发的核心——比线程轻量得多的「协程」
        • 启动一个 Goroutine 只需要在函数调用前加 go 关键字
        • 示例

          package main
          
          import (
              "fmt"
              "time"
          )
          
          func say(s string) {
              for i := 0; i < 3; i++ {
                  fmt.Println(s)
                  time.Sleep(100 * time.Millisecond)
              }
          }
          
          func main() {
              go say("世界")   // 新 goroutine 中运行
              say("你好")      // 当前 goroutine 中运行
          }
          
          // 输出(顺序不确定):
          // 你好
          // 世界
          // 你好
          // 世界
          // 你好
          // 世界
          
        • Goroutine vs 线程
      • GMP 调度模型(概览)
        • Go 运行时用 GMP 模型来调度 Goroutine
        • 示例

          // G = Goroutine(协程)
          // M = Machine(OS 线程)
          // P = Processor(逻辑处理器,调度上下文)
          
          // P 的数量默认等于 CPU 核数,可以设置
          import "runtime"
          
          runtime.GOMAXPROCS(4)              // 设置使用 4 个 P
          fmt.Println(runtime.GOMAXPROCS(0)) // 查看当前 P 数量
          fmt.Println(runtime.NumCPU())      // CPU 核数
          
          // Go 1.5+ 默认 GOMAXPROCS = NumCPU
          // 大多数时候不需要手动设置
          
        • 调度模型示意

          P1 [G1→G2→G3]   P2 [G4→G5→G6]
              ↓                  ↓
              M1                 M2
              ↓                  ↓
          OS Thread          OS Thread
          
          当 G 执行系统调用阻塞时,P 会把 M 换掉继续跑其他 G
          这叫"work stealing"(工作窃取)——空闲的 P 会从别的 P 偷 G 来跑
          
      • sync.WaitGroup 等待协程完成
        • 启动 Goroutine 后,main 函数不会自动等待它们结束
        • 如果 main 退出,所有 Goroutine 都会被强制终止
        • WaitGroup 是等待一组 Goroutine 完成的标准方式
        • 示例

          // ❌ 错误:main 退出,goroutine 还没跑完
          func main() {
              go fmt.Println("可能跑不到")
              // main 立即结束,goroutine 被杀死
          }
          
          // ✅ 用 WaitGroup 等待
          import "sync"
          
          func main() {
              var wg sync.WaitGroup
          
              for i := 0; i < 5; i++ {
                  wg.Add(1)              // 计数器 +1
                  go func(id int) {
                      defer wg.Done()    // 函数结束时计数器 -1
                      fmt.Printf("Worker %d 完成\n", id)
                  }(i)
              }
          
              wg.Wait()  // 阻塞,直到计数器归零
              fmt.Println("所有 Worker 完成")
          }
          
        • WaitGroup 的使用规范

          // ✅ 正确:在 goroutine 启动前 Add
          wg.Add(1)
          go func() {
              defer wg.Done()
              // ...
          }()
          
          // ❌ 错误:在 goroutine 内部 Add(可能来不及)
          go func() {
              wg.Add(1)      // 可能 Wait() 已经过了,竞态!
              defer wg.Done()
              // ...
          }()
          
          // ✅ WaitGroup 不能拷贝,传指针
          func process(wg *sync.WaitGroup) {
              defer wg.Done()
              // ...
          }
          
          var wg sync.WaitGroup
          wg.Add(1)
          go process(&wg)
          wg.Wait()
          
        • 固定节奏
          • 启动 goroutine 前 wg.Add(1)
          • goroutine 内第一行 defer wg.Done()
          • 主流程 wg.Wait()
      • Goroutine 泄露
        • Goroutine 泄漏是 Go 并发最常见的 bug:启动了 Goroutine,但它永远不会退出,一直占用内存和资源
        • 泄露的常见原因

          // ❌ 泄漏一:等待永远不会发送的 channel
          func leak1() {
              ch := make(chan int)
              go func() {
                  val := <-ch  // 永远阻塞!没人往 ch 发数据
                  fmt.Println(val)
              }()
              // 函数返回,但 goroutine 永远卡在这
          }
          
          // ❌ 泄漏二:没有退出机制的无限循环
          func leak2() {
              go func() {
                  for {
                      doWork()  // 没有 break/return/cancel 条件
                  }
              }()
          }
          
          // ❌ 泄漏三:阻塞的 HTTP 请求没有超时
          func leak3() {
              go func() {
                  resp, _ := http.Get("http://slow-server.com")  
                  // 如果服务器不响应,goroutine 永远等着
                  _ = resp
              }()
          }
          
        • 正确的退出机制

          // ✅ 用 done channel 通知退出
          func worker(done <-chan struct{}) {
              for {
                  select {
                  case <-done:
                      fmt.Println("收到退出信号,正在退出")
                      return
                  default:
                      doWork()
                  }
              }
          }
          
          done := make(chan struct{})
          go worker(done)
          
          time.Sleep(5 * time.Second)
          close(done)  // 通知所有监听 done 的 goroutine 退出
          
          // ✅ 用 context.Context(更推荐,Day 10 详讲)
          ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
          defer cancel()
          
          go func() {
              select {
              case <-ctx.Done():
                  fmt.Println("超时退出:", ctx.Err())
                  return
              case result := <-doAsyncWork():
                  fmt.Println("完成:", result)
              }
          }()
          
        • 检测泄露工具 goleak

          // 在测试中检测 goroutine 泄漏
          import "go.uber.org/goleak"
          
          func TestNoLeak(t *testing.T) {
              defer goleak.VerifyNone(t)  // 测试结束时检查是否有泄漏的 goroutine
          
              // 你的业务代码
              runSomeTask()
          }
          
          // 也可以用运行时查看当前 goroutine 数量
          import "runtime"
          fmt.Println("goroutine 数量:", runtime.NumGoroutine())
          
        • Goroutine 的黄金原则
          • 启动 Goroutine 的人负责结束它
          • 每次 go func() 之前,问自己:这个 Goroutine 什么时候、在什么条件下会退出?
      • 数据竞争与 Race Detector
        • 多个 Goroutine 同时读写同一变量时,会产生数据竞争(Data Race)
        • Go 自带了竞态检测器

          // ❌ 数据竞争:多个 goroutine 同时写 counter
          var counter int
          
          func main() {
              var wg sync.WaitGroup
              for i := 0; i < 1000; i++ {
                  wg.Add(1)
                  go func() {
                      defer wg.Done()
                      counter++  // ❌ 非原子操作,存在竞争!
                  }()
              }
              wg.Wait()
              fmt.Println(counter)  // 结果不确定,可能不是 1000
          }
          
          // 运行竞态检测器
          // go run -race main.go
          // 输出:WARNING: DATA RACE
          // 清晰显示哪些行在竞争
          
        • 三种解决方案

          // ✅ 方案一:sync.Mutex 加锁
          var (
              mu      sync.Mutex
              counter int
          )
          
          go func() {
              mu.Lock()
              defer mu.Unlock()
              counter++
          }()
          
          // ✅ 方案二:atomic 原子操作(更快,适合简单计数)
          import "sync/atomic"
          var counter int64
          
          go func() {
              atomic.AddInt64(&counter, 1)
          }()
          result := atomic.LoadInt64(&counter)
          
          // ✅ 方案三:用 channel 传递数据(不共享状态)
          // 这是最地道的 Go 风格(Day 9 详讲)
          ch := make(chan int, 1000)
          
          for i := 0; i < 1000; i++ {
              go func() { ch <- 1 }()
          }
          
          total := 0
          for i := 0; i < 1000; i++ {
              total += <-ch
          }
          fmt.Println(total)  // 1000,保证正确
          
        • -race 标志
          • 开发阶段始终用 go test -race 和 go run -race 运行代码
          • Race Detector 会在运行时检测数据竞争并打印详细报告(文件名、行号、goroutine 栈)
          • 生产环境不用开(有约 20% 性能损耗),但 CI/CD 流水线里一定要跑
      • sync 包核心原语
        • sync 包提供了并发编程的基础工具
        • 除了 WaitGroup,还有互斥锁、读写锁和 Once
        • sync.Mutex 互斥锁

          type SafeCounter struct {
              mu    sync.Mutex
              count int
          }
          
          func (c *SafeCounter) Inc() {
              c.mu.Lock()
              defer c.mu.Unlock()  // 保证解锁
              c.count++
          }
          
          func (c *SafeCounter) Value() int {
              c.mu.Lock()
              defer c.mu.Unlock()
              return c.count
          }
          
          // 使用
          counter := &SafeCounter{}
          var wg sync.WaitGroup
          for i := 0; i < 1000; i++ {
              wg.Add(1)
              go func() {
                  defer wg.Done()
                  counter.Inc()
              }()
          }
          wg.Wait()
          fmt.Println(counter.Value())  // 1000,保证正确
          
        • sync.RWMutex 读写锁 (读多写少场景)

          type Cache struct {
              mu   sync.RWMutex
              data map[string]string
          }
          
          // 写操作:独占锁
          func (c *Cache) Set(key, val string) {
              c.mu.Lock()
              defer c.mu.Unlock()
              c.data[key] = val
          }
          
          // 读操作:共享锁(多个 goroutine 可同时读)
          func (c *Cache) Get(key string) (string, bool) {
              c.mu.RLock()
              defer c.mu.RUnlock()
              v, ok := c.data[key]
              return v, ok
          }
          
          // 读写锁的性能优势:
          // 纯读的场景下,多个 goroutine 可以并发读,不阻塞
          // 只有写时才独占,适合缓存、配置这类读多写少的场景
          
        • sync.Once 单例初始化

          // 保证函数只执行一次,线程安全
          var (
              instance *DB
              once     sync.Once
          )
          
          func GetDB() *DB {
              once.Do(func() {
                  instance = &DB{
                      conn: openConnection(),
                  }
              })
              return instance
          }
          
          // 无论多少 goroutine 并发调用 GetDB()
          // openConnection() 只会执行一次
          // 这是 Go 实现单例模式的标准方式
          
    • Channel 通信
      • Channel 基础
        • Channel 是 Go 并发的通信机制
        • Go 的并发哲学是:「不要通过共享内存来通信,要通过通信来共享内存。」
        • Channel 就是这个哲学的具体实现——Goroutine 之间通过 channel 传递数据,而不是共享变量。
        • 示例

          // 创建 channel:make(chan 类型)
          ch := make(chan int)       // 无缓冲 channel
          ch := make(chan string, 5) // 有缓冲 channel,容量 5
          
          // 发送数据:ch <- value
          ch <- 42
          
          // 接收数据:value := <-ch
          v := <-ch
          
          // 关闭 channel
          close(ch)
          
          // 判断 channel 是否关闭
          v, ok := <-ch
          // ok == false 说明 channel 已关闭且已空
          
          // channel 的零值是 nil
          var ch chan int  // nil channel
          // 向 nil channel 发送/接收会永远阻塞!
          
        • 两个 Goroutine 通信

          func main() {
              ch := make(chan string)
          
              // 发送方 goroutine
              go func() {
                  ch <- "Hello from goroutine!"
              }()
          
              // 接收方(主 goroutine)
              msg := <-ch
              fmt.Println(msg)  // Hello from goroutine!
          }
          
          // ✨ 不需要 WaitGroup!
          // <-ch 会阻塞,直到有数据,天然同步
          
        • Channel 的方向是通信,不是存储
          • Channel 不是消息队列,不是用来存数据的
          • 它是两个 Goroutine 之间的「传送带」,一端发送,另一端接收,数据在两者之间流动
      • 无缓冲 vs 有缓冲
        • 无缓冲和有缓冲 channel 的行为差异很大,选错会导致死锁或意外的并发行为。
        • 无缓冲 Channel (同步)

          ch := make(chan int)  // 无缓冲
          
          // 发送方和接收方必须同时就绪,否则阻塞
          // 就像打电话:双方必须同时在线
          
          // ❌ 死锁:同一个 goroutine 又发又收
          ch <- 1   // 阻塞等待接收方
          v := <-ch // 永远不会执行到这里
          // fatal error: all goroutines are asleep - deadlock!
          
          // ✅ 正确:发送和接收在不同 goroutine
          go func() { ch <- 42 }()  // 另一个 goroutine 发送
          v := <-ch                  // 主 goroutine 接收(会等待)
          fmt.Println(v)             // 42
          
        • 有缓冲 Channel (异步)

          ch := make(chan int, 3)  // 缓冲容量 3
          
          // 就像发短信:发完不用等对方读
          // 只要缓冲区没满,发送不阻塞
          ch <- 1  // 不阻塞
          ch <- 2  // 不阻塞
          ch <- 3  // 不阻塞
          ch <- 4  // 阻塞!缓冲区已满
          
          // 缓冲区为空时,接收阻塞
          v := <-ch  // 1
          
          // 查看状态
          fmt.Println(len(ch))  // 2(当前数据量)
          fmt.Println(cap(ch))  // 3(总容量)
          
        • 用有缓冲的场景
          • 发送方和接收方速度不匹配,例如任务队列、批量处理
          • 缓冲大小要谨慎,太大会掩盖背压问题(下游处理慢时上游应该被限速)
      • Channel 方向限定
        • 在函数参数中,可以限制 channel 只能发送或只能接收

          // chan<- T:只能发送(send-only)
          // <-chan T:只能接收(receive-only)
          // chan T:双向(可以转换为单向)
          
          func producer(out chan<- int) {  // 只能往 out 发送
              for i := 0; i < 5; i++ {
                  out <- i
              }
              close(out)  // 生产完毕,关闭 channel
          }
          
          func consumer(in <-chan int) {   // 只能从 in 接收
              for v := range in {          // for range 自动处理 close
                  fmt.Println("收到:", v)
              }
          }
          
          func main() {
              ch := make(chan int, 5)  // 双向 channel
              go producer(ch)          // 自动转换为 chan<- int
              consumer(ch)             // 自动转换为 <-chan int
          }
          
        • for range 遍历 Channel

          // chan<- T:只能发送(send-only)
          // <-chan T:只能接收(receive-only)
          // chan T:双向(可以转换为单向)
          
          func producer(out chan<- int) {  // 只能往 out 发送
              for i := 0; i < 5; i++ {
                  out <- i
              }
              close(out)  // 生产完毕,关闭 channel
          }
          
          func consumer(in <-chan int) {   // 只能从 in 接收
              for v := range in {          // for range 自动处理 close
                  fmt.Println("收到:", v)
              }
          }
          
          func main() {
              ch := make(chan int, 5)  // 双向 channel
              go producer(ch)          // 自动转换为 chan<- int
              consumer(ch)             // 自动转换为 <-chan int
          }
          
        • 关于 close 的规则
          • 只有发送方应该关闭 channel,接收方不应该关闭
          • 关闭已关闭的 channel 会 panic
          • 向已关闭的 channel 发送会 panic
          • 从已关闭的 channel 接收会立即返回零值
          • 简单原则:谁生产,谁关闭
      • select 多路复用
        • select 是 channel 的多路复用工具,当需要同时监听多个 channel 时,select 是标准答案

          ch1 := make(chan string)
          ch2 := make(chan string)
          
          go func() {
              time.Sleep(1 * time.Second)
              ch1 <- "来自 ch1"
          }()
          go func() {
              time.Sleep(2 * time.Second)
              ch2 <- "来自 ch2"
          }()
          
          // select 等待第一个就绪的 case
          select {
          case msg := <-ch1:
              fmt.Println("ch1:", msg)
          case msg := <-ch2:
              fmt.Println("ch2:", msg)
          }
          // 输出:ch1: 来自 ch1(先到先得)
          
        • 带 default 的非阻塞操作

          ch := make(chan int, 1)
          
          // 非阻塞发送
          select {
          case ch <- 42:
              fmt.Println("发送成功")
          default:
              fmt.Println("channel 已满,丢弃")
          }
          
          // 非阻塞接收
          select {
          case v := <-ch:
              fmt.Println("接收到:", v)
          default:
              fmt.Println("channel 为空,跳过")
          }
          
        • 超时控制 (实战必备)

          func fetchWithTimeout(url string) (string, error) {
              resultCh := make(chan string, 1)
          
              go func() {
                  // 模拟 HTTP 请求
                  time.Sleep(2 * time.Second)
                  resultCh <- "响应内容"
              }()
          
              select {
              case result := <-resultCh:
                  return result, nil
              case <-time.After(1 * time.Second):  // 1 秒超时
                  return "", errors.New("请求超时")
              }
          }
          
        • 循环中使用 select

          func worker(jobs <-chan int, done <-chan struct{}) {
              for {
                  select {
                  case job, ok := <-jobs:
                      if !ok {
                          fmt.Println("jobs channel 已关闭")
                          return
                      }
                      fmt.Println("处理任务:", job)
                  case <-done:
                      fmt.Println("收到退出信号")
                      return
                  }
              }
          }
          
          // 多 case 同时就绪时,select 随机选一个
          // 这是刻意设计的,避免某个 case 被饿死
          
        • select 的随机性
          • 当多个 case 同时就绪时,select 随机选择一个,不保证顺序
          • 这是 Go 刻意的设计——防止某个 case 总被忽略(饥饿)
      • Done Channel 模式
        • Done channel 是 Go 并发里用来广播退出信号的经典模式
        • 通过 close(done) 可以同时通知所有监听它的 Goroutine 退出
        • 示例

          func generator(done <-chan struct{}, nums ...int) <-chan int {
              out := make(chan int)
              go func() {
                  defer close(out)
                  for _, n := range nums {
                      select {
                      case out <- n:       // 正常发送
                      case <-done:         // 收到退出信号
                          return
                      }
                  }
              }()
              return out
          }
          
          func main() {
              done := make(chan struct{})
              defer close(done)  // 函数退出时自动广播退出信号
          
              out := generator(done, 1, 2, 3, 4, 5)
          
              // 只消费 2 个,然后退出
              // done 关闭会通知 generator goroutine 退出
              fmt.Println(<-out)  // 1
              fmt.Println(<-out)  // 2
              // defer close(done) 执行,generator goroutine 收到信号退出
          }
          
        • close 广播的原理

          done := make(chan struct{})
          
          // 启动多个监听 done 的 goroutine
          for i := 0; i < 3; i++ {
              go func(id int) {
                  <-done  // 阻塞等待
                  fmt.Printf("Worker %d 退出\n", id)
              }(i)
          }
          
          time.Sleep(1 * time.Second)
          close(done)  // ✨ 关闭会「广播」到所有接收方
          // 所有 3 个 goroutine 同时收到通知并退出
          
          // 原理:从已关闭的 channel 接收立即返回零值
          // 所以 close 一次,所有 <-done 都会解除阻塞
          
        • Done channel vs context.Context
          • Done channel 是手动实现退出信号,简单直接
          • 新代码优先用 context
      • 生产者-消费者模式
        • 基础版:单生产者、单消费者

          func producer(out chan<- int, count int) {
              defer close(out)
              for i := 0; i < count; i++ {
                  out <- i
                  time.Sleep(50 * time.Millisecond)
              }
          }
          
          func consumer(in <-chan int, results chan<- int) {
              defer close(results)
              for v := range in {
                  results <- v * v  // 计算平方
              }
          }
          
          func main() {
              jobs := make(chan int, 10)
              results := make(chan int, 10)
          
              go producer(jobs, 5)
              go consumer(jobs, results)
          
              for r := range results {
                  fmt.Println(r)  // 0, 1, 4, 9, 16
              }
          }
          
        • Worker Pool: 多消费者并发处理

          func workerPool(jobs <-chan int, results chan<- int, workerCount int) {
              var wg sync.WaitGroup
          
              for i := 0; i < workerCount; i++ {
                  wg.Add(1)
                  go func(id int) {
                      defer wg.Done()
                      for job := range jobs {
                          // 模拟处理耗时
                          time.Sleep(100 * time.Millisecond)
                          result := job * job
                          results <- result
                          fmt.Printf("Worker %d 处理任务 %d -> %d\n", id, job, result)
                      }
                  }(i)
              }
          
              // 所有 worker 完成后关闭 results
              go func() {
                  wg.Wait()
                  close(results)
              }()
          }
          
          func main() {
              jobs := make(chan int, 100)
              results := make(chan int, 100)
          
              // 启动 3 个 worker
              workerPool(jobs, results, 3)
          
              // 发送 9 个任务
              for i := 1; i <= 9; i++ {
                  jobs <- i
              }
              close(jobs)  // 任务发送完毕
          
              // 收集结果
              for r := range results {
                  fmt.Println("结果:", r)
              }
          }
          
          // 3 个 worker 并发处理 9 个任务
          // 总耗时 ≈ 3 * 100ms = 300ms(串行需 900ms)
          
    • 并发模式进阶:context.Context
      • 为什么需要 Context
        • 在 Go 的并发程序里,经常需要跨多个 Goroutine 传递:截止时间、取消信号、请求相关值
        • 没有 context 前,这些靠 done channel 手动实现
        • context 包是 go 官方对这些需求的统一实现
      • context 三大能力
        • 取消信号:主动取消或超时自动取消,自动传播给所有子 context。
        • 截止时间:知道还剩多少时间,让每个环节合理分配时间。
        • 携带值:在调用链上传递请求级别的元数据(traceID、userID 等)。这三点让 context 成为 Go 服务端编程的基础设施。
      • Context 树结构
        • Context 是一棵树:从根节点出发,不断派生子节点
        • 父节点取消时,所有子节点自动取消。这个设计让取消信号天然地从上往下传播
        • 示例

          import "context"
          
          // 根节点(两种)
          ctx := context.Background()  // 最常用的根,永不取消
          ctx := context.TODO()        // 占位用,表示「还没想好用哪个」
          
          // 从根派生子节点
          ctx1, cancel1 := context.WithCancel(context.Background())
          ctx2, cancel2 := context.WithTimeout(ctx1, 5*time.Second)
          ctx3, cancel3 := context.WithDeadline(ctx2, time.Now().Add(10*time.Second))
          ctx4 := context.WithValue(ctx3, "traceID", "abc-123")
          
          // 取消任意一个节点,它的所有子节点都会取消
          cancel1()  // ctx1、ctx2、ctx3、ctx4 全部取消!
          
          // 每个 WithXxx 返回的 cancel 函数必须调用
          // 否则 context 相关资源永远不会释放(泄漏)
          defer cancel1()
          defer cancel2()
          
        • 流程

                  Background()
                      │
                  WithCancel()  ← cancel() 取消这里
                      │
                  WithTimeout() ← 5秒后自动取消(或父被取消)
                      │
                  WithDeadline() ← 到达截止时间取消(或父被取消)
                      │
                  WithValue()   ← 携带 traceID(不产生取消)
          
          父节点取消 → 所有子孙节点自动取消 ✨
          
      • WithCancel 主动取消
        • WithCancel 是最基础的 context 派生方式,返回一个可手动取消的 context
        • 适用于需要主动控制 goroutine 生命周期的场景
        • 示例

          func longRunningTask(ctx context.Context) error {
              for {
                  select {
                  case <-ctx.Done():
                      // ctx.Err() 返回取消原因
                      // context.Canceled:被主动取消
                      // context.DeadlineExceeded:超时
                      return ctx.Err()
                  default:
                      // 执行任务的一步
                      doWork()
                      time.Sleep(100 * time.Millisecond)
                  }
              }
          }
          
          func main() {
              ctx, cancel := context.WithCancel(context.Background())
          
              // 启动任务
              go func() {
                  err := longRunningTask(ctx)
                  fmt.Println("任务结束:", err)  // context canceled
              }()
          
              time.Sleep(1 * time.Second)
              cancel()  // 主动取消,goroutine 收到信号退出
          
              time.Sleep(100 * time.Millisecond)
              fmt.Println("主程序退出")
          }
          
        • 一个父取消,多个子都取消

          func worker(ctx context.Context, id int) {
              for {
                  select {
                  case <-ctx.Done():
                      fmt.Printf("Worker %d 退出: %v\n", id, ctx.Err())
                      return
                  default:
                      fmt.Printf("Worker %d 工作中\n", id)
                      time.Sleep(200 * time.Millisecond)
                  }
              }
          }
          
          func main() {
              ctx, cancel := context.WithCancel(context.Background())
              defer cancel()
          
              // 启动多个 worker,共享同一个 ctx
              for i := 1; i <= 3; i++ {
                  go worker(ctx, i)
              }
          
              time.Sleep(1 * time.Second)
              cancel()  // 一次取消,所有 worker 同时退出 ✨
          
              time.Sleep(100 * time.Millisecond)
          }
          
      • WithTimeout & WithDeadline
        • 超时控制是服务端最常见的需求。
        • WithTimeout 设置相对时间,WithDeadline 设置绝对时间。
        • 两者本质相同,WithTimeout 是对 WithDeadline 的封装。
        • WithTimeout 最常用

          // HTTP 请求超时
          func callExternalAPI(url string) ([]byte, error) {
              ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
              defer cancel()  // 无论成功失败都要调用
          
              req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
              if err != nil {
                  return nil, err
              }
          
              resp, err := http.DefaultClient.Do(req)
              if err != nil {
                  // 超时时 err 会包含 "context deadline exceeded"
                  return nil, fmt.Errorf("请求失败: %w", err)
              }
              defer resp.Body.Close()
          
              return io.ReadAll(resp.Body)
          }
          
          // 数据库查询超时
          func getUser(db *sql.DB, id int) (*User, error) {
              ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
              defer cancel()
          
              var u User
              err := db.QueryRowContext(ctx,
                  "SELECT id, name, email FROM users WHERE id = ?", id,
              ).Scan(&u.ID, &u.Name, &u.Email)
          
              return &u, err
          }
          
        • 检查剩余时间

          ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
          defer cancel()
          
          // 检查截止时间
          deadline, ok := ctx.Deadline()
          if ok {
              remaining := time.Until(deadline)
              fmt.Printf("还剩 %.1f 秒\n", remaining.Seconds())
          }
          
          // 合理分配时间
          // 总共 5 秒,给第一步 2 秒,给第二步 3 秒
          step1Ctx, cancel1 := context.WithTimeout(ctx, 2*time.Second)
          defer cancel1()
          doStep1(step1Ctx)
          
          // step2 用剩余时间(最多 3 秒,但如果父 ctx 已超时则立即取消)
          doStep2(ctx)
          
        • WithDeadline 绝对时间点

          // 设定必须在某个时间点前完成
          deadline := time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC)
          ctx, cancel := context.WithDeadline(context.Background(), deadline)
          defer cancel()
          
          // 检查是否已超过截止时间
          if ctx.Err() == context.DeadlineExceeded {
              return errors.New("已超过截止时间")
          }
          
        • 超时值怎么定
          • 通常根据 SLA(服务等级协议)来定
          • 数据库查询:100ms-2s。内部 RPC:200ms-1s
          • 外部 HTTP:1s-10s
          • 给每一层调用留足缓冲——如果你的总超时是 5s,不要给每个子操作也设 5s,要合理分配
          • 记录 p99 延迟来校准超时值
      • HTTP 服务中的 Context
        • HTTP 服务是 context 最常见的使用场景
        • 每个请求都有一个 context,当客户端断开连接时,context 自动取消,让你能及时释放资源
        • 示例

          // net/http 标准库:每个 Request 自带 context
          func handler(w http.ResponseWriter, r *http.Request) {
              ctx := r.Context()  // 获取请求的 context
          
              // 客户端断开时,ctx.Done() 会收到信号
              result, err := slowDBQuery(ctx)
              if err != nil {
                  if ctx.Err() != nil {
                      // 客户端已断开,不需要返回错误
                      return
                  }
                  http.Error(w, err.Error(), 500)
                  return
              }
          
              json.NewEncoder(w).Encode(result)
          }
          
          // 慢查询感知取消
          func slowDBQuery(ctx context.Context) ([]Row, error) {
              rows, err := db.QueryContext(ctx,
                  "SELECT * FROM large_table WHERE complex_condition",
              )
              if err != nil {
                  return nil, err  // 包含超时/取消信息
              }
              defer rows.Close()
          
              var result []Row
              for rows.Next() {
                  // 检查 context,避免在取消后继续处理
                  if ctx.Err() != nil {
                      return nil, ctx.Err()
                  }
                  var row Row
                  rows.Scan(&row.ID, &row.Name)
                  result = append(result, row)
              }
              return result, nil
          }
          
        • 完整的中间件链

          func main() {
              mux := http.NewServeMux()
              mux.HandleFunc("/api/user", userHandler)
          
              // 中间件链:每层都可以增强/修改 context
              handler := timeoutMiddleware(
                  authMiddleware(
                      traceMiddleware(mux),
                  ),
              )
          
              http.ListenAndServe(":8080", handler)
          }
          
          // 超时中间件:为每个请求加上统一超时
          func timeoutMiddleware(next http.Handler) http.Handler {
              return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                  ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
                  defer cancel()
                  next.ServeHTTP(w, r.WithContext(ctx))
              })
          }
          
          // 认证中间件:注入用户信息
          func authMiddleware(next http.Handler) http.Handler {
              return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                  token := r.Header.Get("Authorization")
                  user, err := validateToken(token)
                  if err != nil {
                      http.Error(w, "未授权", 401)
                      return
                  }
                  ctx := context.WithValue(r.Context(), UserIDKey, user.ID)
                  next.ServeHTTP(w, r.WithContext(ctx))
              })
          }
          
        • context 是第一个参数,不是字段
          • Go 官方规范:context 始终作为函数的第一个参数,命名为 ctx
          • 不要把 context 存在结构体字段里——这会让生命周期管理变得混乱
          • 每次函数调用传入,每次可以传入不同的 context,这才是正确的使用姿势
      • context 最佳实践
        • 规范汇总

          // ✅ 1. ctx 永远是第一个参数
          func doSomething(ctx context.Context, param string) error
          
          // ✅ 2. 函数入口处检查 ctx
          func process(ctx context.Context) error {
              if ctx.Err() != nil {
                  return ctx.Err()
              }
              // ...
          }
          
          // ✅ 3. defer cancel() 紧跟在 WithXxx 后面
          ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
          defer cancel()
          
          // ✅ 4. 传递 ctx,不要存在结构体字段里
          type Service struct {
              db *sql.DB
              // ❌ ctx context.Context  // 不要这样
          }
          func (s *Service) Query(ctx context.Context) error { ... }
          
          // ✅ 5. 长循环中定期检查 ctx
          func processBatch(ctx context.Context, items []Item) error {
              for i, item := range items {
                  if i%100 == 0 {  // 每 100 条检查一次
                      if err := ctx.Err(); err != nil {
                          return fmt.Errorf("在第 %d 条时被取消: %w", i, err)
                      }
                  }
                  process(item)
              }
              return nil
          }
          
          // ✅ 6. 区分 Background 和 TODO
          // Background:程序入口、main、测试、HTTP 根 handler
          // TODO:占位用,表示「这里应该有 ctx 但还没传进来」
          
        • 常见错误模式

          // ❌ 1. 忘记调用 cancel
          ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
          // 忘写 defer cancel(),定时器资源永远不释放
          
          // ❌ 2. 在 goroutine 里使用已取消的 ctx
          ctx, cancel := context.WithTimeout(parent, 1*time.Second)
          defer cancel()
          go func() {
              time.Sleep(2 * time.Second)
              doWork(ctx)  // ❌ ctx 已超时,doWork 立即失败
          }()
          
          // ✅ 应该为新 goroutine 派生新 ctx 或用父 ctx
          go func() {
              newCtx, cancel := context.WithTimeout(parent, 5*time.Second)
              defer cancel()
              doWork(newCtx)
          }()
          
          // ❌ 3. 用 context.Background() 代替传入的 ctx
          func handler(ctx context.Context) error {
              // 丢失了超时/取消信息!
              return db.QueryContext(context.Background(), "SELECT ...")
          }
          
          // ✅ 始终传递原始 ctx
          func handler(ctx context.Context) error {
              return db.QueryContext(ctx, "SELECT ...")
          }
          
        • Context 的使用场景
          • 请求进来 → 用 WithTimeout 加超时 → 中间件用 WithValue 注入元数据 → 传给 handler
          • handler 传给数据库/RPC/外部 HTTP → 客户端断开/超时 → ctx.Done() 触发 → 所有下游自动取消
          • 整条链路只用一个 ctx,层层传递,层层受控
    • 错误处理哲学
      • 错误处理回顾与痛点
        • 示例

          // 基础版:丢失了太多信息
          func readConfig(path string) error {
              _, err := os.ReadFile(path)
              if err != nil {
                  return err  // ❌ 调用方不知道是什么操作失败了
              }
              return nil
          }
          
          // 稍好:加了上下文,但还有问题
          func readConfig(path string) error {
              _, err := os.ReadFile(path)
              if err != nil {
                  return fmt.Errorf("读取配置失败: %v", err)  // ❌ 用 %v,丢失了原始 err 类型!
              }
              return nil
          }
          
          // ✅ Go 1.13+ 正确做法:用 %w 包装,保留原始 error
          func readConfig(path string) error {
              _, err := os.ReadFile(path)
              if err != nil {
                  return fmt.Errorf("读取配置 %s 失败: %w", path, err)  // ✅ %w 包装
              }
              return nil
          }
          
          // 调用方可以拆包检查原始错误
          err := readConfig("/etc/app.yaml")
          if errors.Is(err, os.ErrNotExist) {
              fmt.Println("配置文件不存在,使用默认配置")
          }
          
        • %v 和 %w 的本质区别
          • %v 把 err 转成字符串,原始错误信息被「压扁」成文字,调用方无法再用 errors.Is/As 检查原始类型
          • %w 把原始 err 包裹在新错误里,形成错误链,调用方可以用 errors.Is/As 沿链查找
          • 只要你想让调用方能检查原始错误,就用 %w
      • 错误包装与错误链
        • 错误链就像俄罗斯套娃:最外层是最高级的上下文描述,最内层是根本原因。
        • 每一层都用 %w 包装,形成一条可以追溯的链路。
        • 示例

          // 错误链示例:从底层到顶层
          // 层级:DB驱动 → DB层 → 业务层 → API层
          
          // ① 最底层:数据库驱动报错
          var ErrNotFound = errors.New("记录不存在")
          
          // ② DB 层包装
          func (r *UserRepo) Get(id int) (*User, error) {
              // ...
              return nil, fmt.Errorf("UserRepo.Get id=%d: %w", id, ErrNotFound)
          }
          
          // ③ 业务层包装
          func (s *UserService) GetUser(id int) (*User, error) {
              u, err := s.repo.Get(id)
              if err != nil {
                  return nil, fmt.Errorf("UserService.GetUser: %w", err)
              }
              return u, nil
          }
          
          // ④ API 层包装
          func handleGetUser(id int) {
              _, err := userService.GetUser(id)
              if err != nil {
                  // err.Error() 输出完整链路:
                  // "UserService.GetUser: UserRepo.Get id=42: 记录不存在"
                  fmt.Println(err)
              }
          }
          
        • 手动实现 Unwrap (支持自定义类型)

          // 自定义错误类型也可以支持错误链
          type DBError struct {
              Op  string  // 操作名
              Err error   // 原始错误
          }
          
          func (e *DBError) Error() string {
              return fmt.Sprintf("数据库操作 %s 失败: %v", e.Op, e.Err)
          }
          
          // ✨ 实现 Unwrap 方法,支持 errors.Is/As 穿透
          func (e *DBError) Unwrap() error {
              return e.Err
          }
          
          // 使用
          err := &DBError{Op: "INSERT", Err: ErrDuplicate}
          fmt.Println(errors.Is(err, ErrDuplicate))  // true,因为实现了 Unwrap
          
        • 错误链的价值
          • 一条好的错误链就像一份事故报告:「API层在做什么 → 业务层在做什么 → 数据层在做什么 → 根本原因是什么」
          • 出了问题直接看错误信息就能定位,不用加断点。
          • 这在微服务日志里尤其有价值。
      • errors.Is: 判断错误类型
        • errors.Is 用来判断错误链中是否包含某个特定的「哨兵错误」(Sentinel Error)。
        • 它会递归 Unwrap 整条链,不像 == 只比较最外层。
        • 示例

          // 定义哨兵错误(包级别的可比较 error 值)
          var (
              ErrNotFound    = errors.New("未找到")
              ErrUnauthorized = errors.New("未授权")
              ErrTimeout     = errors.New("超时")
          )
          
          func doSomething() error {
              return fmt.Errorf("操作失败: %w", ErrNotFound)  // 包装了 ErrNotFound
          }
          
          err := doSomething()
          
          // ❌ == 只比较最外层,找不到
          fmt.Println(err == ErrNotFound)        // false!
          
          // ✅ errors.Is 会沿链查找
          fmt.Println(errors.Is(err, ErrNotFound))  // true ✨
          
          // 实际使用场景
          if errors.Is(err, ErrNotFound) {
              http.Error(w, "资源不存在", http.StatusNotFound)
          } else if errors.Is(err, ErrUnauthorized) {
              http.Error(w, "请先登录", http.StatusUnauthorized)
          } else if err != nil {
              http.Error(w, "内部错误", http.StatusInternalServerError)
          }
          
        • 自定义 Is 方法

          // 有时候两个错误「相等」不是值相等,而是逻辑相等
          type StatusError struct {
              Code int
          }
          
          func (e *StatusError) Error() string {
              return fmt.Sprintf("HTTP %d", e.Code)
          }
          
          // 自定义 Is:只要 Code 相同就认为相等
          func (e *StatusError) Is(target error) bool {
              t, ok := target.(*StatusError)
              if !ok {
                  return false
              }
              return e.Code == t.Code
          }
          
          err := fmt.Errorf("请求失败: %w", &StatusError{Code: 404})
          target := &StatusError{Code: 404}
          
          fmt.Println(errors.Is(err, target))  // true,通过自定义 Is 比较
          
      • errors.As: 提取错误详情
        • errors.Is 只判断「是不是」,errors.As 则「取出来」——把错误链中某个具体类型的错误提取出来,访问它的字段
        • 示例

          // 自定义错误类型,携带更多信息
          type ValidationError struct {
              Field   string
              Value   any
              Message string
          }
          
          func (e *ValidationError) Error() string {
              return fmt.Sprintf("字段 %s(值=%v)验证失败: %s",
                  e.Field, e.Value, e.Message)
          }
          
          func validateAge(age int) error {
              if age < 0 {
                  return &ValidationError{
                      Field:   "age",
                      Value:   age,
                      Message: "不能为负数",
                  }
              }
              return nil
          }
          
          func processUser(age int) error {
              if err := validateAge(age); err != nil {
                  return fmt.Errorf("processUser: %w", err)  // 包装了 ValidationError
              }
              return nil
          }
          
          // 调用方提取详情
          err := processUser(-1)
          if err != nil {
              var ve *ValidationError
              if errors.As(err, &ve) {  // ✅ 会沿链查找 *ValidationError 类型
                  fmt.Printf("字段 %q 的值 %v 有误: %s\n",
                      ve.Field, ve.Value, ve.Message)
                  // 字段 "age" 的值 -1 有误: 不能为负数
              } else {
                  fmt.Println("未知错误:", err)
              }
          }
          
      • 包管理与项目结构
        • Go Modules 完整工作流
          • Go Modules 是 Go 官方的依赖管理系统,从 Go 1.16 起默认开启
          • 它解决了早期 GOPATH 时代「所有代码必须在同一目录」的问题,让项目可以放在任意位置,依赖版本清晰可控
        • 核心命令
          • go mod init <模块名> 初始化模块,创建 go.mod
          • go get <包>@<版本> 添加或更新依赖
          • go mod tidy 清理多余依赖,补全缺失依赖
          • go mod download 下载依赖到本地缓存
          • go mod vendor 把依赖复制到 vendor 目录
          • go list -m all 列出所有直接和间接依赖
          • go mod graph 查看依赖关系图
          • go mod verify 验证依赖是否被篡改
        • 完整操作流程

          # 1. 创建项目
          mkdir myapp && cd myapp
          go mod init github.com/yourname/myapp
          
          # 2. 添加依赖
          go get github.com/gin-gonic/gin@v1.9.1
          go get gorm.io/gorm@latest
          
          # 3. 升级依赖
          go get github.com/gin-gonic/gin@latest  # 升级到最新
          go get github.com/gin-gonic/gin@v1.9.0  # 降级到指定版本
          
          # 4. 移除不再使用的依赖(先从代码中删除 import,再运行)
          go mod tidy
          
          # 5. 查看当前所有依赖
          go list -m all
          
          # 6. 检查可以升级的依赖
          go list -m -u all
          
        • 模块名应该是什么
          • 模块名通常是代码仓库的路径,如 github.com/yourname/myapp
          • 如果不打算发布为公共包,用任何路径都行,比如 mycompany.com/myapp 或者就叫 myapp
          • 模块名是唯一标识,同一模块内的包用它作为 import 前缀
      • go.mod 和 go.sum
        • go.mod 和 go.sum 是 Go modules 的两个核心文件
        • go.mod 详解

          // go.mod 示例
          module github.com/yourname/myapp  // ① 模块名
          
          go 1.23  // ② 最低 Go 版本要求
          
          require (
              github.com/gin-gonic/gin v1.9.1    // 直接依赖
              gorm.io/gorm v1.25.0               // 直接依赖
              gorm.io/driver/postgres v1.5.0     // 直接依赖
          
              // indirect 表示间接依赖(你的依赖的依赖)
              github.com/bytedance/sonic v1.9.1  // indirect
              golang.org/x/net v0.15.0           // indirect
          )
          
          // replace:替换依赖源(用本地路径或 fork)
          replace github.com/some/lib => ../local-lib
          replace github.com/some/lib => github.com/your-fork/lib v1.0.0
          
          // exclude:排除特定版本
          exclude github.com/problematic/pkg v1.2.3
          
        • go.sum 的作用

          # go.sum 存储每个依赖的哈希值,确保安全性
          # 格式:模块@版本 算法:哈希值
          
          github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
          github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
          
          # 每个依赖有两行:
          # 1. 源码的哈希(用于验证下载的代码没有被篡改)
          # 2. go.mod 文件本身的哈希
          
          # ✅ 两个文件都要提交到版本控制
          # go.sum 保证了构建的可重现性(reproducibility)
          
        • 语义化版本 (SemVer)

          # v主版本.次版本.补丁版本
          # v1.2.3
          #  ↑  ↑  ↑
          #  │  │  └── 补丁:向后兼容的 bug 修复
          #  │  └───── 次版本:向后兼容的新功能
          #  └──────── 主版本:不兼容的 API 变更
          
          # Go Modules 的主版本规则:
          # v0.x.x 和 v1.x.x:模块名不需要版本后缀
          # v2.x.x 及以上:模块名必须加 /v2 后缀!
          
          # ✅ 正确导入 v2 模块
          import "github.com/some/lib/v2"
          
          # 特殊版本格式
          go get github.com/pkg@latest          # 最新稳定版
          go get github.com/pkg@main            # main 分支的最新 commit
          go get github.com/pkg@abc1234         # 特定 commit(用伪版本)
          
        • MVS: 最小版本选择
          • Go Modules 用 MVS(最小版本选择)算法决定使用哪个版本:在所有依赖要求中,选满足条件的最小版本
          • 这和 npm/pip 的「最新版本」策略不同,MVS 保证了构建的可重现性——同样的 go.mod 在任何地方都产生相同的构建结果
      • 包的可见性规则
        • Go 的可见性只有两种:导出(首字母大写)和未导出(首字母小写)。
        • 这个规则适用于所有标识符:函数、类型、变量、常量、接口、结构体字段。
        • 示例

          // package user
          
          // ✅ 导出:包外可见
          type User struct {
              ID    int     // 导出字段
              Name  string  // 导出字段
              email string  // ❌ 未导出字段,包外不可见(但 json 可以通过 tag 控制)
          }
          
          func NewUser(name string) *User { ... }  // 导出函数
          func (u *User) GetEmail() string { ... } // 导出方法
          
          // ❌ 未导出:只能在 user 包内使用
          type internalState struct { ... }
          func validate(u *User) error { ... }
          var defaultTimeout = 30 * time.Second
          
        • internal 包:包级别的访问控制

          myapp/
          ├── main.go
          ├── internal/           # ✨ internal 目录下的包只能被父目录导入
          │   ├── auth/
          │   │   └── auth.go     # 只有 myapp 模块内部可以导入
          │   └── config/
          │       └── config.go
          ├── pkg/                # 可以被外部导入的公共包
          │   └── middleware/
          └── api/
          
          # 规则:
          # myapp/internal/auth 只能被 myapp/ 下的包导入
          # github.com/other/app 无法导入 myapp/internal/auth
          # 编译时强制执行,违反会报错
          
          // 实战场景:把不想暴露的实现细节放进 internal
          // 对外暴露干净的 API,隐藏内部实现
          
          // myapp/internal/db/connection.go
          package db  // 内部数据库实现
          
          func newConnection(dsn string) (*sql.DB, error) { ... }
          
          // myapp/service/user.go
          import "github.com/yourname/myapp/internal/db"  // ✅ 同模块内,可以导入
          
          // github.com/other/app/main.go
          import "github.com/yourname/myapp/internal/db"  // ❌ 编译错误!外部无法导入
          
        • internal 的最佳实践
          • internal 是 Go 推荐的封装方式
          • 把不稳定的、实验性的、或者只供内部使用的代码放进 internal,防止外部依赖你的内部实现
          • 这让你能自由重构内部代码而不破坏 API
          • 大型项目里几乎每个模块都有 internal 目录
      • 标准项目结构
        • Go 官方没有强制规定项目结构,但社区形成了 Standard Go Project Layout 的约定。
        • 了解这个结构能让你快速读懂任何 Go 项目。
        • Web 服务项目结构

          │
          ├── internal/               # 私有应用代码(不对外暴露)
          │   ├── handler/            # HTTP handler 层
          │   │   ├── user.go
          │   │   └── auth.go
          │   ├── service/            # 业务逻辑层
          │   │   ├── user.go
          │   │   └── auth.go
          │   ├── repository/         # 数据访问层
          │   │   ├── user.go
          │   │   └── user_test.go
          │   ├── model/              # 数据模型
          │   │   └── user.go
          │   └── config/             # 配置加载
          │       └── config.go
          │
          ├── pkg/                    # 可以被外部项目使用的公共库
          │   ├── middleware/
          │   └── utils/
          │
          ├── api/                    # API 定义(OpenAPI/Protobuf)
          │   └── openapi.yaml
          │
          ├── configs/                # 配置文件模板
          │   ├── app.yaml
          │   └── app.prod.yaml
          │
          ├── migrations/             # 数据库迁移文件
          │   ├── 001_create_users.sql
          │   └── 002_add_email_index.sql
          │
          ├── scripts/                # 构建/部署脚本
          │   └── build.sh
          │
          ├── go.mod
          ├── go.sum
          ├── Makefile                # 常用命令
          └── README.md
          
        • 关键原则:分层架构

          // 依赖方向:handler → service → repository → model
          // 每层只能依赖下层,不能反向依赖
          
          // model 层:只有数据结构,零依赖
          // internal/model/user.go
          type User struct {
              ID        int
              Name      string
              Email     string
              CreatedAt time.Time
          }
          
          // repository 层:只管数据存取,依赖 model
          // internal/repository/user.go
          type UserRepository interface {
              FindByID(ctx context.Context, id int) (*model.User, error)
              Create(ctx context.Context, u *model.User) error
          }
          
          // service 层:业务逻辑,依赖 repository 接口
          // internal/service/user.go
          type UserService struct {
              repo repository.UserRepository  // 依赖接口,不依赖具体实现
          }
          
          // handler 层:处理 HTTP 请求,依赖 service 接口
          // internal/handler/user.go
          type UserHandler struct {
              svc *service.UserService
          }
          
      • import 规范
        • Go 对 import 有明确的格式规范,goimports 工具会自动帮你管理。
        • 了解规范能让代码更易读,也是 code review 的基本要求。
        • Import 分组规范

          package main
          
          import (
              // 第一组:标准库
              "context"
              "fmt"
              "net/http"
          
              // 第二组:第三方依赖(空行分隔)
              "github.com/gin-gonic/gin"
              "gorm.io/gorm"
          
              // 第三组:本项目内部包(空行分隔)
              "github.com/yourname/myapp/internal/config"
              "github.com/yourname/myapp/internal/handler"
          )
          
          // goimports 工具会自动排列和分组
          // 安装:go install golang.org/x/tools/cmd/goimports@latest
          // VS Code 保存时自动运行
          
        • 常见 import 技巧

          // 别名:解决包名冲突
          import (
              "crypto/rand"
              mrand "math/rand"  // 别名,避免和 crypto/rand 冲突
          )
          
          // 只执行 init 函数(数据库驱动常用)
          import (
              _ "github.com/lib/pq"          // PostgreSQL 驱动,只要 init
              _ "github.com/mattn/go-sqlite3" // SQLite 驱动
          )
          
          // 点 import(不推荐,污染命名空间)
          import . "fmt"
          Println("hello")  // 不需要 fmt. 前缀了,但容易混淆
          
          // 循环 import 会编译失败:
          // a 包 import b 包,b 包 import a 包 → 编译错误
          // 解决:提取公共代码到第三个包,或重新设计接口
          
        • 循环 import 怎么解决
          • 循环 import 通常说明包的职责划分有问题。
          • 解决方法
            • 把相互依赖的代码提取到新的公共包;
            • 用接口解耦(A 包定义接口,B 包实现,两者都不直接依赖对方);
            • 合并两个包(如果它们本来就属于同一个领域)。
      • Go Workspace 多模块开发
        • Go 1.18 引入了 Workspace 模式,让你可以同时开发多个相互依赖的模块,不需要修改 go.mod 里的 replace 指令。
        # 场景:同时开发 myapp 和它依赖的 mylib
        workspace/
        ├── myapp/
        │   ├── go.mod
        │   └── main.go     # 依赖 mylib
        ├── mylib/
        │   ├── go.mod
        │   └── lib.go      # 被 myapp 依赖
        └── go.work         # Workspace 文件
        
        # 创建 workspace
        cd workspace
        go work init ./myapp ./mylib
        
        # 添加新模块到 workspace
        go work use ./another-module
        
        # go.work 文件内容:
        # go 1.23
        #
        # use (
        #     ./myapp
        #     ./mylib
        # )
        
        # 现在 myapp 直接使用本地的 mylib,
        # 不需要 replace 指令,改动立即生效!
        
        # 注意:go.work 不要提交到版本控制(加入 .gitignore)
        
        // myapp/main.go
        import "github.com/yourname/mylib"  // 自动使用本地的 mylib
        
        func main() {
            mylib.DoSomething()  // 改 mylib 的代码,这里立即反映
        }
        
        // 优势:
        // - 不用修改 go.mod 的 replace 指令
        // - go.sum 不受影响
        // - CI/CD 时不用 workspace(直接用发布的版本)
        
        • Workspace vs replace
          • replace 指令需要修改 go.mod,容易被意外提交到仓库影响其他人。
          • Workspace 的 go.work 放在工作区根目录,不进入版本控制,各个模块的 go.mod 保持干净。
          • 新项目推荐用 Workspace,老项目的 replace 也可以逐步迁移过来。
      • 构建与工具链
        • go build 常用选项

          # 基础构建
          go build ./...                  # 构建所有包(检查编译错误)
          go build -o bin/server ./cmd/server  # 指定输出文件
          
          # 交叉编译(为其他平台构建)
          GOOS=linux GOARCH=amd64 go build -o server-linux ./cmd/server
          GOOS=darwin GOARCH=arm64 go build -o server-mac ./cmd/server
          GOOS=windows GOARCH=amd64 go build -o server.exe ./cmd/server
          
          # 常用 GOOS:linux, darwin, windows
          # 常用 GOARCH:amd64 (x86-64), arm64 (M1/M2 Mac, AWS Graviton)
          
          # 注入版本信息(生产必备)
          go build -ldflags="-X main.Version=v1.2.3 -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" ./cmd/server
          
          # 减小二进制体积
          go build -ldflags="-s -w" ./cmd/server  # 去掉调试信息,体积减少约 30%
          
          # 启用竞态检测(开发阶段)
          go build -race ./...
          
        • Build Tags (条件编译)

          // 文件只在 Linux 下编译
          //go:build linux
          
          package main
          
          // 文件只在测试时编译
          //go:build integration
          
          // 多条件
          //go:build linux && amd64
          
          // 自定义 tag(在 go build -tags 指定)
          //go:build production
          
          // 运行:go build -tags production ./...
          
          // 实战场景:不同环境的配置
          // config_dev.go    //go:build !production
          // config_prod.go   //go:build production
          
        • Makefile: 常用命令封装

          # Makefile 示例
          .PHONY: build run test lint clean
          
          # 变量
          APP=myapp
          VERSION=$(shell git describe --tags --always --dirty)
          BUILD_TIME=$(shell date -u +%Y-%m-%dT%H:%M:%SZ)
          LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -s -w"
          
          build:
              go build $(LDFLAGS) -o bin/$(APP) ./cmd/server
          
          run:
              go run ./cmd/server
          
          test:
              go test -race -cover ./...
          
          test-integration:
              go test -tags integration -race ./...
          
          lint:
              golangci-lint run ./...
          
          clean:
              rm -rf bin/
          
          docker:
              docker build -t $(APP):$(VERSION) .
          
          # 使用:make build / make test / make lint
          
      • 代理与私有模块
        • GOPROXY:模块代理

          # 查看当前代理设置
          go env GOPROXY
          
          # 默认值:https://proxy.golang.org,direct
          # 中国大陆推荐:
          go env -w GOPROXY=https://goproxy.cn,direct
          
          # 格式:代理1,代理2,...,direct
          # direct 表示直连源仓库(代理失败时兜底)
          # off 表示完全禁止下载
          
          # GONOSUMCHECK:跳过校验和检查
          go env -w GONOSUMCHECK="*.internal.company.com"
          
          # GONOSUMDB:跳过 sum 数据库
          go env -w GONOSUMDB="*.internal.company.com"
          
        • 私有模块配置

          # GOPRIVATE:私有模块不走代理也不走 sum 数据库
          go env -w GOPRIVATE="*.corp.example.com,github.com/mycompany/*"
          
          # 配置 git 认证(访问私有仓库)
          git config --global url."https://token:PASSWORD@github.com/mycompany/".insteadOf     "https://github.com/mycompany/"
          
          # 或者用 SSH
          git config --global url."git@github.com:mycompany/".insteadOf     "https://github.com/mycompany/"
          
          # 公司内部代理服务器(Athens/goproxy.io)
          go env -w GOPROXY="https://goproxy.company.com,https://proxy.golang.org,direct"
          go env -w GOPRIVATE="*.company.com"
          
    • 测试与基准测试
      • Go Test 框架基础
        • Go 内置了完整的测试框架,不需要任何第三方库。
        • 测试文件以 _test.go 结尾,测试函数以 Test 开头,接受 *testing.T 参数。这三条规则,就是 Go 测试的全部入场门票。
        • 示例

          // math/add.go
          package math
          
          func Add(a, b int) int {
              return a + b
          }
          
          func Divide(a, b float64) (float64, error) {
              if b == 0 {
                  return 0, errors.New("除数不能为零")
              }
              return a / b, nil
          }
          
          // math/add_test.go(同一个包,_test.go 结尾)
          package math
          
          import "testing"
          
          func TestAdd(t *testing.T) {
              result := Add(1, 2)
              if result != 3 {
                  t.Errorf("Add(1, 2) = %d,期望 3", result)
              }
          }
          
          func TestDivide(t *testing.T) {
              result, err := Divide(10, 2)
              if err != nil {
                  t.Fatalf("意外错误: %v", err)
              }
              if result != 5 {
                  t.Errorf("Divide(10, 2) = %f,期望 5", result)
              }
          }
          
        • 运行测试的常用命令
        • testing.T 的核心方法

          // 测试失败但继续执行
          t.Errorf("期望 %d,实际 %d", expected, actual)
          t.Error("发现问题")
          
          // 测试失败并立即停止(Fatal = Error + runtime.Goexit)
          t.Fatalf("致命错误: %v", err)
          t.Fatal("不可继续")
          
          // 仅打印日志(不影响测试结果)
          t.Logf("调试信息: %v", value)
          t.Log("这里到了")
          
          // 跳过测试
          t.Skip("跳过原因:需要数据库")
          t.Skipf("跳过:CI 环境不支持 %s", feature)
          
          // 标记为并行测试
          t.Parallel()
          
          // 子测试(下一节讲)
          t.Run("子测试名", func(t *testing.T) { ... })
          
      • 表驱动测试
        • 表驱动测试是 Go 最推荐的测试写法。
        • 把所有测试用例放进一个切片,用循环执行,消除重复代码,添加新用例只需加一行。
        • 这是 Go 测试的标准惯用法。
        • 示例

          func TestAdd(t *testing.T) {
              // 定义测试用例表
              tests := []struct {
                  name     string  // 用例名(出错时显示)
                  a, b     int
                  expected int
              }{
                  {"正数相加", 1, 2, 3},
                  {"负数相加", -1, -2, -3},
                  {"正负相加", 5, -3, 2},
                  {"零值", 0, 0, 0},
                  {"大数", 1000000, 2000000, 3000000},
              }
          
              for _, tt := range tests {
                  t.Run(tt.name, func(t *testing.T) {   // ✨ 子测试
                      result := Add(tt.a, tt.b)
                      if result != tt.expected {
                          t.Errorf("Add(%d, %d) = %d,期望 %d",
                              tt.a, tt.b, result, tt.expected)
                      }
                  })
              }
          }
          
        • 测试有 error 返回的函数

          func TestDivide(t *testing.T) {
              tests := []struct {
                  name      string
                  a, b      float64
                  expected  float64
                  wantErr   bool    // 是否期望出错
                  errMsg    string  // 期望的错误信息(可选)
              }{
                  {"正常除法", 10, 2, 5, false, ""},
                  {"除以1", 9, 1, 9, false, ""},
                  {"除以零", 10, 0, 0, true, "除数不能为零"},
                  {"负数除法", -10, 2, -5, false, ""},
              }
          
              for _, tt := range tests {
                  t.Run(tt.name, func(t *testing.T) {
                      result, err := Divide(tt.a, tt.b)
          
                      // 检查是否符合预期的错误行为
                      if tt.wantErr {
                          if err == nil {
                              t.Error("期望有错误,但没有")
                          }
                          if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
                              t.Errorf("错误信息 %q 不包含 %q", err.Error(), tt.errMsg)
                          }
                          return  // 有预期错误,不检查结果
                      }
          
                      if err != nil {
                          t.Fatalf("意外错误: %v", err)
                      }
          
                      if math.Abs(result-tt.expected) > 1e-9 {
                          t.Errorf("Divide(%.2f, %.2f) = %.2f,期望 %.2f",
                              tt.a, tt.b, result, tt.expected)
                      }
                  })
              }
          }
          
        • t.Run 子测试的好处
          • t.Run 为每个用例创建独立的子测试
          • 出错时精确定位是哪个用例失败(如 TestDivide/除以零)
          • 可以单独运行某个子测试 go test -run TestDivide/除以零
          • 支持 t.Parallel() 并行运行各个子测试
      • 测试前后的准备与清理
        • 很多测试需要初始化环境(启动数据库、创建临时文件)和清理(关闭连接、删除文件)。Go 提供了几种机制来处理这些需求。
        • TestMain: 包级别的 setup/teardown

          // 如果定义了 TestMain,go test 会优先调用它
          // 可以在这里做全局初始化
          func TestMain(m *testing.M) {
              // ─── Setup ───
              fmt.Println("全局初始化:启动测试数据库...")
              db, err := setupTestDB()
              if err != nil {
                  log.Fatalf("数据库初始化失败: %v", err)
              }
          
              // ─── 运行所有测试 ───
              exitCode := m.Run()
          
              // ─── Teardown ───
              fmt.Println("全局清理:关闭测试数据库...")
              db.Close()
              cleanupTestData()
          
              // 必须调用 os.Exit,否则 defer 不会执行
              os.Exit(exitCode)
          }
          
        • t.Cleanup: 函数级别的清理

          func TestWriteFile(t *testing.T) {
              // 创建临时目录
              tmpDir := t.TempDir()  // ✨ 测试结束自动删除!
          
              // t.Cleanup 注册清理函数(类似 defer,但属于测试框架)
              tmpFile := filepath.Join(tmpDir, "test.txt")
              t.Cleanup(func() {
                  os.Remove(tmpFile)
                  t.Log("清理临时文件")
              })
          
              // 测试逻辑
              err := os.WriteFile(tmpFile, []byte("hello"), 0644)
              if err != nil {
                  t.Fatal(err)
              }
          
              content, err := os.ReadFile(tmpFile)
              if err != nil {
                  t.Fatal(err)
              }
              if string(content) != "hello" {
                  t.Errorf("内容不匹配")
              }
          }
          
          // t.TempDir() 创建临时目录,测试结束(包括失败)后自动删除
          // 比手写 defer os.RemoveAll 更安全
          
        • 辅助函数: Helper

          // 当多个测试有重复逻辑时,提取为 helper 函数
          // ✨ 注意 t.Helper():让错误报告指向调用 helper 的地方,而不是 helper 内部
          
          func assertNoError(t *testing.T, err error) {
              t.Helper()  // 声明这是 helper 函数
              if err != nil {
                  t.Fatalf("意外错误: %v", err)
              }
          }
          
          func assertEqual(t *testing.T, got, want interface{}) {
              t.Helper()
              if got != want {
                  t.Errorf("期望 %v,实际 %v", want, got)
              }
          }
          
          // 使用
          func TestSomething(t *testing.T) {
              result, err := doWork()
              assertNoError(t, err)      // 出错时报告这一行,不是 assertNoError 内部
              assertEqual(t, result, 42) // 同上
          }
          
      • testify 更好的断言库
        • 标准库的 testing 功能够用,但写法有点啰嗦。
        • testify 是 Go 生态最流行的测试辅助库,提供更简洁的断言、Mock 和测试套件支持。
        • 安装:go get github.com/stretchr/testify
        • assert vs require

          import (
              "testing"
              "github.com/stretchr/testify/assert"
              "github.com/stretchr/testify/require"
          )
          
          func TestWithTestify(t *testing.T) {
              // assert:失败后继续执行(类似 t.Errorf)
              assert.Equal(t, 3, Add(1, 2))
              assert.NotNil(t, someValue)
              assert.NoError(t, err)
              assert.Contains(t, "hello world", "world")
              assert.True(t, isValid)
              assert.Len(t, slice, 3)
          
              // require:失败后立即停止(类似 t.Fatalf)
              require.NoError(t, err)       // err 不为 nil 就立即停止
              require.NotNil(t, result)     // 后续代码依赖 result 不为 nil
          
              // 带自定义消息
              assert.Equal(t, want, got, "用户列表长度应该相等")
          
              // 错误断言
              assert.Error(t, err)                          // 期望有 error
              assert.ErrorIs(t, err, ErrNotFound)           // 等价于 errors.Is
              assert.ErrorAs(t, err, &ValidationError{})    // 等价于 errors.As
          }
          
        • 常用断言

          // 相等性
          assert.Equal(t, expected, actual)         // ==
          assert.NotEqual(t, expected, actual)      // !=
          assert.EqualValues(t, expected, actual)   // 类型不同但值相等
          
          // nil 检查
          assert.Nil(t, val)
          assert.NotNil(t, val)
          
          // 布尔
          assert.True(t, condition)
          assert.False(t, condition)
          
          // 错误
          assert.NoError(t, err)
          assert.Error(t, err)
          assert.ErrorIs(t, err, target)
          
          // 字符串/切片
          assert.Contains(t, "hello", "ell")
          assert.Contains(t, []int{1,2,3}, 2)
          assert.Len(t, slice, 3)
          assert.Empty(t, slice)
          assert.NotEmpty(t, slice)
          
          // 数值比较
          assert.Greater(t, 5, 3)
          assert.GreaterOrEqual(t, 5, 5)
          assert.InDelta(t, 1.5, 1.500001, 0.001)  // 浮点近似相等
          
          // 结构体(深度相等)
          assert.Equal(t, expected, actual)  // 自动深度比较
          // 或用 ObjectsAreEqual:
          require.True(t, reflect.DeepEqual(want, got))
          
        • assert 和 require 的选择原则
          • 当后续的测试逻辑依赖当前断言成立时(比如 err == nil 才能用 result),用 require。
          • 否则用 assert,收集更多失败信息。一个简单口诀:「之后还要用这个值,就 require;不用就 assert」。
      • Mock 测试
        • Mock 是测试中最重要的技术之一。当你的代码依赖外部服务(数据库、HTTP API、邮件服务),测试时用 Mock 替换这些依赖,让测试快速、可靠、独立。
        • 手写 Mock

          // 被测代码依赖接口(Day 12 项目结构的价值体现)
          type UserRepository interface {
              FindByID(ctx context.Context, id int) (*User, error)
              Create(ctx context.Context, u *User) error
          }
          
          type UserService struct {
              repo UserRepository
          }
          
          // 手写 Mock:直接实现接口
          type MockUserRepo struct {
              users  map[int]*User
              nextID int
              // 可以记录调用次数、参数等
              CreateCalled int
              FindCalled   int
          }
          
          func (m *MockUserRepo) FindByID(_ context.Context, id int) (*User, error) {
              m.FindCalled++
              u, ok := m.users[id]
              if !ok {
                  return nil, ErrNotFound
              }
              return u, nil
          }
          
          func (m *MockUserRepo) Create(_ context.Context, u *User) error {
              m.CreateCalled++
              u.ID = m.nextID
              m.users[u.ID] = u
              m.nextID++
              return nil
          }
          
          // 测试时注入 Mock
          func TestUserService_Register(t *testing.T) {
              mockRepo := &MockUserRepo{
                  users:  make(map[int]*User),
                  nextID: 1,
              }
              svc := &UserService{repo: mockRepo}
          
              ctx := context.Background()
              u, err := svc.Register(ctx, "Alice", "alice@example.com")
          
              require.NoError(t, err)
              assert.Equal(t, "Alice", u.Name)
              assert.Equal(t, 1, mockRepo.CreateCalled)  // 验证调用次数
          }
          
        • testify/mock:功能更强的 mock

          import "github.com/stretchr/testify/mock"
          
          // 用 testify/mock 生成 Mock
          type MockUserRepo struct {
              mock.Mock  // 嵌入 mock.Mock
          }
          
          func (m *MockUserRepo) FindByID(ctx context.Context, id int) (*User, error) {
              args := m.Called(ctx, id)  // 记录调用,返回预设值
              if args.Get(0) == nil {
                  return nil, args.Error(1)
              }
              return args.Get(0).(*User), args.Error(1)
          }
          
          func (m *MockUserRepo) Create(ctx context.Context, u *User) error {
              args := m.Called(ctx, u)
              return args.Error(0)
          }
          
          // 使用:设置期望
          func TestWithMock(t *testing.T) {
              mockRepo := new(MockUserRepo)
          
              expectedUser := &User{ID: 1, Name: "Alice"}
          
              // 设置期望:当 FindByID 被用 id=1 调用时,返回 expectedUser
              mockRepo.On("FindByID", mock.Anything, 1).Return(expectedUser, nil)
          
              // 设置期望:Create 被调用时成功
              mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*User")).Return(nil)
          
              svc := &UserService{repo: mockRepo}
          
              u, err := svc.GetUser(context.Background(), 1)
              require.NoError(t, err)
              assert.Equal(t, "Alice", u.Name)
          
              // 验证所有期望都被满足
              mockRepo.AssertExpectations(t)
          }
          
        • Mock 的正确姿势
          • Mock 应该只 Mock 你控制不了的东西:数据库、HTTP 接口、时间、随机数。
          • 不要 Mock 你自己写的业务逻辑——那会让测试失去意义。
          • 最好的测试策略是:单元测试 Mock 外部依赖,集成测试用真实的基础设施(用 testcontainers)。
      • 基准测试
        • 基准测试用来测量代码的性能。函数以 Benchmark 开头,接受 *testing.B 参数。Go 会自动调整循环次数,让结果更准确。
        • 示例

          func BenchmarkAdd(b *testing.B) {
              // b.N 由 go test 自动决定(通常是几百万次)
              for i := 0; i < b.N; i++ {
                  Add(1, 2)
              }
          }
          
          // 运行基准测试
          // go test -bench=. -benchmem ./...
          // -bench=.         运行所有 Benchmark
          // -benchmem        同时显示内存分配
          // -benchtime=5s    指定运行时间
          // -count=3         运行 3 次取平均
          
          // 示例输出:
          // BenchmarkAdd-8    1000000000    0.2891 ns/op    0 B/op    0 allocs/op
          //                  ↑执行次数      ↑每次耗时         ↑内存     ↑分配次数
          
        • 比较两种实现的性能

          // 对比字符串拼接:+ vs strings.Builder
          func BenchmarkStringConcat(b *testing.B) {
              words := []string{"hello", "world", "go", "test"}
          
              b.Run("用+拼接", func(b *testing.B) {
                  for i := 0; i < b.N; i++ {
                      result := ""
                      for _, w := range words {
                          result += w  // 每次拼接都分配新内存
                      }
                      _ = result
                  }
              })
          
              b.Run("用Builder", func(b *testing.B) {
                  for i := 0; i < b.N; i++ {
                      var sb strings.Builder
                      for _, w := range words {
                          sb.WriteString(w)  // 预分配,减少内存分配
                      }
                      _ = sb.String()
                  }
              })
          }
          
          // 输出示例:
          // BenchmarkStringConcat/用+拼接-8     3000000   450 ns/op   128 B/op   4 allocs/op
          // BenchmarkStringConcat/用Builder-8  10000000   120 ns/op    64 B/op   1 allocs/op
          // Builder 快 3.7 倍,内存分配少 4 倍
          
        • b.ResetTimer 和 b.StopTimer

          func BenchmarkWithSetup(b *testing.B) {
              // 做一些耗时的初始化(不计入基准)
              data := generateLargeTestData()
          
              b.ResetTimer()  // 重置计时器,排除初始化时间
          
              for i := 0; i < b.N; i++ {
                  b.StopTimer()  // 暂停计时
                  input := prepareInput(data)  // 每次迭代的准备工作
                  b.StartTimer() // 恢复计时
          
                  processData(input)  // 只测量这部分
              }
          }
          
      • 示例测试
        • Example 函数有双重作用:既是可运行的测试,也是文档。
        • go doc 命令会显示 Example 函数,godoc 网站也会展示它们。
        • 示例

          // 函数名:Example + 函数名
          func ExampleAdd() {
              result := Add(1, 2)
              fmt.Println(result)
              // Output: 3  ← go test 会验证输出与这里一致
          }
          
          func ExampleDivide() {
              result, err := Divide(10, 2)
              fmt.Println(result, err)
              // Output: 5 <nil>
          }
          
          // 无法预测输出顺序时用 Unordered output
          func ExamplePrintMap() {
              m := map[string]int{"a": 1, "b": 2}
              for k, v := range m {
                  fmt.Printf("%s: %d\n", k, v)
              }
              // Unordered output:
              // a: 1
              // b: 2
          }
          
          // 类型/方法的 Example
          func ExampleUser_GetName() {
              u := User{Name: "Alice"}
              fmt.Println(u.GetName())
              // Output: Alice
          }
          
        • Example 是活的文档
          • Example 函数最大的优点是:它会被 go test 执行,如果输出和注释不匹配就会失败。
          • 这意味着你的文档示例代码永远是正确的——代码改了但没更新文档,测试会提醒你。
          • 这是 Go 「文档即测试」哲学的体现。
      • 测试覆盖率
        • 覆盖率不是越高越好,但低覆盖率通常意味着关键路径没有被测试。
        • Go 内置了覆盖率工具,不需要额外安装。
        • 示例

          # 查看覆盖率(百分比)
          go test -cover ./...
          # ok  myapp/internal/service  coverage: 78.5% of statements
          
          # 生成覆盖率文件
          go test -coverprofile=coverage.out ./...
          
          # 用浏览器查看哪些行被覆盖(绿色),哪些没有(红色)
          go tool cover -html=coverage.out
          
          # 按函数查看覆盖率
          go tool cover -func=coverage.out
          # myapp/internal/service/user.go:Register   100.0%
          # myapp/internal/service/user.go:GetUser    75.0%
          # total:                                     82.3%
          
          # 生成覆盖率徽章(常用于 README)
          # 需要第三方工具,如 goveralls、codecov
          
        • 覆盖率的合理目标

          // 什么应该测?
          // ✅ 核心业务逻辑(service 层)
          // ✅ 复杂算法
          // ✅ 错误处理路径
          // ✅ 边界条件(空值、极值)
          
          // 什么可以不测/难测?
          // ❌ main 函数
          // ❌ 简单的 getter/setter
          // ❌ 第三方库(已有自己的测试)
          // ❌ 纯配置代码
          
          // 行业参考值:
          // 核心业务代码:80% 以上
          // 整体项目:60-70% 是健康的
          // 追求 100% 反而可能浪费时间在无价值的测试上
          
          // 一个常见的 .golangci.yml 配置
          // testpackage: 是否要求测试在独立的 _test 包
          // 建议:核心包用 package xxx_test,确保测试只用公开 API
          
        • 测试覆盖率的正确理解
          • 覆盖率是工具,不是目标。
          • 80% 覆盖率但测试都是走正常路径,不如 60% 但每个边界条件都测到。
          • 真正重要的是:关键业务路径有测试、错误路径有测试、修改代码后测试能告诉你哪里坏了。
    • 泛型
      • Go 1.18 引入泛型之前,想写「适用于任意类型」的函数有两种选择:为每种类型写重复代码,或者用 any 丢失类型安全。
      • 泛型解决了这个两难困境——既通用,又类型安全。
      • 没有泛型的问题

        // ❌ 方案一:为每种类型写重复代码
        func SumInt(nums []int) int {
            total := 0
            for _, n := range nums { total += n }
            return total
        }
        func SumFloat64(nums []float64) float64 {
            total := 0.0
            for _, n := range nums { total += n }
            return total
        }
        // 如果要支持 int32、int64、float32... 噩梦
        
        // ❌ 方案二:用 any,丢失类型安全
        func Sum(nums []any) any {
            // 怎么相加?any 不支持 + 运算符
            // 必须类型断言,运行时才发现错误
        }
        
        // ✅ 泛型:一次编写,类型安全,适用所有数字类型
        func Sum[T int | int64 | float64](nums []T) T {
            var total T
            for _, n := range nums { total += n }
            return total
        }
        
        Sum([]int{1, 2, 3})         // 6
        Sum([]float64{1.1, 2.2})    // 3.3
        Sum([]string{"a", "b"})     // ❌ 编译错误:string 不满足约束
        
      • 泛型 vs any 的本质区别
        • any 在运行时丢失类型信息,错误只能在运行时发现。
        • 泛型在编译时确定类型,错误在编译时发现,且不需要类型断言,性能也更好(零运行时开销)。
        • 泛型是编译器的能力,any 是程序员的妥协。
      • 类型参数基础语法
        • 泛型通过「类型参数」实现。
        • 类型参数放在方括号 [] 里,紧跟在函数名或类型名后面。
        • 每个类型参数都有一个「约束」,限制它能接受哪些类型。
        • 泛型函数

          // func 函数名[类型参数 约束](普通参数) 返回值
          func Map[T, R any](slice []T, fn func(T) R) []R {
              result := make([]R, len(slice))
              for i, v := range slice {
                  result[i] = fn(v)
              }
              return result
          }
          
          // 使用:Go 能自动推断类型参数,通常不用显式写
          nums := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })
          // nums = [2, 4, 6]
          
          strs := Map([]int{1, 2, 3}, func(n int) string {
              return fmt.Sprintf("No.%d", n)
          })
          // strs = ["No.1", "No.2", "No.3"]
          
          // 显式指定类型参数(推断失败时)
          result := Map[int, string]([]int{1, 2, 3}, func(n int) string {
              return strconv.Itoa(n)
          })
          
        • 泛型类型 (结构体)

          // type 类型名[类型参数 约束] struct { ... }
          type Stack[T any] struct {
              items []T
          }
          
          func (s *Stack[T]) Push(item T) {
              s.items = append(s.items, item)
          }
          
          func (s *Stack[T]) Pop() (T, bool) {
              if len(s.items) == 0 {
                  var zero T  // ✨ 零值:泛型类型的默认值
                  return zero, false
              }
              top := s.items[len(s.items)-1]
              s.items = s.items[:len(s.items)-1]
              return top, true
          }
          
          func (s *Stack[T]) Peek() (T, bool) {
              if len(s.items) == 0 {
                  var zero T
                  return zero, false
              }
              return s.items[len(s.items)-1], true
          }
          
          func (s *Stack[T]) Len() int { return len(s.items) }
          
          // 使用
          intStack := Stack[int]{}       // 整数栈
          intStack.Push(1)
          intStack.Push(2)
          v, ok := intStack.Pop()        // v=2, ok=true
          
          strStack := Stack[string]{}    // 字符串栈
          strStack.Push("hello")
          
      • 类型泛型
        • 约束定义了类型参数能接受哪些类型。
        • 约束本质上是接口——只不过这个接口里可以包含「类型集合」(type set),不只是方法列表。
        • 内置约束

          // any:没有限制(等价于 interface{})
          func Identity[T any](v T) T { return v }
          
          // comparable:可以用 == 和 != 比较的类型
          // int, string, bool, 指针, channel, 不含 slice/map/func 的 struct 等
          func Contains[T comparable](slice []T, target T) bool {
              for _, v := range slice {
                  if v == target { return true }
              }
              return false
          }
          
          Contains([]int{1, 2, 3}, 2)         // true
          Contains([]string{"a", "b"}, "c")   // false
          
          // 注意:slice、map、func 不是 comparable
          // Contains([][]int{{1,2}}, []int{1,2})  // ❌ 编译错误
          
        • contraints 包的内置约束

          import "golang.org/x/exp/constraints"
          // 或 Go 1.21+ 用标准库 cmp 包
          
          // constraints.Ordered:支持 < > <= >= 的类型
          // int, int8, int16, int32, int64
          // uint, uint8, ..., uintptr
          // float32, float64
          // string
          
          func Min[T constraints.Ordered](a, b T) T {
              if a < b { return a }
              return b
          }
          
          func Max[T constraints.Ordered](a, b T) T {
              if a > b { return a }
              return b
          }
          
          func Clamp[T constraints.Ordered](val, lo, hi T) T {
              return Max(lo, Min(val, hi))
          }
          
          Min(3, 5)          // 3
          Min("apple", "banana")  // "apple"(字符串也可以比较)
          Clamp(15, 0, 10)   // 10(限制在 [0, 10] 范围内)
          
        • 自定义约束

          // 约束是接口,可以包含:
          // 1. 方法列表
          // 2. 类型集合(~ 表示底层类型)
          
          // 类型集合:只允许这些具体类型
          type Integer interface {
              int | int8 | int16 | int32 | int64
          }
          
          // ~ 表示底层类型(包含所有基于这些类型的自定义类型)
          type Number interface {
              ~int | ~int8 | ~int16 | ~int32 | ~int64 |
              ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
              ~float32 | ~float64
          }
          
          func Sum[T Number](nums []T) T {
              var total T
              for _, n := range nums { total += n }
              return total
          }
          
          // ~ 的重要性:
          type MyInt int          // 基于 int 的自定义类型
          Sum([]MyInt{1, 2, 3})  // ✅ ~int 包含 MyInt
          // 不加 ~ 的话,Sum[int | float64] 就不接受 MyInt
          
          // 方法 + 类型混合约束
          type Stringer interface {
              ~string | ~[]byte  // 类型集合
              String() string    // 方法要求
          }
          
        • ~ 操作符的含义
          • ~T 表示「底层类型为 T 的所有类型」
          • type MyInt int 的底层类型是 int,所以 ~int 包含 MyInt
          • 不加 ~ 则只接受 int 本身,不接受基于 int 的自定义类型
          • 实际项目中,数值约束通常都要加 ~
      • 实用泛型函数库
        • 掌握泛型最好的方式是实现那些经典的高阶函数。
        • Go 1.21 的 slices 和 maps 标准包已经内置了很多。
        • 切片操作三件套

          // Map:对每个元素应用函数
          func Map[T, R any](s []T, fn func(T) R) []R {
              result := make([]R, len(s))
              for i, v := range s { result[i] = fn(v) }
              return result
          }
          
          // Filter:保留满足条件的元素
          func Filter[T any](s []T, keep func(T) bool) []T {
              result := make([]T, 0, len(s))
              for _, v := range s {
                  if keep(v) { result = append(result, v) }
              }
              return result
          }
          
          // Reduce:累积计算
          func Reduce[T, R any](s []T, init R, fn func(R, T) R) R {
              result := init
              for _, v := range s { result = fn(result, v) }
              return result
          }
          
          // 使用:组合成管道
          nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
          
          result := Reduce(
              Map(
                  Filter(nums, func(n int) bool { return n%2 == 0 }),
                  func(n int) int { return n * n },
              ),
              0,
              func(acc, n int) int { return acc + n },
          )
          // 偶数的平方和:4+16+36+64+100 = 220
          
        • 通用工具函数

          // Contains:检查元素是否在切片中
          func Contains[T comparable](s []T, target T) bool {
              for _, v := range s {
                  if v == target { return true }
              }
              return false
          }
          
          // Index:查找元素的索引,-1 表示不存在
          func Index[T comparable](s []T, target T) int {
              for i, v := range s {
                  if v == target { return i }
              }
              return -1
          }
          
          // Unique:去重,保持顺序
          func Unique[T comparable](s []T) []T {
              seen := make(map[T]struct{})
              result := make([]T, 0, len(s))
              for _, v := range s {
                  if _, ok := seen[v]; !ok {
                      seen[v] = struct{}{}
                      result = append(result, v)
                  }
              }
              return result
          }
          
          // Keys:取出 map 的所有 key
          func Keys[K comparable, V any](m map[K]V) []K {
              keys := make([]K, 0, len(m))
              for k := range m { keys = append(keys, k) }
              return keys
          }
          
          // Values:取出 map 的所有 value
          func Values[K comparable, V any](m map[K]V) []V {
              vals := make([]V, 0, len(m))
              for _, v := range m { vals = append(vals, v) }
              return vals
          }
          
          // GroupBy:按 key 函数分组
          func GroupBy[T any, K comparable](s []T, key func(T) K) map[K][]T {
              groups := make(map[K][]T)
              for _, v := range s {
                  k := key(v)
                  groups[k] = append(groups[k], v)
              }
              return groups
          }
          
          // 使用 GroupBy
          type Person struct{ Name, City string }
          people := []Person{
              {"Alice", "Tokyo"}, {"Bob", "Tokyo"}, {"Charlie", "Osaka"},
          }
          byCity := GroupBy(people, func(p Person) string { return p.City })
          // byCity["Tokyo"] = [{Alice Tokyo} {Bob Tokyo}]
          
      • 泛型数据结构
        • 泛型最大的价值之一是可以写真正通用的数据结构。下面实现几个常用的容器。
        • 泛型 Set (集合)

          type Set[T comparable] struct {
              items map[T]struct{}
          }
          
          func NewSet[T comparable](items ...T) *Set[T] {
              s := &Set[T]{items: make(map[T]struct{})}
              for _, item := range items { s.Add(item) }
              return s
          }
          
          func (s *Set[T]) Add(item T)         { s.items[item] = struct{}{} }
          func (s *Set[T]) Remove(item T)      { delete(s.items, item) }
          func (s *Set[T]) Contains(item T) bool {
              _, ok := s.items[item]
              return ok
          }
          func (s *Set[T]) Len() int           { return len(s.items) }
          
          // 集合运算
          func (s *Set[T]) Union(other *Set[T]) *Set[T] {
              result := NewSet[T]()
              for k := range s.items { result.Add(k) }
              for k := range other.items { result.Add(k) }
              return result
          }
          
          func (s *Set[T]) Intersection(other *Set[T]) *Set[T] {
              result := NewSet[T]()
              for k := range s.items {
                  if other.Contains(k) { result.Add(k) }
              }
              return result
          }
          
          // 使用
          a := NewSet(1, 2, 3, 4)
          b := NewSet(3, 4, 5, 6)
          union := a.Union(b)         // {1,2,3,4,5,6}
          inter := a.Intersection(b)  // {3,4}
          
        • 泛型 Optional (可选值)

          // 类似 Rust 的 Option<T>,比返回 (T, bool) 更语义化
          type Optional[T any] struct {
              value    T
              hasValue bool
          }
          
          func Some[T any](v T) Optional[T] {
              return Optional[T]{value: v, hasValue: true}
          }
          
          func None[T any]() Optional[T] {
              return Optional[T]{}
          }
          
          func (o Optional[T]) IsPresent() bool { return o.hasValue }
          
          func (o Optional[T]) Get() (T, bool) {
              return o.value, o.hasValue
          }
          
          func (o Optional[T]) OrElse(defaultVal T) T {
              if o.hasValue { return o.value }
              return defaultVal
          }
          
          func (o Optional[T]) Map(fn func(T) T) Optional[T] {
              if !o.hasValue { return None[T]() }
              return Some(fn(o.value))
          }
          
          // 使用
          func findUser(id int) Optional[User] {
              user, ok := db[id]
              if !ok { return None[User]() }
              return Some(user)
          }
          
          result := findUser(42).
              Map(func(u User) User { u.Name = strings.ToUpper(u.Name); return u }).
              OrElse(User{Name: "anonymous"})
          
      • 标准库的泛型包
        • Go 1.21 起标准库加入了几个泛型包,直接使用它们比自己实现更好。
        • slice

          import "slices"
          
          nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
          
          // 排序
          slices.Sort(nums)                    // [1 1 2 3 4 5 6 9]
          slices.SortFunc(nums, func(a, b int) int {
              return b - a                     // 降序
          })
          
          // 查找
          idx := slices.Index(nums, 5)         // 找到返回索引,否则 -1
          ok := slices.Contains(nums, 5)       // true
          
          // 修改
          slices.Reverse(nums)                 // 原地反转
          unique := slices.Compact(nums)       // 去除连续重复
          nums = slices.Delete(nums, 2, 4)     // 删除 [2,4) 区间
          nums = slices.Insert(nums, 1, 99, 88) // 在索引 1 处插入
          
          // 复制(独立副本)
          clone := slices.Clone(nums)
          
          // 比较
          equal := slices.Equal([]int{1,2}, []int{1,2})  // true
          
        • maps 包

          import "maps"
          
          m := map[string]int{"a": 1, "b": 2, "c": 3}
          
          // 复制
          clone := maps.Clone(m)
          
          // 删除满足条件的 key
          maps.DeleteFunc(m, func(k string, v int) bool {
              return v < 2  // 删除 value < 2 的条目
          })
          
          // 判断是否相等
          eq := maps.Equal(m, map[string]int{"b": 2, "c": 3})
          
          // 遍历(Go 1.23+ range over func)
          for k, v := range maps.All(m) {
              fmt.Printf("%s: %d\n", k, v)
          }
          
        • cmp 包

          import "cmp"
          
          // cmp.Ordered:可排序类型的约束(等价于 constraints.Ordered)
          func Min[T cmp.Ordered](a, b T) T {
              return min(a, b)  // Go 1.21+ 内置了 min/max 函数
          }
          
          // cmp.Compare:三路比较(-1, 0, 1)
          cmp.Compare(1, 2)     // -1
          cmp.Compare(2, 2)     // 0
          cmp.Compare(3, 2)     // 1
          cmp.Compare("a", "b") // -1(字符串也支持)
          
          // 配合 slices.SortFunc 使用
          type User struct{ Name string; Age int }
          users := []User{{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}}
          
          slices.SortFunc(users, func(a, b User) int {
              return cmp.Compare(a.Age, b.Age)  // 按年龄排序
          })
          
          // Go 1.21+ 内置 min/max(泛型版本)
          fmt.Println(min(3, 5))         // 3
          fmt.Println(max("a", "b"))     // b
          fmt.Println(min(1, 2, 3, 4))   // 1(支持多参数)
          
      • 泛型的限制与最佳实践
        • 当前的限制

          // ❌ 限制一:不能在方法上定义新的类型参数
          type Foo struct{}
          func (f Foo) Bar[T any](v T) {}  // ❌ 编译错误!方法不能有类型参数
          
          // ✅ 解决:把类型参数放到结构体上
          type Foo[T any] struct{}
          func (f Foo[T]) Bar(v T) {}
          
          // ❌ 限制二:类型参数不能用作类型断言的目标
          func wrong[T any](v any) T {
              return v.(T)  // ❌ 编译错误
          }
          
          // ✅ 解决:用 switch 或 reflect
          func convert[T any](v any) (T, bool) {
              result, ok := v.(T)
              return result, ok
          }
          
          // ❌ 限制三:泛型类型不能直接比较(如果类型参数不是 comparable)
          func Equal[T any](a, b T) bool {
              return a == b  // ❌ 如果 T 是 any,不能用 ==
          }
          
          // ✅ 解决:约束为 comparable
          func Equal[T comparable](a, b T) bool {
              return a == b
          }
          
          // ❌ 限制四:不支持特化(不能为特定类型提供不同实现)
          // Go 泛型目前没有 C++ 那样的模板特化
          
        • 什么时候用泛型,什么时候不用

          // ✅ 适合用泛型:
          // 1. 通用容器(Stack, Queue, Set, Tree)
          // 2. 算法函数(Sort, Map, Filter, Reduce)
          // 3. 工具函数(Min, Max, Contains, GroupBy)
          // 4. 类型安全的包装(Optional, Result)
          
          // ❌ 不适合用泛型:
          // 1. 单一类型的业务逻辑(用具体类型更清晰)
          // 2. 接口能解决的多态问题(优先接口)
          // 3. 能用 any 且不需要类型安全的场景
          
          // 判断口诀:
          // 「如果写了两个只有类型不同的函数,考虑泛型」
          // 「如果行为因类型不同而不同,用接口」
          
          // 反面教材:泛型滥用
          type GenericService[T any] struct {  // ❌ 过度泛型
              repo Repository[T]
          }
          // 如果 T 只有一种可能,直接用具体类型
          
          // 正确:当真正需要通用性时才用
          type Cache[K comparable, V any] struct {  // ✅ 合理
              mu   sync.RWMutex
              data map[K]V
              ttl  time.Duration
          }
          

Web 开发实战

  • Web 开发实战
    • net/http 标准库
      • net/http 核心概念
        • Go 的 net/http 标准库是同类中最强大的之一——不需要任何框架就能构建生产级 HTTP 服务器。
        • 理解标准库是学好 Gin/Echo 等框架的基础,因为框架只是在它之上加了一层糖衣。
        • HTTP 服务器示例

          package main
          
          import (
              "fmt"
              "net/http"
          )
          
          func main() {
              // 注册路由处理器
              http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
                  fmt.Fprintln(w, "Hello, Go Web!")
              })
          
              http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
                  fmt.Fprintln(w, "pong")
              })
          
              // 启动服务器(阻塞)
              fmt.Println("服务器启动在 :8080")
              if err := http.ListenAndServe(":8080", nil); err != nil {
                  panic(err)
              }
          }
          
          // curl http://localhost:8080/
          // → Hello, Go Web!
          // curl http://localhost:8080/ping
          // → pong
          
        • http.Handler 接口:一切方法的核心

          // Handler 接口只有一个方法
          // type Handler interface {
          //     ServeHTTP(ResponseWriter, *Request)
          // }
          
          // ResponseWriter:写响应
          // *Request:读请求
          
          // 实现 Handler 接口的三种方式:
          
          // 方式一:函数(用 http.HandlerFunc 适配)
          func myHandler(w http.ResponseWriter, r *http.Request) {
              w.WriteHeader(http.StatusOK)
              fmt.Fprintln(w, "Hello")
          }
          http.Handle("/", http.HandlerFunc(myHandler))
          
          // 方式二:http.HandleFunc(语法糖,最常用)
          http.HandleFunc("/", myHandler)
          
          // 方式三:实现 ServeHTTP 方法的结构体
          type MyHandler struct{ greeting string }
          
          func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
              fmt.Fprintln(w, h.greeting)
          }
          
          http.Handle("/", &MyHandler{greeting: "Hello from struct!"})
          
        • nil 是默认 ServeMux
          • http.HandleFunc 和 http.Handle 注册到默认的 http.DefaultServeMux 上。
          • ListenAndServe 传 nil 就用这个默认路由器。
          • 生产代码通常自己创建 http.NewServeMux(),避免全局状态污染——特别是在写库的时候。
      • 解析请求
        • *http.Request 包含请求的所有信息。掌握如何读取 URL 参数、Header、Body 是写 API 的基础。
        • URL 参数与路径

          func handler(w http.ResponseWriter, r *http.Request) {
              // 请求方法
              method := r.Method            // "GET", "POST", "PUT", "DELETE"
          
              // URL 路径
              path := r.URL.Path            // "/api/users/42"
          
              // Query 参数(?name=Alice&age=30)
              name := r.URL.Query().Get("name")     // "Alice"
              age := r.URL.Query().Get("age")       // "30"(字符串,需手动转换)
              all := r.URL.Query()                  // url.Values(map[string][]string)
          
              // ⚠️ 标准库没有路径参数(:id 这种)
              // 需要自己解析或用 Go 1.22 的新路由器
              // 例如:从 /api/users/42 提取 42
              parts := strings.Split(r.URL.Path, "/")
              // parts = ["", "api", "users", "42"]
          
              fmt.Fprintf(w, "method=%s path=%s name=%s", method, path, name)
          }
          
        • 读取 Header 和 Body

          func handler(w http.ResponseWriter, r *http.Request) {
              // Headers
              contentType := r.Header.Get("Content-Type")
              token := r.Header.Get("Authorization")
              userAgent := r.Header.Get("User-Agent")
          
              // Body(只能读一次!)
              defer r.Body.Close()
              body, err := io.ReadAll(r.Body)
              if err != nil {
                  http.Error(w, "读取 body 失败", http.StatusBadRequest)
                  return
              }
          
              // JSON 解析
              var payload struct {
                  Name  string `json:"name"`
                  Email string `json:"email"`
              }
              if err := json.Unmarshal(body, &payload); err != nil {
                  http.Error(w, "JSON 解析失败", http.StatusBadRequest)
                  return
              }
          
              // 或者用 Decoder(更高效,不需要先读入 []byte)
              var data map[string]any
              if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
                  http.Error(w, "invalid json", http.StatusBadRequest)
                  return
              }
          
              _ = contentType; _ = token; _ = userAgent
              fmt.Fprintf(w, "name=%s email=%s", payload.Name, payload.Email)
          }
          
        • 表单与文件上传

          func handler(w http.ResponseWriter, r *http.Request) {
              // 解析表单(必须先调用)
              if err := r.ParseForm(); err != nil {
                  http.Error(w, "解析表单失败", http.StatusBadRequest)
                  return
              }
              name := r.FormValue("name")    // 同时读取 form 和 query 参数
          
              // 文件上传
              r.ParseMultipartForm(10 << 20)  // 最大 10MB
              file, header, err := r.FormFile("avatar")
              if err != nil {
                  http.Error(w, "读取文件失败", http.StatusBadRequest)
                  return
              }
              defer file.Close()
          
              fmt.Printf("文件名: %s, 大小: %d\n", header.Filename, header.Size)
              // 保存文件
              dst, _ := os.Create("upload/" + header.Filename)
              defer dst.Close()
              io.Copy(dst, file)
          
              _ = name
              fmt.Fprintln(w, "上传成功")
          }
          
      • 构建响应
        • http.ResponseWriter 用于写响应。
        • 理解写入顺序很重要:必须先设置 Header,再 WriteHeader,最后写 Body。

          func handler(w http.ResponseWriter, r *http.Request) {
              // ⚠️ 顺序很重要:Header → WriteHeader → Body
          
              // 1. 设置响应头(必须在 WriteHeader 之前)
              w.Header().Set("Content-Type", "application/json")
              w.Header().Set("X-Request-ID", "abc-123")
          
              // 2. 设置状态码(只能调用一次,之后会 superfluous)
              w.WriteHeader(http.StatusCreated)  // 201
          
              // 3. 写 Body
              fmt.Fprintln(w, `{"message": "created"}`)
          }
          
          // 常用状态码常量
          http.StatusOK           // 200
          http.StatusCreated      // 201
          http.StatusNoContent    // 204
          http.StatusBadRequest   // 400
          http.StatusUnauthorized // 401
          http.StatusForbidden    // 403
          http.StatusNotFound     // 404
          http.StatusInternalServerError // 500
          
        • JSON 响应的标准写法

          // 封装一个通用的 JSON 响应函数
          func writeJSON(w http.ResponseWriter, status int, data any) {
              w.Header().Set("Content-Type", "application/json")
              w.WriteHeader(status)
              if err := json.NewEncoder(w).Encode(data); err != nil {
                  // 已经 WriteHeader 了,只能 log,不能再改状态码
                  log.Printf("写入 JSON 响应失败: %v", err)
              }
          }
          
          // 封装错误响应
          type ErrorResponse struct {
              Code    int    `json:"code"`
              Message string `json:"message"`
          }
          
          func writeError(w http.ResponseWriter, status int, msg string) {
              writeJSON(w, status, ErrorResponse{Code: status, Message: msg})
          }
          
          // 使用
          func getUserHandler(w http.ResponseWriter, r *http.Request) {
              user, err := findUser(1)
              if err != nil {
                  writeError(w, http.StatusNotFound, "用户不存在")
                  return
              }
              writeJSON(w, http.StatusOK, user)
          }
          
        • 重定向与文件服务

          // 重定向
          http.Redirect(w, r, "/new-path", http.StatusMovedPermanently)   // 301
          http.Redirect(w, r, "/login", http.StatusFound)                  // 302
          
          // 简单错误响应
          http.Error(w, "资源不存在", http.StatusNotFound)
          // 等价于:
          // w.Header().Set("Content-Type", "text/plain; charset=utf-8")
          // w.WriteHeader(http.StatusNotFound)
          // fmt.Fprintln(w, "资源不存在")
          
          // 静态文件服务
          http.Handle("/static/", http.StripPrefix(
              "/static/",
              http.FileServer(http.Dir("./public")),
          ))
          
          // ServeFile:返回单个文件
          http.ServeFile(w, r, "./public/index.html")
          
          // ServeContent:带 Range 支持的文件(视频流)
          http.ServeContent(w, r, "video.mp4", time.Now(), file)
          
      • ServerMux 路由与 Go 1.22 新路由
        • 标准库的 ServeMux 在 Go 1.22 之前功能很有限——不支持路径参数和方法限制。
        • Go 1.22 大幅增强了路由能力,现在可以直接用标准库写出接近框架的路由。
        • 传统 ServerMux

          mux := http.NewServeMux()
          
          // 精确匹配
          mux.HandleFunc("/api/users", usersHandler)
          
          // 前缀匹配(以 / 结尾)
          mux.HandleFunc("/static/", staticHandler)
          
          // ⚠️ 传统 ServeMux 的限制:
          // 1. 不支持路径参数(/users/:id)
          // 2. 不支持 HTTP 方法区分(GET vs POST)
          // 需要在 handler 内手动判断
          func usersHandler(w http.ResponseWriter, r *http.Request) {
              switch r.Method {
              case http.MethodGet:
                  listUsers(w, r)
              case http.MethodPost:
                  createUser(w, r)
              default:
                  http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
              }
          }
          
        • Go 1.22+ 增强路由

          // Go 1.22 的 ServeMux 支持:
          // 1. HTTP 方法前缀
          // 2. 路径参数 {name}
          // 3. 通配符 {path...}
          
          mux := http.NewServeMux()
          
          // 方法 + 路径
          mux.HandleFunc("GET /api/users", listUsers)
          mux.HandleFunc("POST /api/users", createUser)
          
          // 路径参数 {id}
          mux.HandleFunc("GET /api/users/{id}", getUser)
          mux.HandleFunc("PUT /api/users/{id}", updateUser)
          mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
          
          // 通配符(匹配任意后续路径)
          mux.HandleFunc("GET /static/{path...}", staticHandler)
          
          // 在 handler 中读取路径参数
          func getUser(w http.ResponseWriter, r *http.Request) {
              id := r.PathValue("id")  // ✨ Go 1.22 新增!
              fmt.Fprintf(w, "获取用户 ID: %s", id)
          }
          
          // 精确匹配(末尾加 {$} 防止前缀匹配)
          mux.HandleFunc("GET /{$}", homeHandler)  // 只匹配根路径
          
        • 路由组织:分文件

          // 把路由注册拆分到各模块
          // cmd/server/main.go
          func main() {
              mux := http.NewServeMux()
          
              // 注册各模块的路由
              registerUserRoutes(mux)
              registerAuthRoutes(mux)
              registerHealthRoutes(mux)
          
              srv := &http.Server{
                  Addr:         ":8080",
                  Handler:      mux,
                  ReadTimeout:  15 * time.Second,
                  WriteTimeout: 15 * time.Second,
                  IdleTimeout:  60 * time.Second,
              }
              log.Fatal(srv.ListenAndServe())
          }
          
          // internal/handler/user.go
          func registerUserRoutes(mux *http.ServeMux) {
              mux.HandleFunc("GET /api/users", listUsers)
              mux.HandleFunc("POST /api/users", createUser)
              mux.HandleFunc("GET /api/users/{id}", getUser)
              mux.HandleFunc("PUT /api/users/{id}", updateUser)
              mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
          }
          
        • 用 http.Server 而不是 http.ListenrAndServe
          • http.ListenAndServe 是快速启动用的,没办法设置超时。
          • 生产环境必须用 http.Server 并设置 ReadTimeout、WriteTimeout、IdleTimeout。
          • 没有这些超时,慢速客户端可以让你的服务器资源耗尽(slowloris 攻击)。
      • 中间件模式
        • 中间件是包裹 Handler 的函数,在请求前后添加通用逻辑(日志、认证、CORS、限流)。
        • 标准库的中间件模式非常简洁,用函数包装函数实现。
        • 中间件的定义与使用

          // 中间件的签名:接受 Handler,返回 Handler
          type Middleware func(http.Handler) http.Handler
          
          // 日志中间件
          func Logger(next http.Handler) http.Handler {
              return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                  start := time.Now()
                  next.ServeHTTP(w, r)  // 调用下一个 handler
                  log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
              })
          }
          
          // Recovery 中间件(防 panic 崩溃)
          func Recovery(next http.Handler) http.Handler {
              return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                  defer func() {
                      if err := recover(); err != nil {
                          log.Printf("panic 恢复: %v\n%s", err, debug.Stack())
                          http.Error(w, "内部服务错误", http.StatusInternalServerError)
                      }
                  }()
                  next.ServeHTTP(w, r)
              })
          }
          
          // 链式组合多个中间件
          func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
              // 反序应用,保证执行顺序正确
              for i := len(middlewares) - 1; i >= 0; i-- {
                  h = middlewares[i](h)
              }
              return h
          }
          
          // 使用
          mux := http.NewServeMux()
          mux.HandleFunc("GET /api/users", listUsers)
          
          // 应用中间件:Recovery → Logger → mux(从外到内)
          handler := Chain(mux, Recovery, Logger)
          
          http.ListenAndServe(":8080", handler)
          
        • 认证中间件(JWT 示意)

          type contextKey string
          const userIDKey contextKey = "userID"
          
          func Auth(next http.Handler) http.Handler {
              return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                  token := r.Header.Get("Authorization")
                  if token == "" {
                      http.Error(w, "未授权", http.StatusUnauthorized)
                      return
                  }
          
                  // 验证 token,提取 userID
                  userID, err := validateToken(strings.TrimPrefix(token, "Bearer "))
                  if err != nil {
                      http.Error(w, "无效令牌", http.StatusUnauthorized)
                      return
                  }
          
                  // 把 userID 注入 context(Day 10 学的技能)
                  ctx := context.WithValue(r.Context(), userIDKey, userID)
                  next.ServeHTTP(w, r.WithContext(ctx))
              })
          }
          
          // 在 handler 中读取
          func profileHandler(w http.ResponseWriter, r *http.Request) {
              userID, ok := r.Context().Value(userIDKey).(int)
              if !ok {
                  http.Error(w, "未认证", http.StatusUnauthorized)
                  return
              }
              fmt.Fprintf(w, "当前用户 ID: %d", userID)
          }
          
          // 只对特定路由加认证
          mux.Handle("GET /api/profile", Auth(http.HandlerFunc(profileHandler)))
          
        • CORS 中间件

          func CORS(allowOrigin string) Middleware {
              return func(next http.Handler) http.Handler {
                  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                      w.Header().Set("Access-Control-Allow-Origin", allowOrigin)
                      w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
                      w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
          
                      // OPTIONS 预检请求直接返回
                      if r.Method == http.MethodOptions {
                          w.WriteHeader(http.StatusNoContent)
                          return
                      }
          
                      next.ServeHTTP(w, r)
                  })
              }
          }
          
          // 使用
          handler := Chain(mux,
              Recovery,
              Logger,
              CORS("https://myapp.com"),
          )
          
      • 构建完整 REST API
        • 完整用户 CRUD REST API

          package main
          
          import (
              "encoding/json"
              "errors"
              "fmt"
              "log"
              "net/http"
              "strconv"
              "sync"
              "time"
          )
          
          // ───── 模型 ─────
          type User struct {
              ID        int       `json:"id"`
              Name      string    `json:"name"`
              Email     string    `json:"email"`
              CreatedAt time.Time `json:"created_at"`
          }
          
          // ───── 存储(内存)─────
          type Store struct {
              mu     sync.RWMutex
              users  map[int]*User
              nextID int
          }
          
          func NewStore() *Store {
              return &Store{users: make(map[int]*User), nextID: 1}
          }
          
          var ErrNotFound = errors.New("not found")
          
          func (s *Store) List() []*User {
              s.mu.RLock(); defer s.mu.RUnlock()
              list := make([]*User, 0, len(s.users))
              for _, u := range s.users { list = append(list, u) }
              return list
          }
          
          func (s *Store) Get(id int) (*User, error) {
              s.mu.RLock(); defer s.mu.RUnlock()
              u, ok := s.users[id]
              if !ok { return nil, ErrNotFound }
              return u, nil
          }
          
          func (s *Store) Create(u *User) {
              s.mu.Lock(); defer s.mu.Unlock()
              u.ID = s.nextID; u.CreatedAt = time.Now()
              s.users[u.ID] = u; s.nextID++
          }
          
          func (s *Store) Update(id int, name, email string) (*User, error) {
              s.mu.Lock(); defer s.mu.Unlock()
              u, ok := s.users[id]
              if !ok { return nil, ErrNotFound }
              if name != "" { u.Name = name }
              if email != "" { u.Email = email }
              return u, nil
          }
          
          func (s *Store) Delete(id int) error {
              s.mu.Lock(); defer s.mu.Unlock()
              if _, ok := s.users[id]; !ok { return ErrNotFound }
              delete(s.users, id); return nil
          }
          
          // ───── Handler ─────
          type UserHandler struct{ store *Store }
          
          func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
              // 路由分发(Go 1.22 前的方式)
              id, err := strconv.Atoi(r.PathValue("id"))
              if err != nil && r.PathValue("id") != "" {
                  writeError(w, 400, "无效的 ID")
                  return
              }
          
              switch {
              case r.Method == "GET" && id == 0:
                  h.list(w, r)
              case r.Method == "POST" && id == 0:
                  h.create(w, r)
              case r.Method == "GET" && id > 0:
                  h.get(w, r, id)
              case r.Method == "PUT" && id > 0:
                  h.update(w, r, id)
              case r.Method == "DELETE" && id > 0:
                  h.delete(w, r, id)
              default:
                  http.Error(w, "Not Found", 404)
              }
          }
          
          func (h *UserHandler) list(w http.ResponseWriter, r *http.Request) {
              writeJSON(w, 200, h.store.List())
          }
          
          func (h *UserHandler) create(w http.ResponseWriter, r *http.Request) {
              var u User
              if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
                  writeError(w, 400, "无效的请求体")
                  return
              }
              if u.Name == "" || u.Email == "" {
                  writeError(w, 400, "name 和 email 不能为空")
                  return
              }
              h.store.Create(&u)
              writeJSON(w, 201, u)
          }
          
          func (h *UserHandler) get(w http.ResponseWriter, r *http.Request, id int) {
              u, err := h.store.Get(id)
              if errors.Is(err, ErrNotFound) {
                  writeError(w, 404, "用户不存在")
                  return
              }
              writeJSON(w, 200, u)
          }
          
          func (h *UserHandler) update(w http.ResponseWriter, r *http.Request, id int) {
              var req struct {
                  Name  string `json:"name"`
                  Email string `json:"email"`
              }
              json.NewDecoder(r.Body).Decode(&req)
              u, err := h.store.Update(id, req.Name, req.Email)
              if errors.Is(err, ErrNotFound) {
                  writeError(w, 404, "用户不存在")
                  return
              }
              writeJSON(w, 200, u)
          }
          
          func (h *UserHandler) delete(w http.ResponseWriter, r *http.Request, id int) {
              if err := h.store.Delete(id); errors.Is(err, ErrNotFound) {
                  writeError(w, 404, "用户不存在")
                  return
              }
              w.WriteHeader(204)
          }
          
          // ───── 工具函数 ─────
          func writeJSON(w http.ResponseWriter, status int, data any) {
              w.Header().Set("Content-Type", "application/json")
              w.WriteHeader(status)
              json.NewEncoder(w).Encode(data)
          }
          
          func writeError(w http.ResponseWriter, status int, msg string) {
              writeJSON(w, status, map[string]string{"error": msg})
          }
          
          // ───── main ─────
          func main() {
              store := NewStore()
              h := &UserHandler{store: store}
          
              mux := http.NewServeMux()
          
              // Go 1.22+ 路由
              mux.HandleFunc("GET /api/users", h.list)
              mux.HandleFunc("POST /api/users", h.create)
              mux.HandleFunc("GET /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
                  id, _ := strconv.Atoi(r.PathValue("id"))
                  h.get(w, r, id)
              })
              mux.HandleFunc("PUT /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
                  id, _ := strconv.Atoi(r.PathValue("id"))
                  h.update(w, r, id)
              })
              mux.HandleFunc("DELETE /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
                  id, _ := strconv.Atoi(r.PathValue("id"))
                  h.delete(w, r, id)
              })
          
              handler := Chain(mux, Recovery, Logger)
          
              srv := &http.Server{
                  Addr:         ":8080",
                  Handler:      handler,
                  ReadTimeout:  15 * time.Second,
                  WriteTimeout: 15 * time.Second,
              }
          
              fmt.Println("🚀 服务器启动: http://localhost:8080")
              log.Fatal(srv.ListenAndServe())
          }
          
        • 用 curl 测试

          # 创建用户
          curl -X POST http://localhost:8080/api/users \
          -H "Content-Type: application/json" \
          -d '{"name":"Alice","email":"alice@example.com"}'
          # → {"id":1,"name":"Alice","email":"alice@example.com","created_at":"..."}
          
          # 获取所有用户
          curl http://localhost:8080/api/users
          
          # 获取单个用户
          curl http://localhost:8080/api/users/1
          
          # 更新用户
          curl -X PUT http://localhost:8080/api/users/1 \
          -H "Content-Type: application/json" \
          -d '{"name":"Alicia"}'
          
          # 删除用户
          curl -X DELETE http://localhost:8080/api/users/1
          
      • 优雅关闭
        • 生产环境的服务器不能强制关闭——要等正在处理的请求完成后再退出。
        • Go 的 http.Server.Shutdown 方法实现了这个功能,配合系统信号使用。

          package main
          
          import (
              "context"
              "log"
              "net/http"
              "os"
              "os/signal"
              "syscall"
              "time"
          )
          
          func main() {
              mux := http.NewServeMux()
              mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
                  // 模拟慢请求
                  time.Sleep(2 * time.Second)
                  fmt.Fprintln(w, "OK")
              })
          
              srv := &http.Server{
                  Addr:    ":8080",
                  Handler: mux,
              }
          
              // 在独立 goroutine 中启动服务器
              go func() {
                  log.Println("🚀 服务器启动: :8080")
                  if err := srv.ListenAndServe(); err != http.ErrServerClosed {
                      log.Fatalf("服务器错误: %v", err)
                  }
              }()
          
              // 监听系统信号(Ctrl+C 或 kill)
              quit := make(chan os.Signal, 1)
              signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
              <-quit  // 阻塞,等待信号
          
              log.Println("🛑 收到关闭信号,正在优雅关闭...")
          
              // 给正在处理的请求 30 秒完成
              ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
              defer cancel()
          
              if err := srv.Shutdown(ctx); err != nil {
                  log.Printf("强制关闭: %v", err)
              } else {
                  log.Println("✅ 服务器已优雅关闭")
              }
          }
          
          // 测试优雅关闭:
          // 1. 启动服务器
          // 2. 发送慢请求:curl http://localhost:8080/
          // 3. 在请求处理中按 Ctrl+C
          // 4. 观察:服务器等待请求完成后才退出
          
    • Gin 框架入门
      • 为什么用 Gin
        • Gin 是在标准库之上的增强层,保留了所有 handler 逻辑,但是减少了重复代码
        • 提供了更强的路由、更方便的参数绑定、更完善的错误处理
        • 标准库 vs Gin 对比
          • 标准库写法

            // ─── 标准库写法 ───
            func getUser(w http.ResponseWriter, r *http.Request) {
                idStr := r.PathValue("id")
                id, err := strconv.Atoi(idStr)
                if err != nil {
                    w.Header().Set("Content-Type", "application/json")
                    w.WriteHeader(400)
                    json.NewEncoder(w).Encode(map[string]string{"error": "无效的 ID"})
                    return
                }
                user, err := findUser(id)
                if err != nil {
                    w.Header().Set("Content-Type", "application/json")
                    w.WriteHeader(404)
                    json.NewEncoder(w).Encode(map[string]string{"error": "用户不存在"})
                    return
                }
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(200)
                json.NewEncoder(w).Encode(user)
            }
            
            // ─── Gin 写法 ───
            func getUser(c *gin.Context) {
                id, err := strconv.Atoi(c.Param("id"))
                if err != nil {
                    c.JSON(400, gin.H{"error": "无效的 ID"})
                    return
                }
                user, err := findUser(id)
                if err != nil {
                    c.JSON(404, gin.H{"error": "用户不存在"})
                    return
                }
                c.JSON(200, user)
            }
            
          • 安装与快速上手

            go get github.com/gin-gonic/gin
            
            package main
            
            import "github.com/gin-gonic/gin"
            
            func main() {
                r := gin.Default()  // 默认包含 Logger + Recovery 中间件
            
                r.GET("/ping", func(c *gin.Context) {
                    c.JSON(200, gin.H{"message": "pong"})
                })
            
                r.Run(":8080")  // 等价于 http.ListenAndServe(":8080", r)
            }
            
            // gin.H 是 map[string]any 的别名,专为 JSON 响应设计
            // curl http://localhost:8080/ping
            // → {"message":"pong"}
            
          • Gin 的性能
            • Gin 实用 httprouter 为底层路由,比标准库的 ServeMux 更快
            • 官方 benchmark 显示比标准库快 40 倍以上
      • 路由与路由分组
        • Gin 的路由系统非常强大,支持路径参数、通配符、路由分组,还能给分组统一加中间件。
        • 基本路由

          r := gin.Default()
          
          // HTTP 方法
          r.GET("/users", listUsers)
          r.POST("/users", createUser)
          r.PUT("/users/:id", updateUser)
          r.DELETE("/users/:id", deleteUser)
          r.PATCH("/users/:id/status", updateStatus)
          
          // 路径参数
          r.GET("/users/:id", func(c *gin.Context) {
              id := c.Param("id")          // 精确参数:/users/42 → "42"
              fmt.Println(id)
          })
          
          r.GET("/files/*filepath", func(c *gin.Context) {
              path := c.Param("filepath")  // 通配符:/files/a/b/c → "/a/b/c"
              fmt.Println(path)
          })
          
          // Query 参数
          r.GET("/search", func(c *gin.Context) {
              keyword := c.Query("q")              // ?q=golang
              page := c.DefaultQuery("page", "1") // 带默认值
              fmt.Println(keyword, page)
          })
          
          // 任意方法
          r.Any("/webhook", webhookHandler)
          
          // 静态文件
          r.Static("/static", "./public")
          r.StaticFile("/favicon.ico", "./favicon.ico")
          
        • 路由分组 Group

          r := gin.Default()
          
          // 路由分组:共享前缀
          v1 := r.Group("/api/v1")
          {
              v1.GET("/users", listUsers)
              v1.POST("/users", createUser)
              v1.GET("/users/:id", getUser)
          }
          
          v2 := r.Group("/api/v2")
          {
              v2.GET("/users", listUsersV2)  // 新版接口
          }
          
          // 嵌套分组 + 中间件
          admin := r.Group("/admin", AuthMiddleware(), AdminOnlyMiddleware())
          {
              admin.GET("/dashboard", dashboard)
              admin.GET("/users", adminListUsers)
          
              // 嵌套分组
              settings := admin.Group("/settings")
              {
                  settings.GET("/", getSettings)
                  settings.PUT("/", updateSettings)
              }
          }
          
          // 只对部分路由加中间件
          auth := r.Group("/api")
          auth.Use(AuthMiddleware())  // 这组路由都需要认证
          {
              auth.GET("/profile", profile)
              auth.PUT("/profile", updateProfile)
          }
          
        • NoRoute 和 NoMethod

          // 自定义 404
          r.NoRoute(func(c *gin.Context) {
              c.JSON(404, gin.H{"error": "接口不存在"})
          })
          
          // 自定义 405(方法不允许)
          r.NoMethod(func(c *gin.Context) {
              c.JSON(405, gin.H{"error": "方法不允许"})
          })
          
      • gin.Context: 一切的核心
        • *gin.Context 封装了请求和响应,是 Gin handler 的唯一参数。掌握它的所有方法,你就掌握了 Gin 的核心。
        • 读取请求参数

          func handler(c *gin.Context) {
              // ─── 路径参数 ───
              id := c.Param("id")                     // /users/:id
          
              // ─── Query 参数 ───
              q := c.Query("keyword")                  // ?keyword=go
              page := c.DefaultQuery("page", "1")      // 带默认值
              all := c.QueryMap("tags")               // ?tags[a]=1&tags[b]=2
          
              // ─── Form 参数 ───
              name := c.PostForm("name")
              nick := c.DefaultPostForm("nick", "anonymous")
          
              // ─── Header ───
              token := c.GetHeader("Authorization")
              ua := c.GetHeader("User-Agent")
          
              // ─── 请求信息 ───
              ip := c.ClientIP()          // 客户端 IP(处理了代理)
              method := c.Request.Method  // 原始 *http.Request 仍可访问
          
              _ = id; _ = q; _ = page; _ = all
              _ = name; _ = nick; _ = token; _ = ua; _ = ip; _ = method
          }
          
        • Context 的键值存储(在中间件传数据)

          // 中间件设置值
          func AuthMiddleware() gin.HandlerFunc {
              return func(c *gin.Context) {
                  token := c.GetHeader("Authorization")
                  userID, err := validateToken(token)
                  if err != nil {
                      c.AbortWithStatusJSON(401, gin.H{"error": "未授权"})
                      return  // Abort 后必须 return!
                  }
          
                  c.Set("userID", userID)   // 存入 context
                  c.Set("isAdmin", false)
          
                  c.Next()  // 继续执行后续 handler/中间件
              }
          }
          
          // Handler 取值
          func profileHandler(c *gin.Context) {
              userID, exists := c.Get("userID")
              if !exists {
                  c.JSON(401, gin.H{"error": "未认证"})
                  return
              }
          
              // 类型断言
              id := userID.(int)
          
              // 或者用类型安全的方法
              id2, _ := c.GetInt("userID")
              isAdmin, _ := c.GetBool("isAdmin")
          
              c.JSON(200, gin.H{"userID": id, "id2": id2, "isAdmin": isAdmin})
          }
          
        • 响应方法

          func handler(c *gin.Context) {
              // JSON 响应(最常用)
              c.JSON(200, gin.H{"message": "ok"})
              c.JSON(200, user)  // 结构体自动序列化
          
              // 其他格式
              c.String(200, "Hello, %s!", "World")
              c.HTML(200, "index.html", gin.H{"title": "首页"})
              c.XML(200, user)
              c.YAML(200, user)
          
              // 文件响应
              c.File("./public/file.pdf")
              c.FileAttachment("./public/file.pdf", "download.pdf")
              c.Data(200, "image/png", imageBytes)
          
              // 重定向
              c.Redirect(302, "https://example.com")
          
              // 状态码快捷方式
              c.Status(204)  // 只有状态码,无 body
          
              // 中止后续中间件(不中止当前 handler)
              c.Abort()
              c.AbortWithStatus(403)
              c.AbortWithStatusJSON(403, gin.H{"error": "禁止访问"})
          }
          
      • 参数绑定与校验
        • Gin 集成了 validator 库,可以在解析请求参数的同时做校验,大大减少了手动 if err 的代码量。
        • ShouldBind:绑定 + 校验

          // 定义请求结构体,用 binding tag 指定校验规则
          type CreateUserRequest struct {
              Name     string `json:"name"     binding:"required,min=2,max=50"`
              Email    string `json:"email"    binding:"required,email"`
              Age      int    `json:"age"      binding:"gte=0,lte=150"`
              Password string `json:"password" binding:"required,min=8"`
          }
          
          func createUser(c *gin.Context) {
              var req CreateUserRequest
          
              // ShouldBindJSON:解析 JSON body + 校验
              if err := c.ShouldBindJSON(&req); err != nil {
                  c.JSON(400, gin.H{"error": err.Error()})
                  return
              }
          
              // req 现在已经被填充且通过了校验
              user, err := userService.Create(c.Request.Context(), req.Name, req.Email)
              if err != nil {
                  c.JSON(500, gin.H{"error": err.Error()})
                  return
              }
              c.JSON(201, user)
          }
          
        • 各种 Bind 方法

          // ShouldBindJSON:绑定 JSON body
          c.ShouldBindJSON(&req)
          
          // ShouldBindQuery:绑定 query 参数
          type SearchRequest struct {
              Keyword string `form:"q"    binding:"required"`
              Page    int    `form:"page" binding:"gte=1"`
              Size    int    `form:"size" binding:"gte=1,lte=100"`
          }
          c.ShouldBindQuery(&req)
          
          // ShouldBindUri:绑定路径参数
          type UriRequest struct {
              ID int `uri:"id" binding:"required,gt=0"`
          }
          c.ShouldBindUri(&req)
          
          // ShouldBind:自动根据 Content-Type 选择
          c.ShouldBind(&req)
          
          // Bind 和 ShouldBind 的区别:
          // ShouldBind:失败只返回 error,状态码由你决定(推荐)
          // Bind:失败自动设置 400,并写入响应(不灵活)
          
        • 常用校验规则

          type Example struct {
              // 必填
              Name     string `binding:"required"`
          
              // 长度限制
              Bio      string `binding:"max=500"`
              Code     string `binding:"len=6"`
              Tag      string `binding:"min=1,max=20"`
          
              // 数值范围
              Age      int    `binding:"gte=0,lte=150"`
              Score    float64 `binding:"gt=0"`
          
              // 格式校验
              Email    string `binding:"required,email"`
              URL      string `binding:"url"`
              Phone    string `binding:"e164"`  // 国际电话格式
          
              // 枚举值
              Status   string `binding:"oneof=active inactive deleted"`
              Role     string `binding:"oneof=admin editor viewer"`
          
              // 条件校验
              Password string `binding:"required_with=Email"`  // Email 填了就必填
          
              // 自定义:omitempty + 条件
              Nick     string `binding:"omitempty,min=2"`  // 有值才校验
          }
          
          // 自定义校验器
          import "github.com/go-playground/validator/v10"
          
          func validateChinesePhone(fl validator.FieldLevel) bool {
              phone := fl.Field().String()
              matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
              return matched
          }
          
          // 注册到 Gin
          if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
              v.RegisterValidation("cnphone", validateChinesePhone)
          }
          
          type Req struct {
              Phone string `binding:"required,cnphone"`
          }
          
        • 统一处理校验错误
          • validator 返回的错误是 validator.ValidationErrors 类型
          • 可以提取字段名和校验规则,返回更友好的错误信息,而不是直接把 err.Error() 给用户
          • 生产代码应该封装一个 parseValidationError 函数。
      • Gin 中间件
        • Gin 的中间件是 gin.HandlerFunc(等价于 func(*gin.Context)),通过 c.Next() 和 c.Abort() 控制执行流。
        • 中间件的执行模型

          // 中间件的结构:前置逻辑 → Next() → 后置逻辑
          func LoggerMiddleware() gin.HandlerFunc {
              return func(c *gin.Context) {
                  start := time.Now()
                  path := c.Request.URL.Path
          
                  // ─── 前置:请求进来时执行 ───
                  fmt.Printf("→ %s %s\n", c.Request.Method, path)
          
                  c.Next()  // 执行下一个 handler/中间件
          
                  // ─── 后置:响应返回后执行 ───
                  duration := time.Since(start)
                  status := c.Writer.Status()
                  fmt.Printf("← %s %s %d %v\n", c.Request.Method, path, status, duration)
              }
          }
          
          // 全局中间件
          r := gin.New()  // 不用 gin.Default()(它自带 Logger 和 Recovery)
          r.Use(LoggerMiddleware())
          r.Use(RecoveryMiddleware())
          r.Use(CORSMiddleware())
          
        • 认证/限流中间件(完整实现)

          func JWTAuth() gin.HandlerFunc {
              return func(c *gin.Context) {
                  authHeader := c.GetHeader("Authorization")
                  if authHeader == "" {
                      c.AbortWithStatusJSON(401, gin.H{"error": "缺少认证令牌"})
                      return
                  }
          
                  // "Bearer <token>"
                  parts := strings.SplitN(authHeader, " ", 2)
                  if len(parts) != 2 || parts[0] != "Bearer" {
                      c.AbortWithStatusJSON(401, gin.H{"error": "令牌格式错误"})
                      return
                  }
          
                  claims, err := parseJWT(parts[1])
                  if err != nil {
                      c.AbortWithStatusJSON(401, gin.H{"error": "无效的令牌"})
                      return
                  }
          
                  // 把用户信息注入 context
                  c.Set("userID", claims.UserID)
                  c.Set("userEmail", claims.Email)
                  c.Next()
              }
          }
          
          // 限流中间件
          func RateLimit(rpm int) gin.HandlerFunc {
              // 每个 IP 限制每分钟 rpm 次请求
              limiter := rate.NewLimiter(rate.Every(time.Minute/time.Duration(rpm)), rpm)
          
              return func(c *gin.Context) {
                  ip := c.ClientIP()
                  if !getLimiter(ip, limiter).Allow() {
                      c.AbortWithStatusJSON(429, gin.H{
                          "error": "请求过于频繁,请稍后再试",
                      })
                      return
                  }
                  c.Next()
              }
          }
          
        • Abort vs Return

          // ⚠️ 重要:Abort 不会停止当前函数执行!
          // 必须同时 return,否则后续代码还会执行
          
          func badMiddleware(c *gin.Context) {
              c.AbortWithStatusJSON(401, gin.H{"error": "未授权"})
              // ❌ 没有 return,后续代码还会执行!
              doSomethingDangerous()  // 会被执行
          }
          
          func goodMiddleware(c *gin.Context) {
              c.AbortWithStatusJSON(401, gin.H{"error": "未授权"})
              return  // ✅ 明确 return,停止执行
          }
          
          // Abort 的作用是:阻止后续的中间件和 handler 执行
          // 但当前函数中 Abort 之后的代码仍然会执行
          
      • 统一错误处理
        • 在每个 handler 里写重复的错误处理代码很烦。
        • Gin 提供了统一错误处理的机制——handler 只负责「报告」错误,中间件统一「处理」错误。

          // ─── 定义 AppError ───
          type AppError struct {
              Status  int    `json:"-"`
              Code    string `json:"code"`
              Message string `json:"message"`
              Err     error  `json:"-"`
          }
          
          func (e *AppError) Error() string { return e.Message }
          
          // 快捷构造
          func NotFound(msg string) *AppError {
              return &AppError{Status: 404, Code: "NOT_FOUND", Message: msg}
          }
          func BadRequest(msg string) *AppError {
              return &AppError{Status: 400, Code: "BAD_REQUEST", Message: msg}
          }
          func Internal(err error) *AppError {
              return &AppError{Status: 500, Code: "INTERNAL_ERROR", Message: "内部服务错误", Err: err}
          }
          
          // ─── 错误处理中间件 ───
          func ErrorHandler() gin.HandlerFunc {
              return func(c *gin.Context) {
                  c.Next()
          
                  // 收集所有 handler 添加的错误
                  if len(c.Errors) == 0 {
                      return
                  }
          
                  err := c.Errors.Last().Err
                  var appErr *AppError
                  if errors.As(err, &appErr) {
                      c.JSON(appErr.Status, appErr)
                  } else {
                      // 记录未预期的错误
                      log.Printf("未处理的错误: %v", err)
                      c.JSON(500, gin.H{"code": "INTERNAL_ERROR", "message": "内部服务错误"})
                  }
              }
          }
          
          // ─── Handler 里只需要 c.Error(err) ───
          func getUser(c *gin.Context) {
              id := c.Param("id")
              user, err := userService.GetUser(c.Request.Context(), id)
              if err != nil {
                  // 只需添加错误,不需要写响应
                  _ = c.Error(NotFound("用户不存在"))
                  return
              }
              c.JSON(200, user)
          }
          
          // ─── 注册 ───
          r := gin.New()
          r.Use(ErrorHandler())  // 必须注册(会在所有 handler 之后执行)
          r.Use(LoggerMiddleware())
          
      • Gin 项目完整结构
        • 项目结构

          myapp/
          ├── cmd/server/main.go          # 启动入口
          ├── internal/
          │   ├── handler/                # HTTP 层(Gin handlers)
          │   │   ├── router.go           # 路由注册
          │   │   ├── user.go             # 用户相关 handler
          │   │   ├── auth.go             # 认证 handler
          │   │   └── middleware/
          │   │       ├── auth.go         # JWT 认证中间件
          │   │       ├── logger.go       # 日志中间件
          │   │       ├── recovery.go     # panic 恢复
          │   │       └── cors.go         # CORS 中间件
          │   ├── service/                # 业务逻辑层
          │   │   ├── user.go
          │   │   └── auth.go
          │   ├── repository/             # 数据访问层
          │   │   └── user.go
          │   ├── model/                  # 数据模型
          │   │   └── user.go
          │   └── config/                 # 配置
          │       └── config.go
          └── go.mod
          
        • 经典文件

          // internal/handler/router.go
          package handler
          
          import (
              "github.com/gin-gonic/gin"
              "github.com/yourname/myapp/internal/handler/middleware"
              "github.com/yourname/myapp/internal/service"
          )
          
          func SetupRouter(
              userSvc *service.UserService,
              authSvc *service.AuthService,
          ) *gin.Engine {
              // 生产环境关闭调试模式
              gin.SetMode(gin.ReleaseMode)
          
              r := gin.New()  // 不用 Default,手动注册中间件
          
              // 全局中间件
              r.Use(middleware.Recovery())
              r.Use(middleware.Logger())
              r.Use(middleware.CORS())
              r.Use(middleware.RequestID())
          
              // 健康检查(不需要认证)
              r.GET("/health", func(c *gin.Context) {
                  c.JSON(200, gin.H{"status": "ok"})
              })
          
              // API 路由
              api := r.Group("/api/v1")
              {
                  // 认证路由(不需要 JWT)
                  auth := NewAuthHandler(authSvc)
                  api.POST("/register", auth.Register)
                  api.POST("/login", auth.Login)
                  api.POST("/refresh", auth.RefreshToken)
          
                  // 需要认证的路由
                  authed := api.Group("")
                  authed.Use(middleware.JWTAuth())
                  {
                      users := NewUserHandler(userSvc)
                      authed.GET("/users", users.List)
                      authed.GET("/users/:id", users.Get)
                      authed.PUT("/users/:id", users.Update)
                      authed.DELETE("/users/:id", users.Delete)
          
                      authed.GET("/profile", users.Profile)
                  }
              }
          
              return r
          }
          
          // cmd/server/main.go
          func main() {
              cfg, err := config.Load()
              if err != nil { log.Fatal(err) }
          
              // 依赖注入
              repo := repository.NewUserRepo(db)
              userSvc := service.NewUserService(repo)
              authSvc := service.NewAuthService(repo, cfg.JWTSecret)
          
              r := handler.SetupRouter(userSvc, authSvc)
          
              srv := &http.Server{
                  Addr:         ":" + cfg.Port,
                  Handler:      r,
                  ReadTimeout:  15 * time.Second,
                  WriteTimeout: 15 * time.Second,
              }
          
              // 优雅关闭(Day 15 学的)
              go gracefulShutdown(srv)
              log.Fatal(srv.ListenAndServe())
          }
          
    • 数据库操作 GORM
      • GORM 简介与安装
        • GORM 是 Go 生态最流行的 ORM 库。它让你用 Go 结构体操作数据库,不需要手写 SQL(当然也支持原生 SQL)。
        • 支持 MySQL、PostgreSQL、SQLite、SQL Server 等主流数据库。
        • 安装

          # GORM 核心库
          go get gorm.io/gorm
          
          # 数据库驱动(选择你用的数据库)
          go get gorm.io/driver/postgres   # PostgreSQL(推荐)
          go get gorm.io/driver/mysql      # MySQL
          go get gorm.io/driver/sqlite     # SQLite(开发/测试用)
          
        • 连接数据库

          package db
          
          import (
              "gorm.io/driver/postgres"
              "gorm.io/driver/sqlite"
              "gorm.io/gorm"
              "gorm.io/gorm/logger"
          )
          
          // PostgreSQL(生产推荐)
          func NewPostgresDB(dsn string) (*gorm.DB, error) {
              db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
                  Logger: logger.Default.LogMode(logger.Info), // 打印 SQL
              })
              if err != nil {
                  return nil, err
              }
          
              // 连接池配置
              sqlDB, _ := db.DB()
              sqlDB.SetMaxIdleConns(10)          // 最大空闲连接
              sqlDB.SetMaxOpenConns(100)         // 最大打开连接
              sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
          
              return db, nil
          }
          
          // DSN 格式
          // PostgreSQL: "host=localhost user=postgres password=secret dbname=myapp port=5432 sslmode=disable"
          // MySQL:      "user:password@tcp(127.0.0.1:3306)/myapp?charset=utf8mb4&parseTime=True&loc=Local"
          
          // SQLite(快速测试/开发用)
          func NewSQLiteDB(path string) (*gorm.DB, error) {
              return gorm.Open(sqlite.Open(path), &gorm.Config{})
          }
          
          // 内存 SQLite(单元测试专用)
          func NewTestDB() (*gorm.DB, error) {
              return gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
          }
          
        • GORM 和 database/sql 的关系
          • GORM 底层仍然使用 database/sql,只是在它之上封装了 ORM 的能力。
          • 通过 db.DB() 可以拿到底层的 *sql.DB,设置连接池、执行原生 SQL。两者不是替代关系,而是协作关系。
      • 模型定义
        • GORM 模型是普通的 Go 结构体,通过标签(tag)控制数据库映射行为。
        • 嵌入 gorm.Model 可以自动获得 ID、CreatedAt、UpdatedAt、DeletedAt 四个字段。
        • 基础模型定义

          package model
          
          import (
              "time"
              "gorm.io/gorm"
          )
          
          // 嵌入 gorm.Model:自动拥有 id, created_at, updated_at, deleted_at
          type User struct {
              gorm.Model                       // 嵌入基础字段
              Name      string `gorm:"size:100;not null"`
              Email     string `gorm:"uniqueIndex;size:255;not null"`
              Password  string `gorm:"size:255;not null"`
              Age       int    `gorm:"default:0"`
              Active    bool   `gorm:"default:true"`
          }
          
          // gorm.Model 展开后等价于:
          // ID        uint           `gorm:"primarykey"`
          // CreatedAt time.Time
          // UpdatedAt time.Time
          // DeletedAt gorm.DeletedAt `gorm:"index"`  ← 软删除
          
          // 不想用 gorm.Model?自定义主键
          type Article struct {
              ID        int       `gorm:"primaryKey;autoIncrement"`
              Title     string    `gorm:"size:200;not null;index"`
              Content   string    `gorm:"type:text"`
              AuthorID  uint      `gorm:"not null"`
              Published bool      `gorm:"default:false"`
              CreatedAt time.Time
              UpdatedAt time.Time
          }
          
        • 常用 GORM TAG

          type Example struct {
              // 主键
              ID   uint   `gorm:"primaryKey"`
              UUID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()"`
          
              // 列属性
              Name     string  `gorm:"column:full_name;size:100;not null"`
              Price    float64 `gorm:"type:decimal(10,2);default:0"`
              Bio      string  `gorm:"type:text"`
          
              // 索引
              Email    string `gorm:"uniqueIndex"`           // 唯一索引
              City     string `gorm:"index"`                 // 普通索引
              Code     string `gorm:"index:idx_code_type"`   // 联合索引(和 Type 同名)
              Type     string `gorm:"index:idx_code_type"`   // 联合索引
          
              // 忽略字段(不映射到数据库)
              Computed string `gorm:"-"`
          
              // 创建时只写、更新时只写
              CreatedBy string `gorm:"->:false;<-:create"`  // 只在创建时写
          
              // 嵌入结构体
              Address `gorm:"embedded;embeddedPrefix:addr_"` // 前缀 addr_
          }
          
          type Address struct {
              Street string
              City   string
          }
          // 数据库列:addr_street, addr_city
          
        • AutoMigrate:自动建表

          // 自动创建/更新表结构(只加列,不删列,生产用专业迁移工具)
          err := db.AutoMigrate(
              &model.User{},
              &model.Article{},
              &model.Comment{},
          )
          if err != nil {
              log.Fatal("数据库迁移失败:", err)
          }
          
          // AutoMigrate 的行为:
          // ✅ 表不存在 → 创建表
          // ✅ 字段不存在 → 添加字段
          // ✅ 索引不存在 → 创建索引
          // ❌ 字段类型变了 → 不会修改(需要手动迁移)
          // ❌ 删掉字段 → 不会删除数据库列(安全策略)
          
      • CRUD 基础操作
        • Create (创建)

          // 创建单条记录
          user := model.User{Name: "Alice", Email: "alice@example.com"}
          result := db.Create(&user)  // ✨ 创建后 user.ID 会被填充
          if result.Error != nil {
              return result.Error
          }
          fmt.Println("新用户 ID:", user.ID)
          
          // 批量创建
          users := []model.User{
              {Name: "Bob", Email: "bob@example.com"},
              {Name: "Charlie", Email: "charlie@example.com"},
          }
          db.Create(&users)  // 单次 SQL 插入多行,比循环创建高效
          
          // 只创建指定字段
          db.Select("Name", "Email").Create(&user)
          
          // 忽略某些字段
          db.Omit("Age", "Active").Create(&user)
          
        • Read (查询)

          // 按主键查询
          var user model.User
          db.First(&user, 1)           // SELECT * FROM users WHERE id = 1 LIMIT 1
          db.First(&user, "id = ?", 1) // 等价写法
          
          // 查询单条(不按主键排序)
          db.Take(&user, 1)    // SELECT * FROM users WHERE id = 1 LIMIT 1
          db.Last(&user)       // 最后一条(按主键 DESC)
          
          // 条件查询
          db.Where("name = ?", "Alice").First(&user)
          db.Where("age > ? AND active = ?", 18, true).Find(&users)
          
          // IN 查询
          db.Where("id IN ?", []int{1, 2, 3}).Find(&users)
          
          // LIKE 查询
          db.Where("name LIKE ?", "%Alice%").Find(&users)
          
          // 查询多条
          var users []model.User
          result := db.Find(&users)  // SELECT * FROM users(自动过滤 deleted_at IS NULL)
          fmt.Println("查到", result.RowsAffected, "条")
          
          // 查询所有(含软删除的)
          db.Unscoped().Find(&users)
          
          // 只查指定字段
          db.Select("id", "name", "email").Find(&users)
          
          // 检查记录是否存在
          var count int64
          db.Model(&model.User{}).Where("email = ?", email).Count(&count)
          exists := count > 0
          
        • Update (更新)

          // 更新单个字段
          db.Model(&user).Update("name", "Alicia")
          // UPDATE users SET name='Alicia', updated_at=NOW() WHERE id=1
          
          // 更新多个字段(用 map)
          db.Model(&user).Updates(map[string]any{
              "name":   "Alicia",
              "active": false,
          })
          
          // 更新多个字段(用结构体,⚠️ 零值字段会被忽略!)
          db.Model(&user).Updates(model.User{Name: "Alicia", Age: 0})
          // ⚠️ Age=0 是零值,不会被更新!
          
          // 解决零值问题:用 Select 明确指定要更新的字段
          db.Model(&user).Select("Name", "Age").Updates(model.User{Name: "Alicia", Age: 0})
          // Age=0 会被更新
          
          // 带条件的更新
          db.Model(&model.User{}).Where("active = ?", false).Update("deleted", true)
          
          // 不触发 Hook 和 updated_at 的原生更新
          db.Model(&user).UpdateColumn("login_count", gorm.Expr("login_count + 1"))
          
        • Delete (删除)

          // 软删除(如果模型有 DeletedAt 字段)
          // 只设置 deleted_at,不真正删除
          db.Delete(&user, 1)
          // UPDATE users SET deleted_at=NOW() WHERE id=1
          
          // 查询时自动过滤软删除的记录
          db.Find(&users)  // WHERE deleted_at IS NULL
          
          // 硬删除(真正删除)
          db.Unscoped().Delete(&user, 1)
          // DELETE FROM users WHERE id=1
          
          // 批量删除(必须有条件,防止误删全表)
          db.Where("active = ?", false).Delete(&model.User{})
          // 如果不加条件会报错(安全机制)
          
        • 结构体 Updates 的零值陷阱
          • 用结构体更新时,GORM 会忽略零值字段(0、""、false、nil),只更新非零值。
          • 这个设计防止了意外覆盖,但也导致你没法把一个字段更新为零值。
          • 解决方案:用 map[string]any 替代结构体,或者用 Select 明确列出要更新的字段。这是 GORM 面试高频考点!
      • 查询进阶
        • 链式调用构建复杂查询

          // GORM 的链式 API:每个方法返回 *gorm.DB,可以任意组合
          var users []model.User
          
          db.
              Where("age > ?", 18).
              Where("active = ?", true).
              Order("created_at DESC").
              Limit(10).
              Offset(20).  // 跳过 20 条(第 3 页,每页 10 条)
              Find(&users)
          
          // 分页查询(通用封装)
          type Pagination struct {
              Page int
              Size int
          }
          
          func Paginate(p Pagination) func(db *gorm.DB) *gorm.DB {
              return func(db *gorm.DB) *gorm.DB {
                  offset := (p.Page - 1) * p.Size
                  return db.Offset(offset).Limit(p.Size)
              }
          }
          
          // 使用 Scopes 复用查询逻辑
          db.Scopes(Paginate(Pagination{Page: 2, Size: 10})).
              Where("active = ?", true).
              Order("name ASC").
              Find(&users)
          
        • 聚合与分组

          // Count
          var count int64
          db.Model(&model.User{}).Where("active = ?", true).Count(&count)
          
          // Sum, Avg, Max, Min
          var totalAge float64
          db.Model(&model.User{}).Select("COALESCE(SUM(age), 0)").Scan(&totalAge)
          
          // Group By + Having
          type Result struct {
              City  string
              Count int64
          }
          var results []Result
          db.Model(&model.User{}).
              Select("city, COUNT(*) as count").
              Group("city").
              Having("COUNT(*) > ?", 10).
              Scan(&results)
          
          // Distinct
          db.Distinct("name", "email").Find(&users)
          
        • 原生 SQL

          // Raw:执行原生 SQL 并扫描结果
          var users []model.User
          db.Raw("SELECT * FROM users WHERE age > ? AND active = ?", 18, true).Scan(&users)
          
          // Exec:执行不返回行的 SQL
          db.Exec("UPDATE users SET login_count = login_count + 1 WHERE id = ?", userID)
          
          // 扫描到自定义结构体(不一定是模型)
          type UserSummary struct {
              Name  string
              Email string
              Posts int
          }
          var summaries []UserSummary
          db.Raw(`
              SELECT u.name, u.email, COUNT(p.id) as posts
              FROM users u
              LEFT JOIN posts p ON p.author_id = u.id
              GROUP BY u.id
          `).Scan(&summaries)
          
          // Named 参数(更安全)
          db.Where("name = @name AND email = @email",
              sql.Named("name", "Alice"),
              sql.Named("email", "alice@example.com"),
          ).Find(&users)
          
      • 关联关系
        • GORM 支持所有常见的关联关系:一对一、一对多、多对多。理解关联关系是写复杂查询的基础。
        • 一对多

          // 用户有多篇文章
          type User struct {
              gorm.Model
              Name     string
              Articles []Article  // Has Many(一对多)
          }
          
          // 文章属于一个用户
          type Article struct {
              gorm.Model
              Title    string
              Content  string
              UserID   uint    // 外键(约定:类型名+ID)
              User     User    // Belongs To
              Comments []Comment
          }
          
          // 创建关联数据
          user := User{
              Name: "Alice",
              Articles: []Article{
                  {Title: "Go 入门", Content: "..."},
                  {Title: "Gin 框架", Content: "..."},
              },
          }
          db.Create(&user)  // 同时创建用户和文章
          
          // 预加载关联(避免 N+1 问题)
          var users []User
          db.Preload("Articles").Find(&users)
          // SELECT * FROM users
          // SELECT * FROM articles WHERE user_id IN (1,2,3,...)
          
          // 嵌套预加载
          db.Preload("Articles.Comments").Find(&users)
          
          // 条件预加载
          db.Preload("Articles", "published = ?", true).Find(&users)
          
        • 多对多

          // 文章有多个标签,标签也属于多篇文章
          type Article struct {
              gorm.Model
              Title string
              Tags  []Tag `gorm:"many2many:article_tags"`  // 中间表名
          }
          
          type Tag struct {
              gorm.Model
              Name     string `gorm:"uniqueIndex"`
              Articles []Article `gorm:"many2many:article_tags"`
          }
          
          // GORM 自动管理中间表 article_tags (article_id, tag_id)
          
          // 创建时关联标签
          tags := []Tag{{Name: "Go"}, {Name: "Backend"}}
          article := Article{Title: "Go Web 开发", Tags: tags}
          db.Create(&article)
          
          // 追加关联
          db.Model(&article).Association("Tags").Append([]Tag{{Name: "Tutorial"}})
          
          // 替换关联
          db.Model(&article).Association("Tags").Replace(newTags)
          
          // 删除关联(只删关系,不删记录)
          db.Model(&article).Association("Tags").Delete(tag)
          
          // 清空关联
          db.Model(&article).Association("Tags").Clear()
          
          // 计数
          count := db.Model(&article).Association("Tags").Count()
          
        • 一对一

          // 用户有一个个人资料
          type User struct {
              gorm.Model
              Name    string
              Profile Profile  // Has One
          }
          
          type Profile struct {
              gorm.Model
              Bio    string
              Avatar string
              UserID uint  // 外键
          }
          
          // 查询时预加载
          var user User
          db.Preload("Profile").First(&user, 1)
          
          fmt.Println(user.Profile.Bio)
          
      • 事务处理
        • 事务保证一组操作要么全部成功,要么全部回滚。GORM 提供了简洁的事务 API,支持自动和手动两种模式。
        • 自动事务

          // Transaction 函数:返回 error 时自动回滚,nil 时自动提交
          err := db.Transaction(func(tx *gorm.DB) error {
              // 注意:事务内所有操作必须用 tx,不能用 db!
          
              // 创建订单
              order := Order{UserID: userID, Total: 100.0}
              if err := tx.Create(&order).Error; err != nil {
                  return err  // 返回 error → 自动回滚
              }
          
              // 扣减库存
              result := tx.Model(&Product{}).
                  Where("id = ? AND stock > 0", productID).
                  UpdateColumn("stock", gorm.Expr("stock - 1"))
              if result.Error != nil {
                  return result.Error
              }
              if result.RowsAffected == 0 {
                  return errors.New("库存不足")  // 返回 error → 回滚
              }
          
              // 记录流水
              if err := tx.Create(&Transaction{OrderID: order.ID}).Error; err != nil {
                  return err
              }
          
              return nil  // 返回 nil → 自动提交
          })
          
          if err != nil {
              log.Printf("下单失败: %v", err)
          }
          
        • 手动事务 (需要跨函数使用时)

          // 手动控制
          tx := db.Begin()  // 开启事务
          
          defer func() {
              if r := recover(); r != nil {
                  tx.Rollback()  // panic 时回滚
              }
          }()
          
          if err := tx.Error; err != nil {
              return err
          }
          
          if err := tx.Create(&user).Error; err != nil {
              tx.Rollback()
              return err
          }
          
          if err := tx.Create(&profile).Error; err != nil {
              tx.Rollback()
              return err
          }
          
          tx.Commit()  // 全部成功才提交
          return nil
          
          // 保存点(嵌套事务)
          tx.SavePoint("step1")
          // ... 一些操作
          tx.RollbackTo("step1")  // 只回滚到保存点,不全部回滚
          
        • 事务内必须用 tx,不能用 db
          • 这是事务最常见的错误!
          • 在 Transaction 回调里,如果你误用了外部的 db 而不是回调参数 tx,那些操作不属于这个事务,不会被一起回滚。
          • 养成习惯:看到 Transaction 回调,里面所有 db 操作都换成 tx。
      • Hooks 钩子
        • GORM 的 Hook 允许在数据库操作前后自动执行逻辑,比如密码加密、数据校验、审计日志。

          // Hook 方法列表:
          // BeforeCreate, AfterCreate
          // BeforeUpdate, AfterUpdate
          // BeforeDelete, AfterDelete
          // BeforeFind, AfterFind
          
          type User struct {
              gorm.Model
              Name     string
              Email    string
              Password string
          }
          
          // 创建前:自动加密密码
          func (u *User) BeforeCreate(tx *gorm.DB) error {
              if u.Password != "" {
                  hashed, err := bcrypt.GenerateFromPassword(
                      []byte(u.Password), bcrypt.DefaultCost,
                  )
                  if err != nil {
                      return err
                  }
                  u.Password = string(hashed)
              }
              return nil
          }
          
          // 查询后:脱敏处理(不把密码带出去)
          func (u *User) AfterFind(tx *gorm.DB) error {
              u.Password = ""  // 查出来后清空密码字段
              return nil
          }
          
          // 更新前:校验必填字段
          func (u *User) BeforeUpdate(tx *gorm.DB) error {
              if u.Email == "" {
                  return errors.New("email 不能为空")
              }
              return nil
          }
          
          // 删除前:记录审计日志
          func (u *User) BeforeDelete(tx *gorm.DB) error {
              return tx.Create(&AuditLog{
                  Action:   "DELETE",
                  Resource: "users",
                  ResourceID: u.ID,
              }).Error
          }
          
      • Repository 模式整合
        • 把 GORM 封装到 Repository 层,让 Service 层不依赖具体的数据库实现。

          // internal/repository/user.go
          package repository
          
          import (
              "context"
              "errors"
          
              "gorm.io/gorm"
              "github.com/yourname/myapp/internal/model"
          )
          
          var ErrNotFound = errors.New("记录不存在")
          
          // 接口(让 service 层只依赖接口)
          type UserRepository interface {
              FindByID(ctx context.Context, id uint) (*model.User, error)
              FindByEmail(ctx context.Context, email string) (*model.User, error)
              List(ctx context.Context, page, size int) ([]*model.User, int64, error)
              Create(ctx context.Context, u *model.User) error
              Update(ctx context.Context, u *model.User) error
              Delete(ctx context.Context, id uint) error
          }
          
          // GORM 实现
          type gormUserRepo struct {
              db *gorm.DB
          }
          
          func NewUserRepository(db *gorm.DB) UserRepository {
              return &gormUserRepo{db: db}
          }
          
          func (r *gormUserRepo) FindByID(ctx context.Context, id uint) (*model.User, error) {
              var u model.User
              err := r.db.WithContext(ctx).First(&u, id).Error
              if errors.Is(err, gorm.ErrRecordNotFound) {
                  return nil, ErrNotFound
              }
              return &u, err
          }
          
          func (r *gormUserRepo) FindByEmail(ctx context.Context, email string) (*model.User, error) {
              var u model.User
              err := r.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
              if errors.Is(err, gorm.ErrRecordNotFound) {
                  return nil, ErrNotFound
              }
              return &u, err
          }
          
          func (r *gormUserRepo) List(ctx context.Context, page, size int) ([]*model.User, int64, error) {
              var users []*model.User
              var count int64
          
              offset := (page - 1) * size
              db := r.db.WithContext(ctx).Model(&model.User{})
          
              if err := db.Count(&count).Error; err != nil {
                  return nil, 0, err
              }
          
              if err := db.Offset(offset).Limit(size).
                  Order("created_at DESC").Find(&users).Error; err != nil {
                  return nil, 0, err
              }
          
              return users, count, nil
          }
          
          func (r *gormUserRepo) Create(ctx context.Context, u *model.User) error {
              return r.db.WithContext(ctx).Create(u).Error
          }
          
          func (r *gormUserRepo) Update(ctx context.Context, u *model.User) error {
              return r.db.WithContext(ctx).Save(u).Error
          }
          
          func (r *gormUserRepo) Delete(ctx context.Context, id uint) error {
              result := r.db.WithContext(ctx).Delete(&model.User{}, id)
              if result.RowsAffected == 0 {
                  return ErrNotFound
              }
              return result.Error
          }
          
        • WithContext 的重要性

          // ✅ 每个数据库操作都带上 context
          // 当 HTTP 请求超时或客户端断开时,数据库查询会自动取消
          
          func (r *gormUserRepo) FindByID(ctx context.Context, id uint) (*model.User, error) {
              var u model.User
              err := r.db.WithContext(ctx).First(&u, id).Error
              //              ↑ 把 ctx 传给 GORM,超时自动取消 SQL
              return &u, err
          }
          
          // ❌ 没有 WithContext:HTTP 超时后 SQL 还在跑,浪费数据库资源
          func (r *gormUserRepo) FindByID(id uint) (*model.User, error) {
              var u model.User
              err := r.db.First(&u, id).Error  // 没有超时控制
              return &u, err
          }
          
      • GORM 高级查询
        • scrope 复用查询逻辑

          // Scope 是返回 func(db *gorm.DB) *gorm.DB 的函数
          // 让你把常用的查询条件封装成可复用的"查询片段"
          
          // 1. 分页 Scope
          func Paginate(page, size int) func(db *gorm.DB) *gorm.DB {
              return func(db *gorm.DB) *gorm.DB {
                  if page < 1 { page = 1 }
                  if size < 1 || size > 100 { size = 20 }
                  return db.Offset((page-1)*size).Limit(size)
              }
          }
          
          // 2. 只查活跃用户
          func ActiveOnly(db *gorm.DB) *gorm.DB {
              return db.Where("active = ?", true)
          }
          
          // 3. 按时间倒序
          func LatestFirst(db *gorm.DB) *gorm.DB {
              return db.Order("created_at DESC")
          }
          
          // 组合使用:业务代码非常清晰
          var users []User
          db.Scopes(ActiveOnly, LatestFirst, Paginate(2, 20)).Find(&users)
          
          // 等价于:
          // db.Where("active = ?", true).
          //    Order("created_at DESC").
          //    Offset(20).Limit(20).
          //    Find(&users)
          
        • 预加载,解决 N+1 问题

          // ❌ N+1 问题:1 次查用户 + N 次查每个用户的文章
          var users []User
          db.Find(&users)
          for _, u := range users {
              var articles []Article
              db.Where("user_id = ?", u.ID).Find(&articles)  // 每个用户都查一次!
          }
          
          // ✅ Preload:2 次 SQL 解决(无论多少用户)
          db.Preload("Articles").Find(&users)
          // SELECT * FROM users
          // SELECT * FROM articles WHERE user_id IN (1,2,3,...)
          
          // 嵌套预加载(用户 → 文章 → 评论)
          db.Preload("Articles.Comments").Find(&users)
          
          // 条件预加载:只加载已发布的文章
          db.Preload("Articles", "published = ?", true).Find(&users)
          
          // 自定义预加载查询:函数式
          db.Preload("Articles", func(db *gorm.DB) *gorm.DB {
              return db.Where("published = ?", true).
                  Order("created_at DESC").
                  Limit(5)
          }).Find(&users)
          
          // 预加载所有关联(懒人写法,慎用)
          db.Preload(clause.Associations).Find(&users)
          
        • JOIN:按关联条件过滤

          // Preload 适合"加载关联数据"
          // Joins  适合"按关联表字段过滤/排序"
          
          // 查询「至少有 1 篇已发布文章」的用户
          var users []User
          db.Joins("JOIN articles ON articles.user_id = users.id").
              Where("articles.published = ?", true).
              Distinct().
              Find(&users)
          
          // Joins + Preload 组合:用 Joins 过滤,用 Preload 加载完整关联
          db.Joins("JOIN articles ON articles.user_id = users.id AND articles.published = ?", true).
              Preload("Articles").
              Distinct().
              Find(&users)
          
        • 分页:完整封装

          type PageResult[T any] struct {
              Items    []T   `json:"items"`
              Total    int64 `json:"total"`
              Page     int   `json:"page"`
              Size     int   `json:"size"`
              HasNext  bool  `json:"has_next"`
          }
          
          func Page[T any](db *gorm.DB, page, size int) (*PageResult[T], error) {
              var items []T
              var total int64
          
              if err := db.Model(new(T)).Count(&total).Error; err != nil {
                  return nil, err
              }
          
              if err := db.Scopes(Paginate(page, size)).Find(&items).Error; err != nil {
                  return nil, err
              }
          
              return &PageResult[T]{
                  Items:   items,
                  Total:   total,
                  Page:    page,
                  Size:    size,
                  HasNext: int64(page*size) < total,
              }, nil
          }
          
          // 使用
          result, _ := Page[User](db.Where("active = ?", true), 1, 20)
          
        • Preload vs Joins
          • 需要把关联数据填到结构体里 → Preload;
          • 只是按关联字段过滤、不需要关联数据 → Joins;
          • 既要过滤又要加载完整关联 → Joins + Preload。
          • Preload 是 N 次表查询(用 IN),Joins 是 1 次 JOIN 查询。
          • Has Many 关联尤其推荐 Preload,因为 JOIN 后会出现笛卡尔积。
      • 数据库迁移
        • AutoMigrate 适合开发期,但生产环境需要专业的迁移工具:版本化、可回滚、团队协作友好。
        • golang-migrate 是 Go 生态最常用的方案。
        • 为什么不用 AutoMigrate?

          // AutoMigrate 的问题:
          // ❌ 不会删除字段(改名 = 加字段,旧字段一直在)
          // ❌ 不会修改字段类型(VARCHAR(50) → VARCHAR(100) 不生效)
          // ❌ 没有版本记录(不知道线上跑到哪个版本了)
          // ❌ 不能回滚(出问题只能手写 SQL 修复)
          // ❌ 多人协作冲突难发现
          
          // 生产环境正确做法:
          // 1. 用 SQL 文件描述每次变更(向上 + 向下)
          // 2. 工具记录已执行的版本
          // 3. 部署前检查、出错可回滚
          
        • 安装使用 golang-migrate

          # 安装 CLI
          brew install golang-migrate                      # macOS
          # 或
          go install -tags 'postgres' \
          github.com/golang-migrate/migrate/v4/cmd/migrate@latest
          
          # 创建迁移文件(自动生成时间戳前缀)
          migrate create -ext sql -dir db/migrations -seq create_users_table
          
          # 会生成两个文件:
          # db/migrations/000001_create_users_table.up.sql
          # db/migrations/000001_create_users_table.down.sql
          
          # 执行所有未执行的迁移
          migrate -path db/migrations \
          -database "postgres://postgres:secret@localhost:5432/myapp?sslmode=disable" \
          up
          
          # 回滚最后 1 个版本
          migrate -path db/migrations -database "..." down 1
          
          # 强制设置版本(脏数据修复用)
          migrate -path db/migrations -database "..." force 1
          
        • 迁移文件示例

          -- 000001_create_users_table.up.sql
          CREATE TABLE users (
              id          BIGSERIAL PRIMARY KEY,
              name        VARCHAR(100) NOT NULL,
              email       VARCHAR(255) UNIQUE NOT NULL,
              password    VARCHAR(255) NOT NULL,
              active      BOOLEAN DEFAULT TRUE,
              created_at  TIMESTAMP DEFAULT NOW(),
              updated_at  TIMESTAMP DEFAULT NOW(),
              deleted_at  TIMESTAMP
          );
          
          CREATE INDEX idx_users_deleted_at ON users(deleted_at);
          CREATE INDEX idx_users_email ON users(email);
          
          -- 000001_create_users_table.down.sql
          DROP TABLE IF EXISTS users;
          
        • go 程序中嵌入迁移

          // 程序启动时自动执行迁移(用 embed 把 SQL 文件打包进二进制)
          package db
          
          import (
              "embed"
              "github.com/golang-migrate/migrate/v4"
              "github.com/golang-migrate/migrate/v4/database/postgres"
              "github.com/golang-migrate/migrate/v4/source/iofs"
              _ "github.com/lib/pq"
              "database/sql"
          )
          
          //go:embed migrations/*.sql
          var migrationsFS embed.FS
          
          func RunMigrations(dsn string) error {
              sqlDB, err := sql.Open("postgres", dsn)
              if err != nil { return err }
              defer sqlDB.Close()
          
              driver, err := postgres.WithInstance(sqlDB, &postgres.Config{})
              if err != nil { return err }
          
              src, err := iofs.New(migrationsFS, "migrations")
              if err != nil { return err }
          
              m, err := migrate.NewWithInstance("iofs", src, "postgres", driver)
              if err != nil { return err }
          
              if err := m.Up(); err != nil && err != migrate.ErrNoChange {
                  return err
              }
              return nil
          }
          
    • Redis 操作
      • 基础入门
        • go-redis 客户端

          go get github.com/redis/go-redis/v9
          
        • 示例程序

          package cache
          
          import (
              "context"
              "time"
              "github.com/redis/go-redis/v9"
          )
          
          func NewRedis(addr, password string, db int) (*redis.Client, error) {
              rdb := redis.NewClient(&redis.Options{
                  Addr:         addr,         // "localhost:6379"
                  Password:     password,
                  DB:           db,           // 默认 0
                  PoolSize:     50,           // 连接池大小
                  MinIdleConns: 10,
                  DialTimeout:  5 * time.Second,
                  ReadTimeout:  3 * time.Second,
                  WriteTimeout: 3 * time.Second,
              })
          
              // 启动时 Ping 验证连接
              ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
              defer cancel()
              if err := rdb.Ping(ctx).Err(); err != nil {
                  return nil, err
              }
              return rdb, nil
          }
          
      • Redis 常用缓存操作
        • String: 缓存值、计数器
        • ctx := context.Background()
          
          // 设置 + 过期时间
          err := rdb.Set(ctx, "user:1", `{"name":"Alice"}`, 10*time.Minute).Err()
          
          // 读取
          val, err := rdb.Get(ctx, "user:1").Result()
          if err == redis.Nil {
              // key 不存在
          } else if err != nil {
              // 出错
          }
          
          // SetNX:只在不存在时设置(分布式锁的基础)
          ok, _ := rdb.SetNX(ctx, "lock:order:42", "1", 30*time.Second).Result()
          if !ok { return errors.New("已有人在处理") }
          
          // 计数器
          rdb.Incr(ctx, "page_view:home")
          rdb.IncrBy(ctx, "score:user:1", 10)
          
          // 批量获取
          vals, _ := rdb.MGet(ctx, "user:1", "user:2", "user:3").Result()
          
        • Hash: 对象字段缓存

          // 把一个 User 拆成多个字段,比 JSON 字符串更省内存、可单字段更新
          rdb.HSet(ctx, "user:1",
              "name", "Alice",
              "email", "alice@example.com",
              "age", 30)
          
          name, _ := rdb.HGet(ctx, "user:1", "name").Result()
          
          // 一次取所有字段
          all, _ := rdb.HGetAll(ctx, "user:1").Result()
          // map[string]string{"name":"Alice", ...}
          
          // 单字段自增
          rdb.HIncrBy(ctx, "user:1", "login_count", 1)
          
        • List/Set/ZSet

          // List:消息队列、最近浏览
          rdb.LPush(ctx, "queue:emails", "task1", "task2")
          val, _ := rdb.RPop(ctx, "queue:emails").Result()  // 队尾出队
          
          // 阻塞式弹出(适合 worker 消费)
          result, _ := rdb.BLPop(ctx, 5*time.Second, "queue:emails").Result()
          
          // Set:去重集合、标签、好友关系
          rdb.SAdd(ctx, "tags:article:1", "go", "redis", "backend")
          exists, _ := rdb.SIsMember(ctx, "tags:article:1", "go").Result()
          all, _ := rdb.SMembers(ctx, "tags:article:1").Result()
          
          // ZSet:排行榜、按分数排序
          rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 100, Member: "alice"})
          rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 85, Member: "bob"})
          
          // 取前 10 名(分数从高到低)
          top, _ := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 9).Result()
          for _, z := range top {
              fmt.Printf("%v: %v\n", z.Member, z.Score)
          }
          
        • Pipelines:批量操作

          // 一次往返发送多个命令,大幅减少网络开销
          pipe := rdb.Pipeline()
          incr := pipe.Incr(ctx, "page_view:home")
          pipe.Expire(ctx, "page_view:home", 24*time.Hour)
          _, err := pipe.Exec(ctx)
          if err != nil { return err }
          fmt.Println("当前 PV:", incr.Val())
          
      • Cache-Aside 缓存策略
        • Cache-Aside(旁路缓存)是最经典的缓存模式
        • 应用先查缓存,缓存没有再查数据库并回填缓存。简单、可靠、易于理解。
        • 读流程

          func GetUser(ctx context.Context, id uint) (*User, error) {
              key := fmt.Sprintf("user:%d", id)
          
              // 1. 查缓存
              val, err := rdb.Get(ctx, key).Result()
              if err == nil {
                  var u User
                  if err := json.Unmarshal([]byte(val), &u); err == nil {
                      return &u, nil  // 缓存命中
                  }
              } else if err != redis.Nil {
                  // Redis 出错(不是 key 不存在),降级直接查 DB
                  log.Warn("redis error:", err)
              }
          
              // 2. 缓存未命中 → 查数据库
              var u User
              if err := db.WithContext(ctx).First(&u, id).Error; err != nil {
                  if errors.Is(err, gorm.ErrRecordNotFound) {
                      // 防穿透:把"不存在"也缓存(短 TTL)
                      rdb.Set(ctx, key, "", 1*time.Minute)
                      return nil, ErrNotFound
                  }
                  return nil, err
              }
          
              // 3. 回填缓存
              if data, err := json.Marshal(u); err == nil {
                  rdb.Set(ctx, key, data, 10*time.Minute)
              }
              return &u, nil
          }
          
        • 写流程

          // ✅ 推荐:先写 DB,再删缓存
          func UpdateUser(ctx context.Context, u *User) error {
              if err := db.WithContext(ctx).Save(u).Error; err != nil {
                  return err
              }
              // 删除缓存(下次读会从 DB 重新加载)
              rdb.Del(ctx, fmt.Sprintf("user:%d", u.ID))
              return nil
          }
          
          // ❌ 不推荐:先写 DB,再更新缓存
          // 问题:两个并发更新可能让旧值覆盖新值(A 写 DB → B 写 DB → B 写缓存 → A 写缓存)
          
          // 删除策略选 "delete" 而不是 "update" 的核心原因:
          // 1. 删除幂等,更新可能写入旧数据
          // 2. 下次读自然回填,避免缓存写入热点
          // 3. 简单(不用维护缓存的写一致性)
          
        • 封装通用缓存层

          // 用泛型封装一个通用的"查缓存 → 回源 → 回填"模式
          func GetOrLoad[T any](
              ctx context.Context,
              rdb *redis.Client,
              key string,
              ttl time.Duration,
              loader func(ctx context.Context) (*T, error),
          ) (*T, error) {
              // 1. 查缓存
              if val, err := rdb.Get(ctx, key).Bytes(); err == nil {
                  var v T
                  if err := json.Unmarshal(val, &v); err == nil {
                      return &v, nil
                  }
              }
          
              // 2. 回源
              v, err := loader(ctx)
              if err != nil { return nil, err }
          
              // 3. 回填(异步、不阻塞返回)
              go func() {
                  if data, err := json.Marshal(v); err == nil {
                      rdb.Set(context.Background(), key, data, ttl)
                  }
              }()
              return v, nil
          }
          
          // 使用
          user, err := GetOrLoad(ctx, rdb,
              fmt.Sprintf("user:%d", id),
              10*time.Minute,
              func(ctx context.Context) (*User, error) {
                  var u User
                  return &u, db.WithContext(ctx).First(&u, id).Error
              },
          )
          
      • 缓存三类问题
        • 缓存穿透

          // 问题:恶意请求大量不存在的 ID
          //   → 缓存永远没有 → 每次都打到 DB → DB 被打挂
          
          // 方案 1:缓存空值(短 TTL)
          if errors.Is(err, gorm.ErrRecordNotFound) {
              rdb.Set(ctx, key, "NULL_PLACEHOLDER", 1*time.Minute)
              return nil, ErrNotFound
          }
          
          // 读取时识别空值
          val, _ := rdb.Get(ctx, key).Result()
          if val == "NULL_PLACEHOLDER" { return nil, ErrNotFound }
          
          // 方案 2:布隆过滤器
          // 启动时把所有有效 ID 加入布隆过滤器
          // 查询前先问布隆过滤器:这个 ID 可能存在吗?
          // "肯定不存在" → 直接拒绝,不查 DB
          
        • 缓存击穿

          // 问题:1 个超热点 key(比如首页配置)刚好过期
          //   → 大量请求同时穿透到 DB → DB 瞬间被打挂
          
          // 方案:用 SetNX 实现单飞(singleflight)
          //      只让 1 个请求去查 DB,其他请求等待结果
          
          import "golang.org/x/sync/singleflight"
          
          var sf singleflight.Group
          
          func GetHotConfig(ctx context.Context) (*Config, error) {
              if val, err := rdb.Get(ctx, "config:home").Bytes(); err == nil {
                  var c Config
                  json.Unmarshal(val, &c)
                  return &c, nil
              }
          
              // singleflight:同一 key 同时只有 1 次回源
              v, err, _ := sf.Do("config:home", func() (any, error) {
                  var c Config
                  if err := db.First(&c).Error; err != nil { return nil, err }
                  if data, err := json.Marshal(c); err == nil {
                      rdb.Set(ctx, "config:home", data, 10*time.Minute)
                  }
                  return &c, nil
              })
              if err != nil { return nil, err }
              return v.(*Config), nil
          }
          
        • 缓存雪崩

          // 问题:缓存预热时大批 key 设了相同的 TTL
          //   → 同一时刻全部过期 → 请求全打到 DB
          
          // 方案:TTL 加随机抖动
          func RandomTTL(base time.Duration) time.Duration {
              jitter := time.Duration(rand.Int63n(int64(base / 5)))  // ±20%
              return base + jitter
          }
          
          rdb.Set(ctx, key, val, RandomTTL(10*time.Minute))
          // 实际过期时间分布在 10~12 分钟之间,自然错峰
          
          // 方案 2:多级缓存(本地 + Redis),Redis 挂了还有本地兜底
          // 方案 3:限流降级,DB 压力过大时直接返回旧数据或默认值
          
      • 分布式锁

        // 简化版分布式锁
        type RedisLock struct {
            rdb   *redis.Client
            key   string
            value string  // 锁的"持有者"标识,释放时校验
            ttl   time.Duration
        }
        
        func NewLock(rdb *redis.Client, key string, ttl time.Duration) *RedisLock {
            return &RedisLock{
                rdb:   rdb,
                key:   key,
                value: uuid.New().String(),  // 唯一 ID
                ttl:   ttl,
            }
        }
        
        // 获取锁(非阻塞)
        func (l *RedisLock) TryLock(ctx context.Context) (bool, error) {
            return l.rdb.SetNX(ctx, l.key, l.value, l.ttl).Result()
        }
        
        // 释放锁(用 Lua 脚本保证原子性:「校验是我加的锁」+「删除」)
        var unlockScript = redis.NewScript(`
        if redis.call("GET", KEYS[1]) == ARGV[1] then
            return redis.call("DEL", KEYS[1])
        else
            return 0
        end
        `)
        
        func (l *RedisLock) Unlock(ctx context.Context) error {
            return unlockScript.Run(ctx, l.rdb, []string{l.key}, l.value).Err()
        }
        
        // 使用
        lock := NewLock(rdb, "lock:order:42", 30*time.Second)
        ok, err := lock.TryLock(ctx)
        if err != nil || !ok { return errors.New("已有人在处理") }
        defer lock.Unlock(ctx)
        
        // 临界区:处理订单
        
    • 认证与授权 (JWT+RBAC)
      • 一个 Web 服务的安全模块通常包含两层:先认证(Authentication,你是谁),再授权(Authorization,你能做什么)
      • 两个核心概念

        Authentication(认证 / AuthN)
        → 你是谁?
        → 例如:用户名密码登录、Token 校验、SSO
        
        Authorization(授权 / AuthZ)
        → 你被允许做什么?
        → 例如:管理员可以删用户,普通用户不行;A 用户只能编辑自己的文章
        
        实际请求的处理顺序:
        HTTP Request
            → 中间件 1:解析 Token,确认身份(认证)
            → 中间件 2:检查权限,判断操作(授权)
            → 业务 Handler
        
      • 无状态认证 vs 有状态

        传统 Session 方案:
            登录后服务器内存(或 Redis)保存 sessionID → 用户信息
            浏览器用 Cookie 携带 sessionID
            ✅ 服务端可主动失效
            ❌ 服务端要存状态,多实例需要共享存储;跨域 Cookie 麻烦
        
        JWT 方案(Stateless):
            登录后服务器签发一个自包含 Token,里面带用户信息和过期时间
            客户端把 Token 放在 Authorization Header 里
            ✅ 服务端无状态,水平扩展友好;天然跨域
            ❌ 签发后无法主动失效(需要黑名单或短 TTL + 刷新)
        
        现代后端通常选 JWT,配合 Refresh Token 机制弥补失效问题。
        
      • golang-jwt 实战
        • Go 生态最常用的库是 golang-jwt/jwt。我们封装一个 TokenService,统一管理签发和校验。
        • 安装

          go get github.com/golang-jwt/jwt/v5
          
        • 封装 TokenService

          package auth
          
          import (
              "errors"
              "time"
              "github.com/golang-jwt/jwt/v5"
          )
          
          // 自定义 Claims:内嵌标准字段 + 业务字段
          type Claims struct {
              UserID uint   `json:"uid"`
              Email  string `json:"email"`
              Role   string `json:"role"`
              jwt.RegisteredClaims    // 提供 ExpiresAt / IssuedAt / Issuer 等
          }
          
          type TokenService struct {
              secret      []byte
              issuer      string
              accessTTL   time.Duration
              refreshTTL  time.Duration
          }
          
          func NewTokenService(secret string) *TokenService {
              return &TokenService{
                  secret:     []byte(secret),
                  issuer:     "myapp",
                  accessTTL:  15 * time.Minute,    // Access Token 短
                  refreshTTL: 7 * 24 * time.Hour,  // Refresh Token 长
              }
          }
          
          // 签发 Access Token
          func (s *TokenService) IssueAccess(uid uint, email, role string) (string, error) {
              claims := Claims{
                  UserID: uid,
                  Email:  email,
                  Role:   role,
                  RegisteredClaims: jwt.RegisteredClaims{
                      Issuer:    s.issuer,
                      Subject:   fmt.Sprint(uid),
                      IssuedAt:  jwt.NewNumericDate(time.Now()),
                      ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.accessTTL)),
                  },
              }
              token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
              return token.SignedString(s.secret)
          }
          
          // 校验 Token
          func (s *TokenService) Parse(tokenStr string) (*Claims, error) {
              token, err := jwt.ParseWithClaims(tokenStr, &Claims{},
                  func(t *jwt.Token) (any, error) {
                      // ⚠️ 必须校验签名算法,防止 alg=none 攻击
                      if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
                          return nil, errors.New("unexpected signing method")
                      }
                      return s.secret, nil
                  },
                  jwt.WithIssuer(s.issuer),
                  jwt.WithExpirationRequired(),
              )
              if err != nil { return nil, err }
          
              claims, ok := token.Claims.(*Claims)
              if !ok || !token.Valid {
                  return nil, errors.New("invalid token")
              }
              return claims, nil
          }
          
        • 认证中间件
          • 中间件从 Authorization Header 取 Token,校验通过后把用户信息塞进 context,后续 Handler 直接读取。
          • 示例

            package middleware
            
            import (
                "context"
                "net/http"
                "strings"
                "github.com/gin-gonic/gin"
            )
            
            type ctxKey string
            const ClaimsKey ctxKey = "claims"
            
            func JWTAuth(svc *auth.TokenService) gin.HandlerFunc {
                return func(c *gin.Context) {
                    h := c.GetHeader("Authorization")
                    if !strings.HasPrefix(h, "Bearer ") {
                        c.AbortWithStatusJSON(401, gin.H{"error":"missing or invalid Authorization header"})
                        return
                    }
                    tokenStr := strings.TrimPrefix(h, "Bearer ")
            
                    claims, err := svc.Parse(tokenStr)
                    if err != nil {
                        c.AbortWithStatusJSON(401, gin.H{"error":"invalid token: " + err.Error()})
                        return
                    }
            
                    // 把 claims 写入 context
                    ctx := context.WithValue(c.Request.Context(), ClaimsKey, claims)
                    c.Request = c.Request.WithContext(ctx)
                    c.Set("claims", claims)  // Gin 也提供了内部 Set/Get
                    c.Next()
                }
            }
            
            // Handler 里取出当前用户
            func ClaimsFrom(c *gin.Context) *auth.Claims {
                if v, ok := c.Get("claims"); ok {
                    return v.(*auth.Claims)
                }
                return nil
            }
            
            // 使用
            r := gin.Default()
            r.POST("/api/v1/auth/login", loginHandler)              // 公开
            authed := r.Group("/api/v1", JWTAuth(tokenSvc))         // 需要登录
            {
                authed.GET("/me", meHandler)
                authed.POST("/articles", createArticleHandler)
            }
            
        • 密码加密:bcypt
          • 绝对禁止把密码明文存数据库。bcrypt 是密码哈希的事实标准:自带盐、计算慢(抗暴力破解)、参数可调。
          • 安装
          go get golang.org/x/crypto/bcrypt
          
          • 示例

            package auth
            
            import "golang.org/x/crypto/bcrypt"
            
            // Cost 推荐 12,越高越慢但越安全(2025 年合理范围 12~14)
            // 每 +1 计算时间翻倍:cost=10 ≈ 100ms, cost=12 ≈ 250ms
            const PasswordCost = 12
            
            func HashPassword(plain string) (string, error) {
                h, err := bcrypt.GenerateFromPassword([]byte(plain), PasswordCost)
                if err != nil { return "", err }
                return string(h), nil
            }
            
            func VerifyPassword(plain, hashed string) bool {
                err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
                return err == nil
            }
            
            // 数据库里存的 hash 形如:
            // $2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
            //   ↑   ↑                  ↑
            //  算法 cost              salt + hash
            //
            // 不需要单独存 salt——它已经嵌在 hash 字符串里了!
            
          • 集成到 User 模型(GORM Hook)

            type User struct {
                gorm.Model
                Email    string `gorm:"uniqueIndex"`
                Password string `gorm:"size:255;not null" json:"-"`  // json:"-" 防序列化
                Role     string `gorm:"default:user"`                // user / admin
            }
            
            // 创建/更新前自动加密(已经是哈希就跳过)
            func (u *User) BeforeSave(tx *gorm.DB) error {
                // 检测是否已经是 bcrypt 哈希
                if strings.HasPrefix(u.Password, "$2") { return nil }
                if u.Password == "" { return nil }
                h, err := HashPassword(u.Password)
                if err != nil { return err }
                u.Password = h
                return nil
            }