Go 复习

基础 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 的概念 - 每个类型都有一个确定的零值,声明变量时不赋值就是零值 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/04/18/20260418230943782.png,200,180) - ```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 线程 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/04/19/20260419160732615.png,310,100) - 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(总容量) ``` - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/04/20/20260420213936856.png,310,90) - 用有缓冲的场景 - 发送方和接收方速度不匹配,例如任务队列、批量处理 - 缓冲大小要谨慎,太大会掩盖背压问题(下游处理慢时上游应该被限速) - 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) } } ``` - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/04/20/20260420233148929.png,310,100) - 包管理与项目结构 - 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) } } ``` - 运行测试的常用命令 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/04/21/20260421000815190.png,310,160) - 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 } ```

四月 18, 2026

GoLang 开发环境配置

卸载已有的 Go 确认 Go 的安装路径,一般是官方路径 /usr/local/go 1 which go which 命令可以查看并显示指定命令的可执行文件路径,能帮助确定当前系统正在使用的命令安装在哪个位置。 ...

八月 23, 2025

常用 Go 命令

Go 命令 作用 go build 编译 go 文件 go run 编译并执行 go 文件 go fmt 格式化当前目录下的所有代码 go install 编译和安装包 go get 下载源码或者其他人的包 go test 运行单元测试

八月 23, 2025

Go 开发拾遗

变量 变量声明 声明变量 1 var a int = 6 // 修饰变量声明的关键字 变量名 变量类型 = 初始值 Go 会将变量名放在类型前。 ...

八月 21, 2025