Go 复习四月 18, 2026基础#Go 环境搭建与工具链# - Go 环境搭建与工具链 - 安装 Go - 官网直接安装 - ```bash # 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 环境管理 - ```bash # 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 ``` - 理解环境变量 - 常用环境变量 - ```bash # 查看所有 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 - 设置常用模块 - ```bash # 确保模块模式开启 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 - ```go 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 - ```go // ① 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 - 常见陷阱 - 短声明的遮蔽 - ```go 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 的概念 - 每个类型都有一个确定的零值,声明变量时不赋值就是零值 -  - ```go // 零值是安全的,可以直接使用 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 自增器 - 基本常量 ```go const Pi = 3.14159 const AppName = "MyApp" // 批量声明 const ( StatusOK = 200 StatusNotFound = 404 StatusError = 500 ) ``` - iota Go 的枚举利器 ```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 高级用法 ```Go // 用法一:跳过某个值 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 // ❌ 隐式转换在 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 条件判断 - 条件不需要括号 - 允许在条件前加入一个初始化语句 - 基本语法 - ```Go 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 特色) - ```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 有多种形式,能覆盖其他语言的各种循环要求 - 经典三段式 - ```Go for i := 0; i < 10; i++ { fmt.Println(i) } ``` - 类 while 循环 (只有条件) - ```Go n := 10 for n > 0 { fmt.Println(n) n-- } ``` - 无限循环 - ```Go for { // 死循环,用 break 退出 if someCondition() { break } } // 常用于服务器主循环 for { conn, err := listener.Accept() if err != nil { continue } go handleConn(conn) } ``` - for-range 遍历 - ```Go // 遍历切片/数组 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 // 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、多值匹配、任意表达式 - 基本用法:默认不穿透 - ```go 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 链的优雅写法) - ```go 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 { ... } // ... ``` - 前置初始化语句 - ```go 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:显式穿透 - ```go // 如果真的需要穿透到下一个 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 - ```go for i := 0; i < 10; i++ { if i == 3 { continue // 跳过本次迭代 } if i == 7 { break // 退出循环 } fmt.Println(i) } // 输出:0 1 2 4 5 6 ``` - 标签:跳出嵌套循环 - ```go // 问题: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 的标志性特性,用来注册「函数退出前必须执行」的逻辑 - 它彻底解决了资源清理、错误恢复等场景 - 基本行为:函数返回前执行 - ```go func main() { fmt.Println("1. 开始") defer fmt.Println("3. 延迟执行") fmt.Println("2. 中间") } // 输出: // 1. 开始 // 2. 中间 // 3. 延迟执行 ← 在函数返回前执行 ``` - 典型用法:资源清理 - ```go // 文件操作 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 栈顺序 - ```go func main() { defer fmt.Println("1") defer fmt.Println("2") defer fmt.Println("3") fmt.Println("main") } // 输出: // main // 3 ← 后进先出(栈) // 2 // 1 ``` - defer 语句参数 - defer 语句的参数在注册是就计算好,不是执行时 - ```go 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 在前,返回值类型在后 - ```go // 基本语法: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 支持函数式编程风格,比如高阶函数、回调、中间件模式等 - ```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 原生支持多返回值 - ```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) - ```go // 几乎所有可能失败的操作都返回 (结果, 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 允许给返回值命名,在返回值多,复杂逻辑时有用 - ```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 - ```go // 在 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 就是典型的可变参数函数。 - 示例 - ```go // 求任意个数字的和 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 ``` - 混合使用固定参数和可变参数 - ```go // 可变参数必须是最后一个参数 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 的闭包语法简洁,是实现迭代器、中间件、回调等模式的基础 - 匿名函数 - ```go // 定义后立即调用(IIFE) func() { fmt.Println("我是匿名函数") }() // 赋值给变量 add := func(a, b int) int { return a + b } fmt.Println(add(1, 2)) // 3 ``` - 闭包:捕获外部变量 - ```go // 计数器生成器 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 继续累加) ``` - 闭包实战:装饰器模式 - ```go // 为函数添加日志功能 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 的本质:一个接口 - ```go // 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 // ✅ 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 ``` - 提前返回:降低嵌套 - ```go // ❌ 过度嵌套(来自 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 的不一样,它是值类型,长度是类型的一部分 - 实际开发很少使用数组,几乎都使用切片 - 示例 - ```go // 声明数组:类型 [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) } ``` - 数组是值类型 - 把数组赋值给另一个变量,或者传入函数,都会完整拷贝一份 - 大数组这样性能会很差,实际开发总是使用切片 - ```go 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 中最常用的数据结构 - 看起来像动态数组,但底层是对数组的视图 - 切片的三要素 - ```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(容量) ``` - 创建切片的五种方式 - ```go // ① 字面量 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] - ```go 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: 扩容机制 - ```go 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] ``` - 扩容规则 - ```go // 当 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) // 不会触发扩容 } ``` - 切片陷阱 - 切割共享底层数组 - ```go 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 可能修改原切片 - ```go 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] ← 原数组安全 ``` - 大切片导致内存泄露 - ```go // 读一个 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 // 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) ``` - 插入元素 - ```go 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) ``` - 复制、反转、排序 - ```go // 复制 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 不能写入 - 声明与初始化 - ```go // ❌ 错误:只声明,没初始化 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) // 提前分配空间 ``` - 增删改查 - ```go 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)) ``` - 遍历(顺序随机) - ```go 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 进阶:零值技巧与并发 - 零值技巧:计数器模式 - ```go // 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 // 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 不是并发安全的 - ```go // ❌ 多个 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 + 方法已经能表达几乎所有面向对象的特性,而且更简单直接 - 定义与初始化 - ```go // 定义结构体 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" ``` - 访问和修改字段 - ```go 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 是值类型。赋值、传参、返回都是完整拷贝 - ```go 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 ``` - 什么时候用指针 - ```go // ✅ 使用指针的场景: // 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 的方法语法有点特别——接收者写在函数名前面 - 方法定义语法 - ```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 指针接收者 - ```go 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 // 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 上,任何在当前包定义的类型都可以有方法 - ```go // 给 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 面向对象的核心——组合优于继承 - 基本嵌入 - ```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) // 同上 ``` - 方法覆盖 - ```go // 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岁,品种:柴犬" ``` - 嵌入接口 - ```go // 也可以嵌入接口,常见于标准库 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 函数返回初始化好的实例,这样可以封装验证、默认值、依赖注入等逻辑。 - 示例 - ```go 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) ``` - 函数选项模式 - ```go // 问题:构造函数参数太多怎么办? // 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 序列化、数据库映射、表单校验都用它 - 示例 - ```go 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 用法一览 - ```go 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。 - 这叫做「隐式接口实现」,也叫鸭子类型。 - 接口示例 - ```go // 定义接口:只有方法签名,没有实现 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) - 示例 - ```go // 接口值 = (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 // 这是 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 - 类型断言 - ```go 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 - ```go 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:自定义打印格式 - ```go // 标准库定义: // 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:自定义错误类型 - ```go // 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 的基石 - ```go // 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 鼓励定义小接口,再通过组合构建大接口 - 示例 - ```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 } ``` - 接口应该越小越好 - ```go // ❌ 大接口:实现困难,测试困难,耦合高 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{} 的别名,更简洁 - 示例 - ```go // 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 的代价 - ```go // 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 是最后的选择 - 接口定义最佳实践 - 在使用方定义接口,不在实现方 - ```go // ❌ 在实现包里定义接口(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} } ``` - 接口让测试更容易 - ```go // 生产代码 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 关键字 - 示例 - ```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 - 示例 - ```go // 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 完成的标准方式 - 示例 - ```go // ❌ 错误: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 的使用规范 - ```go // ✅ 正确:在 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,但它永远不会退出,一直占用内存和资源 - 泄露的常见原因 - ```go // ❌ 泄漏一:等待永远不会发送的 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 }() } ``` - 正确的退出机制 - ```go // ✅ 用 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 - ```go // 在测试中检测 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 自带了竞态检测器 - ```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 // 清晰显示哪些行在竞争 ``` - 三种解决方案 - ```go // ✅ 方案一: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 互斥锁 - ```go 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 读写锁 (读多写少场景) - ```go 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 单例初始化 - ```go // 保证函数只执行一次,线程安全 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 传递数据,而不是共享变量。 - 示例 - ```go // 创建 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 通信 - ```go 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 (同步) - ```go 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 (异步) - ```go 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 只能发送或只能接收 - ```go // 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 - ```go // 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 是标准答案 - ```go 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 的非阻塞操作 - ```go 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 为空,跳过") } ``` - 超时控制 (实战必备) - ```go 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 - ```go 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 退出 - 示例 - ```go 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 广播的原理 - ```go 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 - 生产者-消费者模式 - 基础版:单生产者、单消费者 - ```go 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: 多消费者并发处理 - ```go 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 是一棵树:从根节点出发,不断派生子节点 - 父节点取消时,所有子节点自动取消。这个设计让取消信号天然地从上往下传播 - 示例 - ```go 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 生命周期的场景 - 示例 - ```go 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("主程序退出") } ``` - 一个父取消,多个子都取消 - ```go 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 最常用 - ```go // 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 } ``` - 检查剩余时间 - ```go 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 绝对时间点 - ```go // 设定必须在某个时间点前完成 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 自动取消,让你能及时释放资源 - 示例 - ```go // 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 } ``` - 完整的中间件链 - ```go 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 最佳实践 - 规范汇总 - ```go // ✅ 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 但还没传进来」 ``` - 常见错误模式 - ```go // ❌ 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,层层传递,层层受控 - 错误处理哲学 - 错误处理回顾与痛点 - 示例 - ```go // 基础版:丢失了太多信息 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 包装,形成一条可以追溯的链路。 - 示例 - ```go // 错误链示例:从底层到顶层 // 层级: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 (支持自定义类型) - ```go // 自定义错误类型也可以支持错误链 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 整条链,不像 == 只比较最外层。 - 示例 - ```go // 定义哨兵错误(包级别的可比较 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 方法 - ```go // 有时候两个错误「相等」不是值相等,而是逻辑相等 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 则「取出来」——把错误链中某个具体类型的错误提取出来,访问它的字段 - 示例 - ```go // 自定义错误类型,携带更多信息 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 验证依赖是否被篡改 - 完整操作流程 - ```go # 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 // 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 # 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) - ```go # 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 的可见性只有两种:导出(首字母大写)和未导出(首字母小写)。 - 这个规则适用于所有标识符:函数、类型、变量、常量、接口、结构体字段。 - 示例 - ```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 包:包级别的访问控制 - ```go 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 # 编译时强制执行,违反会报错 ``` - ```go // 实战场景:把不想暴露的实现细节放进 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 服务项目结构 - ```go │ ├── 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 ``` - 关键原则:分层架构 - ```go // 依赖方向: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 分组规范 - ```go 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 技巧 - ```go // 别名:解决包名冲突 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 指令。 - ```go # 场景:同时开发 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) ``` - ```go // 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 常用选项 - ```bash # 基础构建 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 (条件编译) - ```go // 文件只在 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: 常用命令封装 - ```go # 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 # 查看当前代理设置 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" ``` - 私有模块配置 - ```go # 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 测试的全部入场门票。 - 示例 - ```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 的核心方法 - ```go // 测试失败但继续执行 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 测试的标准惯用法。 - 示例 - ```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 返回的函数 - ```go 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 - ```go // 如果定义了 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: 函数级别的清理 - ```go 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 - ```go // 当多个测试有重复逻辑时,提取为 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 - ```go 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 } ``` - 常用断言 - ```go // 相等性 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 - ```go // 被测代码依赖接口(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 - ```go 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 会自动调整循环次数,让结果更准确。 - 示例 - ```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 // ↑执行次数 ↑每次耗时 ↑内存 ↑分配次数 ``` - 比较两种实现的性能 - ```go // 对比字符串拼接:+ 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 - ```go 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 网站也会展示它们。 - 示例 - ```go // 函数名: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 # 查看覆盖率(百分比) 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 ``` - 覆盖率的合理目标 - ```go // 什么应该测? // ✅ 核心业务逻辑(service 层) // ✅ 复杂算法 // ✅ 错误处理路径 // ✅ 边界条件(空值、极值) // 什么可以不测/难测? // ❌ main 函数 // ❌ 简单的 getter/setter // ❌ 第三方库(已有自己的测试) // ❌ 纯配置代码 // 行业参考值: // 核心业务代码:80% 以上 // 整体项目:60-70% 是健康的 // 追求 100% 反而可能浪费时间在无价值的测试上 // 一个常见的 .golangci.yml 配置 // testpackage: 是否要求测试在独立的 _test 包 // 建议:核心包用 package xxx_test,确保测试只用公开 API ``` - 测试覆盖率的正确理解 - 覆盖率是工具,不是目标。 - 80% 覆盖率但测试都是走正常路径,不如 60% 但每个边界条件都测到。 - 真正重要的是:关键业务路径有测试、错误路径有测试、修改代码后测试能告诉你哪里坏了。 - 泛型 - Go 1.18 引入泛型之前,想写「适用于任意类型」的函数有两种选择:为每种类型写重复代码,或者用 any 丢失类型安全。 - 泛型解决了这个两难困境——既通用,又类型安全。 - 没有泛型的问题 - ```go // ❌ 方案一:为每种类型写重复代码 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 是程序员的妥协。 - 类型参数基础语法 - 泛型通过「类型参数」实现。 - 类型参数放在方括号 [] 里,紧跟在函数名或类型名后面。 - 每个类型参数都有一个「约束」,限制它能接受哪些类型。 - 泛型函数 - ```go // 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) }) ``` - 泛型类型 (结构体) - ```go // 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),不只是方法列表。 - 内置约束 - ```go // 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 包的内置约束 - ```go 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] 范围内) ``` - 自定义约束 - ```go // 约束是接口,可以包含: // 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 标准包已经内置了很多。 - 切片操作三件套 - ```go // 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 ``` - 通用工具函数 - ```go // 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 (集合) - ```go 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 (可选值) - ```go // 类似 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 - ```go 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 包 - ```go 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 包 - ```go 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(支持多参数) ``` - 泛型的限制与最佳实践 - 当前的限制 - ```go // ❌ 限制一:不能在方法上定义新的类型参数 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++ 那样的模板特化 ``` - 什么时候用泛型,什么时候不用 - ```go // ✅ 适合用泛型: // 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 服务器示例 - ```go 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 接口:一切方法的核心 - ```go // 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 参数与路径 - ```go 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 - ```go 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) } ``` - 表单与文件上传 - ```go 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。 - ```go 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 响应的标准写法 - ```go // 封装一个通用的 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) } ``` - 重定向与文件服务 - ```go // 重定向 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 - ```go 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 // 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) // 只匹配根路径 ``` - 路由组织:分文件 - ```go // 把路由注册拆分到各模块 // 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、限流)。 - 标准库的中间件模式非常简洁,用函数包装函数实现。 - 中间件的定义与使用 - ```go // 中间件的签名:接受 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 示意) - ```go 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 中间件 - ```go 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 - ```go 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 测试 - ```go # 创建用户 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 方法实现了这个功能,配合系统信号使用。 - ```go 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 对比 - 标准库写法 - ```go // ─── 标准库写法 ─── 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 go get github.com/gin-gonic/gin ``` - ```go 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 的路由系统非常强大,支持路径参数、通配符、路由分组,还能给分组统一加中间件。 - 基本路由 - ```go 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 - ```go 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 - ```go // 自定义 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 的核心。 - 读取请求参数 - ```go 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 的键值存储(在中间件传数据) - ```go // 中间件设置值 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}) } ``` - 响应方法 - ```go 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:绑定 + 校验 - ```go // 定义请求结构体,用 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 方法 - ```go // 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,并写入响应(不灵活) ``` - 常用校验规则 - ```go 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() 控制执行流。 - 中间件的执行模型 - ```go // 中间件的结构:前置逻辑 → 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()) ``` - 认证/限流中间件(完整实现) - ```go 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 - ```go // ⚠️ 重要: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 只负责「报告」错误,中间件统一「处理」错误。 - ```go // ─── 定义 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 项目完整结构 - 项目结构 - ```go 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 ``` - 经典文件 - ```go // 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 等主流数据库。 - 安装 - ```go # 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(开发/测试用) ``` - 连接数据库 - ```go 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 四个字段。 - 基础模型定义 - ```go 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 - ```go 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:自动建表 - ```go // 自动创建/更新表结构(只加列,不删列,生产用专业迁移工具) err := db.AutoMigrate( &model.User{}, &model.Article{}, &model.Comment{}, ) if err != nil { log.Fatal("数据库迁移失败:", err) } // AutoMigrate 的行为: // ✅ 表不存在 → 创建表 // ✅ 字段不存在 → 添加字段 // ✅ 索引不存在 → 创建索引 // ❌ 字段类型变了 → 不会修改(需要手动迁移) // ❌ 删掉字段 → 不会删除数据库列(安全策略) ``` - CRUD 基础操作 - Create (创建) - ```go // 创建单条记录 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 (查询) - ```go // 按主键查询 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 (更新) - ```go // 更新单个字段 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 (删除) - ```go // 软删除(如果模型有 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 面试高频考点! - 查询进阶 - 链式调用构建复杂查询 - ```go // 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) ``` - 聚合与分组 - ```go // 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 - ```go // 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 支持所有常见的关联关系:一对一、一对多、多对多。理解关联关系是写复杂查询的基础。 - 一对多 - ```go // 用户有多篇文章 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) ``` - 多对多 - ```go // 文章有多个标签,标签也属于多篇文章 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() ``` - 一对一 - ```go // 用户有一个个人资料 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,支持自动和手动两种模式。 - 自动事务 - ```go // 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) } ``` - 手动事务 (需要跨函数使用时) - ```go // 手动控制 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 允许在数据库操作前后自动执行逻辑,比如密码加密、数据校验、审计日志。 - ```go // 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 层不依赖具体的数据库实现。 - ```go // 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 的重要性 - ```go // ✅ 每个数据库操作都带上 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 复用查询逻辑 - ```go // 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 问题 - ```go // ❌ 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:按关联条件过滤 - ```go // 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) ``` - 分页:完整封装 - ```go 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? - ```go // AutoMigrate 的问题: // ❌ 不会删除字段(改名 = 加字段,旧字段一直在) // ❌ 不会修改字段类型(VARCHAR(50) → VARCHAR(100) 不生效) // ❌ 没有版本记录(不知道线上跑到哪个版本了) // ❌ 不能回滚(出问题只能手写 SQL 修复) // ❌ 多人协作冲突难发现 // 生产环境正确做法: // 1. 用 SQL 文件描述每次变更(向上 + 向下) // 2. 工具记录已执行的版本 // 3. 部署前检查、出错可回滚 ``` - 安装使用 golang-migrate - ```go # 安装 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 ``` - 迁移文件示例 - ```go -- 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 程序中嵌入迁移 - ```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 go get github.com/redis/go-redis/v9 ``` - 示例程序 - ```go 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: 缓存值、计数器 - ```go 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: 对象字段缓存 - ```go // 把一个 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 - ```go // 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:批量操作 - ```go // 一次往返发送多个命令,大幅减少网络开销 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(旁路缓存)是最经典的缓存模式 - 应用先查缓存,缓存没有再查数据库并回填缓存。简单、可靠、易于理解。 - 读流程 - ```go 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 } ``` - 写流程 - ```go // ✅ 推荐:先写 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. 简单(不用维护缓存的写一致性) ``` - 封装通用缓存层 - ```go // 用泛型封装一个通用的"查缓存 → 回源 → 回填"模式 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 }, ) ``` - 缓存三类问题 - 缓存穿透 - ```go // 问题:恶意请求大量不存在的 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 ``` - 缓存击穿 - ```go // 问题: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 } ``` - 缓存雪崩 - ```go // 问题:缓存预热时大批 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 压力过大时直接返回旧数据或默认值 ``` - 分布式锁 - ```go // 简化版分布式锁 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,你能做什么) - 两个核心概念 - ```go Authentication(认证 / AuthN) → 你是谁? → 例如:用户名密码登录、Token 校验、SSO Authorization(授权 / AuthZ) → 你被允许做什么? → 例如:管理员可以删用户,普通用户不行;A 用户只能编辑自己的文章 实际请求的处理顺序: HTTP Request → 中间件 1:解析 Token,确认身份(认证) → 中间件 2:检查权限,判断操作(授权) → 业务 Handler ``` - 无状态认证 vs 有状态 - ```go 传统 Session 方案: 登录后服务器内存(或 Redis)保存 sessionID → 用户信息 浏览器用 Cookie 携带 sessionID ✅ 服务端可主动失效 ❌ 服务端要存状态,多实例需要共享存储;跨域 Cookie 麻烦 JWT 方案(Stateless): 登录后服务器签发一个自包含 Token,里面带用户信息和过期时间 客户端把 Token 放在 Authorization Header 里 ✅ 服务端无状态,水平扩展友好;天然跨域 ❌ 签发后无法主动失效(需要黑名单或短 TTL + 刷新) 现代后端通常选 JWT,配合 Refresh Token 机制弥补失效问题。 ``` - golang-jwt 实战 - Go 生态最常用的库是 golang-jwt/jwt。我们封装一个 TokenService,统一管理签发和校验。 - 安装 - ```go go get github.com/golang-jwt/jwt/v5 ``` - 封装 TokenService - ```go 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 直接读取。 - 示例 - ```go 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 go get golang.org/x/crypto/bcrypt ``` - 示例 - ```go 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) - ```go 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 } ```