<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Go on 安橙的博客</title><link>https://blog.ans20xx.com/tags/go/</link><description>Recent content in Go on 安橙的博客</description><generator>Hugo -- 0.161.1</generator><language>zh</language><lastBuildDate>Sat, 18 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.ans20xx.com/tags/go/index.xml" rel="self" type="application/rss+xml"/><item><title>Go 复习</title><link>https://blog.ans20xx.com/posts/tools/go-%E5%A4%8D%E4%B9%A0/</link><pubDate>Sat, 18 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog.ans20xx.com/posts/tools/go-%E5%A4%8D%E4%B9%A0/</guid><description>&lt;h1 id="基础"&gt;基础&lt;/h1&gt;
&lt;h2 id="go-环境搭建与工具链"&gt;Go 环境搭建与工具链&lt;/h2&gt;
&lt;div
class="mindmap-container"
id="mindmap-46821753"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-46821753" style="display:none;"&gt;
- 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 # 模块模式（应该是 &amp;#34;on&amp;#34;）
```
- 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 &amp;lt;模块名&amp;gt; 初始化一个新模块
- go run main.go 编译并直接运行(不生成二进制)
- go build 编译生成可执行文件
- go fmt ./... 格式化代码
- go test ./... 运行所有测试
- go get &amp;lt;包名&amp;gt; 添加依赖到当前模块
- go mod tidy 清理未使用的依赖
- go vet ./... 静态分析，检查常用错误
- go doc &amp;lt;包名&amp;gt;
&lt;/textarea&gt;
&lt;h2 id="类型系统与变量"&gt;类型系统与变量&lt;/h2&gt;
&lt;div
class="mindmap-container"
id="mindmap-48312765"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-48312765" style="display:none;"&gt;
- 类型系统与变量
- 基本类型总览
- 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 := &amp;#34;Hello你好&amp;#34;
// byte 视角：看到的是字节
fmt.Println(len(s)) // 11（5个ASCII &amp;#43; 6个中文字节）
// rune 视角：看到的是字符
fmt.Println(len([]rune(s))) // 7（5个英文字符 &amp;#43; 2个中文字符）
// for range 按 rune 遍历
for i, ch := range s {
fmt.Printf(&amp;#34;索引=%d 字符=%c\n&amp;#34;, i, ch)
}
// 索引=0 字符=H
// 索引=1 字符=e
// ...
// 索引=5 字符=你 ← 注意索引跳了
// 索引=8 字符=好
```
- 变量声明的四种方式
- 核心原则：函数内用 :=，函数外用 var
-
```go
// ① var &amp;#43; 类型（最完整的写法）
var name string = &amp;#34;Gopher&amp;#34;
// ② var &amp;#43; 类型推断（省略类型）
var age = 25 // Go 推断为 int
// ③ 短变量声明（最常用，只能在函数内）
city := &amp;#34;Tokyo&amp;#34; // 等价于 var city = &amp;#34;Tokyo&amp;#34;
// ④ 批量声明（常用于包级别变量）
var (
host = &amp;#34;localhost&amp;#34;
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&amp;#43;&amp;#43;
var name string // &amp;#34;&amp;#34;，可以直接 name &amp;#43;= &amp;#34;Go&amp;#34;
var ok bool // false
// 但 nil 类型需要初始化后才能使用！
var m map[string]int // nil，直接 m[&amp;#34;key&amp;#34;] = 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 = &amp;#34;MyApp&amp;#34;
// 批量声明
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 &amp;lt;&amp;lt; iota // 1 (001)
Write // 2 (010)
Execute // 4 (100)
)
// 组合权限
userPerm := Read | Write // 3 (011)
fmt.Println(userPerm &amp; Read != 0) // true，有读权限
fmt.Println(userPerm &amp; Execute != 0) // false，无执行权限
// 用法三：文件大小单位
const (
_ = iota
KB = 1 &amp;lt;&amp;lt; (10 * iota) // 1 &amp;lt;&amp;lt; 10 = 1024
MB // 1 &amp;lt;&amp;lt; 20
GB // 1 &amp;lt;&amp;lt; 30
TB // 1 &amp;lt;&amp;lt; 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 &amp;#34;strconv&amp;#34;
s := strconv.Itoa(42) // int → string: &amp;#34;42&amp;#34;
n, err := strconv.Atoi(&amp;#34;42&amp;#34;) // string → int: 42
f, err := strconv.ParseFloat(&amp;#34;3.14&amp;#34;, 64) // string → float64
// 字符串 ↔ 字节切片
bytes := []byte(&amp;#34;Hello&amp;#34;) // string → []byte
str := string(bytes) // []byte → string
// ⚠️ 精度丢失要注意
big := int64(1&amp;lt;&amp;lt;62)
small := int32(big) // 溢出！结果不可预测
```
- fmt.Sprintf 万能转字符串
- 性能不如 strconv，高频调用优先用 strconv
&lt;/textarea&gt;
&lt;h2 id="流程控制"&gt;流程控制&lt;/h2&gt;
&lt;div
class="mindmap-container"
id="mindmap-12753684"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-12753684" style="display:none;"&gt;
- 流程控制
- if/else 条件判断
- 条件不需要括号
- 允许在条件前加入一个初始化语句
- 基本语法
-
```Go
score := 85
if score &amp;gt;= 90 {
fmt.Println(&amp;#34;优秀&amp;#34;)
} else if score &amp;gt;= 60 {
fmt.Println(&amp;#34;及格&amp;#34;)
} else {
fmt.Println(&amp;#34;不及格&amp;#34;)
}
// 注意：
// 1. 条件不需要括号 (score &amp;gt;= 90) 写成 score &amp;gt;= 90
// 2. 大括号必须有，即使只有一行
// 3. else 必须和右大括号同一行
```
- 前置初始化语句 (GO 特色)
-
```Go
// 在条件中声明并使用变量
if err := doSomething(); err != nil {
fmt.Println(&amp;#34;出错了:&amp;#34;, err)
return
}
// err 的作用域仅限于 if-else 块内，块外不可见
// 对比：不用前置初始化的写法
err := doSomething()
if err != nil {
fmt.Println(&amp;#34;出错了:&amp;#34;, err)
return
}
// 这里 err 仍在作用域内，可能造成后续变量名冲突
```
- 前置初始化的好处
- 限制了变量的作用域
- 避免污染外部命名空间
- 在错误处理中常见
- for 循环
- Go 只有 for 一个循环关键字
- for 有多种形式，能覆盖其他语言的各种循环要求
- 经典三段式
-
```Go
for i := 0; i &amp;lt; 10; i&amp;#43;&amp;#43; {
fmt.Println(i)
}
```
- 类 while 循环 (只有条件)
-
```Go
n := 10
for n &amp;gt; 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(&amp;#34;索引=%d 值=%d\n&amp;#34;, i, v)
}
// 只要索引
for i := range nums {
fmt.Println(i)
}
// 只要值（用 _ 忽略索引）
for _, v := range nums {
fmt.Println(v)
}
// 遍历 map（顺序随机！）
m := map[string]int{&amp;#34;a&amp;#34;: 1, &amp;#34;b&amp;#34;: 2}
for k, v := range m {
fmt.Printf(&amp;#34;%s=%d\n&amp;#34;, k, v)
}
// 遍历字符串（按 rune）
for i, ch := range &amp;#34;Hello你好&amp;#34; {
fmt.Printf(&amp;#34;%d: %c\n&amp;#34;, i, ch)
}
// 遍历 channel（Day 9 会讲）
// for msg := range ch { ... }
```
- 循环复用坑
- 在 Go 1.22 之前，for-range 的循环变量是复用的，在 go routine 中捕获会出现问题
-
```Go
// Go 1.22&amp;#43; 这样写是安全的
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(&amp;#34;周一&amp;#34;)
case 2:
fmt.Println(&amp;#34;周二&amp;#34;)
case 3:
fmt.Println(&amp;#34;周三&amp;#34;)
// 默认自动 break，不会掉到 case 4
case 4, 5: // ✨ 多值匹配
fmt.Println(&amp;#34;周四或周五&amp;#34;)
case 6, 7:
fmt.Println(&amp;#34;周末&amp;#34;)
default:
fmt.Println(&amp;#34;无效&amp;#34;)
}
```
- 无条件 switch (if-else 链的优雅写法)
-
```go
score := 85
// 省略 switch 后的条件，相当于 switch true
switch {
case score &amp;gt;= 90:
fmt.Println(&amp;#34;A&amp;#34;)
case score &amp;gt;= 80:
fmt.Println(&amp;#34;B&amp;#34;)
case score &amp;gt;= 60:
fmt.Println(&amp;#34;C&amp;#34;)
default:
fmt.Println(&amp;#34;F&amp;#34;)
}
// 比 if-else 链更清晰
// 等价于：
// if score &amp;gt;= 90 { ... }
// else if score &amp;gt;= 80 { ... }
// ...
```
- 前置初始化语句
-
```go
switch os := runtime.GOOS; os {
case &amp;#34;linux&amp;#34;:
fmt.Println(&amp;#34;Linux&amp;#34;)
case &amp;#34;darwin&amp;#34;:
fmt.Println(&amp;#34;macOS&amp;#34;)
case &amp;#34;windows&amp;#34;:
fmt.Println(&amp;#34;Windows&amp;#34;)
default:
fmt.Printf(&amp;#34;未知系统: %s\n&amp;#34;, os)
}
```
- fallthrough:显式穿透
-
```go
// 如果真的需要穿透到下一个 case，用 fallthrough
switch n := 1; n {
case 1:
fmt.Println(&amp;#34;一&amp;#34;)
fallthrough // 继续执行下一个 case
case 2:
fmt.Println(&amp;#34;二&amp;#34;) // 会被打印
case 3:
fmt.Println(&amp;#34;三&amp;#34;) // 不会打印，fallthrough 只穿透一层
}
// 输出：一、二
```
- break、continue、goto
- 基础 break 和 continue
-
```go
for i := 0; i &amp;lt; 10; i&amp;#43;&amp;#43; {
if i == 3 {
continue // 跳过本次迭代
}
if i == 7 {
break // 退出循环
}
fmt.Println(i)
}
// 输出：0 1 2 4 5 6
```
- 标签：跳出嵌套循环
-
```go
// 问题：break 只能跳出一层循环
// 在嵌套循环中如何跳出外层？
OuterLoop:
for i := 0; i &amp;lt; 5; i&amp;#43;&amp;#43; {
for j := 0; j &amp;lt; 5; j&amp;#43;&amp;#43; {
if i*j &amp;gt; 6 {
break OuterLoop // 直接跳出外层循环
}
fmt.Printf(&amp;#34;%d*%d &amp;#34;, i, j)
}
}
// continue 也支持标签：continue OuterLoop 会回到外层循环的下一次迭代
```
- goto 存在，但是不建议用
- defer 延迟执行
- defer 是 Go 的标志性特性，用来注册「函数退出前必须执行」的逻辑
- 它彻底解决了资源清理、错误恢复等场景
- 基本行为：函数返回前执行
-
```go
func main() {
fmt.Println(&amp;#34;1. 开始&amp;#34;)
defer fmt.Println(&amp;#34;3. 延迟执行&amp;#34;)
fmt.Println(&amp;#34;2. 中间&amp;#34;)
}
// 输出：
// 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&amp;#43;&amp;#43;
// 即使后续代码 panic，锁也能释放
}
```
- 多个 defer: LIFO 栈顺序
-
```go
func main() {
defer fmt.Println(&amp;#34;1&amp;#34;)
defer fmt.Println(&amp;#34;2&amp;#34;)
defer fmt.Println(&amp;#34;3&amp;#34;)
fmt.Println(&amp;#34;main&amp;#34;)
}
// 输出：
// main
// 3 ← 后进先出（栈）
// 2
// 1
```
- defer 语句参数
- defer 语句的参数在注册是就计算好，不是执行时
-
```go
func main() {
x := 10
defer fmt.Println(&amp;#34;defer:&amp;#34;, x) // x 的值 10 被立即捕获
x = 20
fmt.Println(&amp;#34;main:&amp;#34;, x)
}
// 输出：
// main: 20
// defer: 10 ← 不是 20！
// 如果想用最终值，用闭包：
func main() {
x := 10
defer func() {
fmt.Println(&amp;#34;defer:&amp;#34;, x) // 闭包捕获变量引用
}()
x = 20
}
// 输出 defer: 20
```
&lt;/textarea&gt;
&lt;h2 id="函数"&gt;函数&lt;/h2&gt;
&lt;div
class="mindmap-container"
id="mindmap-63781245"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-63781245" style="display:none;"&gt;
- 函数深入
- 函数基础
- Go 的函数定义和 C 系列语言不一样：关键字 func 在前，返回值类型在后
-
```go
// 基本语法：func 函数名(参数列表) 返回值类型 { 函数体 }
func add(a int, b int) int {
return a &amp;#43; b
}
// 参数同类型可以合并声明
func add2(a, b int) int {
return a &amp;#43; b
}
// 无返回值
func greet(name string) {
fmt.Println(&amp;#34;Hello,&amp;#34;, name)
}
// 无参数也无返回值
func printVersion() {
fmt.Println(&amp;#34;v1.0.0&amp;#34;)
}
```
- 函数是一等公民
- 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 &amp;#34;&amp;#34;, err // 失败时返回零值 &amp;#43; 错误
}
return string(data), nil // 成功时返回结果 &amp;#43; nil
}
// 调用方的标准模式
content, err := readFile(&amp;#34;hello.txt&amp;#34;)
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(&amp;#34;除数不能为零&amp;#34;)
}
return a / b, nil
}
// 命名返回值版本
func divide2(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New(&amp;#34;除数不能为零&amp;#34;)
return // 裸 return，自动返回 result=0, err=上面的值
}
result = a / b
return // 自动返回 result, err
}
```
- 命名返回值 &amp;#43; defer
-
```go
// 在 defer 中修改返回值
func doWork() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf(&amp;#34;恢复自 panic: %v&amp;#34;, r)
// ✨ 这里能修改返回值 err！
}
}()
// 可能 panic 的代码
panic(&amp;#34;something bad&amp;#34;)
}
result := doWork() // result 不是 nil，而是包装后的 error
```
- 什么时候用命名返回值
- 返回值很多且含义复杂时，命名可以当文档
- 需要在 defer 中修改返回值时（recover 模式）
- 可变参数函数
- 用 ...T 表示可变参数，函数内部会收到一个 []T 类型的切片
- fmt.Println 就是典型的可变参数函数。
- 示例
-
```go
// 求任意个数字的和
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total &amp;#43;= 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(&amp;#34;%s, %s!\n&amp;#34;, greeting, name)
}
}
greet(&amp;#34;Hello&amp;#34;, &amp;#34;Alice&amp;#34;, &amp;#34;Bob&amp;#34;, &amp;#34;Charlie&amp;#34;)
// Hello, Alice!
// Hello, Bob!
// Hello, Charlie!
```
- 闭包与匿名函数
- 闭包是能访问外部作用域变量的函数
- Go 的闭包语法简洁，是实现迭代器、中间件、回调等模式的基础
- 匿名函数
-
```go
// 定义后立即调用（IIFE）
func() {
fmt.Println(&amp;#34;我是匿名函数&amp;#34;)
}()
// 赋值给变量
add := func(a, b int) int {
return a &amp;#43; b
}
fmt.Println(add(1, 2)) // 3
```
- 闭包：捕获外部变量
-
```go
// 计数器生成器
func makeCounter() func() int {
count := 0
return func() int {
count&amp;#43;&amp;#43; // ✨ 闭包捕获了外部的 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(&amp;#34;调用 %s(%d)\n&amp;#34;, name, x)
result := fn(x)
fmt.Printf(&amp;#34;%s(%d) = %d\n&amp;#34;, name, x, result)
return result
}
}
double := func(x int) int { return x * 2 }
loggedDouble := withLogging(&amp;#34;double&amp;#34;, 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 &amp;#34;errors&amp;#34;
err := errors.New(&amp;#34;文件不存在&amp;#34;)
// 方式二：fmt.Errorf（支持格式化）
err = fmt.Errorf(&amp;#34;文件 %s 不存在&amp;#34;, filename)
// 方式三：自定义类型（后续再深入）
```
- 标准返回模式
-
```go
// ✅ Go 惯用写法：err 作为最后一个返回值
func findUser(id int) (*User, error) {
if id &amp;lt; 0 {
return nil, fmt.Errorf(&amp;#34;无效 ID: %d&amp;#34;, id)
}
user, ok := userCache[id]
if !ok {
return nil, errors.New(&amp;#34;用户不存在&amp;#34;)
}
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)
}
```
&lt;/textarea&gt;
&lt;h2 id="数组切片与-map"&gt;数组、切片与 Map&lt;/h2&gt;
&lt;div
class="mindmap-container"
id="mindmap-72816345"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-72816345" style="display:none;"&gt;
- 数组、切片与 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(&amp;#34;b[%d] = %d\n&amp;#34;, 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&amp;#43; 简化）：
// cap &amp;lt; 256：翻倍（1 → 2 → 4 → 8 ...）
// cap &amp;gt;= 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 &amp;lt; 1000; i&amp;#43;&amp;#43; {
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&amp;#43;1:]...)
// s = [1 2 4 5]
// Go 1.21&amp;#43; 用 slices.Delete
import &amp;#34;slices&amp;#34;
s = slices.Delete(s, i, i&amp;#43;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&amp;#43; 用 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&amp;#43; 最简洁
dst := slices.Clone(src)
// 排序
import &amp;#34;sort&amp;#34;
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&amp;#43; 更简洁
slices.Sort(nums)
slices.Reverse(nums)
// 自定义排序
sort.Slice(nums, func(i, j int) bool {
return nums[i] &amp;gt; nums[j] // 降序
})
```
- map: 键值对容器
- Go 的 map 是哈希表实现，支持任何可比较类型作为 key
- 使用前必须初始化，nil map 不能写入
- 声明与初始化
-
```go
// ❌ 错误：只声明，没初始化
var m1 map[string]int
m1[&amp;#34;a&amp;#34;] = 1 // panic: assignment to entry in nil map
// ✅ 方式一：make
m2 := make(map[string]int)
m2[&amp;#34;a&amp;#34;] = 1
// ✅ 方式二：字面量
m3 := map[string]int{
&amp;#34;one&amp;#34;: 1,
&amp;#34;two&amp;#34;: 2,
&amp;#34;three&amp;#34;: 3,
}
// ✅ 方式三：make 指定初始容量（性能优化）
m4 := make(map[string]int, 1000) // 提前分配空间
```
- 增删改查
-
```go
m := map[string]int{&amp;#34;a&amp;#34;: 1, &amp;#34;b&amp;#34;: 2}
// 增/改（语法相同）
m[&amp;#34;c&amp;#34;] = 3 // 新增
m[&amp;#34;a&amp;#34;] = 100 // 修改
// 查
v := m[&amp;#34;a&amp;#34;] // 100
missing := m[&amp;#34;x&amp;#34;] // 0（不存在时返回零值！）
// ✨ 判断 key 是否存在：逗号 ok 惯用法
v, ok := m[&amp;#34;a&amp;#34;] // v=100, ok=true
v, ok := m[&amp;#34;x&amp;#34;] // v=0, ok=false
if ok {
fmt.Println(&amp;#34;存在&amp;#34;)
}
// 删除（不存在也不会报错）
delete(m, &amp;#34;a&amp;#34;)
// 长度
fmt.Println(len(m))
```
- 遍历(顺序随机)
-
```go
m := map[string]int{&amp;#34;a&amp;#34;: 1, &amp;#34;b&amp;#34;: 2, &amp;#34;c&amp;#34;: 3}
// 每次遍历顺序都可能不同！
for k, v := range m {
fmt.Printf(&amp;#34;%s=%d\n&amp;#34;, 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(&amp;#34;%s=%d\n&amp;#34;, k, m[k])
}
```
- map 进阶：零值技巧与并发
- 零值技巧：计数器模式
-
```go
// map 访问不存在的 key 返回零值，这特性很好用
words := []string{&amp;#34;go&amp;#34;, &amp;#34;rust&amp;#34;, &amp;#34;go&amp;#34;, &amp;#34;python&amp;#34;, &amp;#34;go&amp;#34;, &amp;#34;rust&amp;#34;}
count := make(map[string]int)
for _, w := range words {
count[w]&amp;#43;&amp;#43; // ✨ 不存在时 count[w] 是 0，&amp;#43;1 变成 1
}
// count = {&amp;#34;go&amp;#34;: 3, &amp;#34;rust&amp;#34;: 2, &amp;#34;python&amp;#34;: 1}
// 分组：map &amp;#43; 切片
type User struct {
Name string
City string
}
users := []User{
{&amp;#34;Alice&amp;#34;, &amp;#34;Tokyo&amp;#34;},
{&amp;#34;Bob&amp;#34;, &amp;#34;Tokyo&amp;#34;},
{&amp;#34;Charlie&amp;#34;, &amp;#34;Osaka&amp;#34;},
}
byCity := make(map[string][]User)
for _, u := range users {
byCity[u.City] = append(byCity[u.City], u)
}
// byCity[&amp;#34;Tokyo&amp;#34;] = [{Alice Tokyo} {Bob Tokyo}]
```
- map 作为集合
-
```go
// Go 没有内置 Set，用 map[T]struct{} 模拟
set := map[string]struct{}{}
// 添加
set[&amp;#34;apple&amp;#34;] = struct{}{}
set[&amp;#34;banana&amp;#34;] = struct{}{}
// 判断存在
_, ok := set[&amp;#34;apple&amp;#34;] // true
// 删除
delete(set, &amp;#34;apple&amp;#34;)
// 为什么用 struct{} 而不是 bool？
// struct{} 不占内存（0 字节），比 bool（1字节）更省空间
```
- map 不是并发安全的
-
```go
// ❌ 多个 goroutine 同时读写 map 会 panic！
m := make(map[string]int)
go func() { m[&amp;#34;a&amp;#34;] = 1 }()
go func() { m[&amp;#34;b&amp;#34;] = 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[&amp;#34;a&amp;#34;] = 1
}()
// ✅ 方式二：sync.Map（适合读多写少场景）
var sm sync.Map
sm.Store(&amp;#34;a&amp;#34;, 1)
v, ok := sm.Load(&amp;#34;a&amp;#34;)
sm.Delete(&amp;#34;a&amp;#34;)
sm.Range(func(k, v any) bool {
fmt.Println(k, v)
return true // 返回 false 停止遍历
})
```
&lt;/textarea&gt;
&lt;h2 id="结构体与方法"&gt;结构体与方法&lt;/h2&gt;
&lt;div
class="mindmap-container"
id="mindmap-26357148"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-26357148" style="display:none;"&gt;
- 结构体与方法
- 结构体基础
- 结构体是 Go 中组织数据的核心方式
- Go 没有 class，但 struct &amp;#43; 方法已经能表达几乎所有面向对象的特性，而且更简单直接
- 定义与初始化
-
```go
// 定义结构体
type User struct {
ID int
Name string
Email string
Active bool
}
// 初始化方式一：字段名（推荐，最清晰）
u1 := User{
ID: 1,
Name: &amp;#34;Alice&amp;#34;,
Email: &amp;#34;alice@example.com&amp;#34;,
Active: true,
}
// 初始化方式二：按字段顺序（脆弱，字段增减会出问题）
u2 := User{2, &amp;#34;Bob&amp;#34;, &amp;#34;bob@example.com&amp;#34;, false}
// 初始化方式三：零值
var u3 User // 所有字段都是零值：{0, &amp;#34;&amp;#34;, &amp;#34;&amp;#34;, false}
u3.Name = &amp;#34;Charlie&amp;#34;
// 初始化方式四：new（返回指针）
u4 := new(User) // 等价于 &amp;User{}
u4.Name = &amp;#34;David&amp;#34;
```
- 访问和修改字段
-
```go
u := User{Name: &amp;#34;Alice&amp;#34;}
// 访问字段
fmt.Println(u.Name)
// 修改字段
u.Name = &amp;#34;Alicia&amp;#34;
// 指针也用 . 访问（Go 自动解引用）
p := &amp;u
p.Name = &amp;#34;Alice 2&amp;#34; // 等价于 (*p).Name，Go 帮你省了*
fmt.Println(u.Name) // Alice 2
```
- 字段名大小写
- 首字母大写的字段是导出的（public），可以被包外访问
- 小写的是未导出的（private），只能包内使用
- 值类型 vs 指针
- struct 是值类型。赋值、传参、返回都是完整拷贝
-
```go
u1 := User{Name: &amp;#34;Alice&amp;#34;}
u2 := u1 // ✨ 完全拷贝！u1 和 u2 是两个独立对象
u2.Name = &amp;#34;Bob&amp;#34;
fmt.Println(u1.Name) // Alice（没变）
fmt.Println(u2.Name) // Bob
// 传入函数也是拷贝
func modify(u User) {
u.Name = &amp;#34;Changed&amp;#34; // 只修改了拷贝
}
modify(u1)
fmt.Println(u1.Name) // Alice（没变！）
// 想修改原对象？传指针
func modifyPtr(u *User) {
u.Name = &amp;#34;Changed&amp;#34;
}
modifyPtr(&amp;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 &amp;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 &amp;#43; 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&amp;#43;&amp;#43; // 只修改了副本
}
// ✅ 指针接收者：修改原对象
func (c *Counter) Increment() {
c.count&amp;#43;&amp;#43;
}
// ✅ 只读方法用值接收者
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
```
- 选择接受者类型的原则
- 需要修改对象 -&amp;gt; 必须用指针接收者
- 结构体很大 -&amp;gt; 用指针避免拷贝
- 包含 sync.Mutex 等不可拷贝字段 -&amp;gt; 必须用指针
- 其他情况两者都行，但是一个类型的方法应该统一
- 要么全值接收者，要么全指针接收者
- go 的自动转换
-
```go
// Go 会在值和指针之间自动转换，调用方法时不用纠结
c := Counter{} // 值
c.Increment() // ✨ Go 自动取地址 (&amp;c).Increment()
c.Get() // 值调用值方法，直接
p := &amp;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 &amp;#43; 32
}
func (c Celsius) String() string {
return fmt.Sprintf(&amp;#34;%.1f°C&amp;#34;, float64(c))
}
// 给切片起别名并加方法
type IntSlice []int
func (s IntSlice) Sum() int {
total := 0
for _, n := range s {
total &amp;#43;= n
}
return total
}
func (s IntSlice) Max() int {
if len(s) == 0 {
return 0
}
m := s[0]
for _, n := range s[1:] {
if n &amp;gt; 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(&amp;#34;%s, %d岁&amp;#34;, a.Name, a.Age)
}
// Dog 嵌入 Animal
type Dog struct {
Animal // ✨ 嵌入字段（没有字段名）
Breed string
}
d := Dog{
Animal: Animal{Name: &amp;#34;旺财&amp;#34;, Age: 3},
Breed: &amp;#34;柴犬&amp;#34;,
}
// 直接访问嵌入字段的属性和方法（提升）
fmt.Println(d.Name) // &amp;#34;旺财&amp;#34;（不用 d.Animal.Name）
fmt.Println(d.Describe()) // &amp;#34;旺财, 3岁&amp;#34;（继承了 Animal 的方法）
fmt.Println(d.Breed) // &amp;#34;柴犬&amp;#34;
// 也可以显式访问
fmt.Println(d.Animal.Name) // 同上
```
- 方法覆盖
-
```go
// Dog 可以定义同名方法「覆盖」Animal 的
func (d Dog) Describe() string {
// 可以调用被覆盖的方法
base := d.Animal.Describe()
return fmt.Sprintf(&amp;#34;%s，品种：%s&amp;#34;, base, d.Breed)
}
d := Dog{Animal: Animal{Name: &amp;#34;旺财&amp;#34;, Age: 3}, Breed: &amp;#34;柴犬&amp;#34;}
fmt.Println(d.Describe()) // &amp;#34;旺财, 3岁，品种：柴犬&amp;#34;
```
- 嵌入接口
-
```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(&amp;#34;SQL: %s&amp;#34;, 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(),
}
}
// 更常见：返回指针 &amp;#43; error
func NewUser(name, email string) (*User, error) {
if name == &amp;#34;&amp;#34; {
return nil, errors.New(&amp;#34;name 不能为空&amp;#34;)
}
if !strings.Contains(email, &amp;#34;@&amp;#34;) {
return nil, fmt.Errorf(&amp;#34;无效的 email: %s&amp;#34;, email)
}
return &amp;User{
Name: name,
Email: email,
CreatedAt: time.Now(),
}, nil
}
// 使用
u, err := NewUser(&amp;#34;Alice&amp;#34;, &amp;#34;alice@example.com&amp;#34;)
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 := &amp;Server{
host: host,
port: 80,
timeout: 30 * time.Second,
tls: false,
}
// 应用选项
for _, opt := range opts {
opt(s)
}
return s
}
// 使用：非常灵活
s1 := NewServer(&amp;#34;localhost&amp;#34;)
s2 := NewServer(&amp;#34;example.com&amp;#34;, WithPort(443), WithTLS())
s3 := NewServer(&amp;#34;api.com&amp;#34;, WithTimeout(10*time.Second))
```
- 函数选项模式的威力
- 这个模式在 Go 生态广泛使用（gRPC、Kubernetes、各种库的配置都是这个模式）
- 优势：可选参数不用硬编码、扩展性好（加新选项不破坏现有代码）、可读性强（调用方清楚写出每个选项）
- 结构体标签
- Struct Tag 是字段后面的反引号字符串，为字段附加元数据
- JSON 序列化、数据库映射、表单校验都用它
- 示例
-
```go
type User struct {
ID int `json:&amp;#34;id&amp;#34;`
Name string `json:&amp;#34;name&amp;#34; validate:&amp;#34;required,min=2&amp;#34;`
Email string `json:&amp;#34;email&amp;#34; validate:&amp;#34;required,email&amp;#34;`
Password string `json:&amp;#34;-&amp;#34;` // - 表示不参与 JSON 序列化
Age int `json:&amp;#34;age,omitempty&amp;#34;` // omitempty：零值时忽略
Role string `json:&amp;#34;role&amp;#34; db:&amp;#34;user_role&amp;#34;` // 同一字段可有多个 tag
}
// JSON 序列化
u := User{
ID: 1,
Name: &amp;#34;Alice&amp;#34;,
Email: &amp;#34;alice@example.com&amp;#34;,
Password: &amp;#34;secret&amp;#34;,
// Age 是 0，会被忽略
}
data, _ := json.Marshal(u)
fmt.Println(string(data))
// {&amp;#34;id&amp;#34;:1,&amp;#34;name&amp;#34;:&amp;#34;Alice&amp;#34;,&amp;#34;email&amp;#34;:&amp;#34;alice@example.com&amp;#34;,&amp;#34;role&amp;#34;:&amp;#34;&amp;#34;}
// 注意：password 不在输出中，age 也被忽略
```
- 常见 Tag 用法一览
-
```go
type Article struct {
// encoding/json
ID int `json:&amp;#34;id&amp;#34;`
Title string `json:&amp;#34;title&amp;#34;`
Content string `json:&amp;#34;content,omitempty&amp;#34;` // 空时忽略
Internal string `json:&amp;#34;-&amp;#34;` // 从不序列化
// GORM（Day 17-18）
CreatedAt time.Time `gorm:&amp;#34;autoCreateTime&amp;#34;`
Slug string `gorm:&amp;#34;uniqueIndex;size:100&amp;#34;`
// validator（参数校验）
Author string `validate:&amp;#34;required,min=2,max=50&amp;#34;`
Views int `validate:&amp;#34;gte=0&amp;#34;`
// 组合使用
Email string `json:&amp;#34;email&amp;#34; validate:&amp;#34;required,email&amp;#34; db:&amp;#34;user_email&amp;#34;`
}
```
- tag 只是字符串
- Tag 本身只是给字段附加的字符串元数据，语言本身不处理
- 是各个库（encoding/json、gorm、validator...）通过反射读取 tag 并做相应处理
- tag 的语法完全由使用它的库定义
&lt;/textarea&gt;
&lt;h2 id="接口与多态"&gt;接口与多态&lt;/h2&gt;
&lt;div
class="mindmap-container"
id="mindmap-21375648"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-21375648" style="display:none;"&gt;
- 接口与多态
- 接口是什么
- 接口是 Go 最优雅的特性。
- 它定义了一组方法签名，任何实现了这些方法的类型都「自动」满足这个接口——不需要显式声明 implements。
- 这叫做「隐式接口实现」，也叫鸭子类型。
- 接口示例
-
```go
// 定义接口：只有方法签名，没有实现
type Animal interface {
Sound() string
Name() string
}
// Dog 实现了 Animal 接口
// 注意：不需要写 &amp;#34;implements Animal&amp;#34;！
type Dog struct{ name string }
func (d Dog) Sound() string { return &amp;#34;汪汪&amp;#34; }
func (d Dog) Name() string { return d.name }
// Cat 也实现了 Animal 接口
type Cat struct{ name string }
func (c Cat) Sound() string { return &amp;#34;喵喵&amp;#34; }
func (c Cat) Name() string { return c.name }
// 函数接受接口类型
func Describe(a Animal) {
fmt.Printf(&amp;#34;%s 说：%s\n&amp;#34;, a.Name(), a.Sound())
}
func main() {
dog := Dog{name: &amp;#34;旺财&amp;#34;}
cat := Cat{name: &amp;#34;咪咪&amp;#34;}
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: &amp;#34;旺财&amp;#34;}
// a 现在是 (Dog, {name:&amp;#34;旺财&amp;#34;})
// 打印类型和值
fmt.Printf(&amp;#34;类型: %T\n&amp;#34;, a) // main.Dog
fmt.Printf(&amp;#34;值: %v\n&amp;#34;, 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: &amp;#34;旺财&amp;#34;}
// 方式一：直接断言（不安全，失败会 panic）
d := a.(Dog)
fmt.Println(d.name) // 旺财
// 方式二：逗号 ok（推荐，安全）
d, ok := a.(Dog)
if ok {
fmt.Println(&amp;#34;是 Dog:&amp;#34;, d.name)
} else {
fmt.Println(&amp;#34;不是 Dog&amp;#34;)
}
// 断言为接口（检查是否实现了另一个接口）
type Swimmer interface {
Swim() string
}
if s, ok := a.(Swimmer); ok {
fmt.Println(s.Swim())
} else {
fmt.Println(&amp;#34;不会游泳&amp;#34;)
}
```
- type switch
-
```go
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf(&amp;#34;整数: %d，翻倍是 %d\n&amp;#34;, v, v*2)
case string:
fmt.Printf(&amp;#34;字符串: %q，长度 %d\n&amp;#34;, v, len(v))
case bool:
fmt.Printf(&amp;#34;布尔值: %t\n&amp;#34;, v)
case []int:
fmt.Printf(&amp;#34;整数切片，长度 %d\n&amp;#34;, len(v))
case nil:
fmt.Println(&amp;#34;是 nil&amp;#34;)
default:
fmt.Printf(&amp;#34;未知类型: %T\n&amp;#34;, v)
}
}
describe(42) // 整数: 42，翻倍是 84
describe(&amp;#34;hello&amp;#34;) // 字符串: &amp;#34;hello&amp;#34;，长度 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(&amp;#34;(%d, %d)&amp;#34;, p.X, p.Y)
}
p := Point{3, 4}
fmt.Println(p) // (3, 4) ← 自动调用 String()
fmt.Printf(&amp;#34;%v\n&amp;#34;, p) // (3, 4)
fmt.Printf(&amp;#34;%s\n&amp;#34;, 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(&amp;#34;字段 %s 验证失败: %s&amp;#34;, e.Field, e.Message)
}
// 使用
func validateAge(age int) error {
if age &amp;lt; 0 || age &amp;gt; 150 {
return &amp;ValidationError{
Field: &amp;#34;age&amp;#34;,
Message: fmt.Sprintf(&amp;#34;值 %d 超出合法范围 [0, 150]&amp;#34;, age),
}
}
return nil
}
err := validateAge(-1)
if err != nil {
fmt.Println(err) // 字段 age 验证失败: 值 -1 超出合法范围 [0, 150]
// 用类型断言获取详细信息
if ve, ok := err.(*ValidationError); ok {
fmt.Println(&amp;#34;出问题的字段:&amp;#34;, 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(&amp;#34;input.txt&amp;#34;)
buf := &amp;bytes.Buffer{}
copyData(buf, file) // 文件 → 内存
copyData(os.Stdout, buf) // 内存 → 标准输出
copyData(os.Stdout, strings.NewReader(&amp;#34;hello&amp;#34;)) // 字符串 → 标准输出
// 自己实现 io.Writer（比如日志收集器）
type LogWriter struct {
prefix string
}
func (w *LogWriter) Write(p []byte) (int, error) {
fmt.Printf(&amp;#34;[%s] %s&amp;#34;, w.prefix, p)
return len(p), nil
}
lw := &amp;LogWriter{prefix: &amp;#34;INFO&amp;#34;}
fmt.Fprintf(lw, &amp;#34;服务器启动在端口 %d\n&amp;#34;, 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 = &amp;#34;hello&amp;#34;
x = []int{1, 2, 3}
x = struct{ Name string }{&amp;#34;Alice&amp;#34;}
// 常见于需要处理任意类型的场景
func printAll(values ...any) {
for _, v := range values {
fmt.Printf(&amp;#34;%T: %v\n&amp;#34;, v, v)
}
}
printAll(1, &amp;#34;hello&amp;#34;, true, 3.14)
// int: 1
// string: hello
// bool: true
// float64: 3.14
// map 存储任意类型（类似 JSON 对象）
config := map[string]any{
&amp;#34;host&amp;#34;: &amp;#34;localhost&amp;#34;,
&amp;#34;port&amp;#34;: 8080,
&amp;#34;debug&amp;#34;: true,
&amp;#34;timeout&amp;#34;: 30.5,
}
```
- 使用 any 的代价
-
```go
// any 丢失了类型信息，使用时必须断言
var v any = 42
// ❌ 不能直接运算
// fmt.Println(v &amp;#43; 1) // 编译错误
// ✅ 先断言再使用
if n, ok := v.(int); ok {
fmt.Println(n &amp;#43; 1) // 43
}
// any 的性能也比具体类型差（有装箱开销）
// 能用泛型（Day 14）就用泛型，不要滥用 any
// 合理使用场景：
// 1. JSON 解析（结构未知时）
// 2. 通用容器（在泛型之前的历史代码）
// 3. fmt.Println 这种需要接受任意值的工具函数
```
- any 不是银弹
- any 让你绕过了类型系统，失去了编译器的保护
- 能用具体类型就用具体类型，能用泛型就用泛型（Go 1.18&amp;#43;），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 &amp;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(&amp;#34;not found&amp;#34;)
}
return u, nil
}
func (m *MockStore) CreateUser(name, email string) (*User, error) {
u := &amp;User{ID: len(m.users) &amp;#43; 1, Name: name, Email: email}
m.users[u.ID] = u
return u, nil
}
// 测试
func TestHandler(t *testing.T) {
mock := &amp;MockStore{users: map[int]*User{
1: {ID: 1, Name: &amp;#34;Alice&amp;#34;},
}}
h := NewHandler(mock) // 注入 Mock
// ... 测试 handler 逻辑，完全不依赖数据库
}
```
- 接口是 Go 依赖注入的基础
- Go 不需要 Spring 这样的 DI 框架
- 接口 &amp;#43; 构造函数注入就够了
- 生产环境注入真实实现，测试时注入 Mock
- 这让代码既解耦又易测试
- 这是 Go 后端项目的标准架构模式
&lt;/textarea&gt;
&lt;h1 id="并发与工程化"&gt;并发与工程化&lt;/h1&gt;
&lt;div
class="mindmap-container"
id="mindmap-58726134"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-58726134" style="display:none;"&gt;
- 并发与工程化
- Goroutine 入门
- Goroutine 是什么
- Goroutine 是 Go 并发的核心——比线程轻量得多的「协程」
- 启动一个 Goroutine 只需要在函数调用前加 go 关键字
- 示例
-
```go
package main
import (
&amp;#34;fmt&amp;#34;
&amp;#34;time&amp;#34;
)
func say(s string) {
for i := 0; i &amp;lt; 3; i&amp;#43;&amp;#43; {
fmt.Println(s)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go say(&amp;#34;世界&amp;#34;) // 新 goroutine 中运行
say(&amp;#34;你好&amp;#34;) // 当前 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 &amp;#34;runtime&amp;#34;
runtime.GOMAXPROCS(4) // 设置使用 4 个 P
fmt.Println(runtime.GOMAXPROCS(0)) // 查看当前 P 数量
fmt.Println(runtime.NumCPU()) // CPU 核数
// Go 1.5&amp;#43; 默认 GOMAXPROCS = NumCPU
// 大多数时候不需要手动设置
```
- 调度模型示意
-
```
P1 [G1→G2→G3] P2 [G4→G5→G6]
↓ ↓
M1 M2
↓ ↓
OS Thread OS Thread
当 G 执行系统调用阻塞时，P 会把 M 换掉继续跑其他 G
这叫&amp;#34;work stealing&amp;#34;（工作窃取）——空闲的 P 会从别的 P 偷 G 来跑
```
- sync.WaitGroup 等待协程完成
- 启动 Goroutine 后，main 函数不会自动等待它们结束
- 如果 main 退出，所有 Goroutine 都会被强制终止
- WaitGroup 是等待一组 Goroutine 完成的标准方式
- 示例
-
```go
// ❌ 错误：main 退出，goroutine 还没跑完
func main() {
go fmt.Println(&amp;#34;可能跑不到&amp;#34;)
// main 立即结束，goroutine 被杀死
}
// ✅ 用 WaitGroup 等待
import &amp;#34;sync&amp;#34;
func main() {
var wg sync.WaitGroup
for i := 0; i &amp;lt; 5; i&amp;#43;&amp;#43; {
wg.Add(1) // 计数器 &amp;#43;1
go func(id int) {
defer wg.Done() // 函数结束时计数器 -1
fmt.Printf(&amp;#34;Worker %d 完成\n&amp;#34;, id)
}(i)
}
wg.Wait() // 阻塞，直到计数器归零
fmt.Println(&amp;#34;所有 Worker 完成&amp;#34;)
}
```
- 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(&amp;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 := &amp;lt;-ch // 永远阻塞！没人往 ch 发数据
fmt.Println(val)
}()
// 函数返回，但 goroutine 永远卡在这
}
// ❌ 泄漏二：没有退出机制的无限循环
func leak2() {
go func() {
for {
doWork() // 没有 break/return/cancel 条件
}
}()
}
// ❌ 泄漏三：阻塞的 HTTP 请求没有超时
func leak3() {
go func() {
resp, _ := http.Get(&amp;#34;http://slow-server.com&amp;#34;)
// 如果服务器不响应，goroutine 永远等着
_ = resp
}()
}
```
- 正确的退出机制
-
```go
// ✅ 用 done channel 通知退出
func worker(done &amp;lt;-chan struct{}) {
for {
select {
case &amp;lt;-done:
fmt.Println(&amp;#34;收到退出信号，正在退出&amp;#34;)
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 &amp;lt;-ctx.Done():
fmt.Println(&amp;#34;超时退出:&amp;#34;, ctx.Err())
return
case result := &amp;lt;-doAsyncWork():
fmt.Println(&amp;#34;完成:&amp;#34;, result)
}
}()
```
- 检测泄露工具 goleak
-
```go
// 在测试中检测 goroutine 泄漏
import &amp;#34;go.uber.org/goleak&amp;#34;
func TestNoLeak(t *testing.T) {
defer goleak.VerifyNone(t) // 测试结束时检查是否有泄漏的 goroutine
// 你的业务代码
runSomeTask()
}
// 也可以用运行时查看当前 goroutine 数量
import &amp;#34;runtime&amp;#34;
fmt.Println(&amp;#34;goroutine 数量:&amp;#34;, 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 &amp;lt; 1000; i&amp;#43;&amp;#43; {
wg.Add(1)
go func() {
defer wg.Done()
counter&amp;#43;&amp;#43; // ❌ 非原子操作，存在竞争！
}()
}
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&amp;#43;&amp;#43;
}()
// ✅ 方案二：atomic 原子操作（更快，适合简单计数）
import &amp;#34;sync/atomic&amp;#34;
var counter int64
go func() {
atomic.AddInt64(&amp;counter, 1)
}()
result := atomic.LoadInt64(&amp;counter)
// ✅ 方案三：用 channel 传递数据（不共享状态）
// 这是最地道的 Go 风格（Day 9 详讲）
ch := make(chan int, 1000)
for i := 0; i &amp;lt; 1000; i&amp;#43;&amp;#43; {
go func() { ch &amp;lt;- 1 }()
}
total := 0
for i := 0; i &amp;lt; 1000; i&amp;#43;&amp;#43; {
total &amp;#43;= &amp;lt;-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&amp;#43;&amp;#43;
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// 使用
counter := &amp;SafeCounter{}
var wg sync.WaitGroup
for i := 0; i &amp;lt; 1000; i&amp;#43;&amp;#43; {
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 = &amp;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 &amp;lt;- value
ch &amp;lt;- 42
// 接收数据：value := &amp;lt;-ch
v := &amp;lt;-ch
// 关闭 channel
close(ch)
// 判断 channel 是否关闭
v, ok := &amp;lt;-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 &amp;lt;- &amp;#34;Hello from goroutine!&amp;#34;
}()
// 接收方（主 goroutine）
msg := &amp;lt;-ch
fmt.Println(msg) // Hello from goroutine!
}
// ✨ 不需要 WaitGroup！
// &amp;lt;-ch 会阻塞，直到有数据，天然同步
```
- Channel 的方向是通信，不是存储
- Channel 不是消息队列，不是用来存数据的
- 它是两个 Goroutine 之间的「传送带」，一端发送，另一端接收，数据在两者之间流动
- 无缓冲 vs 有缓冲
- 无缓冲和有缓冲 channel 的行为差异很大，选错会导致死锁或意外的并发行为。
- 无缓冲 Channel (同步)
-
```go
ch := make(chan int) // 无缓冲
// 发送方和接收方必须同时就绪，否则阻塞
// 就像打电话：双方必须同时在线
// ❌ 死锁：同一个 goroutine 又发又收
ch &amp;lt;- 1 // 阻塞等待接收方
v := &amp;lt;-ch // 永远不会执行到这里
// fatal error: all goroutines are asleep - deadlock!
// ✅ 正确：发送和接收在不同 goroutine
go func() { ch &amp;lt;- 42 }() // 另一个 goroutine 发送
v := &amp;lt;-ch // 主 goroutine 接收（会等待）
fmt.Println(v) // 42
```
- 有缓冲 Channel (异步)
-
```go
ch := make(chan int, 3) // 缓冲容量 3
// 就像发短信：发完不用等对方读
// 只要缓冲区没满，发送不阻塞
ch &amp;lt;- 1 // 不阻塞
ch &amp;lt;- 2 // 不阻塞
ch &amp;lt;- 3 // 不阻塞
ch &amp;lt;- 4 // 阻塞！缓冲区已满
// 缓冲区为空时，接收阻塞
v := &amp;lt;-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&amp;lt;- T：只能发送（send-only）
// &amp;lt;-chan T：只能接收（receive-only）
// chan T：双向（可以转换为单向）
func producer(out chan&amp;lt;- int) { // 只能往 out 发送
for i := 0; i &amp;lt; 5; i&amp;#43;&amp;#43; {
out &amp;lt;- i
}
close(out) // 生产完毕，关闭 channel
}
func consumer(in &amp;lt;-chan int) { // 只能从 in 接收
for v := range in { // for range 自动处理 close
fmt.Println(&amp;#34;收到:&amp;#34;, v)
}
}
func main() {
ch := make(chan int, 5) // 双向 channel
go producer(ch) // 自动转换为 chan&amp;lt;- int
consumer(ch) // 自动转换为 &amp;lt;-chan int
}
```
- for range 遍历 Channel
-
```go
// chan&amp;lt;- T：只能发送（send-only）
// &amp;lt;-chan T：只能接收（receive-only）
// chan T：双向（可以转换为单向）
func producer(out chan&amp;lt;- int) { // 只能往 out 发送
for i := 0; i &amp;lt; 5; i&amp;#43;&amp;#43; {
out &amp;lt;- i
}
close(out) // 生产完毕，关闭 channel
}
func consumer(in &amp;lt;-chan int) { // 只能从 in 接收
for v := range in { // for range 自动处理 close
fmt.Println(&amp;#34;收到:&amp;#34;, v)
}
}
func main() {
ch := make(chan int, 5) // 双向 channel
go producer(ch) // 自动转换为 chan&amp;lt;- int
consumer(ch) // 自动转换为 &amp;lt;-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 &amp;lt;- &amp;#34;来自 ch1&amp;#34;
}()
go func() {
time.Sleep(2 * time.Second)
ch2 &amp;lt;- &amp;#34;来自 ch2&amp;#34;
}()
// select 等待第一个就绪的 case
select {
case msg := &amp;lt;-ch1:
fmt.Println(&amp;#34;ch1:&amp;#34;, msg)
case msg := &amp;lt;-ch2:
fmt.Println(&amp;#34;ch2:&amp;#34;, msg)
}
// 输出：ch1: 来自 ch1（先到先得）
```
- 带 default 的非阻塞操作
-
```go
ch := make(chan int, 1)
// 非阻塞发送
select {
case ch &amp;lt;- 42:
fmt.Println(&amp;#34;发送成功&amp;#34;)
default:
fmt.Println(&amp;#34;channel 已满，丢弃&amp;#34;)
}
// 非阻塞接收
select {
case v := &amp;lt;-ch:
fmt.Println(&amp;#34;接收到:&amp;#34;, v)
default:
fmt.Println(&amp;#34;channel 为空，跳过&amp;#34;)
}
```
- 超时控制 (实战必备)
-
```go
func fetchWithTimeout(url string) (string, error) {
resultCh := make(chan string, 1)
go func() {
// 模拟 HTTP 请求
time.Sleep(2 * time.Second)
resultCh &amp;lt;- &amp;#34;响应内容&amp;#34;
}()
select {
case result := &amp;lt;-resultCh:
return result, nil
case &amp;lt;-time.After(1 * time.Second): // 1 秒超时
return &amp;#34;&amp;#34;, errors.New(&amp;#34;请求超时&amp;#34;)
}
}
```
- 循环中使用 select
-
```go
func worker(jobs &amp;lt;-chan int, done &amp;lt;-chan struct{}) {
for {
select {
case job, ok := &amp;lt;-jobs:
if !ok {
fmt.Println(&amp;#34;jobs channel 已关闭&amp;#34;)
return
}
fmt.Println(&amp;#34;处理任务:&amp;#34;, job)
case &amp;lt;-done:
fmt.Println(&amp;#34;收到退出信号&amp;#34;)
return
}
}
}
// 多 case 同时就绪时，select 随机选一个
// 这是刻意设计的，避免某个 case 被饿死
```
- select 的随机性
- 当多个 case 同时就绪时，select 随机选择一个，不保证顺序
- 这是 Go 刻意的设计——防止某个 case 总被忽略（饥饿）
- Done Channel 模式
- Done channel 是 Go 并发里用来广播退出信号的经典模式
- 通过 close(done) 可以同时通知所有监听它的 Goroutine 退出
- 示例
-
```go
func generator(done &amp;lt;-chan struct{}, nums ...int) &amp;lt;-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
select {
case out &amp;lt;- n: // 正常发送
case &amp;lt;-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(&amp;lt;-out) // 1
fmt.Println(&amp;lt;-out) // 2
// defer close(done) 执行，generator goroutine 收到信号退出
}
```
- close 广播的原理
-
```go
done := make(chan struct{})
// 启动多个监听 done 的 goroutine
for i := 0; i &amp;lt; 3; i&amp;#43;&amp;#43; {
go func(id int) {
&amp;lt;-done // 阻塞等待
fmt.Printf(&amp;#34;Worker %d 退出\n&amp;#34;, id)
}(i)
}
time.Sleep(1 * time.Second)
close(done) // ✨ 关闭会「广播」到所有接收方
// 所有 3 个 goroutine 同时收到通知并退出
// 原理：从已关闭的 channel 接收立即返回零值
// 所以 close 一次，所有 &amp;lt;-done 都会解除阻塞
```
- Done channel vs context.Context
- Done channel 是手动实现退出信号，简单直接
- 新代码优先用 context
- 生产者-消费者模式
- 基础版：单生产者、单消费者
-
```go
func producer(out chan&amp;lt;- int, count int) {
defer close(out)
for i := 0; i &amp;lt; count; i&amp;#43;&amp;#43; {
out &amp;lt;- i
time.Sleep(50 * time.Millisecond)
}
}
func consumer(in &amp;lt;-chan int, results chan&amp;lt;- int) {
defer close(results)
for v := range in {
results &amp;lt;- 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 &amp;lt;-chan int, results chan&amp;lt;- int, workerCount int) {
var wg sync.WaitGroup
for i := 0; i &amp;lt; workerCount; i&amp;#43;&amp;#43; {
wg.Add(1)
go func(id int) {
defer wg.Done()
for job := range jobs {
// 模拟处理耗时
time.Sleep(100 * time.Millisecond)
result := job * job
results &amp;lt;- result
fmt.Printf(&amp;#34;Worker %d 处理任务 %d -&amp;gt; %d\n&amp;#34;, 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 &amp;lt;= 9; i&amp;#43;&amp;#43; {
jobs &amp;lt;- i
}
close(jobs) // 任务发送完毕
// 收集结果
for r := range results {
fmt.Println(&amp;#34;结果:&amp;#34;, 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 &amp;#34;context&amp;#34;
// 根节点（两种）
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, &amp;#34;traceID&amp;#34;, &amp;#34;abc-123&amp;#34;)
// 取消任意一个节点，它的所有子节点都会取消
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 &amp;lt;-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(&amp;#34;任务结束:&amp;#34;, err) // context canceled
}()
time.Sleep(1 * time.Second)
cancel() // 主动取消，goroutine 收到信号退出
time.Sleep(100 * time.Millisecond)
fmt.Println(&amp;#34;主程序退出&amp;#34;)
}
```
- 一个父取消，多个子都取消
-
```go
func worker(ctx context.Context, id int) {
for {
select {
case &amp;lt;-ctx.Done():
fmt.Printf(&amp;#34;Worker %d 退出: %v\n&amp;#34;, id, ctx.Err())
return
default:
fmt.Printf(&amp;#34;Worker %d 工作中\n&amp;#34;, id)
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 启动多个 worker，共享同一个 ctx
for i := 1; i &amp;lt;= 3; i&amp;#43;&amp;#43; {
go worker(ctx, i)
}
time.Sleep(1 * time.Second)
cancel() // 一次取消，所有 worker 同时退出 ✨
time.Sleep(100 * time.Millisecond)
}
```
- WithTimeout &amp; 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, &amp;#34;GET&amp;#34;, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
// 超时时 err 会包含 &amp;#34;context deadline exceeded&amp;#34;
return nil, fmt.Errorf(&amp;#34;请求失败: %w&amp;#34;, 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,
&amp;#34;SELECT id, name, email FROM users WHERE id = ?&amp;#34;, id,
).Scan(&amp;u.ID, &amp;u.Name, &amp;u.Email)
return &amp;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(&amp;#34;还剩 %.1f 秒\n&amp;#34;, 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(&amp;#34;已超过截止时间&amp;#34;)
}
```
- 超时值怎么定
- 通常根据 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,
&amp;#34;SELECT * FROM large_table WHERE complex_condition&amp;#34;,
)
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(&amp;row.ID, &amp;row.Name)
result = append(result, row)
}
return result, nil
}
```
- 完整的中间件链
-
```go
func main() {
mux := http.NewServeMux()
mux.HandleFunc(&amp;#34;/api/user&amp;#34;, userHandler)
// 中间件链：每层都可以增强/修改 context
handler := timeoutMiddleware(
authMiddleware(
traceMiddleware(mux),
),
)
http.ListenAndServe(&amp;#34;:8080&amp;#34;, 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(&amp;#34;Authorization&amp;#34;)
user, err := validateToken(token)
if err != nil {
http.Error(w, &amp;#34;未授权&amp;#34;, 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(&amp;#34;在第 %d 条时被取消: %w&amp;#34;, 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(), &amp;#34;SELECT ...&amp;#34;)
}
// ✅ 始终传递原始 ctx
func handler(ctx context.Context) error {
return db.QueryContext(ctx, &amp;#34;SELECT ...&amp;#34;)
}
```
- 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(&amp;#34;读取配置失败: %v&amp;#34;, err) // ❌ 用 %v，丢失了原始 err 类型！
}
return nil
}
// ✅ Go 1.13&amp;#43; 正确做法：用 %w 包装，保留原始 error
func readConfig(path string) error {
_, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf(&amp;#34;读取配置 %s 失败: %w&amp;#34;, path, err) // ✅ %w 包装
}
return nil
}
// 调用方可以拆包检查原始错误
err := readConfig(&amp;#34;/etc/app.yaml&amp;#34;)
if errors.Is(err, os.ErrNotExist) {
fmt.Println(&amp;#34;配置文件不存在，使用默认配置&amp;#34;)
}
```
- %v 和 %w 的本质区别
- %v 把 err 转成字符串，原始错误信息被「压扁」成文字，调用方无法再用 errors.Is/As 检查原始类型
- %w 把原始 err 包裹在新错误里，形成错误链，调用方可以用 errors.Is/As 沿链查找
- 只要你想让调用方能检查原始错误，就用 %w
- 错误包装与错误链
- 错误链就像俄罗斯套娃：最外层是最高级的上下文描述，最内层是根本原因。
- 每一层都用 %w 包装，形成一条可以追溯的链路。
- 示例
-
```go
// 错误链示例：从底层到顶层
// 层级：DB驱动 → DB层 → 业务层 → API层
// ① 最底层：数据库驱动报错
var ErrNotFound = errors.New(&amp;#34;记录不存在&amp;#34;)
// ② DB 层包装
func (r *UserRepo) Get(id int) (*User, error) {
// ...
return nil, fmt.Errorf(&amp;#34;UserRepo.Get id=%d: %w&amp;#34;, id, ErrNotFound)
}
// ③ 业务层包装
func (s *UserService) GetUser(id int) (*User, error) {
u, err := s.repo.Get(id)
if err != nil {
return nil, fmt.Errorf(&amp;#34;UserService.GetUser: %w&amp;#34;, err)
}
return u, nil
}
// ④ API 层包装
func handleGetUser(id int) {
_, err := userService.GetUser(id)
if err != nil {
// err.Error() 输出完整链路：
// &amp;#34;UserService.GetUser: UserRepo.Get id=42: 记录不存在&amp;#34;
fmt.Println(err)
}
}
```
- 手动实现 Unwrap (支持自定义类型)
-
```go
// 自定义错误类型也可以支持错误链
type DBError struct {
Op string // 操作名
Err error // 原始错误
}
func (e *DBError) Error() string {
return fmt.Sprintf(&amp;#34;数据库操作 %s 失败: %v&amp;#34;, e.Op, e.Err)
}
// ✨ 实现 Unwrap 方法，支持 errors.Is/As 穿透
func (e *DBError) Unwrap() error {
return e.Err
}
// 使用
err := &amp;DBError{Op: &amp;#34;INSERT&amp;#34;, Err: ErrDuplicate}
fmt.Println(errors.Is(err, ErrDuplicate)) // true，因为实现了 Unwrap
```
- 错误链的价值
- 一条好的错误链就像一份事故报告：「API层在做什么 → 业务层在做什么 → 数据层在做什么 → 根本原因是什么」
- 出了问题直接看错误信息就能定位，不用加断点。
- 这在微服务日志里尤其有价值。
- errors.Is: 判断错误类型
- errors.Is 用来判断错误链中是否包含某个特定的「哨兵错误」（Sentinel Error）。
- 它会递归 Unwrap 整条链，不像 == 只比较最外层。
- 示例
-
```go
// 定义哨兵错误（包级别的可比较 error 值）
var (
ErrNotFound = errors.New(&amp;#34;未找到&amp;#34;)
ErrUnauthorized = errors.New(&amp;#34;未授权&amp;#34;)
ErrTimeout = errors.New(&amp;#34;超时&amp;#34;)
)
func doSomething() error {
return fmt.Errorf(&amp;#34;操作失败: %w&amp;#34;, 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, &amp;#34;资源不存在&amp;#34;, http.StatusNotFound)
} else if errors.Is(err, ErrUnauthorized) {
http.Error(w, &amp;#34;请先登录&amp;#34;, http.StatusUnauthorized)
} else if err != nil {
http.Error(w, &amp;#34;内部错误&amp;#34;, http.StatusInternalServerError)
}
```
- 自定义 Is 方法
-
```go
// 有时候两个错误「相等」不是值相等，而是逻辑相等
type StatusError struct {
Code int
}
func (e *StatusError) Error() string {
return fmt.Sprintf(&amp;#34;HTTP %d&amp;#34;, 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(&amp;#34;请求失败: %w&amp;#34;, &amp;StatusError{Code: 404})
target := &amp;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(&amp;#34;字段 %s（值=%v）验证失败: %s&amp;#34;,
e.Field, e.Value, e.Message)
}
func validateAge(age int) error {
if age &amp;lt; 0 {
return &amp;ValidationError{
Field: &amp;#34;age&amp;#34;,
Value: age,
Message: &amp;#34;不能为负数&amp;#34;,
}
}
return nil
}
func processUser(age int) error {
if err := validateAge(age); err != nil {
return fmt.Errorf(&amp;#34;processUser: %w&amp;#34;, err) // 包装了 ValidationError
}
return nil
}
// 调用方提取详情
err := processUser(-1)
if err != nil {
var ve *ValidationError
if errors.As(err, &amp;ve) { // ✅ 会沿链查找 *ValidationError 类型
fmt.Printf(&amp;#34;字段 %q 的值 %v 有误: %s\n&amp;#34;,
ve.Field, ve.Value, ve.Message)
// 字段 &amp;#34;age&amp;#34; 的值 -1 有误: 不能为负数
} else {
fmt.Println(&amp;#34;未知错误:&amp;#34;, 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 &amp;lt;模块名&amp;gt; 初始化模块，创建 go.mod
- go get &amp;lt;包&amp;gt;@&amp;lt;版本&amp;gt; 添加或更新依赖
- go mod tidy 清理多余依赖，补全缺失依赖
- go mod download 下载依赖到本地缓存
- go mod vendor 把依赖复制到 vendor 目录
- go list -m all 列出所有直接和间接依赖
- go mod graph 查看依赖关系图
- go mod verify 验证依赖是否被篡改
- 完整操作流程
-
```go
# 1. 创建项目
mkdir myapp &amp;&amp; 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 =&amp;gt; ../local-lib
replace github.com/some/lib =&amp;gt; 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&amp;#43;Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP&amp;#43;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 &amp;#34;github.com/some/lib/v2&amp;#34;
# 特殊版本格式
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 &amp;#34;github.com/yourname/myapp/internal/db&amp;#34; // ✅ 同模块内，可以导入
// github.com/other/app/main.go
import &amp;#34;github.com/yourname/myapp/internal/db&amp;#34; // ❌ 编译错误！外部无法导入
```
- 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 (
// 第一组：标准库
&amp;#34;context&amp;#34;
&amp;#34;fmt&amp;#34;
&amp;#34;net/http&amp;#34;
// 第二组：第三方依赖（空行分隔）
&amp;#34;github.com/gin-gonic/gin&amp;#34;
&amp;#34;gorm.io/gorm&amp;#34;
// 第三组：本项目内部包（空行分隔）
&amp;#34;github.com/yourname/myapp/internal/config&amp;#34;
&amp;#34;github.com/yourname/myapp/internal/handler&amp;#34;
)
// goimports 工具会自动排列和分组
// 安装：go install golang.org/x/tools/cmd/goimports@latest
// VS Code 保存时自动运行
```
- 常见 import 技巧
-
```go
// 别名：解决包名冲突
import (
&amp;#34;crypto/rand&amp;#34;
mrand &amp;#34;math/rand&amp;#34; // 别名，避免和 crypto/rand 冲突
)
// 只执行 init 函数（数据库驱动常用）
import (
_ &amp;#34;github.com/lib/pq&amp;#34; // PostgreSQL 驱动，只要 init
_ &amp;#34;github.com/mattn/go-sqlite3&amp;#34; // SQLite 驱动
)
// 点 import（不推荐，污染命名空间）
import . &amp;#34;fmt&amp;#34;
Println(&amp;#34;hello&amp;#34;) // 不需要 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 &amp;#34;github.com/yourname/mylib&amp;#34; // 自动使用本地的 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=&amp;#34;-X main.Version=v1.2.3 -X main.BuildTime=$(date -u &amp;#43;%Y-%m-%dT%H:%M:%SZ)&amp;#34; ./cmd/server
# 减小二进制体积
go build -ldflags=&amp;#34;-s -w&amp;#34; ./cmd/server # 去掉调试信息，体积减少约 30%
# 启用竞态检测（开发阶段）
go build -race ./...
```
- Build Tags (条件编译)
-
```go
// 文件只在 Linux 下编译
//go:build linux
package main
// 文件只在测试时编译
//go:build integration
// 多条件
//go:build linux &amp;&amp; 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 &amp;#43;%Y-%m-%dT%H:%M:%SZ)
LDFLAGS=-ldflags &amp;#34;-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -s -w&amp;#34;
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=&amp;#34;*.internal.company.com&amp;#34;
# GONOSUMDB：跳过 sum 数据库
go env -w GONOSUMDB=&amp;#34;*.internal.company.com&amp;#34;
```
- 私有模块配置
-
```go
# GOPRIVATE：私有模块不走代理也不走 sum 数据库
go env -w GOPRIVATE=&amp;#34;*.corp.example.com,github.com/mycompany/*&amp;#34;
# 配置 git 认证（访问私有仓库）
git config --global url.&amp;#34;https://token:PASSWORD@github.com/mycompany/&amp;#34;.insteadOf &amp;#34;https://github.com/mycompany/&amp;#34;
# 或者用 SSH
git config --global url.&amp;#34;git@github.com:mycompany/&amp;#34;.insteadOf &amp;#34;https://github.com/mycompany/&amp;#34;
# 公司内部代理服务器（Athens/goproxy.io）
go env -w GOPROXY=&amp;#34;https://goproxy.company.com,https://proxy.golang.org,direct&amp;#34;
go env -w GOPRIVATE=&amp;#34;*.company.com&amp;#34;
```
- 测试与基准测试
- Go Test 框架基础
- Go 内置了完整的测试框架，不需要任何第三方库。
- 测试文件以 _test.go 结尾，测试函数以 Test 开头，接受 *testing.T 参数。这三条规则，就是 Go 测试的全部入场门票。
- 示例
-
```go
// math/add.go
package math
func Add(a, b int) int {
return a &amp;#43; b
}
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New(&amp;#34;除数不能为零&amp;#34;)
}
return a / b, nil
}
// math/add_test.go（同一个包，_test.go 结尾）
package math
import &amp;#34;testing&amp;#34;
func TestAdd(t *testing.T) {
result := Add(1, 2)
if result != 3 {
t.Errorf(&amp;#34;Add(1, 2) = %d，期望 3&amp;#34;, result)
}
}
func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatalf(&amp;#34;意外错误: %v&amp;#34;, err)
}
if result != 5 {
t.Errorf(&amp;#34;Divide(10, 2) = %f，期望 5&amp;#34;, result)
}
}
```
- 运行测试的常用命令
- ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/04/21/20260421000815190.png,310,160)
- testing.T 的核心方法
-
```go
// 测试失败但继续执行
t.Errorf(&amp;#34;期望 %d，实际 %d&amp;#34;, expected, actual)
t.Error(&amp;#34;发现问题&amp;#34;)
// 测试失败并立即停止（Fatal = Error &amp;#43; runtime.Goexit）
t.Fatalf(&amp;#34;致命错误: %v&amp;#34;, err)
t.Fatal(&amp;#34;不可继续&amp;#34;)
// 仅打印日志（不影响测试结果）
t.Logf(&amp;#34;调试信息: %v&amp;#34;, value)
t.Log(&amp;#34;这里到了&amp;#34;)
// 跳过测试
t.Skip(&amp;#34;跳过原因：需要数据库&amp;#34;)
t.Skipf(&amp;#34;跳过：CI 环境不支持 %s&amp;#34;, feature)
// 标记为并行测试
t.Parallel()
// 子测试（下一节讲）
t.Run(&amp;#34;子测试名&amp;#34;, func(t *testing.T) { ... })
```
- 表驱动测试
- 表驱动测试是 Go 最推荐的测试写法。
- 把所有测试用例放进一个切片，用循环执行，消除重复代码，添加新用例只需加一行。
- 这是 Go 测试的标准惯用法。
- 示例
-
```go
func TestAdd(t *testing.T) {
// 定义测试用例表
tests := []struct {
name string // 用例名（出错时显示）
a, b int
expected int
}{
{&amp;#34;正数相加&amp;#34;, 1, 2, 3},
{&amp;#34;负数相加&amp;#34;, -1, -2, -3},
{&amp;#34;正负相加&amp;#34;, 5, -3, 2},
{&amp;#34;零值&amp;#34;, 0, 0, 0},
{&amp;#34;大数&amp;#34;, 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(&amp;#34;Add(%d, %d) = %d，期望 %d&amp;#34;,
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 // 期望的错误信息（可选）
}{
{&amp;#34;正常除法&amp;#34;, 10, 2, 5, false, &amp;#34;&amp;#34;},
{&amp;#34;除以1&amp;#34;, 9, 1, 9, false, &amp;#34;&amp;#34;},
{&amp;#34;除以零&amp;#34;, 10, 0, 0, true, &amp;#34;除数不能为零&amp;#34;},
{&amp;#34;负数除法&amp;#34;, -10, 2, -5, false, &amp;#34;&amp;#34;},
}
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(&amp;#34;期望有错误，但没有&amp;#34;)
}
if tt.errMsg != &amp;#34;&amp;#34; &amp;&amp; !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf(&amp;#34;错误信息 %q 不包含 %q&amp;#34;, err.Error(), tt.errMsg)
}
return // 有预期错误，不检查结果
}
if err != nil {
t.Fatalf(&amp;#34;意外错误: %v&amp;#34;, err)
}
if math.Abs(result-tt.expected) &amp;gt; 1e-9 {
t.Errorf(&amp;#34;Divide(%.2f, %.2f) = %.2f，期望 %.2f&amp;#34;,
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(&amp;#34;全局初始化：启动测试数据库...&amp;#34;)
db, err := setupTestDB()
if err != nil {
log.Fatalf(&amp;#34;数据库初始化失败: %v&amp;#34;, err)
}
// ─── 运行所有测试 ───
exitCode := m.Run()
// ─── Teardown ───
fmt.Println(&amp;#34;全局清理：关闭测试数据库...&amp;#34;)
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, &amp;#34;test.txt&amp;#34;)
t.Cleanup(func() {
os.Remove(tmpFile)
t.Log(&amp;#34;清理临时文件&amp;#34;)
})
// 测试逻辑
err := os.WriteFile(tmpFile, []byte(&amp;#34;hello&amp;#34;), 0644)
if err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(tmpFile)
if err != nil {
t.Fatal(err)
}
if string(content) != &amp;#34;hello&amp;#34; {
t.Errorf(&amp;#34;内容不匹配&amp;#34;)
}
}
// 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(&amp;#34;意外错误: %v&amp;#34;, err)
}
}
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf(&amp;#34;期望 %v，实际 %v&amp;#34;, 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 (
&amp;#34;testing&amp;#34;
&amp;#34;github.com/stretchr/testify/assert&amp;#34;
&amp;#34;github.com/stretchr/testify/require&amp;#34;
)
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, &amp;#34;hello world&amp;#34;, &amp;#34;world&amp;#34;)
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, &amp;#34;用户列表长度应该相等&amp;#34;)
// 错误断言
assert.Error(t, err) // 期望有 error
assert.ErrorIs(t, err, ErrNotFound) // 等价于 errors.Is
assert.ErrorAs(t, err, &amp;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, &amp;#34;hello&amp;#34;, &amp;#34;ell&amp;#34;)
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&amp;#43;&amp;#43;
u, ok := m.users[id]
if !ok {
return nil, ErrNotFound
}
return u, nil
}
func (m *MockUserRepo) Create(_ context.Context, u *User) error {
m.CreateCalled&amp;#43;&amp;#43;
u.ID = m.nextID
m.users[u.ID] = u
m.nextID&amp;#43;&amp;#43;
return nil
}
// 测试时注入 Mock
func TestUserService_Register(t *testing.T) {
mockRepo := &amp;MockUserRepo{
users: make(map[int]*User),
nextID: 1,
}
svc := &amp;UserService{repo: mockRepo}
ctx := context.Background()
u, err := svc.Register(ctx, &amp;#34;Alice&amp;#34;, &amp;#34;alice@example.com&amp;#34;)
require.NoError(t, err)
assert.Equal(t, &amp;#34;Alice&amp;#34;, u.Name)
assert.Equal(t, 1, mockRepo.CreateCalled) // 验证调用次数
}
```
- testify/mock：功能更强的 mock
-
```go
import &amp;#34;github.com/stretchr/testify/mock&amp;#34;
// 用 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 := &amp;User{ID: 1, Name: &amp;#34;Alice&amp;#34;}
// 设置期望：当 FindByID 被用 id=1 调用时，返回 expectedUser
mockRepo.On(&amp;#34;FindByID&amp;#34;, mock.Anything, 1).Return(expectedUser, nil)
// 设置期望：Create 被调用时成功
mockRepo.On(&amp;#34;Create&amp;#34;, mock.Anything, mock.AnythingOfType(&amp;#34;*User&amp;#34;)).Return(nil)
svc := &amp;UserService{repo: mockRepo}
u, err := svc.GetUser(context.Background(), 1)
require.NoError(t, err)
assert.Equal(t, &amp;#34;Alice&amp;#34;, 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 &amp;lt; b.N; i&amp;#43;&amp;#43; {
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
// 对比字符串拼接：&amp;#43; vs strings.Builder
func BenchmarkStringConcat(b *testing.B) {
words := []string{&amp;#34;hello&amp;#34;, &amp;#34;world&amp;#34;, &amp;#34;go&amp;#34;, &amp;#34;test&amp;#34;}
b.Run(&amp;#34;用&amp;#43;拼接&amp;#34;, func(b *testing.B) {
for i := 0; i &amp;lt; b.N; i&amp;#43;&amp;#43; {
result := &amp;#34;&amp;#34;
for _, w := range words {
result &amp;#43;= w // 每次拼接都分配新内存
}
_ = result
}
})
b.Run(&amp;#34;用Builder&amp;#34;, func(b *testing.B) {
for i := 0; i &amp;lt; b.N; i&amp;#43;&amp;#43; {
var sb strings.Builder
for _, w := range words {
sb.WriteString(w) // 预分配，减少内存分配
}
_ = sb.String()
}
})
}
// 输出示例：
// BenchmarkStringConcat/用&amp;#43;拼接-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 &amp;lt; b.N; i&amp;#43;&amp;#43; {
b.StopTimer() // 暂停计时
input := prepareInput(data) // 每次迭代的准备工作
b.StartTimer() // 恢复计时
processData(input) // 只测量这部分
}
}
```
- 示例测试
- Example 函数有双重作用：既是可运行的测试，也是文档。
- go doc 命令会显示 Example 函数，godoc 网站也会展示它们。
- 示例
-
```go
// 函数名：Example &amp;#43; 函数名
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 &amp;lt;nil&amp;gt;
}
// 无法预测输出顺序时用 Unordered output
func ExamplePrintMap() {
m := map[string]int{&amp;#34;a&amp;#34;: 1, &amp;#34;b&amp;#34;: 2}
for k, v := range m {
fmt.Printf(&amp;#34;%s: %d\n&amp;#34;, k, v)
}
// Unordered output:
// a: 1
// b: 2
}
// 类型/方法的 Example
func ExampleUser_GetName() {
u := User{Name: &amp;#34;Alice&amp;#34;}
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 &amp;#43;= n }
return total
}
func SumFloat64(nums []float64) float64 {
total := 0.0
for _, n := range nums { total &amp;#43;= n }
return total
}
// 如果要支持 int32、int64、float32... 噩梦
// ❌ 方案二：用 any，丢失类型安全
func Sum(nums []any) any {
// 怎么相加？any 不支持 &amp;#43; 运算符
// 必须类型断言，运行时才发现错误
}
// ✅ 泛型：一次编写，类型安全，适用所有数字类型
func Sum[T int | int64 | float64](nums []T) T {
var total T
for _, n := range nums { total &amp;#43;= n }
return total
}
Sum([]int{1, 2, 3}) // 6
Sum([]float64{1.1, 2.2}) // 3.3
Sum([]string{&amp;#34;a&amp;#34;, &amp;#34;b&amp;#34;}) // ❌ 编译错误：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(&amp;#34;No.%d&amp;#34;, n)
})
// strs = [&amp;#34;No.1&amp;#34;, &amp;#34;No.2&amp;#34;, &amp;#34;No.3&amp;#34;]
// 显式指定类型参数（推断失败时）
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(&amp;#34;hello&amp;#34;)
```
- 类型泛型
- 约束定义了类型参数能接受哪些类型。
- 约束本质上是接口——只不过这个接口里可以包含「类型集合」（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{&amp;#34;a&amp;#34;, &amp;#34;b&amp;#34;}, &amp;#34;c&amp;#34;) // false
// 注意：slice、map、func 不是 comparable
// Contains([][]int{{1,2}}, []int{1,2}) // ❌ 编译错误
```
- contraints 包的内置约束
-
```go
import &amp;#34;golang.org/x/exp/constraints&amp;#34;
// 或 Go 1.21&amp;#43; 用标准库 cmp 包
// constraints.Ordered：支持 &amp;lt; &amp;gt; &amp;lt;= &amp;gt;= 的类型
// int, int8, int16, int32, int64
// uint, uint8, ..., uintptr
// float32, float64
// string
func Min[T constraints.Ordered](a, b T) T {
if a &amp;lt; b { return a }
return b
}
func Max[T constraints.Ordered](a, b T) T {
if a &amp;gt; 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(&amp;#34;apple&amp;#34;, &amp;#34;banana&amp;#34;) // &amp;#34;apple&amp;#34;（字符串也可以比较）
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 &amp;#43;= n }
return total
}
// ~ 的重要性：
type MyInt int // 基于 int 的自定义类型
Sum([]MyInt{1, 2, 3}) // ✅ ~int 包含 MyInt
// 不加 ~ 的话，Sum[int | float64] 就不接受 MyInt
// 方法 &amp;#43; 类型混合约束
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 &amp;#43; n },
)
// 偶数的平方和：4&amp;#43;16&amp;#43;36&amp;#43;64&amp;#43;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{
{&amp;#34;Alice&amp;#34;, &amp;#34;Tokyo&amp;#34;}, {&amp;#34;Bob&amp;#34;, &amp;#34;Tokyo&amp;#34;}, {&amp;#34;Charlie&amp;#34;, &amp;#34;Osaka&amp;#34;},
}
byCity := GroupBy(people, func(p Person) string { return p.City })
// byCity[&amp;#34;Tokyo&amp;#34;] = [{Alice Tokyo} {Bob Tokyo}]
```
- 泛型数据结构
- 泛型最大的价值之一是可以写真正通用的数据结构。下面实现几个常用的容器。
- 泛型 Set (集合)
-
```go
type Set[T comparable] struct {
items map[T]struct{}
}
func NewSet[T comparable](items ...T) *Set[T] {
s := &amp;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&amp;lt;T&amp;gt;，比返回 (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: &amp;#34;anonymous&amp;#34;})
```
- 标准库的泛型包
- Go 1.21 起标准库加入了几个泛型包，直接使用它们比自己实现更好。
- slice
-
```go
import &amp;#34;slices&amp;#34;
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 &amp;#34;maps&amp;#34;
m := map[string]int{&amp;#34;a&amp;#34;: 1, &amp;#34;b&amp;#34;: 2, &amp;#34;c&amp;#34;: 3}
// 复制
clone := maps.Clone(m)
// 删除满足条件的 key
maps.DeleteFunc(m, func(k string, v int) bool {
return v &amp;lt; 2 // 删除 value &amp;lt; 2 的条目
})
// 判断是否相等
eq := maps.Equal(m, map[string]int{&amp;#34;b&amp;#34;: 2, &amp;#34;c&amp;#34;: 3})
// 遍历（Go 1.23&amp;#43; range over func）
for k, v := range maps.All(m) {
fmt.Printf(&amp;#34;%s: %d\n&amp;#34;, k, v)
}
```
- cmp 包
-
```go
import &amp;#34;cmp&amp;#34;
// cmp.Ordered：可排序类型的约束（等价于 constraints.Ordered）
func Min[T cmp.Ordered](a, b T) T {
return min(a, b) // Go 1.21&amp;#43; 内置了 min/max 函数
}
// cmp.Compare：三路比较（-1, 0, 1）
cmp.Compare(1, 2) // -1
cmp.Compare(2, 2) // 0
cmp.Compare(3, 2) // 1
cmp.Compare(&amp;#34;a&amp;#34;, &amp;#34;b&amp;#34;) // -1（字符串也支持）
// 配合 slices.SortFunc 使用
type User struct{ Name string; Age int }
users := []User{{&amp;#34;Alice&amp;#34;, 30}, {&amp;#34;Bob&amp;#34;, 25}, {&amp;#34;Charlie&amp;#34;, 35}}
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(a.Age, b.Age) // 按年龄排序
})
// Go 1.21&amp;#43; 内置 min/max（泛型版本）
fmt.Println(min(3, 5)) // 3
fmt.Println(max(&amp;#34;a&amp;#34;, &amp;#34;b&amp;#34;)) // 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&amp;#43;&amp;#43; 那样的模板特化
```
- 什么时候用泛型，什么时候不用
-
```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
}
```
&lt;/textarea&gt;
&lt;h1 id="web-开发实战"&gt;Web 开发实战&lt;/h1&gt;
&lt;div
class="mindmap-container"
id="mindmap-12635784"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-12635784" style="display:none;"&gt;
- Web 开发实战
- net/http 标准库
- net/http 核心概念
- Go 的 net/http 标准库是同类中最强大的之一——不需要任何框架就能构建生产级 HTTP 服务器。
- 理解标准库是学好 Gin/Echo 等框架的基础，因为框架只是在它之上加了一层糖衣。
- HTTP 服务器示例
-
```go
package main
import (
&amp;#34;fmt&amp;#34;
&amp;#34;net/http&amp;#34;
)
func main() {
// 注册路由处理器
http.HandleFunc(&amp;#34;/&amp;#34;, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, &amp;#34;Hello, Go Web!&amp;#34;)
})
http.HandleFunc(&amp;#34;/ping&amp;#34;, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, &amp;#34;pong&amp;#34;)
})
// 启动服务器（阻塞）
fmt.Println(&amp;#34;服务器启动在 :8080&amp;#34;)
if err := http.ListenAndServe(&amp;#34;:8080&amp;#34;, 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, &amp;#34;Hello&amp;#34;)
}
http.Handle(&amp;#34;/&amp;#34;, http.HandlerFunc(myHandler))
// 方式二：http.HandleFunc（语法糖，最常用）
http.HandleFunc(&amp;#34;/&amp;#34;, myHandler)
// 方式三：实现 ServeHTTP 方法的结构体
type MyHandler struct{ greeting string }
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, h.greeting)
}
http.Handle(&amp;#34;/&amp;#34;, &amp;MyHandler{greeting: &amp;#34;Hello from struct!&amp;#34;})
```
- 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 // &amp;#34;GET&amp;#34;, &amp;#34;POST&amp;#34;, &amp;#34;PUT&amp;#34;, &amp;#34;DELETE&amp;#34;
// URL 路径
path := r.URL.Path // &amp;#34;/api/users/42&amp;#34;
// Query 参数（?name=Alice&amp;age=30）
name := r.URL.Query().Get(&amp;#34;name&amp;#34;) // &amp;#34;Alice&amp;#34;
age := r.URL.Query().Get(&amp;#34;age&amp;#34;) // &amp;#34;30&amp;#34;（字符串，需手动转换）
all := r.URL.Query() // url.Values（map[string][]string）
// ⚠️ 标准库没有路径参数（:id 这种）
// 需要自己解析或用 Go 1.22 的新路由器
// 例如：从 /api/users/42 提取 42
parts := strings.Split(r.URL.Path, &amp;#34;/&amp;#34;)
// parts = [&amp;#34;&amp;#34;, &amp;#34;api&amp;#34;, &amp;#34;users&amp;#34;, &amp;#34;42&amp;#34;]
fmt.Fprintf(w, &amp;#34;method=%s path=%s name=%s&amp;#34;, method, path, name)
}
```
- 读取 Header 和 Body
-
```go
func handler(w http.ResponseWriter, r *http.Request) {
// Headers
contentType := r.Header.Get(&amp;#34;Content-Type&amp;#34;)
token := r.Header.Get(&amp;#34;Authorization&amp;#34;)
userAgent := r.Header.Get(&amp;#34;User-Agent&amp;#34;)
// Body（只能读一次！）
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, &amp;#34;读取 body 失败&amp;#34;, http.StatusBadRequest)
return
}
// JSON 解析
var payload struct {
Name string `json:&amp;#34;name&amp;#34;`
Email string `json:&amp;#34;email&amp;#34;`
}
if err := json.Unmarshal(body, &amp;payload); err != nil {
http.Error(w, &amp;#34;JSON 解析失败&amp;#34;, http.StatusBadRequest)
return
}
// 或者用 Decoder（更高效，不需要先读入 []byte）
var data map[string]any
if err := json.NewDecoder(r.Body).Decode(&amp;data); err != nil {
http.Error(w, &amp;#34;invalid json&amp;#34;, http.StatusBadRequest)
return
}
_ = contentType; _ = token; _ = userAgent
fmt.Fprintf(w, &amp;#34;name=%s email=%s&amp;#34;, payload.Name, payload.Email)
}
```
- 表单与文件上传
-
```go
func handler(w http.ResponseWriter, r *http.Request) {
// 解析表单（必须先调用）
if err := r.ParseForm(); err != nil {
http.Error(w, &amp;#34;解析表单失败&amp;#34;, http.StatusBadRequest)
return
}
name := r.FormValue(&amp;#34;name&amp;#34;) // 同时读取 form 和 query 参数
// 文件上传
r.ParseMultipartForm(10 &amp;lt;&amp;lt; 20) // 最大 10MB
file, header, err := r.FormFile(&amp;#34;avatar&amp;#34;)
if err != nil {
http.Error(w, &amp;#34;读取文件失败&amp;#34;, http.StatusBadRequest)
return
}
defer file.Close()
fmt.Printf(&amp;#34;文件名: %s, 大小: %d\n&amp;#34;, header.Filename, header.Size)
// 保存文件
dst, _ := os.Create(&amp;#34;upload/&amp;#34; &amp;#43; header.Filename)
defer dst.Close()
io.Copy(dst, file)
_ = name
fmt.Fprintln(w, &amp;#34;上传成功&amp;#34;)
}
```
- 构建响应
- http.ResponseWriter 用于写响应。
- 理解写入顺序很重要：必须先设置 Header，再 WriteHeader，最后写 Body。
-
```go
func handler(w http.ResponseWriter, r *http.Request) {
// ⚠️ 顺序很重要：Header → WriteHeader → Body
// 1. 设置响应头（必须在 WriteHeader 之前）
w.Header().Set(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/json&amp;#34;)
w.Header().Set(&amp;#34;X-Request-ID&amp;#34;, &amp;#34;abc-123&amp;#34;)
// 2. 设置状态码（只能调用一次，之后会 superfluous）
w.WriteHeader(http.StatusCreated) // 201
// 3. 写 Body
fmt.Fprintln(w, `{&amp;#34;message&amp;#34;: &amp;#34;created&amp;#34;}`)
}
// 常用状态码常量
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(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/json&amp;#34;)
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
// 已经 WriteHeader 了，只能 log，不能再改状态码
log.Printf(&amp;#34;写入 JSON 响应失败: %v&amp;#34;, err)
}
}
// 封装错误响应
type ErrorResponse struct {
Code int `json:&amp;#34;code&amp;#34;`
Message string `json:&amp;#34;message&amp;#34;`
}
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, &amp;#34;用户不存在&amp;#34;)
return
}
writeJSON(w, http.StatusOK, user)
}
```
- 重定向与文件服务
-
```go
// 重定向
http.Redirect(w, r, &amp;#34;/new-path&amp;#34;, http.StatusMovedPermanently) // 301
http.Redirect(w, r, &amp;#34;/login&amp;#34;, http.StatusFound) // 302
// 简单错误响应
http.Error(w, &amp;#34;资源不存在&amp;#34;, http.StatusNotFound)
// 等价于：
// w.Header().Set(&amp;#34;Content-Type&amp;#34;, &amp;#34;text/plain; charset=utf-8&amp;#34;)
// w.WriteHeader(http.StatusNotFound)
// fmt.Fprintln(w, &amp;#34;资源不存在&amp;#34;)
// 静态文件服务
http.Handle(&amp;#34;/static/&amp;#34;, http.StripPrefix(
&amp;#34;/static/&amp;#34;,
http.FileServer(http.Dir(&amp;#34;./public&amp;#34;)),
))
// ServeFile：返回单个文件
http.ServeFile(w, r, &amp;#34;./public/index.html&amp;#34;)
// ServeContent：带 Range 支持的文件（视频流）
http.ServeContent(w, r, &amp;#34;video.mp4&amp;#34;, time.Now(), file)
```
- ServerMux 路由与 Go 1.22 新路由
- 标准库的 ServeMux 在 Go 1.22 之前功能很有限——不支持路径参数和方法限制。
- Go 1.22 大幅增强了路由能力，现在可以直接用标准库写出接近框架的路由。
- 传统 ServerMux
-
```go
mux := http.NewServeMux()
// 精确匹配
mux.HandleFunc(&amp;#34;/api/users&amp;#34;, usersHandler)
// 前缀匹配（以 / 结尾）
mux.HandleFunc(&amp;#34;/static/&amp;#34;, 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, &amp;#34;Method Not Allowed&amp;#34;, http.StatusMethodNotAllowed)
}
}
```
- Go 1.22&amp;#43; 增强路由
-
```go
// Go 1.22 的 ServeMux 支持：
// 1. HTTP 方法前缀
// 2. 路径参数 {name}
// 3. 通配符 {path...}
mux := http.NewServeMux()
// 方法 &amp;#43; 路径
mux.HandleFunc(&amp;#34;GET /api/users&amp;#34;, listUsers)
mux.HandleFunc(&amp;#34;POST /api/users&amp;#34;, createUser)
// 路径参数 {id}
mux.HandleFunc(&amp;#34;GET /api/users/{id}&amp;#34;, getUser)
mux.HandleFunc(&amp;#34;PUT /api/users/{id}&amp;#34;, updateUser)
mux.HandleFunc(&amp;#34;DELETE /api/users/{id}&amp;#34;, deleteUser)
// 通配符（匹配任意后续路径）
mux.HandleFunc(&amp;#34;GET /static/{path...}&amp;#34;, staticHandler)
// 在 handler 中读取路径参数
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue(&amp;#34;id&amp;#34;) // ✨ Go 1.22 新增！
fmt.Fprintf(w, &amp;#34;获取用户 ID: %s&amp;#34;, id)
}
// 精确匹配（末尾加 {$} 防止前缀匹配）
mux.HandleFunc(&amp;#34;GET /{$}&amp;#34;, homeHandler) // 只匹配根路径
```
- 路由组织：分文件
-
```go
// 把路由注册拆分到各模块
// cmd/server/main.go
func main() {
mux := http.NewServeMux()
// 注册各模块的路由
registerUserRoutes(mux)
registerAuthRoutes(mux)
registerHealthRoutes(mux)
srv := &amp;http.Server{
Addr: &amp;#34;:8080&amp;#34;,
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(&amp;#34;GET /api/users&amp;#34;, listUsers)
mux.HandleFunc(&amp;#34;POST /api/users&amp;#34;, createUser)
mux.HandleFunc(&amp;#34;GET /api/users/{id}&amp;#34;, getUser)
mux.HandleFunc(&amp;#34;PUT /api/users/{id}&amp;#34;, updateUser)
mux.HandleFunc(&amp;#34;DELETE /api/users/{id}&amp;#34;, 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(&amp;#34;%s %s %v&amp;#34;, 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(&amp;#34;panic 恢复: %v\n%s&amp;#34;, err, debug.Stack())
http.Error(w, &amp;#34;内部服务错误&amp;#34;, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// 链式组合多个中间件
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
// 反序应用，保证执行顺序正确
for i := len(middlewares) - 1; i &amp;gt;= 0; i-- {
h = middlewares[i](h)
}
return h
}
// 使用
mux := http.NewServeMux()
mux.HandleFunc(&amp;#34;GET /api/users&amp;#34;, listUsers)
// 应用中间件：Recovery → Logger → mux（从外到内）
handler := Chain(mux, Recovery, Logger)
http.ListenAndServe(&amp;#34;:8080&amp;#34;, handler)
```
- 认证中间件（JWT 示意）
-
```go
type contextKey string
const userIDKey contextKey = &amp;#34;userID&amp;#34;
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get(&amp;#34;Authorization&amp;#34;)
if token == &amp;#34;&amp;#34; {
http.Error(w, &amp;#34;未授权&amp;#34;, http.StatusUnauthorized)
return
}
// 验证 token，提取 userID
userID, err := validateToken(strings.TrimPrefix(token, &amp;#34;Bearer &amp;#34;))
if err != nil {
http.Error(w, &amp;#34;无效令牌&amp;#34;, 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, &amp;#34;未认证&amp;#34;, http.StatusUnauthorized)
return
}
fmt.Fprintf(w, &amp;#34;当前用户 ID: %d&amp;#34;, userID)
}
// 只对特定路由加认证
mux.Handle(&amp;#34;GET /api/profile&amp;#34;, 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(&amp;#34;Access-Control-Allow-Origin&amp;#34;, allowOrigin)
w.Header().Set(&amp;#34;Access-Control-Allow-Methods&amp;#34;, &amp;#34;GET, POST, PUT, DELETE, OPTIONS&amp;#34;)
w.Header().Set(&amp;#34;Access-Control-Allow-Headers&amp;#34;, &amp;#34;Content-Type, Authorization&amp;#34;)
// OPTIONS 预检请求直接返回
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
// 使用
handler := Chain(mux,
Recovery,
Logger,
CORS(&amp;#34;https://myapp.com&amp;#34;),
)
```
- 构建完整 REST API
- 完整用户 CRUD REST API
-
```go
package main
import (
&amp;#34;encoding/json&amp;#34;
&amp;#34;errors&amp;#34;
&amp;#34;fmt&amp;#34;
&amp;#34;log&amp;#34;
&amp;#34;net/http&amp;#34;
&amp;#34;strconv&amp;#34;
&amp;#34;sync&amp;#34;
&amp;#34;time&amp;#34;
)
// ───── 模型 ─────
type User struct {
ID int `json:&amp;#34;id&amp;#34;`
Name string `json:&amp;#34;name&amp;#34;`
Email string `json:&amp;#34;email&amp;#34;`
CreatedAt time.Time `json:&amp;#34;created_at&amp;#34;`
}
// ───── 存储（内存）─────
type Store struct {
mu sync.RWMutex
users map[int]*User
nextID int
}
func NewStore() *Store {
return &amp;Store{users: make(map[int]*User), nextID: 1}
}
var ErrNotFound = errors.New(&amp;#34;not found&amp;#34;)
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&amp;#43;&amp;#43;
}
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 != &amp;#34;&amp;#34; { u.Name = name }
if email != &amp;#34;&amp;#34; { 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(&amp;#34;id&amp;#34;))
if err != nil &amp;&amp; r.PathValue(&amp;#34;id&amp;#34;) != &amp;#34;&amp;#34; {
writeError(w, 400, &amp;#34;无效的 ID&amp;#34;)
return
}
switch {
case r.Method == &amp;#34;GET&amp;#34; &amp;&amp; id == 0:
h.list(w, r)
case r.Method == &amp;#34;POST&amp;#34; &amp;&amp; id == 0:
h.create(w, r)
case r.Method == &amp;#34;GET&amp;#34; &amp;&amp; id &amp;gt; 0:
h.get(w, r, id)
case r.Method == &amp;#34;PUT&amp;#34; &amp;&amp; id &amp;gt; 0:
h.update(w, r, id)
case r.Method == &amp;#34;DELETE&amp;#34; &amp;&amp; id &amp;gt; 0:
h.delete(w, r, id)
default:
http.Error(w, &amp;#34;Not Found&amp;#34;, 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(&amp;u); err != nil {
writeError(w, 400, &amp;#34;无效的请求体&amp;#34;)
return
}
if u.Name == &amp;#34;&amp;#34; || u.Email == &amp;#34;&amp;#34; {
writeError(w, 400, &amp;#34;name 和 email 不能为空&amp;#34;)
return
}
h.store.Create(&amp;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, &amp;#34;用户不存在&amp;#34;)
return
}
writeJSON(w, 200, u)
}
func (h *UserHandler) update(w http.ResponseWriter, r *http.Request, id int) {
var req struct {
Name string `json:&amp;#34;name&amp;#34;`
Email string `json:&amp;#34;email&amp;#34;`
}
json.NewDecoder(r.Body).Decode(&amp;req)
u, err := h.store.Update(id, req.Name, req.Email)
if errors.Is(err, ErrNotFound) {
writeError(w, 404, &amp;#34;用户不存在&amp;#34;)
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, &amp;#34;用户不存在&amp;#34;)
return
}
w.WriteHeader(204)
}
// ───── 工具函数 ─────
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/json&amp;#34;)
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{&amp;#34;error&amp;#34;: msg})
}
// ───── main ─────
func main() {
store := NewStore()
h := &amp;UserHandler{store: store}
mux := http.NewServeMux()
// Go 1.22&amp;#43; 路由
mux.HandleFunc(&amp;#34;GET /api/users&amp;#34;, h.list)
mux.HandleFunc(&amp;#34;POST /api/users&amp;#34;, h.create)
mux.HandleFunc(&amp;#34;GET /api/users/{id}&amp;#34;, func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue(&amp;#34;id&amp;#34;))
h.get(w, r, id)
})
mux.HandleFunc(&amp;#34;PUT /api/users/{id}&amp;#34;, func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue(&amp;#34;id&amp;#34;))
h.update(w, r, id)
})
mux.HandleFunc(&amp;#34;DELETE /api/users/{id}&amp;#34;, func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue(&amp;#34;id&amp;#34;))
h.delete(w, r, id)
})
handler := Chain(mux, Recovery, Logger)
srv := &amp;http.Server{
Addr: &amp;#34;:8080&amp;#34;,
Handler: handler,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
}
fmt.Println(&amp;#34;🚀 服务器启动: http://localhost:8080&amp;#34;)
log.Fatal(srv.ListenAndServe())
}
```
- 用 curl 测试
-
```go
# 创建用户
curl -X POST http://localhost:8080/api/users \
-H &amp;#34;Content-Type: application/json&amp;#34; \
-d &amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;Alice&amp;#34;,&amp;#34;email&amp;#34;:&amp;#34;alice@example.com&amp;#34;}&amp;#39;
# → {&amp;#34;id&amp;#34;:1,&amp;#34;name&amp;#34;:&amp;#34;Alice&amp;#34;,&amp;#34;email&amp;#34;:&amp;#34;alice@example.com&amp;#34;,&amp;#34;created_at&amp;#34;:&amp;#34;...&amp;#34;}
# 获取所有用户
curl http://localhost:8080/api/users
# 获取单个用户
curl http://localhost:8080/api/users/1
# 更新用户
curl -X PUT http://localhost:8080/api/users/1 \
-H &amp;#34;Content-Type: application/json&amp;#34; \
-d &amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;Alicia&amp;#34;}&amp;#39;
# 删除用户
curl -X DELETE http://localhost:8080/api/users/1
```
- 优雅关闭
- 生产环境的服务器不能强制关闭——要等正在处理的请求完成后再退出。
- Go 的 http.Server.Shutdown 方法实现了这个功能，配合系统信号使用。
-
```go
package main
import (
&amp;#34;context&amp;#34;
&amp;#34;log&amp;#34;
&amp;#34;net/http&amp;#34;
&amp;#34;os&amp;#34;
&amp;#34;os/signal&amp;#34;
&amp;#34;syscall&amp;#34;
&amp;#34;time&amp;#34;
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc(&amp;#34;GET /&amp;#34;, func(w http.ResponseWriter, r *http.Request) {
// 模拟慢请求
time.Sleep(2 * time.Second)
fmt.Fprintln(w, &amp;#34;OK&amp;#34;)
})
srv := &amp;http.Server{
Addr: &amp;#34;:8080&amp;#34;,
Handler: mux,
}
// 在独立 goroutine 中启动服务器
go func() {
log.Println(&amp;#34;🚀 服务器启动: :8080&amp;#34;)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf(&amp;#34;服务器错误: %v&amp;#34;, err)
}
}()
// 监听系统信号（Ctrl&amp;#43;C 或 kill）
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
&amp;lt;-quit // 阻塞，等待信号
log.Println(&amp;#34;🛑 收到关闭信号，正在优雅关闭...&amp;#34;)
// 给正在处理的请求 30 秒完成
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf(&amp;#34;强制关闭: %v&amp;#34;, err)
} else {
log.Println(&amp;#34;✅ 服务器已优雅关闭&amp;#34;)
}
}
// 测试优雅关闭：
// 1. 启动服务器
// 2. 发送慢请求：curl http://localhost:8080/
// 3. 在请求处理中按 Ctrl&amp;#43;C
// 4. 观察：服务器等待请求完成后才退出
```
- Gin 框架入门
- 为什么用 Gin
- Gin 是在标准库之上的增强层，保留了所有 handler 逻辑，但是减少了重复代码
- 提供了更强的路由、更方便的参数绑定、更完善的错误处理
- 标准库 vs Gin 对比
- 标准库写法
-
```go
// ─── 标准库写法 ───
func getUser(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue(&amp;#34;id&amp;#34;)
id, err := strconv.Atoi(idStr)
if err != nil {
w.Header().Set(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/json&amp;#34;)
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{&amp;#34;error&amp;#34;: &amp;#34;无效的 ID&amp;#34;})
return
}
user, err := findUser(id)
if err != nil {
w.Header().Set(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/json&amp;#34;)
w.WriteHeader(404)
json.NewEncoder(w).Encode(map[string]string{&amp;#34;error&amp;#34;: &amp;#34;用户不存在&amp;#34;})
return
}
w.Header().Set(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/json&amp;#34;)
w.WriteHeader(200)
json.NewEncoder(w).Encode(user)
}
// ─── Gin 写法 ───
func getUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param(&amp;#34;id&amp;#34;))
if err != nil {
c.JSON(400, gin.H{&amp;#34;error&amp;#34;: &amp;#34;无效的 ID&amp;#34;})
return
}
user, err := findUser(id)
if err != nil {
c.JSON(404, gin.H{&amp;#34;error&amp;#34;: &amp;#34;用户不存在&amp;#34;})
return
}
c.JSON(200, user)
}
```
- 安装与快速上手
-
```go
go get github.com/gin-gonic/gin
```
-
```go
package main
import &amp;#34;github.com/gin-gonic/gin&amp;#34;
func main() {
r := gin.Default() // 默认包含 Logger &amp;#43; Recovery 中间件
r.GET(&amp;#34;/ping&amp;#34;, func(c *gin.Context) {
c.JSON(200, gin.H{&amp;#34;message&amp;#34;: &amp;#34;pong&amp;#34;})
})
r.Run(&amp;#34;:8080&amp;#34;) // 等价于 http.ListenAndServe(&amp;#34;:8080&amp;#34;, r)
}
// gin.H 是 map[string]any 的别名，专为 JSON 响应设计
// curl http://localhost:8080/ping
// → {&amp;#34;message&amp;#34;:&amp;#34;pong&amp;#34;}
```
- Gin 的性能
- Gin 实用 httprouter 为底层路由，比标准库的 ServeMux 更快
- 官方 benchmark 显示比标准库快 40 倍以上
- 路由与路由分组
- Gin 的路由系统非常强大，支持路径参数、通配符、路由分组，还能给分组统一加中间件。
- 基本路由
-
```go
r := gin.Default()
// HTTP 方法
r.GET(&amp;#34;/users&amp;#34;, listUsers)
r.POST(&amp;#34;/users&amp;#34;, createUser)
r.PUT(&amp;#34;/users/:id&amp;#34;, updateUser)
r.DELETE(&amp;#34;/users/:id&amp;#34;, deleteUser)
r.PATCH(&amp;#34;/users/:id/status&amp;#34;, updateStatus)
// 路径参数
r.GET(&amp;#34;/users/:id&amp;#34;, func(c *gin.Context) {
id := c.Param(&amp;#34;id&amp;#34;) // 精确参数：/users/42 → &amp;#34;42&amp;#34;
fmt.Println(id)
})
r.GET(&amp;#34;/files/*filepath&amp;#34;, func(c *gin.Context) {
path := c.Param(&amp;#34;filepath&amp;#34;) // 通配符：/files/a/b/c → &amp;#34;/a/b/c&amp;#34;
fmt.Println(path)
})
// Query 参数
r.GET(&amp;#34;/search&amp;#34;, func(c *gin.Context) {
keyword := c.Query(&amp;#34;q&amp;#34;) // ?q=golang
page := c.DefaultQuery(&amp;#34;page&amp;#34;, &amp;#34;1&amp;#34;) // 带默认值
fmt.Println(keyword, page)
})
// 任意方法
r.Any(&amp;#34;/webhook&amp;#34;, webhookHandler)
// 静态文件
r.Static(&amp;#34;/static&amp;#34;, &amp;#34;./public&amp;#34;)
r.StaticFile(&amp;#34;/favicon.ico&amp;#34;, &amp;#34;./favicon.ico&amp;#34;)
```
- 路由分组 Group
-
```go
r := gin.Default()
// 路由分组：共享前缀
v1 := r.Group(&amp;#34;/api/v1&amp;#34;)
{
v1.GET(&amp;#34;/users&amp;#34;, listUsers)
v1.POST(&amp;#34;/users&amp;#34;, createUser)
v1.GET(&amp;#34;/users/:id&amp;#34;, getUser)
}
v2 := r.Group(&amp;#34;/api/v2&amp;#34;)
{
v2.GET(&amp;#34;/users&amp;#34;, listUsersV2) // 新版接口
}
// 嵌套分组 &amp;#43; 中间件
admin := r.Group(&amp;#34;/admin&amp;#34;, AuthMiddleware(), AdminOnlyMiddleware())
{
admin.GET(&amp;#34;/dashboard&amp;#34;, dashboard)
admin.GET(&amp;#34;/users&amp;#34;, adminListUsers)
// 嵌套分组
settings := admin.Group(&amp;#34;/settings&amp;#34;)
{
settings.GET(&amp;#34;/&amp;#34;, getSettings)
settings.PUT(&amp;#34;/&amp;#34;, updateSettings)
}
}
// 只对部分路由加中间件
auth := r.Group(&amp;#34;/api&amp;#34;)
auth.Use(AuthMiddleware()) // 这组路由都需要认证
{
auth.GET(&amp;#34;/profile&amp;#34;, profile)
auth.PUT(&amp;#34;/profile&amp;#34;, updateProfile)
}
```
- NoRoute 和 NoMethod
-
```go
// 自定义 404
r.NoRoute(func(c *gin.Context) {
c.JSON(404, gin.H{&amp;#34;error&amp;#34;: &amp;#34;接口不存在&amp;#34;})
})
// 自定义 405（方法不允许）
r.NoMethod(func(c *gin.Context) {
c.JSON(405, gin.H{&amp;#34;error&amp;#34;: &amp;#34;方法不允许&amp;#34;})
})
```
- gin.Context: 一切的核心
- *gin.Context 封装了请求和响应，是 Gin handler 的唯一参数。掌握它的所有方法，你就掌握了 Gin 的核心。
- 读取请求参数
-
```go
func handler(c *gin.Context) {
// ─── 路径参数 ───
id := c.Param(&amp;#34;id&amp;#34;) // /users/:id
// ─── Query 参数 ───
q := c.Query(&amp;#34;keyword&amp;#34;) // ?keyword=go
page := c.DefaultQuery(&amp;#34;page&amp;#34;, &amp;#34;1&amp;#34;) // 带默认值
all := c.QueryMap(&amp;#34;tags&amp;#34;) // ?tags[a]=1&amp;tags[b]=2
// ─── Form 参数 ───
name := c.PostForm(&amp;#34;name&amp;#34;)
nick := c.DefaultPostForm(&amp;#34;nick&amp;#34;, &amp;#34;anonymous&amp;#34;)
// ─── Header ───
token := c.GetHeader(&amp;#34;Authorization&amp;#34;)
ua := c.GetHeader(&amp;#34;User-Agent&amp;#34;)
// ─── 请求信息 ───
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(&amp;#34;Authorization&amp;#34;)
userID, err := validateToken(token)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{&amp;#34;error&amp;#34;: &amp;#34;未授权&amp;#34;})
return // Abort 后必须 return！
}
c.Set(&amp;#34;userID&amp;#34;, userID) // 存入 context
c.Set(&amp;#34;isAdmin&amp;#34;, false)
c.Next() // 继续执行后续 handler/中间件
}
}
// Handler 取值
func profileHandler(c *gin.Context) {
userID, exists := c.Get(&amp;#34;userID&amp;#34;)
if !exists {
c.JSON(401, gin.H{&amp;#34;error&amp;#34;: &amp;#34;未认证&amp;#34;})
return
}
// 类型断言
id := userID.(int)
// 或者用类型安全的方法
id2, _ := c.GetInt(&amp;#34;userID&amp;#34;)
isAdmin, _ := c.GetBool(&amp;#34;isAdmin&amp;#34;)
c.JSON(200, gin.H{&amp;#34;userID&amp;#34;: id, &amp;#34;id2&amp;#34;: id2, &amp;#34;isAdmin&amp;#34;: isAdmin})
}
```
- 响应方法
-
```go
func handler(c *gin.Context) {
// JSON 响应（最常用）
c.JSON(200, gin.H{&amp;#34;message&amp;#34;: &amp;#34;ok&amp;#34;})
c.JSON(200, user) // 结构体自动序列化
// 其他格式
c.String(200, &amp;#34;Hello, %s!&amp;#34;, &amp;#34;World&amp;#34;)
c.HTML(200, &amp;#34;index.html&amp;#34;, gin.H{&amp;#34;title&amp;#34;: &amp;#34;首页&amp;#34;})
c.XML(200, user)
c.YAML(200, user)
// 文件响应
c.File(&amp;#34;./public/file.pdf&amp;#34;)
c.FileAttachment(&amp;#34;./public/file.pdf&amp;#34;, &amp;#34;download.pdf&amp;#34;)
c.Data(200, &amp;#34;image/png&amp;#34;, imageBytes)
// 重定向
c.Redirect(302, &amp;#34;https://example.com&amp;#34;)
// 状态码快捷方式
c.Status(204) // 只有状态码，无 body
// 中止后续中间件（不中止当前 handler）
c.Abort()
c.AbortWithStatus(403)
c.AbortWithStatusJSON(403, gin.H{&amp;#34;error&amp;#34;: &amp;#34;禁止访问&amp;#34;})
}
```
- 参数绑定与校验
- Gin 集成了 validator 库，可以在解析请求参数的同时做校验，大大减少了手动 if err 的代码量。
- ShouldBind：绑定 &amp;#43; 校验
-
```go
// 定义请求结构体，用 binding tag 指定校验规则
type CreateUserRequest struct {
Name string `json:&amp;#34;name&amp;#34; binding:&amp;#34;required,min=2,max=50&amp;#34;`
Email string `json:&amp;#34;email&amp;#34; binding:&amp;#34;required,email&amp;#34;`
Age int `json:&amp;#34;age&amp;#34; binding:&amp;#34;gte=0,lte=150&amp;#34;`
Password string `json:&amp;#34;password&amp;#34; binding:&amp;#34;required,min=8&amp;#34;`
}
func createUser(c *gin.Context) {
var req CreateUserRequest
// ShouldBindJSON：解析 JSON body &amp;#43; 校验
if err := c.ShouldBindJSON(&amp;req); err != nil {
c.JSON(400, gin.H{&amp;#34;error&amp;#34;: err.Error()})
return
}
// req 现在已经被填充且通过了校验
user, err := userService.Create(c.Request.Context(), req.Name, req.Email)
if err != nil {
c.JSON(500, gin.H{&amp;#34;error&amp;#34;: err.Error()})
return
}
c.JSON(201, user)
}
```
- 各种 Bind 方法
-
```go
// ShouldBindJSON：绑定 JSON body
c.ShouldBindJSON(&amp;req)
// ShouldBindQuery：绑定 query 参数
type SearchRequest struct {
Keyword string `form:&amp;#34;q&amp;#34; binding:&amp;#34;required&amp;#34;`
Page int `form:&amp;#34;page&amp;#34; binding:&amp;#34;gte=1&amp;#34;`
Size int `form:&amp;#34;size&amp;#34; binding:&amp;#34;gte=1,lte=100&amp;#34;`
}
c.ShouldBindQuery(&amp;req)
// ShouldBindUri：绑定路径参数
type UriRequest struct {
ID int `uri:&amp;#34;id&amp;#34; binding:&amp;#34;required,gt=0&amp;#34;`
}
c.ShouldBindUri(&amp;req)
// ShouldBind：自动根据 Content-Type 选择
c.ShouldBind(&amp;req)
// Bind 和 ShouldBind 的区别：
// ShouldBind：失败只返回 error，状态码由你决定（推荐）
// Bind：失败自动设置 400，并写入响应（不灵活）
```
- 常用校验规则
-
```go
type Example struct {
// 必填
Name string `binding:&amp;#34;required&amp;#34;`
// 长度限制
Bio string `binding:&amp;#34;max=500&amp;#34;`
Code string `binding:&amp;#34;len=6&amp;#34;`
Tag string `binding:&amp;#34;min=1,max=20&amp;#34;`
// 数值范围
Age int `binding:&amp;#34;gte=0,lte=150&amp;#34;`
Score float64 `binding:&amp;#34;gt=0&amp;#34;`
// 格式校验
Email string `binding:&amp;#34;required,email&amp;#34;`
URL string `binding:&amp;#34;url&amp;#34;`
Phone string `binding:&amp;#34;e164&amp;#34;` // 国际电话格式
// 枚举值
Status string `binding:&amp;#34;oneof=active inactive deleted&amp;#34;`
Role string `binding:&amp;#34;oneof=admin editor viewer&amp;#34;`
// 条件校验
Password string `binding:&amp;#34;required_with=Email&amp;#34;` // Email 填了就必填
// 自定义：omitempty &amp;#43; 条件
Nick string `binding:&amp;#34;omitempty,min=2&amp;#34;` // 有值才校验
}
// 自定义校验器
import &amp;#34;github.com/go-playground/validator/v10&amp;#34;
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(&amp;#34;cnphone&amp;#34;, validateChinesePhone)
}
type Req struct {
Phone string `binding:&amp;#34;required,cnphone&amp;#34;`
}
```
- 统一处理校验错误
- 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(&amp;#34;→ %s %s\n&amp;#34;, c.Request.Method, path)
c.Next() // 执行下一个 handler/中间件
// ─── 后置：响应返回后执行 ───
duration := time.Since(start)
status := c.Writer.Status()
fmt.Printf(&amp;#34;← %s %s %d %v\n&amp;#34;, 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(&amp;#34;Authorization&amp;#34;)
if authHeader == &amp;#34;&amp;#34; {
c.AbortWithStatusJSON(401, gin.H{&amp;#34;error&amp;#34;: &amp;#34;缺少认证令牌&amp;#34;})
return
}
// &amp;#34;Bearer &amp;lt;token&amp;gt;&amp;#34;
parts := strings.SplitN(authHeader, &amp;#34; &amp;#34;, 2)
if len(parts) != 2 || parts[0] != &amp;#34;Bearer&amp;#34; {
c.AbortWithStatusJSON(401, gin.H{&amp;#34;error&amp;#34;: &amp;#34;令牌格式错误&amp;#34;})
return
}
claims, err := parseJWT(parts[1])
if err != nil {
c.AbortWithStatusJSON(401, gin.H{&amp;#34;error&amp;#34;: &amp;#34;无效的令牌&amp;#34;})
return
}
// 把用户信息注入 context
c.Set(&amp;#34;userID&amp;#34;, claims.UserID)
c.Set(&amp;#34;userEmail&amp;#34;, 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{
&amp;#34;error&amp;#34;: &amp;#34;请求过于频繁，请稍后再试&amp;#34;,
})
return
}
c.Next()
}
}
```
- Abort vs Return
-
```go
// ⚠️ 重要：Abort 不会停止当前函数执行！
// 必须同时 return，否则后续代码还会执行
func badMiddleware(c *gin.Context) {
c.AbortWithStatusJSON(401, gin.H{&amp;#34;error&amp;#34;: &amp;#34;未授权&amp;#34;})
// ❌ 没有 return，后续代码还会执行！
doSomethingDangerous() // 会被执行
}
func goodMiddleware(c *gin.Context) {
c.AbortWithStatusJSON(401, gin.H{&amp;#34;error&amp;#34;: &amp;#34;未授权&amp;#34;})
return // ✅ 明确 return，停止执行
}
// Abort 的作用是：阻止后续的中间件和 handler 执行
// 但当前函数中 Abort 之后的代码仍然会执行
```
- 统一错误处理
- 在每个 handler 里写重复的错误处理代码很烦。
- Gin 提供了统一错误处理的机制——handler 只负责「报告」错误，中间件统一「处理」错误。
-
```go
// ─── 定义 AppError ───
type AppError struct {
Status int `json:&amp;#34;-&amp;#34;`
Code string `json:&amp;#34;code&amp;#34;`
Message string `json:&amp;#34;message&amp;#34;`
Err error `json:&amp;#34;-&amp;#34;`
}
func (e *AppError) Error() string { return e.Message }
// 快捷构造
func NotFound(msg string) *AppError {
return &amp;AppError{Status: 404, Code: &amp;#34;NOT_FOUND&amp;#34;, Message: msg}
}
func BadRequest(msg string) *AppError {
return &amp;AppError{Status: 400, Code: &amp;#34;BAD_REQUEST&amp;#34;, Message: msg}
}
func Internal(err error) *AppError {
return &amp;AppError{Status: 500, Code: &amp;#34;INTERNAL_ERROR&amp;#34;, Message: &amp;#34;内部服务错误&amp;#34;, 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, &amp;appErr) {
c.JSON(appErr.Status, appErr)
} else {
// 记录未预期的错误
log.Printf(&amp;#34;未处理的错误: %v&amp;#34;, err)
c.JSON(500, gin.H{&amp;#34;code&amp;#34;: &amp;#34;INTERNAL_ERROR&amp;#34;, &amp;#34;message&amp;#34;: &amp;#34;内部服务错误&amp;#34;})
}
}
}
// ─── Handler 里只需要 c.Error(err) ───
func getUser(c *gin.Context) {
id := c.Param(&amp;#34;id&amp;#34;)
user, err := userService.GetUser(c.Request.Context(), id)
if err != nil {
// 只需添加错误，不需要写响应
_ = c.Error(NotFound(&amp;#34;用户不存在&amp;#34;))
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 (
&amp;#34;github.com/gin-gonic/gin&amp;#34;
&amp;#34;github.com/yourname/myapp/internal/handler/middleware&amp;#34;
&amp;#34;github.com/yourname/myapp/internal/service&amp;#34;
)
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(&amp;#34;/health&amp;#34;, func(c *gin.Context) {
c.JSON(200, gin.H{&amp;#34;status&amp;#34;: &amp;#34;ok&amp;#34;})
})
// API 路由
api := r.Group(&amp;#34;/api/v1&amp;#34;)
{
// 认证路由（不需要 JWT）
auth := NewAuthHandler(authSvc)
api.POST(&amp;#34;/register&amp;#34;, auth.Register)
api.POST(&amp;#34;/login&amp;#34;, auth.Login)
api.POST(&amp;#34;/refresh&amp;#34;, auth.RefreshToken)
// 需要认证的路由
authed := api.Group(&amp;#34;&amp;#34;)
authed.Use(middleware.JWTAuth())
{
users := NewUserHandler(userSvc)
authed.GET(&amp;#34;/users&amp;#34;, users.List)
authed.GET(&amp;#34;/users/:id&amp;#34;, users.Get)
authed.PUT(&amp;#34;/users/:id&amp;#34;, users.Update)
authed.DELETE(&amp;#34;/users/:id&amp;#34;, users.Delete)
authed.GET(&amp;#34;/profile&amp;#34;, 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 := &amp;http.Server{
Addr: &amp;#34;:&amp;#34; &amp;#43; 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 (
&amp;#34;gorm.io/driver/postgres&amp;#34;
&amp;#34;gorm.io/driver/sqlite&amp;#34;
&amp;#34;gorm.io/gorm&amp;#34;
&amp;#34;gorm.io/gorm/logger&amp;#34;
)
// PostgreSQL（生产推荐）
func NewPostgresDB(dsn string) (*gorm.DB, error) {
db, err := gorm.Open(postgres.Open(dsn), &amp;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: &amp;#34;host=localhost user=postgres password=secret dbname=myapp port=5432 sslmode=disable&amp;#34;
// MySQL: &amp;#34;user:password@tcp(127.0.0.1:3306)/myapp?charset=utf8mb4&amp;parseTime=True&amp;loc=Local&amp;#34;
// SQLite（快速测试/开发用）
func NewSQLiteDB(path string) (*gorm.DB, error) {
return gorm.Open(sqlite.Open(path), &amp;gorm.Config{})
}
// 内存 SQLite（单元测试专用）
func NewTestDB() (*gorm.DB, error) {
return gorm.Open(sqlite.Open(&amp;#34;:memory:&amp;#34;), &amp;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 (
&amp;#34;time&amp;#34;
&amp;#34;gorm.io/gorm&amp;#34;
)
// 嵌入 gorm.Model：自动拥有 id, created_at, updated_at, deleted_at
type User struct {
gorm.Model // 嵌入基础字段
Name string `gorm:&amp;#34;size:100;not null&amp;#34;`
Email string `gorm:&amp;#34;uniqueIndex;size:255;not null&amp;#34;`
Password string `gorm:&amp;#34;size:255;not null&amp;#34;`
Age int `gorm:&amp;#34;default:0&amp;#34;`
Active bool `gorm:&amp;#34;default:true&amp;#34;`
}
// gorm.Model 展开后等价于：
// ID uint `gorm:&amp;#34;primarykey&amp;#34;`
// CreatedAt time.Time
// UpdatedAt time.Time
// DeletedAt gorm.DeletedAt `gorm:&amp;#34;index&amp;#34;` ← 软删除
// 不想用 gorm.Model？自定义主键
type Article struct {
ID int `gorm:&amp;#34;primaryKey;autoIncrement&amp;#34;`
Title string `gorm:&amp;#34;size:200;not null;index&amp;#34;`
Content string `gorm:&amp;#34;type:text&amp;#34;`
AuthorID uint `gorm:&amp;#34;not null&amp;#34;`
Published bool `gorm:&amp;#34;default:false&amp;#34;`
CreatedAt time.Time
UpdatedAt time.Time
}
```
- 常用 GORM TAG
-
```go
type Example struct {
// 主键
ID uint `gorm:&amp;#34;primaryKey&amp;#34;`
UUID string `gorm:&amp;#34;primaryKey;type:uuid;default:gen_random_uuid()&amp;#34;`
// 列属性
Name string `gorm:&amp;#34;column:full_name;size:100;not null&amp;#34;`
Price float64 `gorm:&amp;#34;type:decimal(10,2);default:0&amp;#34;`
Bio string `gorm:&amp;#34;type:text&amp;#34;`
// 索引
Email string `gorm:&amp;#34;uniqueIndex&amp;#34;` // 唯一索引
City string `gorm:&amp;#34;index&amp;#34;` // 普通索引
Code string `gorm:&amp;#34;index:idx_code_type&amp;#34;` // 联合索引（和 Type 同名）
Type string `gorm:&amp;#34;index:idx_code_type&amp;#34;` // 联合索引
// 忽略字段（不映射到数据库）
Computed string `gorm:&amp;#34;-&amp;#34;`
// 创建时只写、更新时只写
CreatedBy string `gorm:&amp;#34;-&amp;gt;:false;&amp;lt;-:create&amp;#34;` // 只在创建时写
// 嵌入结构体
Address `gorm:&amp;#34;embedded;embeddedPrefix:addr_&amp;#34;` // 前缀 addr_
}
type Address struct {
Street string
City string
}
// 数据库列：addr_street, addr_city
```
- AutoMigrate：自动建表
-
```go
// 自动创建/更新表结构（只加列，不删列，生产用专业迁移工具）
err := db.AutoMigrate(
&amp;model.User{},
&amp;model.Article{},
&amp;model.Comment{},
)
if err != nil {
log.Fatal(&amp;#34;数据库迁移失败:&amp;#34;, err)
}
// AutoMigrate 的行为：
// ✅ 表不存在 → 创建表
// ✅ 字段不存在 → 添加字段
// ✅ 索引不存在 → 创建索引
// ❌ 字段类型变了 → 不会修改（需要手动迁移）
// ❌ 删掉字段 → 不会删除数据库列（安全策略）
```
- CRUD 基础操作
- Create (创建)
-
```go
// 创建单条记录
user := model.User{Name: &amp;#34;Alice&amp;#34;, Email: &amp;#34;alice@example.com&amp;#34;}
result := db.Create(&amp;user) // ✨ 创建后 user.ID 会被填充
if result.Error != nil {
return result.Error
}
fmt.Println(&amp;#34;新用户 ID:&amp;#34;, user.ID)
// 批量创建
users := []model.User{
{Name: &amp;#34;Bob&amp;#34;, Email: &amp;#34;bob@example.com&amp;#34;},
{Name: &amp;#34;Charlie&amp;#34;, Email: &amp;#34;charlie@example.com&amp;#34;},
}
db.Create(&amp;users) // 单次 SQL 插入多行，比循环创建高效
// 只创建指定字段
db.Select(&amp;#34;Name&amp;#34;, &amp;#34;Email&amp;#34;).Create(&amp;user)
// 忽略某些字段
db.Omit(&amp;#34;Age&amp;#34;, &amp;#34;Active&amp;#34;).Create(&amp;user)
```
- Read (查询)
-
```go
// 按主键查询
var user model.User
db.First(&amp;user, 1) // SELECT * FROM users WHERE id = 1 LIMIT 1
db.First(&amp;user, &amp;#34;id = ?&amp;#34;, 1) // 等价写法
// 查询单条（不按主键排序）
db.Take(&amp;user, 1) // SELECT * FROM users WHERE id = 1 LIMIT 1
db.Last(&amp;user) // 最后一条（按主键 DESC）
// 条件查询
db.Where(&amp;#34;name = ?&amp;#34;, &amp;#34;Alice&amp;#34;).First(&amp;user)
db.Where(&amp;#34;age &amp;gt; ? AND active = ?&amp;#34;, 18, true).Find(&amp;users)
// IN 查询
db.Where(&amp;#34;id IN ?&amp;#34;, []int{1, 2, 3}).Find(&amp;users)
// LIKE 查询
db.Where(&amp;#34;name LIKE ?&amp;#34;, &amp;#34;%Alice%&amp;#34;).Find(&amp;users)
// 查询多条
var users []model.User
result := db.Find(&amp;users) // SELECT * FROM users（自动过滤 deleted_at IS NULL）
fmt.Println(&amp;#34;查到&amp;#34;, result.RowsAffected, &amp;#34;条&amp;#34;)
// 查询所有（含软删除的）
db.Unscoped().Find(&amp;users)
// 只查指定字段
db.Select(&amp;#34;id&amp;#34;, &amp;#34;name&amp;#34;, &amp;#34;email&amp;#34;).Find(&amp;users)
// 检查记录是否存在
var count int64
db.Model(&amp;model.User{}).Where(&amp;#34;email = ?&amp;#34;, email).Count(&amp;count)
exists := count &amp;gt; 0
```
- Update (更新)
-
```go
// 更新单个字段
db.Model(&amp;user).Update(&amp;#34;name&amp;#34;, &amp;#34;Alicia&amp;#34;)
// UPDATE users SET name=&amp;#39;Alicia&amp;#39;, updated_at=NOW() WHERE id=1
// 更新多个字段（用 map）
db.Model(&amp;user).Updates(map[string]any{
&amp;#34;name&amp;#34;: &amp;#34;Alicia&amp;#34;,
&amp;#34;active&amp;#34;: false,
})
// 更新多个字段（用结构体，⚠️ 零值字段会被忽略！）
db.Model(&amp;user).Updates(model.User{Name: &amp;#34;Alicia&amp;#34;, Age: 0})
// ⚠️ Age=0 是零值，不会被更新！
// 解决零值问题：用 Select 明确指定要更新的字段
db.Model(&amp;user).Select(&amp;#34;Name&amp;#34;, &amp;#34;Age&amp;#34;).Updates(model.User{Name: &amp;#34;Alicia&amp;#34;, Age: 0})
// Age=0 会被更新
// 带条件的更新
db.Model(&amp;model.User{}).Where(&amp;#34;active = ?&amp;#34;, false).Update(&amp;#34;deleted&amp;#34;, true)
// 不触发 Hook 和 updated_at 的原生更新
db.Model(&amp;user).UpdateColumn(&amp;#34;login_count&amp;#34;, gorm.Expr(&amp;#34;login_count &amp;#43; 1&amp;#34;))
```
- Delete (删除)
-
```go
// 软删除（如果模型有 DeletedAt 字段）
// 只设置 deleted_at，不真正删除
db.Delete(&amp;user, 1)
// UPDATE users SET deleted_at=NOW() WHERE id=1
// 查询时自动过滤软删除的记录
db.Find(&amp;users) // WHERE deleted_at IS NULL
// 硬删除（真正删除）
db.Unscoped().Delete(&amp;user, 1)
// DELETE FROM users WHERE id=1
// 批量删除（必须有条件，防止误删全表）
db.Where(&amp;#34;active = ?&amp;#34;, false).Delete(&amp;model.User{})
// 如果不加条件会报错（安全机制）
```
- 结构体 Updates 的零值陷阱
- 用结构体更新时，GORM 会忽略零值字段（0、&amp;#34;&amp;#34;、false、nil），只更新非零值。
- 这个设计防止了意外覆盖，但也导致你没法把一个字段更新为零值。
- 解决方案：用 map[string]any 替代结构体，或者用 Select 明确列出要更新的字段。这是 GORM 面试高频考点！
- 查询进阶
- 链式调用构建复杂查询
-
```go
// GORM 的链式 API：每个方法返回 *gorm.DB，可以任意组合
var users []model.User
db.
Where(&amp;#34;age &amp;gt; ?&amp;#34;, 18).
Where(&amp;#34;active = ?&amp;#34;, true).
Order(&amp;#34;created_at DESC&amp;#34;).
Limit(10).
Offset(20). // 跳过 20 条（第 3 页，每页 10 条）
Find(&amp;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(&amp;#34;active = ?&amp;#34;, true).
Order(&amp;#34;name ASC&amp;#34;).
Find(&amp;users)
```
- 聚合与分组
-
```go
// Count
var count int64
db.Model(&amp;model.User{}).Where(&amp;#34;active = ?&amp;#34;, true).Count(&amp;count)
// Sum, Avg, Max, Min
var totalAge float64
db.Model(&amp;model.User{}).Select(&amp;#34;COALESCE(SUM(age), 0)&amp;#34;).Scan(&amp;totalAge)
// Group By &amp;#43; Having
type Result struct {
City string
Count int64
}
var results []Result
db.Model(&amp;model.User{}).
Select(&amp;#34;city, COUNT(*) as count&amp;#34;).
Group(&amp;#34;city&amp;#34;).
Having(&amp;#34;COUNT(*) &amp;gt; ?&amp;#34;, 10).
Scan(&amp;results)
// Distinct
db.Distinct(&amp;#34;name&amp;#34;, &amp;#34;email&amp;#34;).Find(&amp;users)
```
- 原生 SQL
-
```go
// Raw：执行原生 SQL 并扫描结果
var users []model.User
db.Raw(&amp;#34;SELECT * FROM users WHERE age &amp;gt; ? AND active = ?&amp;#34;, 18, true).Scan(&amp;users)
// Exec：执行不返回行的 SQL
db.Exec(&amp;#34;UPDATE users SET login_count = login_count &amp;#43; 1 WHERE id = ?&amp;#34;, 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(&amp;summaries)
// Named 参数（更安全）
db.Where(&amp;#34;name = @name AND email = @email&amp;#34;,
sql.Named(&amp;#34;name&amp;#34;, &amp;#34;Alice&amp;#34;),
sql.Named(&amp;#34;email&amp;#34;, &amp;#34;alice@example.com&amp;#34;),
).Find(&amp;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 // 外键（约定：类型名&amp;#43;ID）
User User // Belongs To
Comments []Comment
}
// 创建关联数据
user := User{
Name: &amp;#34;Alice&amp;#34;,
Articles: []Article{
{Title: &amp;#34;Go 入门&amp;#34;, Content: &amp;#34;...&amp;#34;},
{Title: &amp;#34;Gin 框架&amp;#34;, Content: &amp;#34;...&amp;#34;},
},
}
db.Create(&amp;user) // 同时创建用户和文章
// 预加载关联（避免 N&amp;#43;1 问题）
var users []User
db.Preload(&amp;#34;Articles&amp;#34;).Find(&amp;users)
// SELECT * FROM users
// SELECT * FROM articles WHERE user_id IN (1,2,3,...)
// 嵌套预加载
db.Preload(&amp;#34;Articles.Comments&amp;#34;).Find(&amp;users)
// 条件预加载
db.Preload(&amp;#34;Articles&amp;#34;, &amp;#34;published = ?&amp;#34;, true).Find(&amp;users)
```
- 多对多
-
```go
// 文章有多个标签，标签也属于多篇文章
type Article struct {
gorm.Model
Title string
Tags []Tag `gorm:&amp;#34;many2many:article_tags&amp;#34;` // 中间表名
}
type Tag struct {
gorm.Model
Name string `gorm:&amp;#34;uniqueIndex&amp;#34;`
Articles []Article `gorm:&amp;#34;many2many:article_tags&amp;#34;`
}
// GORM 自动管理中间表 article_tags (article_id, tag_id)
// 创建时关联标签
tags := []Tag{{Name: &amp;#34;Go&amp;#34;}, {Name: &amp;#34;Backend&amp;#34;}}
article := Article{Title: &amp;#34;Go Web 开发&amp;#34;, Tags: tags}
db.Create(&amp;article)
// 追加关联
db.Model(&amp;article).Association(&amp;#34;Tags&amp;#34;).Append([]Tag{{Name: &amp;#34;Tutorial&amp;#34;}})
// 替换关联
db.Model(&amp;article).Association(&amp;#34;Tags&amp;#34;).Replace(newTags)
// 删除关联（只删关系，不删记录）
db.Model(&amp;article).Association(&amp;#34;Tags&amp;#34;).Delete(tag)
// 清空关联
db.Model(&amp;article).Association(&amp;#34;Tags&amp;#34;).Clear()
// 计数
count := db.Model(&amp;article).Association(&amp;#34;Tags&amp;#34;).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(&amp;#34;Profile&amp;#34;).First(&amp;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(&amp;order).Error; err != nil {
return err // 返回 error → 自动回滚
}
// 扣减库存
result := tx.Model(&amp;Product{}).
Where(&amp;#34;id = ? AND stock &amp;gt; 0&amp;#34;, productID).
UpdateColumn(&amp;#34;stock&amp;#34;, gorm.Expr(&amp;#34;stock - 1&amp;#34;))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New(&amp;#34;库存不足&amp;#34;) // 返回 error → 回滚
}
// 记录流水
if err := tx.Create(&amp;Transaction{OrderID: order.ID}).Error; err != nil {
return err
}
return nil // 返回 nil → 自动提交
})
if err != nil {
log.Printf(&amp;#34;下单失败: %v&amp;#34;, 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(&amp;user).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&amp;profile).Error; err != nil {
tx.Rollback()
return err
}
tx.Commit() // 全部成功才提交
return nil
// 保存点（嵌套事务）
tx.SavePoint(&amp;#34;step1&amp;#34;)
// ... 一些操作
tx.RollbackTo(&amp;#34;step1&amp;#34;) // 只回滚到保存点，不全部回滚
```
- 事务内必须用 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 != &amp;#34;&amp;#34; {
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 = &amp;#34;&amp;#34; // 查出来后清空密码字段
return nil
}
// 更新前：校验必填字段
func (u *User) BeforeUpdate(tx *gorm.DB) error {
if u.Email == &amp;#34;&amp;#34; {
return errors.New(&amp;#34;email 不能为空&amp;#34;)
}
return nil
}
// 删除前：记录审计日志
func (u *User) BeforeDelete(tx *gorm.DB) error {
return tx.Create(&amp;AuditLog{
Action: &amp;#34;DELETE&amp;#34;,
Resource: &amp;#34;users&amp;#34;,
ResourceID: u.ID,
}).Error
}
```
- Repository 模式整合
- 把 GORM 封装到 Repository 层，让 Service 层不依赖具体的数据库实现。
-
```go
// internal/repository/user.go
package repository
import (
&amp;#34;context&amp;#34;
&amp;#34;errors&amp;#34;
&amp;#34;gorm.io/gorm&amp;#34;
&amp;#34;github.com/yourname/myapp/internal/model&amp;#34;
)
var ErrNotFound = errors.New(&amp;#34;记录不存在&amp;#34;)
// 接口（让 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 &amp;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(&amp;u, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return &amp;u, err
}
func (r *gormUserRepo) FindByEmail(ctx context.Context, email string) (*model.User, error) {
var u model.User
err := r.db.WithContext(ctx).Where(&amp;#34;email = ?&amp;#34;, email).First(&amp;u).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return &amp;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(&amp;model.User{})
if err := db.Count(&amp;count).Error; err != nil {
return nil, 0, err
}
if err := db.Offset(offset).Limit(size).
Order(&amp;#34;created_at DESC&amp;#34;).Find(&amp;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(&amp;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(&amp;u, id).Error
// ↑ 把 ctx 传给 GORM，超时自动取消 SQL
return &amp;u, err
}
// ❌ 没有 WithContext：HTTP 超时后 SQL 还在跑，浪费数据库资源
func (r *gormUserRepo) FindByID(id uint) (*model.User, error) {
var u model.User
err := r.db.First(&amp;u, id).Error // 没有超时控制
return &amp;u, err
}
```
- GORM 高级查询
- scrope 复用查询逻辑
-
```go
// Scope 是返回 func(db *gorm.DB) *gorm.DB 的函数
// 让你把常用的查询条件封装成可复用的&amp;#34;查询片段&amp;#34;
// 1. 分页 Scope
func Paginate(page, size int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page &amp;lt; 1 { page = 1 }
if size &amp;lt; 1 || size &amp;gt; 100 { size = 20 }
return db.Offset((page-1)*size).Limit(size)
}
}
// 2. 只查活跃用户
func ActiveOnly(db *gorm.DB) *gorm.DB {
return db.Where(&amp;#34;active = ?&amp;#34;, true)
}
// 3. 按时间倒序
func LatestFirst(db *gorm.DB) *gorm.DB {
return db.Order(&amp;#34;created_at DESC&amp;#34;)
}
// 组合使用：业务代码非常清晰
var users []User
db.Scopes(ActiveOnly, LatestFirst, Paginate(2, 20)).Find(&amp;users)
// 等价于：
// db.Where(&amp;#34;active = ?&amp;#34;, true).
// Order(&amp;#34;created_at DESC&amp;#34;).
// Offset(20).Limit(20).
// Find(&amp;users)
```
- 预加载，解决 N&amp;#43;1 问题
-
```go
// ❌ N&amp;#43;1 问题：1 次查用户 &amp;#43; N 次查每个用户的文章
var users []User
db.Find(&amp;users)
for _, u := range users {
var articles []Article
db.Where(&amp;#34;user_id = ?&amp;#34;, u.ID).Find(&amp;articles) // 每个用户都查一次！
}
// ✅ Preload：2 次 SQL 解决（无论多少用户）
db.Preload(&amp;#34;Articles&amp;#34;).Find(&amp;users)
// SELECT * FROM users
// SELECT * FROM articles WHERE user_id IN (1,2,3,...)
// 嵌套预加载（用户 → 文章 → 评论）
db.Preload(&amp;#34;Articles.Comments&amp;#34;).Find(&amp;users)
// 条件预加载：只加载已发布的文章
db.Preload(&amp;#34;Articles&amp;#34;, &amp;#34;published = ?&amp;#34;, true).Find(&amp;users)
// 自定义预加载查询：函数式
db.Preload(&amp;#34;Articles&amp;#34;, func(db *gorm.DB) *gorm.DB {
return db.Where(&amp;#34;published = ?&amp;#34;, true).
Order(&amp;#34;created_at DESC&amp;#34;).
Limit(5)
}).Find(&amp;users)
// 预加载所有关联（懒人写法，慎用）
db.Preload(clause.Associations).Find(&amp;users)
```
- JOIN：按关联条件过滤
-
```go
// Preload 适合&amp;#34;加载关联数据&amp;#34;
// Joins 适合&amp;#34;按关联表字段过滤/排序&amp;#34;
// 查询「至少有 1 篇已发布文章」的用户
var users []User
db.Joins(&amp;#34;JOIN articles ON articles.user_id = users.id&amp;#34;).
Where(&amp;#34;articles.published = ?&amp;#34;, true).
Distinct().
Find(&amp;users)
// Joins &amp;#43; Preload 组合：用 Joins 过滤，用 Preload 加载完整关联
db.Joins(&amp;#34;JOIN articles ON articles.user_id = users.id AND articles.published = ?&amp;#34;, true).
Preload(&amp;#34;Articles&amp;#34;).
Distinct().
Find(&amp;users)
```
- 分页：完整封装
-
```go
type PageResult[T any] struct {
Items []T `json:&amp;#34;items&amp;#34;`
Total int64 `json:&amp;#34;total&amp;#34;`
Page int `json:&amp;#34;page&amp;#34;`
Size int `json:&amp;#34;size&amp;#34;`
HasNext bool `json:&amp;#34;has_next&amp;#34;`
}
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(&amp;total).Error; err != nil {
return nil, err
}
if err := db.Scopes(Paginate(page, size)).Find(&amp;items).Error; err != nil {
return nil, err
}
return &amp;PageResult[T]{
Items: items,
Total: total,
Page: page,
Size: size,
HasNext: int64(page*size) &amp;lt; total,
}, nil
}
// 使用
result, _ := Page[User](db.Where(&amp;#34;active = ?&amp;#34;, true), 1, 20)
```
- Preload vs Joins
- 需要把关联数据填到结构体里 → Preload；
- 只是按关联字段过滤、不需要关联数据 → Joins；
- 既要过滤又要加载完整关联 → Joins &amp;#43; 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 文件描述每次变更（向上 &amp;#43; 向下）
// 2. 工具记录已执行的版本
// 3. 部署前检查、出错可回滚
```
- 安装使用 golang-migrate
-
```go
# 安装 CLI
brew install golang-migrate # macOS
# 或
go install -tags &amp;#39;postgres&amp;#39; \
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 &amp;#34;postgres://postgres:secret@localhost:5432/myapp?sslmode=disable&amp;#34; \
up
# 回滚最后 1 个版本
migrate -path db/migrations -database &amp;#34;...&amp;#34; down 1
# 强制设置版本（脏数据修复用）
migrate -path db/migrations -database &amp;#34;...&amp;#34; 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 (
&amp;#34;embed&amp;#34;
&amp;#34;github.com/golang-migrate/migrate/v4&amp;#34;
&amp;#34;github.com/golang-migrate/migrate/v4/database/postgres&amp;#34;
&amp;#34;github.com/golang-migrate/migrate/v4/source/iofs&amp;#34;
_ &amp;#34;github.com/lib/pq&amp;#34;
&amp;#34;database/sql&amp;#34;
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
func RunMigrations(dsn string) error {
sqlDB, err := sql.Open(&amp;#34;postgres&amp;#34;, dsn)
if err != nil { return err }
defer sqlDB.Close()
driver, err := postgres.WithInstance(sqlDB, &amp;postgres.Config{})
if err != nil { return err }
src, err := iofs.New(migrationsFS, &amp;#34;migrations&amp;#34;)
if err != nil { return err }
m, err := migrate.NewWithInstance(&amp;#34;iofs&amp;#34;, src, &amp;#34;postgres&amp;#34;, driver)
if err != nil { return err }
if err := m.Up(); err != nil &amp;&amp; err != migrate.ErrNoChange {
return err
}
return nil
}
```
- Redis 操作
- 基础入门
- go-redis 客户端
-
```go
go get github.com/redis/go-redis/v9
```
- 示例程序
-
```go
package cache
import (
&amp;#34;context&amp;#34;
&amp;#34;time&amp;#34;
&amp;#34;github.com/redis/go-redis/v9&amp;#34;
)
func NewRedis(addr, password string, db int) (*redis.Client, error) {
rdb := redis.NewClient(&amp;redis.Options{
Addr: addr, // &amp;#34;localhost:6379&amp;#34;
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()
// 设置 &amp;#43; 过期时间
err := rdb.Set(ctx, &amp;#34;user:1&amp;#34;, `{&amp;#34;name&amp;#34;:&amp;#34;Alice&amp;#34;}`, 10*time.Minute).Err()
// 读取
val, err := rdb.Get(ctx, &amp;#34;user:1&amp;#34;).Result()
if err == redis.Nil {
// key 不存在
} else if err != nil {
// 出错
}
// SetNX：只在不存在时设置（分布式锁的基础）
ok, _ := rdb.SetNX(ctx, &amp;#34;lock:order:42&amp;#34;, &amp;#34;1&amp;#34;, 30*time.Second).Result()
if !ok { return errors.New(&amp;#34;已有人在处理&amp;#34;) }
// 计数器
rdb.Incr(ctx, &amp;#34;page_view:home&amp;#34;)
rdb.IncrBy(ctx, &amp;#34;score:user:1&amp;#34;, 10)
// 批量获取
vals, _ := rdb.MGet(ctx, &amp;#34;user:1&amp;#34;, &amp;#34;user:2&amp;#34;, &amp;#34;user:3&amp;#34;).Result()
```
- Hash: 对象字段缓存
-
```go
// 把一个 User 拆成多个字段，比 JSON 字符串更省内存、可单字段更新
rdb.HSet(ctx, &amp;#34;user:1&amp;#34;,
&amp;#34;name&amp;#34;, &amp;#34;Alice&amp;#34;,
&amp;#34;email&amp;#34;, &amp;#34;alice@example.com&amp;#34;,
&amp;#34;age&amp;#34;, 30)
name, _ := rdb.HGet(ctx, &amp;#34;user:1&amp;#34;, &amp;#34;name&amp;#34;).Result()
// 一次取所有字段
all, _ := rdb.HGetAll(ctx, &amp;#34;user:1&amp;#34;).Result()
// map[string]string{&amp;#34;name&amp;#34;:&amp;#34;Alice&amp;#34;, ...}
// 单字段自增
rdb.HIncrBy(ctx, &amp;#34;user:1&amp;#34;, &amp;#34;login_count&amp;#34;, 1)
```
- List/Set/ZSet
-
```go
// List：消息队列、最近浏览
rdb.LPush(ctx, &amp;#34;queue:emails&amp;#34;, &amp;#34;task1&amp;#34;, &amp;#34;task2&amp;#34;)
val, _ := rdb.RPop(ctx, &amp;#34;queue:emails&amp;#34;).Result() // 队尾出队
// 阻塞式弹出（适合 worker 消费）
result, _ := rdb.BLPop(ctx, 5*time.Second, &amp;#34;queue:emails&amp;#34;).Result()
// Set：去重集合、标签、好友关系
rdb.SAdd(ctx, &amp;#34;tags:article:1&amp;#34;, &amp;#34;go&amp;#34;, &amp;#34;redis&amp;#34;, &amp;#34;backend&amp;#34;)
exists, _ := rdb.SIsMember(ctx, &amp;#34;tags:article:1&amp;#34;, &amp;#34;go&amp;#34;).Result()
all, _ := rdb.SMembers(ctx, &amp;#34;tags:article:1&amp;#34;).Result()
// ZSet：排行榜、按分数排序
rdb.ZAdd(ctx, &amp;#34;leaderboard&amp;#34;, redis.Z{Score: 100, Member: &amp;#34;alice&amp;#34;})
rdb.ZAdd(ctx, &amp;#34;leaderboard&amp;#34;, redis.Z{Score: 85, Member: &amp;#34;bob&amp;#34;})
// 取前 10 名（分数从高到低）
top, _ := rdb.ZRevRangeWithScores(ctx, &amp;#34;leaderboard&amp;#34;, 0, 9).Result()
for _, z := range top {
fmt.Printf(&amp;#34;%v: %v\n&amp;#34;, z.Member, z.Score)
}
```
- Pipelines：批量操作
-
```go
// 一次往返发送多个命令，大幅减少网络开销
pipe := rdb.Pipeline()
incr := pipe.Incr(ctx, &amp;#34;page_view:home&amp;#34;)
pipe.Expire(ctx, &amp;#34;page_view:home&amp;#34;, 24*time.Hour)
_, err := pipe.Exec(ctx)
if err != nil { return err }
fmt.Println(&amp;#34;当前 PV:&amp;#34;, incr.Val())
```
- Cache-Aside 缓存策略
- Cache-Aside（旁路缓存）是最经典的缓存模式
- 应用先查缓存，缓存没有再查数据库并回填缓存。简单、可靠、易于理解。
- 读流程
-
```go
func GetUser(ctx context.Context, id uint) (*User, error) {
key := fmt.Sprintf(&amp;#34;user:%d&amp;#34;, id)
// 1. 查缓存
val, err := rdb.Get(ctx, key).Result()
if err == nil {
var u User
if err := json.Unmarshal([]byte(val), &amp;u); err == nil {
return &amp;u, nil // 缓存命中
}
} else if err != redis.Nil {
// Redis 出错（不是 key 不存在），降级直接查 DB
log.Warn(&amp;#34;redis error:&amp;#34;, err)
}
// 2. 缓存未命中 → 查数据库
var u User
if err := db.WithContext(ctx).First(&amp;u, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 防穿透：把&amp;#34;不存在&amp;#34;也缓存（短 TTL）
rdb.Set(ctx, key, &amp;#34;&amp;#34;, 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 &amp;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(&amp;#34;user:%d&amp;#34;, u.ID))
return nil
}
// ❌ 不推荐：先写 DB，再更新缓存
// 问题：两个并发更新可能让旧值覆盖新值（A 写 DB → B 写 DB → B 写缓存 → A 写缓存）
// 删除策略选 &amp;#34;delete&amp;#34; 而不是 &amp;#34;update&amp;#34; 的核心原因：
// 1. 删除幂等，更新可能写入旧数据
// 2. 下次读自然回填，避免缓存写入热点
// 3. 简单（不用维护缓存的写一致性）
```
- 封装通用缓存层
-
```go
// 用泛型封装一个通用的&amp;#34;查缓存 → 回源 → 回填&amp;#34;模式
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, &amp;v); err == nil {
return &amp;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(&amp;#34;user:%d&amp;#34;, id),
10*time.Minute,
func(ctx context.Context) (*User, error) {
var u User
return &amp;u, db.WithContext(ctx).First(&amp;u, id).Error
},
)
```
- 缓存三类问题
- 缓存穿透
-
```go
// 问题：恶意请求大量不存在的 ID
// → 缓存永远没有 → 每次都打到 DB → DB 被打挂
// 方案 1：缓存空值（短 TTL）
if errors.Is(err, gorm.ErrRecordNotFound) {
rdb.Set(ctx, key, &amp;#34;NULL_PLACEHOLDER&amp;#34;, 1*time.Minute)
return nil, ErrNotFound
}
// 读取时识别空值
val, _ := rdb.Get(ctx, key).Result()
if val == &amp;#34;NULL_PLACEHOLDER&amp;#34; { return nil, ErrNotFound }
// 方案 2：布隆过滤器
// 启动时把所有有效 ID 加入布隆过滤器
// 查询前先问布隆过滤器：这个 ID 可能存在吗？
// &amp;#34;肯定不存在&amp;#34; → 直接拒绝，不查 DB
```
- 缓存击穿
-
```go
// 问题：1 个超热点 key（比如首页配置）刚好过期
// → 大量请求同时穿透到 DB → DB 瞬间被打挂
// 方案：用 SetNX 实现单飞（singleflight）
// 只让 1 个请求去查 DB，其他请求等待结果
import &amp;#34;golang.org/x/sync/singleflight&amp;#34;
var sf singleflight.Group
func GetHotConfig(ctx context.Context) (*Config, error) {
if val, err := rdb.Get(ctx, &amp;#34;config:home&amp;#34;).Bytes(); err == nil {
var c Config
json.Unmarshal(val, &amp;c)
return &amp;c, nil
}
// singleflight：同一 key 同时只有 1 次回源
v, err, _ := sf.Do(&amp;#34;config:home&amp;#34;, func() (any, error) {
var c Config
if err := db.First(&amp;c).Error; err != nil { return nil, err }
if data, err := json.Marshal(c); err == nil {
rdb.Set(ctx, &amp;#34;config:home&amp;#34;, data, 10*time.Minute)
}
return &amp;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 &amp;#43; jitter
}
rdb.Set(ctx, key, val, RandomTTL(10*time.Minute))
// 实际过期时间分布在 10~12 分钟之间，自然错峰
// 方案 2：多级缓存（本地 &amp;#43; Redis），Redis 挂了还有本地兜底
// 方案 3：限流降级，DB 压力过大时直接返回旧数据或默认值
```
- 分布式锁
-
```go
// 简化版分布式锁
type RedisLock struct {
rdb *redis.Client
key string
value string // 锁的&amp;#34;持有者&amp;#34;标识，释放时校验
ttl time.Duration
}
func NewLock(rdb *redis.Client, key string, ttl time.Duration) *RedisLock {
return &amp;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 脚本保证原子性：「校验是我加的锁」&amp;#43;「删除」）
var unlockScript = redis.NewScript(`
if redis.call(&amp;#34;GET&amp;#34;, KEYS[1]) == ARGV[1] then
return redis.call(&amp;#34;DEL&amp;#34;, 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, &amp;#34;lock:order:42&amp;#34;, 30*time.Second)
ok, err := lock.TryLock(ctx)
if err != nil || !ok { return errors.New(&amp;#34;已有人在处理&amp;#34;) }
defer lock.Unlock(ctx)
// 临界区：处理订单
```
- 认证与授权 (JWT&amp;#43;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 &amp;#43; 刷新）
现代后端通常选 JWT，配合 Refresh Token 机制弥补失效问题。
```
- golang-jwt 实战
- Go 生态最常用的库是 golang-jwt/jwt。我们封装一个 TokenService，统一管理签发和校验。
- 安装
-
```go
go get github.com/golang-jwt/jwt/v5
```
- 封装 TokenService
-
```go
package auth
import (
&amp;#34;errors&amp;#34;
&amp;#34;time&amp;#34;
&amp;#34;github.com/golang-jwt/jwt/v5&amp;#34;
)
// 自定义 Claims：内嵌标准字段 &amp;#43; 业务字段
type Claims struct {
UserID uint `json:&amp;#34;uid&amp;#34;`
Email string `json:&amp;#34;email&amp;#34;`
Role string `json:&amp;#34;role&amp;#34;`
jwt.RegisteredClaims // 提供 ExpiresAt / IssuedAt / Issuer 等
}
type TokenService struct {
secret []byte
issuer string
accessTTL time.Duration
refreshTTL time.Duration
}
func NewTokenService(secret string) *TokenService {
return &amp;TokenService{
secret: []byte(secret),
issuer: &amp;#34;myapp&amp;#34;,
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, &amp;Claims{},
func(t *jwt.Token) (any, error) {
// ⚠️ 必须校验签名算法，防止 alg=none 攻击
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New(&amp;#34;unexpected signing method&amp;#34;)
}
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(&amp;#34;invalid token&amp;#34;)
}
return claims, nil
}
```
- 认证中间件
- 中间件从 Authorization Header 取 Token，校验通过后把用户信息塞进 context，后续 Handler 直接读取。
- 示例
-
```go
package middleware
import (
&amp;#34;context&amp;#34;
&amp;#34;net/http&amp;#34;
&amp;#34;strings&amp;#34;
&amp;#34;github.com/gin-gonic/gin&amp;#34;
)
type ctxKey string
const ClaimsKey ctxKey = &amp;#34;claims&amp;#34;
func JWTAuth(svc *auth.TokenService) gin.HandlerFunc {
return func(c *gin.Context) {
h := c.GetHeader(&amp;#34;Authorization&amp;#34;)
if !strings.HasPrefix(h, &amp;#34;Bearer &amp;#34;) {
c.AbortWithStatusJSON(401, gin.H{&amp;#34;error&amp;#34;:&amp;#34;missing or invalid Authorization header&amp;#34;})
return
}
tokenStr := strings.TrimPrefix(h, &amp;#34;Bearer &amp;#34;)
claims, err := svc.Parse(tokenStr)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{&amp;#34;error&amp;#34;:&amp;#34;invalid token: &amp;#34; &amp;#43; err.Error()})
return
}
// 把 claims 写入 context
ctx := context.WithValue(c.Request.Context(), ClaimsKey, claims)
c.Request = c.Request.WithContext(ctx)
c.Set(&amp;#34;claims&amp;#34;, claims) // Gin 也提供了内部 Set/Get
c.Next()
}
}
// Handler 里取出当前用户
func ClaimsFrom(c *gin.Context) *auth.Claims {
if v, ok := c.Get(&amp;#34;claims&amp;#34;); ok {
return v.(*auth.Claims)
}
return nil
}
// 使用
r := gin.Default()
r.POST(&amp;#34;/api/v1/auth/login&amp;#34;, loginHandler) // 公开
authed := r.Group(&amp;#34;/api/v1&amp;#34;, JWTAuth(tokenSvc)) // 需要登录
{
authed.GET(&amp;#34;/me&amp;#34;, meHandler)
authed.POST(&amp;#34;/articles&amp;#34;, createArticleHandler)
}
```
- 密码加密：bcypt
- 绝对禁止把密码明文存数据库。bcrypt 是密码哈希的事实标准：自带盐、计算慢（抗暴力破解）、参数可调。
- 安装
```go
go get golang.org/x/crypto/bcrypt
```
- 示例
-
```go
package auth
import &amp;#34;golang.org/x/crypto/bcrypt&amp;#34;
// Cost 推荐 12，越高越慢但越安全（2025 年合理范围 12~14）
// 每 &amp;#43;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 &amp;#34;&amp;#34;, 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 &amp;#43; hash
//
// 不需要单独存 salt——它已经嵌在 hash 字符串里了！
```
- 集成到 User 模型（GORM Hook）
-
```go
type User struct {
gorm.Model
Email string `gorm:&amp;#34;uniqueIndex&amp;#34;`
Password string `gorm:&amp;#34;size:255;not null&amp;#34; json:&amp;#34;-&amp;#34;` // json:&amp;#34;-&amp;#34; 防序列化
Role string `gorm:&amp;#34;default:user&amp;#34;` // user / admin
}
// 创建/更新前自动加密（已经是哈希就跳过）
func (u *User) BeforeSave(tx *gorm.DB) error {
// 检测是否已经是 bcrypt 哈希
if strings.HasPrefix(u.Password, &amp;#34;$2&amp;#34;) { return nil }
if u.Password == &amp;#34;&amp;#34; { return nil }
h, err := HashPassword(u.Password)
if err != nil { return err }
u.Password = h
return nil
}
```
&lt;/textarea&gt;</description></item><item><title>GoLang 开发环境配置</title><link>https://blog.ans20xx.com/posts/backend/go-setup/</link><pubDate>Sat, 23 Aug 2025 00:00:00 +0000</pubDate><guid>https://blog.ans20xx.com/posts/backend/go-setup/</guid><description>配置 Golang 开发环境</description></item><item><title>常用 Go 命令</title><link>https://blog.ans20xx.com/posts/backend/%E5%B8%B8%E7%94%A8go%E5%91%BD%E4%BB%A4/</link><pubDate>Sat, 23 Aug 2025 00:00:00 +0000</pubDate><guid>https://blog.ans20xx.com/posts/backend/%E5%B8%B8%E7%94%A8go%E5%91%BD%E4%BB%A4/</guid><description>常用 Go 命令</description></item><item><title>Go 开发拾遗</title><link>https://blog.ans20xx.com/posts/backend/go-%E5%BC%80%E5%8F%91%E6%8B%BE%E9%81%97/</link><pubDate>Thu, 21 Aug 2025 00:00:00 +0000</pubDate><guid>https://blog.ans20xx.com/posts/backend/go-%E5%BC%80%E5%8F%91%E6%8B%BE%E9%81%97/</guid><description>Go 开发拾遗</description></item></channel></rss>