Go 复习四月 18, 2026基础# - 基础 - Go 环境搭建与工具链 - 安装 Go - 官网直接安装 - ```bash # macOS(用 Homebrew) brew install go # Linux(手动安装) wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz export PATH=$PATH:/usr/local/go/bin # Windows # 直接下载 .msi 安装包:https://go.dev/dl/ # 验证安装 go version # 输出:go version go1.23.0 darwin/arm64 ``` - goenv 环境管理 - ```bash # Linux(手动安装) wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz export PATH=$PATH:/usr/local/go/bin # Windows # 直接下载 .msi 安装包:https://go.dev/dl/ # 验证安装 go version # 输出:go version go1.23.0 darwin/arm64 ``` - 理解环境变量 - 常用环境变量 - ```bash # 查看所有 Go 环境变量 go env # 最重要的几个: go env GOROOT # Go 的安装路径(一般不需要改) go env GOPATH # 工作目录,默认 ~/go go env GOMODULE # 模块模式(应该是 "on") ``` - GOPATH vs Go Modules - 早期 Go 用 GOPATH 管理所有的代码,所有项目必须放在 $GOPATH/src 下 - Go 1.11 引入了 Go Modules,可以在任意目录创建项目 - 现在 Go Modules 是默认模式,不需要关注 GOPATH 了 - 只需要确认 GO111MODULE=on - 设置常用模块 - ```bash # 确保模块模式开启 go env -w GO111MODULE=on # 设置国内代理(如果你在中国大陆) go env -w GOPROXY=https://goproxy.cn,direct ``` - VsCode 插件 - Go 官方插件 - Go Test Exploerer 可视化运行测试 - Error Lens 行内显示错误信息 - 核心命令 - go mod init <模块名> 初始化一个新模块 - go run main.go 编译并直接运行(不生成二进制) - go build 编译生成可执行文件 - go fmt ./... 格式化代码 - go test ./... 运行所有测试 - go get <包名> 添加依赖到当前模块 - go mod tidy 清理未使用的依赖 - go vet ./... 静态分析,检查常用错误 - go doc <包名> - 类型系统与变量 - 基本类型总览 - Go 是强类型语言,每个类型都有明确的类型, - 整数 - int/uint 平台相关(32 位或 64 位) 最常用 - int8/int16/int32/int64 指定位数的有符号整数 - uint8(byte)/uint16/uint32/uint64 无符号整数 - 浮点数 - float32 单进度浮点数 (7 位有效数字) - float64 双进度浮点数 (15 位有效数字) - 其他 - bool true/false - string UTF-8 编码的不可变字节序列 - byte uint8 的别名,表示一个字节 - rune int32 的别名,表示一个 Unicode 码点 - byte vs rune - byte 是一个字节,用于处理原始字节数据 - rune 是一个 Unicode 码点,用于处理字符 - 一个中文字符占 3 个 byte,但只是一个 1 rune - 遍历字符串,for range 遍历的是 rune,for i 遍历的是 byte - ```go s := "Hello你好" // byte 视角:看到的是字节 fmt.Println(len(s)) // 11(5个ASCII + 6个中文字节) // rune 视角:看到的是字符 fmt.Println(len([]rune(s))) // 7(5个英文字符 + 2个中文字符) // for range 按 rune 遍历 for i, ch := range s { fmt.Printf("索引=%d 字符=%c\n", i, ch) } // 索引=0 字符=H // 索引=1 字符=e // ... // 索引=5 字符=你 ← 注意索引跳了 // 索引=8 字符=好 ``` - 变量声明的四种方式 - 核心原则:函数内用 :=,函数外用 var - ```go // ① var + 类型(最完整的写法) var name string = "Gopher" // ② var + 类型推断(省略类型) var age = 25 // Go 推断为 int // ③ 短变量声明(最常用,只能在函数内) city := "Tokyo" // 等价于 var city = "Tokyo" // ④ 批量声明(常用于包级别变量) var ( host = "localhost" port = 8080 debug = false ) ``` - 选用参考 - 函数内:几乎总是使用 := 短声明,简洁明了 - 函数外:只能用 var,因为 := 只能在函数内使用 - 需要显式指定类型时,用 var name Type - 声明但是暂不赋值,用var name Type - 常见陷阱 - 短声明的遮蔽 - ```go x := 1 fmt.Println(x) // 1 if true { x := 2 // 注意:这是一个新的 x,遮蔽了外层的 x! fmt.Println(x) // 2 } fmt.Println(x) // 1 ← 外层的 x 没有变! // 正确做法:用 = 而不是 := if true { x = 2 // 修改的是外层的 x } fmt.Println(x) // 2 ``` - 零值机制 - Go 没有 undefined、null、None 的概念 - 每个类型都有一个确定的零值,声明变量时不赋值就是零值 -  - ```go // 零值是安全的,可以直接使用 var count int // 0,可以直接 count++ var name string // "",可以直接 name += "Go" var ok bool // false // 但 nil 类型需要初始化后才能使用! var m map[string]int // nil,直接 m["key"] = 1 会 panic! m = make(map[string]int) // 先 make 初始化 var s []int // nil,但 append 是安全的 s = append(s, 1) // ✅ nil 切片可以直接 append ``` - 常量与 iota 枚举 - Go 用 const 定义常量 - Go 没有 enum 关键字,但是提供了 iota 自增器 - 基本常量 ```go const Pi = 3.14159 const AppName = "MyApp" // 批量声明 const ( StatusOK = 200 StatusNotFound = 404 StatusError = 500 ) ``` - iota Go 的枚举利器 ```Go // iota 在 const 块中从 0 开始,每行自增 1 type Weekday int const ( Sunday Weekday = iota // 0 Monday // 1(自动继承 iota 表达式) Tuesday // 2 Wednesday // 3 Thursday // 4 Friday // 5 Saturday // 6 ) ``` - iota 高级用法 ```Go // 用法一:跳过某个值 type Color int const ( _ Color = iota // 0,用 _ 跳过 Red // 1 Green // 2 Blue // 3 ) // 用法二:位掩码(权限系统常用) type Permission uint const ( Read Permission = 1 << iota // 1 (001) Write // 2 (010) Execute // 4 (100) ) // 组合权限 userPerm := Read | Write // 3 (011) fmt.Println(userPerm & Read != 0) // true,有读权限 fmt.Println(userPerm & Execute != 0) // false,无执行权限 // 用法三:文件大小单位 const ( _ = iota KB = 1 << (10 * iota) // 1 << 10 = 1024 MB // 1 << 20 GB // 1 << 30 TB // 1 << 40 ) ``` - iota 的本质 - 不是一个值,是 const 块的行索引计数器 - 每次遇到一个新的 const 就重置为 0 - 同一行的多个 iota 值相同 - 类型转换 - ```Go // ❌ 隐式转换在 Go 中不存在 var a int = 42 var b float64 = a // 编译错误! var c int64 = a // 编译错误!即使都是整数 // ✅ 必须显式转换 var b float64 = float64(a) // 42.0 var c int64 = int64(a) // 42 // 字符串 ↔ 数字(用 strconv 包) import "strconv" s := strconv.Itoa(42) // int → string: "42" n, err := strconv.Atoi("42") // string → int: 42 f, err := strconv.ParseFloat("3.14", 64) // string → float64 // 字符串 ↔ 字节切片 bytes := []byte("Hello") // string → []byte str := string(bytes) // []byte → string // ⚠️ 精度丢失要注意 big := int64(1<<62) small := int32(big) // 溢出!结果不可预测 ``` - fmt.Sprintf 万能转字符串 - 性能不如 strconv,高频调用优先用 strconv - 流程控制 - if/else 条件判断 - 条件不需要括号 - 允许在条件前加入一个初始化语句 - 基本语法 - ```Go score := 85 if score >= 90 { fmt.Println("优秀") } else if score >= 60 { fmt.Println("及格") } else { fmt.Println("不及格") } // 注意: // 1. 条件不需要括号 (score >= 90) 写成 score >= 90 // 2. 大括号必须有,即使只有一行 // 3. else 必须和右大括号同一行 ``` - 前置初始化语句 (GO 特色) - ```Go // 在条件中声明并使用变量 if err := doSomething(); err != nil { fmt.Println("出错了:", err) return } // err 的作用域仅限于 if-else 块内,块外不可见 // 对比:不用前置初始化的写法 err := doSomething() if err != nil { fmt.Println("出错了:", err) return } // 这里 err 仍在作用域内,可能造成后续变量名冲突 ``` - 前置初始化的好处 - 限制了变量的作用域 - 避免污染外部命名空间 - 在错误处理中常见 - for 循环 - Go 只有 for 一个循环关键字 - for 有多种形式,能覆盖其他语言的各种循环要求 - 经典三段式 - ```Go for i := 0; i < 10; i++ { fmt.Println(i) } ``` - 类 while 循环 (只有条件) - ```Go n := 10 for n > 0 { fmt.Println(n) n-- } ``` - 无限循环 - ```Go for { // 死循环,用 break 退出 if someCondition() { break } } // 常用于服务器主循环 for { conn, err := listener.Accept() if err != nil { continue } go handleConn(conn) } ``` - for-range 遍历 - ```Go // 遍历切片/数组 nums := []int{10, 20, 30} for i, v := range nums { fmt.Printf("索引=%d 值=%d\n", i, v) } // 只要索引 for i := range nums { fmt.Println(i) } // 只要值(用 _ 忽略索引) for _, v := range nums { fmt.Println(v) } // 遍历 map(顺序随机!) m := map[string]int{"a": 1, "b": 2} for k, v := range m { fmt.Printf("%s=%d\n", k, v) } // 遍历字符串(按 rune) for i, ch := range "Hello你好" { fmt.Printf("%d: %c\n", i, ch) } // 遍历 channel(Day 9 会讲) // for msg := range ch { ... } ``` - 循环复用坑 - 在 Go 1.22 之前,for-range 的循环变量是复用的,在 go routine 中捕获会出现问题 - ```Go // Go 1.22+ 这样写是安全的 for _, v := range []int{1, 2, 3} { go func() { fmt.Println(v) // 1.22 前可能全打印 3 }() } // Go 1.22 之前的兼容写法: for _, v := range []int{1, 2, 3} { v := v // 显式创建副本 go func() { fmt.Println(v) }() } ``` - switch 分支 - 支持默认 break、多值匹配、任意表达式 - 基本用法:默认不穿透 - ```go day := 3 switch day { case 1: fmt.Println("周一") case 2: fmt.Println("周二") case 3: fmt.Println("周三") // 默认自动 break,不会掉到 case 4 case 4, 5: // ✨ 多值匹配 fmt.Println("周四或周五") case 6, 7: fmt.Println("周末") default: fmt.Println("无效") } ``` - 无条件 switch (if-else 链的优雅写法) - ```go score := 85 // 省略 switch 后的条件,相当于 switch true switch { case score >= 90: fmt.Println("A") case score >= 80: fmt.Println("B") case score >= 60: fmt.Println("C") default: fmt.Println("F") } // 比 if-else 链更清晰 // 等价于: // if score >= 90 { ... } // else if score >= 80 { ... } // ... ``` - 前置初始化语句 - ```go switch os := runtime.GOOS; os { case "linux": fmt.Println("Linux") case "darwin": fmt.Println("macOS") case "windows": fmt.Println("Windows") default: fmt.Printf("未知系统: %s\n", os) } ``` - fallthrough:显式穿透 - ```go // 如果真的需要穿透到下一个 case,用 fallthrough switch n := 1; n { case 1: fmt.Println("一") fallthrough // 继续执行下一个 case case 2: fmt.Println("二") // 会被打印 case 3: fmt.Println("三") // 不会打印,fallthrough 只穿透一层 } // 输出:一、二 ``` - break、continue、goto - 基础 break 和 continue - ```go for i := 0; i < 10; i++ { if i == 3 { continue // 跳过本次迭代 } if i == 7 { break // 退出循环 } fmt.Println(i) } // 输出:0 1 2 4 5 6 ``` - 标签:跳出嵌套循环 - ```go // 问题:break 只能跳出一层循环 // 在嵌套循环中如何跳出外层? OuterLoop: for i := 0; i < 5; i++ { for j := 0; j < 5; j++ { if i*j > 6 { break OuterLoop // 直接跳出外层循环 } fmt.Printf("%d*%d ", i, j) } } // continue 也支持标签:continue OuterLoop 会回到外层循环的下一次迭代 ``` - goto 存在,但是不建议用 - defer 延迟执行 - defer 是 Go 的标志性特性,用来注册「函数退出前必须执行」的逻辑 - 它彻底解决了资源清理、错误恢复等场景 - 基本行为:函数返回前执行 - ```go func main() { fmt.Println("1. 开始") defer fmt.Println("3. 延迟执行") fmt.Println("2. 中间") } // 输出: // 1. 开始 // 2. 中间 // 3. 延迟执行 ← 在函数返回前执行 ``` - 典型用法:资源清理 - ```go // 文件操作 func readFile(name string) error { f, err := os.Open(name) if err != nil { return err } defer f.Close() // ✨ 立刻注册关闭,无论函数怎么退出都会执行 // 读取文件内容... // 即使这里 return 或 panic,f.Close() 都会被执行 return nil } // 锁 func updateCounter() { mu.Lock() defer mu.Unlock() // ✨ 保证解锁,不会死锁 counter++ // 即使后续代码 panic,锁也能释放 } ``` - 多个 defer: LIFO 栈顺序 - ```go func main() { defer fmt.Println("1") defer fmt.Println("2") defer fmt.Println("3") fmt.Println("main") } // 输出: // main // 3 ← 后进先出(栈) // 2 // 1 ``` - defer 语句参数 - defer 语句的参数在注册是就计算好,不是执行时 - ```go func main() { x := 10 defer fmt.Println("defer:", x) // x 的值 10 被立即捕获 x = 20 fmt.Println("main:", x) } // 输出: // main: 20 // defer: 10 ← 不是 20! // 如果想用最终值,用闭包: func main() { x := 10 defer func() { fmt.Println("defer:", x) // 闭包捕获变量引用 }() x = 20 } // 输出 defer: 20 ``` - 函数深入 - 函数基础 - Go 的函数定义和 C 系列语言不一样:关键字 func 在前,返回值类型在后 - ```go // 基本语法:func 函数名(参数列表) 返回值类型 { 函数体 } func add(a int, b int) int { return a + b } // 参数同类型可以合并声明 func add2(a, b int) int { return a + b } // 无返回值 func greet(name string) { fmt.Println("Hello,", name) } // 无参数也无返回值 func printVersion() { fmt.Println("v1.0.0") } ``` - 函数是一等公民 - Go 的函数可以赋值给变量、作为参数传递、作为返回值返回 - 这让 Go 支持函数式编程风格,比如高阶函数、回调、中间件模式等 - ```go // 函数赋值给变量 var operation func(int, int) int = add result := operation(1, 2) // 3 // 函数作为参数 func apply(nums []int, fn func(int) int) []int { result := make([]int, len(nums)) for i, n := range nums { result[i] = fn(n) } return result } doubled := apply([]int{1, 2, 3}, func(x int) int { return x * 2 }) // [2, 4, 6] ``` - 多返回值 - go 原生支持多返回值 - ```go // 返回商和余数 func divmod(a, b int) (int, int) { return a / b, a % b } q, r := divmod(10, 3) // q=3, r=1 // 只想要其中一个,用 _ 忽略 q, _ := divmod(10, 3) // 只要商 _, r = divmod(10, 3) // 只要余数 ``` - 几乎所有主流库都用这个特性返回 (result, error) - go 的惯用法:(result,error) - ```go // 几乎所有可能失败的操作都返回 (结果, error) func readFile(name string) (string, error) { data, err := os.ReadFile(name) if err != nil { return "", err // 失败时返回零值 + 错误 } return string(data), nil // 成功时返回结果 + nil } // 调用方的标准模式 content, err := readFile("hello.txt") if err != nil { log.Fatal(err) } fmt.Println(content) ``` - 命名返回值 - go 允许给返回值命名,在返回值多,复杂逻辑时有用 - ```go // 普通多返回值 func divide1(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("除数不能为零") } return a / b, nil } // 命名返回值版本 func divide2(a, b float64) (result float64, err error) { if b == 0 { err = errors.New("除数不能为零") return // 裸 return,自动返回 result=0, err=上面的值 } result = a / b return // 自动返回 result, err } ``` - 命名返回值 + defer - ```go // 在 defer 中修改返回值 func doWork() (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("恢复自 panic: %v", r) // ✨ 这里能修改返回值 err! } }() // 可能 panic 的代码 panic("something bad") } result := doWork() // result 不是 nil,而是包装后的 error ``` - 什么时候用命名返回值 - 返回值很多且含义复杂时,命名可以当文档 - 需要在 defer 中修改返回值时(recover 模式) - 可变参数函数 - 用 ...T 表示可变参数,函数内部会收到一个 []T 类型的切片 - fmt.Println 就是典型的可变参数函数。 - 示例 - ```go // 求任意个数字的和 func sum(nums ...int) int { total := 0 for _, n := range nums { total += n } return total } sum() // 0(传递空切片) sum(1, 2, 3) // 6 sum(1, 2, 3, 4, 5) // 15 // 把切片展开传递,用 ... 展开 nums := []int{10, 20, 30} sum(nums...) // 60,注意是 nums... 而不是 nums ``` - 混合使用固定参数和可变参数 - ```go // 可变参数必须是最后一个参数 func greet(greeting string, names ...string) { for _, name := range names { fmt.Printf("%s, %s!\n", greeting, name) } } greet("Hello", "Alice", "Bob", "Charlie") // Hello, Alice! // Hello, Bob! // Hello, Charlie! ``` - 闭包与匿名函数 - 闭包是能访问外部作用域变量的函数 - Go 的闭包语法简洁,是实现迭代器、中间件、回调等模式的基础 - 匿名函数 - ```go // 定义后立即调用(IIFE) func() { fmt.Println("我是匿名函数") }() // 赋值给变量 add := func(a, b int) int { return a + b } fmt.Println(add(1, 2)) // 3 ``` - 闭包:捕获外部变量 - ```go // 计数器生成器 func makeCounter() func() int { count := 0 return func() int { count++ // ✨ 闭包捕获了外部的 count return count } } c1 := makeCounter() fmt.Println(c1()) // 1 fmt.Println(c1()) // 2 fmt.Println(c1()) // 3 // 每个闭包有自己独立的状态 c2 := makeCounter() fmt.Println(c2()) // 1(c2 的 count 是独立的) fmt.Println(c1()) // 4(c1 继续累加) ``` - 闭包实战:装饰器模式 - ```go // 为函数添加日志功能 func withLogging(name string, fn func(int) int) func(int) int { return func(x int) int { fmt.Printf("调用 %s(%d)\n", name, x) result := fn(x) fmt.Printf("%s(%d) = %d\n", name, x, result) return result } } double := func(x int) int { return x * 2 } loggedDouble := withLogging("double", double) loggedDouble(5) // 调用 double(5) // double(5) = 10 // 这是 HTTP 中间件的基础思想! ``` - 闭包的捕获规则 - 闭包捕获的是引用,不是值 - 修改外部变量会反映到闭包内 - 闭包修改也会反映到外部 - error 作为 返回值 - Go 把 error 当作普通值处理,而不是特殊的异常机制 - error 的本质:一个接口 - ```go // error 其实就是标准库定义的一个接口 // 任何实现了 Error() string 方法的类型都是 error type error interface { Error() string } // 创建 error 的几种方式: // 方式一:errors.New import "errors" err := errors.New("文件不存在") // 方式二:fmt.Errorf(支持格式化) err = fmt.Errorf("文件 %s 不存在", filename) // 方式三:自定义类型(后续再深入) ``` - 标准返回模式 - ```go // ✅ Go 惯用写法:err 作为最后一个返回值 func findUser(id int) (*User, error) { if id < 0 { return nil, fmt.Errorf("无效 ID: %d", id) } user, ok := userCache[id] if !ok { return nil, errors.New("用户不存在") } return user, nil } // 调用方必须检查 err user, err := findUser(42) if err != nil { return err } // 这里 user 保证不是 nil ``` - 提前返回:降低嵌套 - ```go // ❌ 过度嵌套(来自 Java/Python 习惯) func processDataBad(path string) error { data, err := readFile(path) if err == nil { parsed, err := parse(data) if err == nil { result, err := transform(parsed) if err == nil { save(result) return nil } return err } return err } return err } // ✅ Go 风格:早期返回,扁平化 func processData(path string) error { data, err := readFile(path) if err != nil { return err } parsed, err := parse(data) if err != nil { return err } result, err := transform(parsed) if err != nil { return err } return save(result) } ``` - 数组、切片与 Map - 数组:固定长度的值类型 - 数组:固定长度的值类型 - Go 的数组和 C/Java 的不一样,它是值类型,长度是类型的一部分 - 实际开发很少使用数组,几乎都使用切片 - 示例 - ```go // 声明数组:类型 [N]T var a [3]int // [0, 0, 0],零值初始化 b := [3]int{1, 2, 3} // 字面量初始化 c := [...]int{1, 2, 3} // ... 让编译器推断长度 // 长度是类型的一部分! var x [3]int var y [4]int x = y // ❌ 编译错误:[3]int 和 [4]int 是不同类型 // 访问和修改 fmt.Println(b[0]) // 1 b[0] = 100 fmt.Println(len(b)) // 3 // 遍历 for i, v := range b { fmt.Printf("b[%d] = %d\n", i, v) } ``` - 数组是值类型 - 把数组赋值给另一个变量,或者传入函数,都会完整拷贝一份 - 大数组这样性能会很差,实际开发总是使用切片 - ```go a := [3]int{1, 2, 3} b := a // ✨ 完全拷贝!b 和 a 是独立的 b[0] = 100 fmt.Println(a) // [1 2 3] ← a 没变 fmt.Println(b) // [100 2 3] // 传入函数也是拷贝 func modify(arr [3]int) { arr[0] = 999 } modify(a) fmt.Println(a) // [1 2 3] ← 依然没变! ``` - 切片:Go 的主力军 - 切片是 Go 中最常用的数据结构 - 看起来像动态数组,但底层是对数组的视图 - 切片的三要素 - ```go // 切片在内存中是一个结构体(概念上): // type slice struct { // ptr *T // 指向底层数组的指针 // len int // 当前长度 // cap int // 容量(底层数组剩余空间) // } s := []int{10, 20, 30} fmt.Println(len(s)) // 3(长度) fmt.Println(cap(s)) // 3(容量) ``` - 创建切片的五种方式 - ```go // ① 字面量 s1 := []int{1, 2, 3} // ② make:指定长度(和容量) s2 := make([]int, 5) // len=5, cap=5,值全是 0 s3 := make([]int, 3, 10) // len=3, cap=10 // ③ nil 切片 var s4 []int // nil,len=0, cap=0 fmt.Println(s4 == nil) // true // 但 nil 切片可以直接 append,不会 panic // ④ 空切片(和 nil 切片 len/cap 相同但底层不同) s5 := []int{} fmt.Println(s5 == nil) // false // ⑤ 切割已有数组/切片(下一节详讲) arr := [5]int{1, 2, 3, 4, 5} s6 := arr[1:4] // [2, 3, 4] ``` - ni 切片 vs 空切片 - 两者几乎可以互换 - 都可以 append,都可以 range - JSON 序列化时 nil 变为 null,空切片变为 【】 - 推荐总是优先使用 nil 切片 - 切片操作:切割与 append - 切割语法 s[low:high:max] - ```go s := []int{0, 1, 2, 3, 4, 5} // 基本切割 [low:high] s[1:4] // [1, 2, 3] ← 左闭右开 s[:3] // [0, 1, 2] 省略 low 默认 0 s[3:] // [3, 4, 5] 省略 high 默认 len s[:] // [0, 1, 2, 3, 4, 5] 完整拷贝引用 // 三参数切割 [low:high:max] —— 限制容量 s2 := s[1:4:4] fmt.Println(len(s2), cap(s2)) // 3, 3(容量被限制) // 不加 :max 的话,cap(s2) 会是 5(底层数组剩余容量) ``` - append: 扩容机制 - ```go s := []int{1, 2, 3} s = append(s, 4) // [1 2 3 4] s = append(s, 5, 6, 7) // 添加多个 // 合并两个切片 a := []int{1, 2} b := []int{3, 4} c := append(a, b...) // 注意 ...,展开 b // c = [1 2 3 4] ``` - 扩容规则 - ```go // 当 append 超过 cap 时,Go 会分配新的底层数组 // 扩容规则(Go 1.18+ 简化): // cap < 256:翻倍(1 → 2 → 4 → 8 ...) // cap >= 256:每次增加约 25% s := make([]int, 0, 2) fmt.Println(cap(s)) // 2 s = append(s, 1, 2, 3) fmt.Println(cap(s)) // 4(翻倍) // 扩容是昂贵的!如果知道最终大小,一开始就 make 好 s := make([]int, 0, 1000) // ✅ 预分配 for i := 0; i < 1000; i++ { s = append(s, i) // 不会触发扩容 } ``` - 切片陷阱 - 切割共享底层数组 - ```go original := []int{1, 2, 3, 4, 5} sub := original[1:3] // [2, 3],但底层数组是共享的! sub[0] = 999 fmt.Println(original) // [1 999 3 4 5] ← 原数组被改了! fmt.Println(sub) // [999 3] // 安全做法:显式拷贝 sub := make([]int, 2) copy(sub, original[1:3]) // sub 是独立的 sub[0] = 999 fmt.Println(original) // [1 2 3 4 5] ← 不受影响 ``` - append 可能修改原切片 - ```go original := []int{1, 2, 3, 4, 5} sub := original[:3] // [1 2 3],cap=5 sub = append(sub, 999) // append 时 cap 够用,复用底层数组! fmt.Println(original) // [1 2 3 999 5] ← 原数组第 4 位被改了! // 用三参数切割限制容量,强制 append 时分配新数组 sub := original[:3:3] // cap=3 sub = append(sub, 999) // 容量不够,分配新数组 fmt.Println(original) // [1 2 3 4 5] ← 原数组安全 ``` - 大切片导致内存泄露 - ```go // 读一个 1GB 大文件,只保留前 100 字节 func readSmallPart(filename string) []byte { big, _ := os.ReadFile(filename) // 1GB return big[:100] // ⚠️ 返回的切片引用着整个 1GB 数组! // 只要返回值存活,1GB 内存就不会被 GC } // 正确做法:主动拷贝脱离引用 func readSmallPart(filename string) []byte { big, _ := os.ReadFile(filename) small := make([]byte, 100) copy(small, big) return small // ✅ 只引用 100 字节,big 可以被 GC } ``` - 切片常见操作 - 删除元素 - ```go // Go 没有内置的删除函数,用切片拼接 s := []int{1, 2, 3, 4, 5} i := 2 // 要删除的索引 // 删除索引 i 的元素 s = append(s[:i], s[i+1:]...) // s = [1 2 4 5] // Go 1.21+ 用 slices.Delete import "slices" s = slices.Delete(s, i, i+1) ``` - 插入元素 - ```go s := []int{1, 2, 4, 5} i := 2 // 插入位置 val := 3 // 要插入的值 // 在索引 i 处插入 val s = append(s[:i], append([]int{val}, s[i:]...)...) // s = [1 2 3 4 5] // Go 1.21+ 用 slices.Insert(推荐) s = slices.Insert(s, i, val) ``` - 复制、反转、排序 - ```go // 复制 src := []int{1, 2, 3} dst := make([]int, len(src)) n := copy(dst, src) // n 是实际复制的元素数 // Go 1.21+ 最简洁 dst := slices.Clone(src) // 排序 import "sort" nums := []int{3, 1, 4, 1, 5, 9, 2, 6} sort.Ints(nums) // [1 1 2 3 4 5 6 9] sort.Sort(sort.Reverse(sort.IntSlice(nums))) // 降序 // Go 1.21+ 更简洁 slices.Sort(nums) slices.Reverse(nums) // 自定义排序 sort.Slice(nums, func(i, j int) bool { return nums[i] > nums[j] // 降序 }) ``` - map: 键值对容器 - Go 的 map 是哈希表实现,支持任何可比较类型作为 key - 使用前必须初始化,nil map 不能写入 - 声明与初始化 - ```go // ❌ 错误:只声明,没初始化 var m1 map[string]int m1["a"] = 1 // panic: assignment to entry in nil map // ✅ 方式一:make m2 := make(map[string]int) m2["a"] = 1 // ✅ 方式二:字面量 m3 := map[string]int{ "one": 1, "two": 2, "three": 3, } // ✅ 方式三:make 指定初始容量(性能优化) m4 := make(map[string]int, 1000) // 提前分配空间 ``` - 增删改查 - ```go m := map[string]int{"a": 1, "b": 2} // 增/改(语法相同) m["c"] = 3 // 新增 m["a"] = 100 // 修改 // 查 v := m["a"] // 100 missing := m["x"] // 0(不存在时返回零值!) // ✨ 判断 key 是否存在:逗号 ok 惯用法 v, ok := m["a"] // v=100, ok=true v, ok := m["x"] // v=0, ok=false if ok { fmt.Println("存在") } // 删除(不存在也不会报错) delete(m, "a") // 长度 fmt.Println(len(m)) ``` - 遍历(顺序随机) - ```go m := map[string]int{"a": 1, "b": 2, "c": 3} // 每次遍历顺序都可能不同! for k, v := range m { fmt.Printf("%s=%d\n", k, v) } // 需要有序遍历:把 key 取出来排序 keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Printf("%s=%d\n", k, m[k]) } ``` - map 进阶:零值技巧与并发 - 零值技巧:计数器模式 - ```go // map 访问不存在的 key 返回零值,这特性很好用 words := []string{"go", "rust", "go", "python", "go", "rust"} count := make(map[string]int) for _, w := range words { count[w]++ // ✨ 不存在时 count[w] 是 0,+1 变成 1 } // count = {"go": 3, "rust": 2, "python": 1} // 分组:map + 切片 type User struct { Name string City string } users := []User{ {"Alice", "Tokyo"}, {"Bob", "Tokyo"}, {"Charlie", "Osaka"}, } byCity := make(map[string][]User) for _, u := range users { byCity[u.City] = append(byCity[u.City], u) } // byCity["Tokyo"] = [{Alice Tokyo} {Bob Tokyo}] ``` - map 作为集合 - ```go // Go 没有内置 Set,用 map[T]struct{} 模拟 set := map[string]struct{}{} // 添加 set["apple"] = struct{}{} set["banana"] = struct{}{} // 判断存在 _, ok := set["apple"] // true // 删除 delete(set, "apple") // 为什么用 struct{} 而不是 bool? // struct{} 不占内存(0 字节),比 bool(1字节)更省空间 ``` - map 不是并发安全的 - ```go // ❌ 多个 goroutine 同时读写 map 会 panic! m := make(map[string]int) go func() { m["a"] = 1 }() go func() { m["b"] = 2 }() // 运行时报错:fatal error: concurrent map writes // ✅ 方式一:sync.Mutex 加锁 var mu sync.Mutex m := make(map[string]int) go func() { mu.Lock() defer mu.Unlock() m["a"] = 1 }() // ✅ 方式二:sync.Map(适合读多写少场景) var sm sync.Map sm.Store("a", 1) v, ok := sm.Load("a") sm.Delete("a") sm.Range(func(k, v any) bool { fmt.Println(k, v) return true // 返回 false 停止遍历 }) ``` - 结构体与方法 - 结构体基础 - 结构体是 Go 中组织数据的核心方式 - Go 没有 class,但 struct + 方法已经能表达几乎所有面向对象的特性,而且更简单直接 - 定义与初始化 - ```go // 定义结构体 type User struct { ID int Name string Email string Active bool } // 初始化方式一:字段名(推荐,最清晰) u1 := User{ ID: 1, Name: "Alice", Email: "alice@example.com", Active: true, } // 初始化方式二:按字段顺序(脆弱,字段增减会出问题) u2 := User{2, "Bob", "bob@example.com", false} // 初始化方式三:零值 var u3 User // 所有字段都是零值:{0, "", "", false} u3.Name = "Charlie" // 初始化方式四:new(返回指针) u4 := new(User) // 等价于 &User{} u4.Name = "David" ``` - 访问和修改字段 - ```go u := User{Name: "Alice"} // 访问字段 fmt.Println(u.Name) // 修改字段 u.Name = "Alicia" // 指针也用 . 访问(Go 自动解引用) p := &u p.Name = "Alice 2" // 等价于 (*p).Name,Go 帮你省了* fmt.Println(u.Name) // Alice 2 ``` - 字段名大小写 - 首字母大写的字段是导出的(public),可以被包外访问 - 小写的是未导出的(private),只能包内使用 - 值类型 vs 指针 - struct 是值类型。赋值、传参、返回都是完整拷贝 - ```go u1 := User{Name: "Alice"} u2 := u1 // ✨ 完全拷贝!u1 和 u2 是两个独立对象 u2.Name = "Bob" fmt.Println(u1.Name) // Alice(没变) fmt.Println(u2.Name) // Bob // 传入函数也是拷贝 func modify(u User) { u.Name = "Changed" // 只修改了拷贝 } modify(u1) fmt.Println(u1.Name) // Alice(没变!) // 想修改原对象?传指针 func modifyPtr(u *User) { u.Name = "Changed" } modifyPtr(&u1) fmt.Println(u1.Name) // Changed ``` - 什么时候用指针 - ```go // ✅ 使用指针的场景: // 1. 需要修改原对象 func (u *User) SetName(name string) { u.Name = name } // 2. 结构体很大,拷贝开销大 type Config struct { // 假设有 50 个字段 } func process(c *Config) { ... } // 传指针避免拷贝 // 3. 想表达「可能为空」的语义 func findUser(id int) *User { if !found { return nil // 用 nil 表示未找到 } return &user } // ❌ 不用指针的场景: // - 小结构体(几个字段的) // - 不修改的只读操作 // - 代表不可变概念(如 time.Time) ``` - 经验法则 - 拿不准就用指针 - Go 代码中 *User 比 User 更常见 - 但像 time.Time、image.Point 这种小且不可变的类型,用值类型更自然 - 方法:给类型加行为 - 方法是绑定到特定类型的函数 - Go 的方法语法有点特别——接收者写在函数名前面 - 方法定义语法 - ```go type Rectangle struct { Width, Height float64 } // 方法定义:func (接收者 类型) 方法名(参数) 返回值 func (r Rectangle) Area() float64 { return r.Width * r.Height } func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) } // 调用 rect := Rectangle{Width: 3, Height: 4} fmt.Println(rect.Area()) // 12 fmt.Println(rect.Perimeter()) // 14 ``` - 值接收者 vs 指针接收者 - ```go type Counter struct { count int } // ❌ 值接收者:修改不会生效! func (c Counter) IncrementWrong() { c.count++ // 只修改了副本 } // ✅ 指针接收者:修改原对象 func (c *Counter) Increment() { c.count++ } // ✅ 只读方法用值接收者 func (c Counter) Get() int { return c.count } c := Counter{} c.IncrementWrong() fmt.Println(c.Get()) // 0 ← 没变! c.Increment() fmt.Println(c.Get()) // 1 ← 生效了 c.Increment() fmt.Println(c.Get()) // 2 ``` - 选择接受者类型的原则 - 需要修改对象 -> 必须用指针接收者 - 结构体很大 -> 用指针避免拷贝 - 包含 sync.Mutex 等不可拷贝字段 -> 必须用指针 - 其他情况两者都行,但是一个类型的方法应该统一 - 要么全值接收者,要么全指针接收者 - go 的自动转换 - ```go // Go 会在值和指针之间自动转换,调用方法时不用纠结 c := Counter{} // 值 c.Increment() // ✨ Go 自动取地址 (&c).Increment() c.Get() // 值调用值方法,直接 p := &Counter{} // 指针 p.Increment() // 指针调用指针方法,直接 p.Get() // ✨ Go 自动解引用 (*p).Get() // 唯一限制:不能在「不可寻址」的值上调用指针方法 Counter{}.Increment() // ❌ 编译错误,字面量不可寻址 // 但可以: c := Counter{} c.Increment() // ✅ 变量可寻址 ``` - 给任意类型定义方法 - 方法不只能定义在 struct 上,任何在当前包定义的类型都可以有方法 - ```go // 给 int 起别名并加方法 type Celsius float64 func (c Celsius) ToFahrenheit() float64 { return float64(c)*9/5 + 32 } func (c Celsius) String() string { return fmt.Sprintf("%.1f°C", float64(c)) } // 给切片起别名并加方法 type IntSlice []int func (s IntSlice) Sum() int { total := 0 for _, n := range s { total += n } return total } func (s IntSlice) Max() int { if len(s) == 0 { return 0 } m := s[0] for _, n := range s[1:] { if n > m { m = n } } return m } func main() { temp := Celsius(25) fmt.Println(temp) // 25.0°C(调用了 String 方法) fmt.Println(temp.ToFahrenheit()) // 77 nums := IntSlice{1, 5, 3, 8, 2} fmt.Println(nums.Sum()) // 19 fmt.Println(nums.Max()) // 8 } ``` - 只能给本包类型加方法 - 不能直接给 int、string 等内置类型加方法,也不能给其他包的类型加方法 - 需要的话就 type MyInt int 定义一个新类型。这个限制防止混乱——每个类型的方法归属清晰 - 结构体嵌入 - Go 没有继承,但有「嵌入」 - 把一个 struct 嵌入到另一个里面,外层可以直接访问内层的字段和方法 - 这就是 Go 面向对象的核心——组合优于继承 - 基本嵌入 - ```go type Animal struct { Name string Age int } func (a Animal) Describe() string { return fmt.Sprintf("%s, %d岁", a.Name, a.Age) } // Dog 嵌入 Animal type Dog struct { Animal // ✨ 嵌入字段(没有字段名) Breed string } d := Dog{ Animal: Animal{Name: "旺财", Age: 3}, Breed: "柴犬", } // 直接访问嵌入字段的属性和方法(提升) fmt.Println(d.Name) // "旺财"(不用 d.Animal.Name) fmt.Println(d.Describe()) // "旺财, 3岁"(继承了 Animal 的方法) fmt.Println(d.Breed) // "柴犬" // 也可以显式访问 fmt.Println(d.Animal.Name) // 同上 ``` - 方法覆盖 - ```go // Dog 可以定义同名方法「覆盖」Animal 的 func (d Dog) Describe() string { // 可以调用被覆盖的方法 base := d.Animal.Describe() return fmt.Sprintf("%s,品种:%s", base, d.Breed) } d := Dog{Animal: Animal{Name: "旺财", Age: 3}, Breed: "柴犬"} fmt.Println(d.Describe()) // "旺财, 3岁,品种:柴犬" ``` - 嵌入接口 - ```go // 也可以嵌入接口,常见于标准库 type ReadCloser interface { io.Reader // 嵌入接口 io.Closer // 嵌入接口 } // 等价于: // type ReadCloser interface { // Read(p []byte) (n int, err error) // Close() error // } // 实战场景:给一个已有类型「增强」 type LoggedDB struct { *sql.DB // 嵌入指针,继承所有方法 logger *log.Logger } // 只需要定义想增强的方法 func (db *LoggedDB) Query(query string, args ...any) (*sql.Rows, error) { db.logger.Printf("SQL: %s", query) return db.DB.Query(query, args...) } // 其他方法(Ping、Exec 等)自动继承自 *sql.DB ``` - 嵌入 vs 继承 - 嵌入不是继承 - 嵌入字段是一个真实存在的字段,外层 struct has-a 内层,不是 is-a - 你可以嵌入多个类型(多继承的感觉),但本质是组合:Dog 不是 Animal 的子类,而是「包含了一个 Animal」 - 构造函数模式 - Go 没有 constructor 关键字 - 惯例是定义 NewXxx 函数返回初始化好的实例,这样可以封装验证、默认值、依赖注入等逻辑。 - 示例 - ```go type User struct { ID int Name string Email string CreatedAt time.Time } // 标准构造函数:返回值 func NewUser(name, email string) User { return User{ Name: name, Email: email, CreatedAt: time.Now(), } } // 更常见:返回指针 + error func NewUser(name, email string) (*User, error) { if name == "" { return nil, errors.New("name 不能为空") } if !strings.Contains(email, "@") { return nil, fmt.Errorf("无效的 email: %s", email) } return &User{ Name: name, Email: email, CreatedAt: time.Now(), }, nil } // 使用 u, err := NewUser("Alice", "alice@example.com") if err != nil { log.Fatal(err) } fmt.Println(u.Name) ``` - 函数选项模式 - ```go // 问题:构造函数参数太多怎么办? // func NewServer(host string, port int, timeout time.Duration, tls bool, ...) // 解决:函数选项模式(Functional Options) type Server struct { host string port int timeout time.Duration tls bool } // Option 是一个修改 Server 的函数 type Option func(*Server) func WithPort(p int) Option { return func(s *Server) { s.port = p } } func WithTimeout(t time.Duration) Option { return func(s *Server) { s.timeout = t } } func WithTLS() Option { return func(s *Server) { s.tls = true } } // 构造函数接受任意个 Option func NewServer(host string, opts ...Option) *Server { // 默认值 s := &Server{ host: host, port: 80, timeout: 30 * time.Second, tls: false, } // 应用选项 for _, opt := range opts { opt(s) } return s } // 使用:非常灵活 s1 := NewServer("localhost") s2 := NewServer("example.com", WithPort(443), WithTLS()) s3 := NewServer("api.com", WithTimeout(10*time.Second)) ``` - 函数选项模式的威力 - 这个模式在 Go 生态广泛使用(gRPC、Kubernetes、各种库的配置都是这个模式) - 优势:可选参数不用硬编码、扩展性好(加新选项不破坏现有代码)、可读性强(调用方清楚写出每个选项) - 结构体标签 - Struct Tag 是字段后面的反引号字符串,为字段附加元数据 - JSON 序列化、数据库映射、表单校验都用它 - 示例 - ```go type User struct { ID int `json:"id"` Name string `json:"name" validate:"required,min=2"` Email string `json:"email" validate:"required,email"` Password string `json:"-"` // - 表示不参与 JSON 序列化 Age int `json:"age,omitempty"` // omitempty:零值时忽略 Role string `json:"role" db:"user_role"` // 同一字段可有多个 tag } // JSON 序列化 u := User{ ID: 1, Name: "Alice", Email: "alice@example.com", Password: "secret", // Age 是 0,会被忽略 } data, _ := json.Marshal(u) fmt.Println(string(data)) // {"id":1,"name":"Alice","email":"alice@example.com","role":""} // 注意:password 不在输出中,age 也被忽略 ``` - 常见 Tag 用法一览 - ```go type Article struct { // encoding/json ID int `json:"id"` Title string `json:"title"` Content string `json:"content,omitempty"` // 空时忽略 Internal string `json:"-"` // 从不序列化 // GORM(Day 17-18) CreatedAt time.Time `gorm:"autoCreateTime"` Slug string `gorm:"uniqueIndex;size:100"` // validator(参数校验) Author string `validate:"required,min=2,max=50"` Views int `validate:"gte=0"` // 组合使用 Email string `json:"email" validate:"required,email" db:"user_email"` } ``` - tag 只是字符串 - Tag 本身只是给字段附加的字符串元数据,语言本身不处理 - 是各个库(encoding/json、gorm、validator...)通过反射读取 tag 并做相应处理 - tag 的语法完全由使用它的库定义 - 接口与多态 - 接口是什么 - 接口是 Go 最优雅的特性。 - 它定义了一组方法签名,任何实现了这些方法的类型都「自动」满足这个接口——不需要显式声明 implements。 - 这叫做「隐式接口实现」,也叫鸭子类型。 - 接口示例 - ```go // 定义接口:只有方法签名,没有实现 type Animal interface { Sound() string Name() string } // Dog 实现了 Animal 接口 // 注意:不需要写 "implements Animal"! type Dog struct{ name string } func (d Dog) Sound() string { return "汪汪" } func (d Dog) Name() string { return d.name } // Cat 也实现了 Animal 接口 type Cat struct{ name string } func (c Cat) Sound() string { return "喵喵" } func (c Cat) Name() string { return c.name } // 函数接受接口类型 func Describe(a Animal) { fmt.Printf("%s 说:%s\n", a.Name(), a.Sound()) } func main() { dog := Dog{name: "旺财"} cat := Cat{name: "咪咪"} Describe(dog) // 旺财 说:汪汪 Describe(cat) // 咪咪 说:喵喵 // 接口变量可以持有任意满足条件的类型 var a Animal = dog a = cat // 随时切换 } ``` - 隐式实现的威力 - Java/C# 要写 implements Animal,Go 不需要 - 只要你的类型有对应的方法,它就是那个接口 - 你可以让一个你不能修改的第三方类型满足你的接口 - 也可以在不改动现有代码的情况下新定义接口来约束它们 - 这是 Go 解耦的核心武器 - 接口值的内部结构 - 接口值在底层由两部分组成:动态类型(type)和动态值(value) - 示例 - ```go // 接口值 = (type, value) var a Animal // (nil, nil),零值 a = Dog{name: "旺财"} // a 现在是 (Dog, {name:"旺财"}) // 打印类型和值 fmt.Printf("类型: %T\n", a) // main.Dog fmt.Printf("值: %v\n", a) // {旺财} ``` - nil 接口 vs 含 nil 的接口 - ```go // 这是 Go 最反直觉的陷阱之一! var d *Dog = nil // d 是 nil 指针 var a Animal = d // a 的 type=*Dog, value=nil // a 不是 nil!因为 type 部分有值 fmt.Println(a == nil) // false ← 让人意外! fmt.Println(d == nil) // true // 正确判断接口是否有效 func process(a Animal) { // ❌ 错误:a != nil 不代表 a 里的值不是 nil if a != nil { // 如果 a = (*Dog)(nil),这里调用方法会 panic } // ✅ 正确:用 reflect 或 type switch 检查 if a == nil { return } } // 最佳实践:函数返回接口时,失败直接返回 nil func findAnimal() Animal { var d *Dog = nil return d // ❌ 坑!返回的接口不是 nil return nil // ✅ 这才是真正的 nil 接口 } ``` - 「含有 nil 指针的接口,不等于 nil 接口。」 - 一个接口值只有当 type 和 value 都是 nil 时,它才等于 nil - 函数返回接口类型时,失败直接 return nil,而不是 return (*ConcreteType)(nil) - 类型断言与 type switch - 有时候你需要从接口里把具体类型「取出来」 - Go 提供了两种方式:类型断言和 type switch - 类型断言 - ```go var a Animal = Dog{name: "旺财"} // 方式一:直接断言(不安全,失败会 panic) d := a.(Dog) fmt.Println(d.name) // 旺财 // 方式二:逗号 ok(推荐,安全) d, ok := a.(Dog) if ok { fmt.Println("是 Dog:", d.name) } else { fmt.Println("不是 Dog") } // 断言为接口(检查是否实现了另一个接口) type Swimmer interface { Swim() string } if s, ok := a.(Swimmer); ok { fmt.Println(s.Swim()) } else { fmt.Println("不会游泳") } ``` - type switch - ```go func describe(i interface{}) { switch v := i.(type) { case int: fmt.Printf("整数: %d,翻倍是 %d\n", v, v*2) case string: fmt.Printf("字符串: %q,长度 %d\n", v, len(v)) case bool: fmt.Printf("布尔值: %t\n", v) case []int: fmt.Printf("整数切片,长度 %d\n", len(v)) case nil: fmt.Println("是 nil") default: fmt.Printf("未知类型: %T\n", v) } } describe(42) // 整数: 42,翻倍是 84 describe("hello") // 字符串: "hello",长度 5 describe(true) // 布尔值: true describe([]int{1,2,3}) // 整数切片,长度 3 describe(nil) // 是 nil ``` - 类型断言 vs 类型转换 - 类型断言 a.(Dog) 是从接口里取出具体类型 - 运行时才知道结果,失败会 panic - 类型转换 int(3.14) 是编译时已知的类型转换,不会失败(但可能丢精度) - 标准库核心接口 - Go 标准库定义了很多小接口,每个接口只有 1-3 个方法 - fmt.Stringer:自定义打印格式 - ```go // 标准库定义: // type Stringer interface { // String() string // } type Point struct{ X, Y int } // 实现 Stringer 接口 func (p Point) String() string { return fmt.Sprintf("(%d, %d)", p.X, p.Y) } p := Point{3, 4} fmt.Println(p) // (3, 4) ← 自动调用 String() fmt.Printf("%v\n", p) // (3, 4) fmt.Printf("%s\n", p) // (3, 4) ``` - error:自定义错误类型 - ```go // error 接口只有一个方法: // type error interface { // Error() string // } // 自定义错误类型(携带更多信息) type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("字段 %s 验证失败: %s", e.Field, e.Message) } // 使用 func validateAge(age int) error { if age < 0 || age > 150 { return &ValidationError{ Field: "age", Message: fmt.Sprintf("值 %d 超出合法范围 [0, 150]", age), } } return nil } err := validateAge(-1) if err != nil { fmt.Println(err) // 字段 age 验证失败: 值 -1 超出合法范围 [0, 150] // 用类型断言获取详细信息 if ve, ok := err.(*ValidationError); ok { fmt.Println("出问题的字段:", ve.Field) } } ``` - io.Reader 和 io.Writer:I/O 的基石 - ```go // io.Reader:能被读取的任何东西 // type Reader interface { // Read(p []byte) (n int, err error) // } // io.Writer:能被写入的任何东西 // type Writer interface { // Write(p []byte) (n int, err error) // } // 这两个接口让函数极其通用: func copyData(dst io.Writer, src io.Reader) (int64, error) { return io.Copy(dst, src) } // 文件、网络连接、内存 buffer 都实现了这两个接口: file, _ := os.Open("input.txt") buf := &bytes.Buffer{} copyData(buf, file) // 文件 → 内存 copyData(os.Stdout, buf) // 内存 → 标准输出 copyData(os.Stdout, strings.NewReader("hello")) // 字符串 → 标准输出 // 自己实现 io.Writer(比如日志收集器) type LogWriter struct { prefix string } func (w *LogWriter) Write(p []byte) (int, error) { fmt.Printf("[%s] %s", w.prefix, p) return len(p), nil } lw := &LogWriter{prefix: "INFO"} fmt.Fprintf(lw, "服务器启动在端口 %d\n", 8080) // [INFO] 服务器启动在端口 8080 ``` - 接口组合 - 和结构体嵌入一样,接口也可以嵌入其他接口 - Go 鼓励定义小接口,再通过组合构建大接口 - 示例 - ```go // 小接口:单一职责 type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type Closer interface { Close() error } // 通过组合构建复合接口 type ReadWriter interface { Reader Writer } type ReadWriteCloser interface { Reader Writer Closer } // 实践中的例子 type Shape interface { Area() float64 Perimeter() float64 } type Drawable interface { Draw() string } // 组合成「可绘制的形状」 type DrawableShape interface { Shape Drawable } ``` - 接口应该越小越好 - ```go // ❌ 大接口:实现困难,测试困难,耦合高 type UserService interface { CreateUser(name, email string) (*User, error) GetUser(id int) (*User, error) UpdateUser(id int, name string) error DeleteUser(id int) error ListUsers() ([]*User, error) AuthenticateUser(email, password string) (string, error) ResetPassword(email string) error // ... 还有 20 个方法 } // ✅ 小接口:精准,易测试,低耦合 type UserCreator interface { CreateUser(name, email string) (*User, error) } type UserFinder interface { GetUser(id int) (*User, error) } type Authenticator interface { Authenticate(email, password string) (string, error) } // 函数只声明它真正需要的 func registerHandler(creator UserCreator) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // 只需要 CreateUser,不依赖整个 UserService } } ``` - 空接口与 any - 空接口 interface{} 没有任何方法要求,所以所有类型都满足它 - Go 1.18 引入了 any 作为 interface{} 的别名,更简洁 - 示例 - ```go // interface{} 和 any 完全等价 var x any = 42 x = "hello" x = []int{1, 2, 3} x = struct{ Name string }{"Alice"} // 常见于需要处理任意类型的场景 func printAll(values ...any) { for _, v := range values { fmt.Printf("%T: %v\n", v, v) } } printAll(1, "hello", true, 3.14) // int: 1 // string: hello // bool: true // float64: 3.14 // map 存储任意类型(类似 JSON 对象) config := map[string]any{ "host": "localhost", "port": 8080, "debug": true, "timeout": 30.5, } ``` - 使用 any 的代价 - ```go // any 丢失了类型信息,使用时必须断言 var v any = 42 // ❌ 不能直接运算 // fmt.Println(v + 1) // 编译错误 // ✅ 先断言再使用 if n, ok := v.(int); ok { fmt.Println(n + 1) // 43 } // any 的性能也比具体类型差(有装箱开销) // 能用泛型(Day 14)就用泛型,不要滥用 any // 合理使用场景: // 1. JSON 解析(结构未知时) // 2. 通用容器(在泛型之前的历史代码) // 3. fmt.Println 这种需要接受任意值的工具函数 ``` - any 不是银弹 - any 让你绕过了类型系统,失去了编译器的保护 - 能用具体类型就用具体类型,能用泛型就用泛型(Go 1.18+),any 是最后的选择 - 接口定义最佳实践 - 在使用方定义接口,不在实现方 - ```go // ❌ 在实现包里定义接口(Java 风格) // package userservice // type UserService interface { ... } // type UserServiceImpl struct { ... } // ✅ Go 风格:在需要的地方定义接口 // package handler type userStore interface { // 小写,包私有 GetUser(id int) (*User, error) CreateUser(name, email string) (*User, error) } type Handler struct { store userStore // 依赖接口,不依赖具体类型 } // 任何实现了 GetUser 和 CreateUser 的类型都可以注入 func NewHandler(store userStore) *Handler { return &Handler{store: store} } ``` - 接口让测试更容易 - ```go // 生产代码 type DBStore struct { db *sql.DB } func (s *DBStore) GetUser(id int) (*User, error) { /* 查数据库 */ } // 测试用 Mock(不需要数据库!) type MockStore struct { users map[int]*User } func (m *MockStore) GetUser(id int) (*User, error) { u, ok := m.users[id] if !ok { return nil, errors.New("not found") } return u, nil } func (m *MockStore) CreateUser(name, email string) (*User, error) { u := &User{ID: len(m.users) + 1, Name: name, Email: email} m.users[u.ID] = u return u, nil } // 测试 func TestHandler(t *testing.T) { mock := &MockStore{users: map[int]*User{ 1: {ID: 1, Name: "Alice"}, }} h := NewHandler(mock) // 注入 Mock // ... 测试 handler 逻辑,完全不依赖数据库 } ``` - 接口是 Go 依赖注入的基础 - Go 不需要 Spring 这样的 DI 框架 - 接口 + 构造函数注入就够了 - 生产环境注入真实实现,测试时注入 Mock - 这让代码既解耦又易测试 - 这是 Go 后端项目的标准架构模式 并发与工程化# - 并发与工程化 - Goroutine 入门 - Goroutine 是什么 - Goroutine 是 Go 并发的核心——比线程轻量得多的「协程」 - 启动一个 Goroutine 只需要在函数调用前加 go 关键字 - 示例 - ```go package main import ( "fmt" "time" ) func say(s string) { for i := 0; i < 3; i++ { fmt.Println(s) time.Sleep(100 * time.Millisecond) } } func main() { go say("世界") // 新 goroutine 中运行 say("你好") // 当前 goroutine 中运行 } // 输出(顺序不确定): // 你好 // 世界 // 你好 // 世界 // 你好 // 世界 ``` - Goroutine vs 线程 -  - GMP 调度模型(概览) - Go 运行时用 GMP 模型来调度 Goroutine - 示例 - ```go // G = Goroutine(协程) // M = Machine(OS 线程) // P = Processor(逻辑处理器,调度上下文) // P 的数量默认等于 CPU 核数,可以设置 import "runtime" runtime.GOMAXPROCS(4) // 设置使用 4 个 P fmt.Println(runtime.GOMAXPROCS(0)) // 查看当前 P 数量 fmt.Println(runtime.NumCPU()) // CPU 核数 // Go 1.5+ 默认 GOMAXPROCS = NumCPU // 大多数时候不需要手动设置 ``` - 调度模型示意 - ``` P1 [G1→G2→G3] P2 [G4→G5→G6] ↓ ↓ M1 M2 ↓ ↓ OS Thread OS Thread 当 G 执行系统调用阻塞时,P 会把 M 换掉继续跑其他 G 这叫"work stealing"(工作窃取)——空闲的 P 会从别的 P 偷 G 来跑 ``` - sync.WaitGroup 等待协程完成 - 启动 Goroutine 后,main 函数不会自动等待它们结束 - 如果 main 退出,所有 Goroutine 都会被强制终止 - WaitGroup 是等待一组 Goroutine 完成的标准方式 - 示例 - ```go // ❌ 错误:main 退出,goroutine 还没跑完 func main() { go fmt.Println("可能跑不到") // main 立即结束,goroutine 被杀死 } // ✅ 用 WaitGroup 等待 import "sync" func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) // 计数器 +1 go func(id int) { defer wg.Done() // 函数结束时计数器 -1 fmt.Printf("Worker %d 完成\n", id) }(i) } wg.Wait() // 阻塞,直到计数器归零 fmt.Println("所有 Worker 完成") } ``` - WaitGroup 的使用规范 - ```go // ✅ 正确:在 goroutine 启动前 Add wg.Add(1) go func() { defer wg.Done() // ... }() // ❌ 错误:在 goroutine 内部 Add(可能来不及) go func() { wg.Add(1) // 可能 Wait() 已经过了,竞态! defer wg.Done() // ... }() // ✅ WaitGroup 不能拷贝,传指针 func process(wg *sync.WaitGroup) { defer wg.Done() // ... } var wg sync.WaitGroup wg.Add(1) go process(&wg) wg.Wait() ``` - 固定节奏 - 启动 goroutine 前 wg.Add(1) - goroutine 内第一行 defer wg.Done() - 主流程 wg.Wait() - Goroutine 泄露 - Goroutine 泄漏是 Go 并发最常见的 bug:启动了 Goroutine,但它永远不会退出,一直占用内存和资源 - 泄露的常见原因 - ```go // ❌ 泄漏一:等待永远不会发送的 channel func leak1() { ch := make(chan int) go func() { val := <-ch // 永远阻塞!没人往 ch 发数据 fmt.Println(val) }() // 函数返回,但 goroutine 永远卡在这 } // ❌ 泄漏二:没有退出机制的无限循环 func leak2() { go func() { for { doWork() // 没有 break/return/cancel 条件 } }() } // ❌ 泄漏三:阻塞的 HTTP 请求没有超时 func leak3() { go func() { resp, _ := http.Get("http://slow-server.com") // 如果服务器不响应,goroutine 永远等着 _ = resp }() } ``` - 正确的退出机制 - ```go // ✅ 用 done channel 通知退出 func worker(done <-chan struct{}) { for { select { case <-done: fmt.Println("收到退出信号,正在退出") return default: doWork() } } } done := make(chan struct{}) go worker(done) time.Sleep(5 * time.Second) close(done) // 通知所有监听 done 的 goroutine 退出 // ✅ 用 context.Context(更推荐,Day 10 详讲) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() go func() { select { case <-ctx.Done(): fmt.Println("超时退出:", ctx.Err()) return case result := <-doAsyncWork(): fmt.Println("完成:", result) } }() ``` - 检测泄露工具 goleak - ```go // 在测试中检测 goroutine 泄漏 import "go.uber.org/goleak" func TestNoLeak(t *testing.T) { defer goleak.VerifyNone(t) // 测试结束时检查是否有泄漏的 goroutine // 你的业务代码 runSomeTask() } // 也可以用运行时查看当前 goroutine 数量 import "runtime" fmt.Println("goroutine 数量:", runtime.NumGoroutine()) ``` - Goroutine 的黄金原则 - 启动 Goroutine 的人负责结束它 - 每次 go func() 之前,问自己:这个 Goroutine 什么时候、在什么条件下会退出? - 数据竞争与 Race Detector - 多个 Goroutine 同时读写同一变量时,会产生数据竞争(Data Race) - Go 自带了竞态检测器 - ```go // ❌ 数据竞争:多个 goroutine 同时写 counter var counter int func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter++ // ❌ 非原子操作,存在竞争! }() } wg.Wait() fmt.Println(counter) // 结果不确定,可能不是 1000 } // 运行竞态检测器 // go run -race main.go // 输出:WARNING: DATA RACE // 清晰显示哪些行在竞争 ``` - 三种解决方案 - ```go // ✅ 方案一:sync.Mutex 加锁 var ( mu sync.Mutex counter int ) go func() { mu.Lock() defer mu.Unlock() counter++ }() // ✅ 方案二:atomic 原子操作(更快,适合简单计数) import "sync/atomic" var counter int64 go func() { atomic.AddInt64(&counter, 1) }() result := atomic.LoadInt64(&counter) // ✅ 方案三:用 channel 传递数据(不共享状态) // 这是最地道的 Go 风格(Day 9 详讲) ch := make(chan int, 1000) for i := 0; i < 1000; i++ { go func() { ch <- 1 }() } total := 0 for i := 0; i < 1000; i++ { total += <-ch } fmt.Println(total) // 1000,保证正确 ``` - -race 标志 - 开发阶段始终用 go test -race 和 go run -race 运行代码 - Race Detector 会在运行时检测数据竞争并打印详细报告(文件名、行号、goroutine 栈) - 生产环境不用开(有约 20% 性能损耗),但 CI/CD 流水线里一定要跑 - sync 包核心原语 - sync 包提供了并发编程的基础工具 - 除了 WaitGroup,还有互斥锁、读写锁和 Once - sync.Mutex 互斥锁 - ```go type SafeCounter struct { mu sync.Mutex count int } func (c *SafeCounter) Inc() { c.mu.Lock() defer c.mu.Unlock() // 保证解锁 c.count++ } func (c *SafeCounter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.count } // 使用 counter := &SafeCounter{} var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Inc() }() } wg.Wait() fmt.Println(counter.Value()) // 1000,保证正确 ``` - sync.RWMutex 读写锁 (读多写少场景) - ```go type Cache struct { mu sync.RWMutex data map[string]string } // 写操作:独占锁 func (c *Cache) Set(key, val string) { c.mu.Lock() defer c.mu.Unlock() c.data[key] = val } // 读操作:共享锁(多个 goroutine 可同时读) func (c *Cache) Get(key string) (string, bool) { c.mu.RLock() defer c.mu.RUnlock() v, ok := c.data[key] return v, ok } // 读写锁的性能优势: // 纯读的场景下,多个 goroutine 可以并发读,不阻塞 // 只有写时才独占,适合缓存、配置这类读多写少的场景 ``` - sync.Once 单例初始化 - ```go // 保证函数只执行一次,线程安全 var ( instance *DB once sync.Once ) func GetDB() *DB { once.Do(func() { instance = &DB{ conn: openConnection(), } }) return instance } // 无论多少 goroutine 并发调用 GetDB() // openConnection() 只会执行一次 // 这是 Go 实现单例模式的标准方式 ``` - Channel 通信 - Channel 基础 - Channel 是 Go 并发的通信机制 - Go 的并发哲学是:「不要通过共享内存来通信,要通过通信来共享内存。」 - Channel 就是这个哲学的具体实现——Goroutine 之间通过 channel 传递数据,而不是共享变量。 - 示例 - ```go // 创建 channel:make(chan 类型) ch := make(chan int) // 无缓冲 channel ch := make(chan string, 5) // 有缓冲 channel,容量 5 // 发送数据:ch <- value ch <- 42 // 接收数据:value := <-ch v := <-ch // 关闭 channel close(ch) // 判断 channel 是否关闭 v, ok := <-ch // ok == false 说明 channel 已关闭且已空 // channel 的零值是 nil var ch chan int // nil channel // 向 nil channel 发送/接收会永远阻塞! ``` - 两个 Goroutine 通信 - ```go func main() { ch := make(chan string) // 发送方 goroutine go func() { ch <- "Hello from goroutine!" }() // 接收方(主 goroutine) msg := <-ch fmt.Println(msg) // Hello from goroutine! } // ✨ 不需要 WaitGroup! // <-ch 会阻塞,直到有数据,天然同步 ``` - Channel 的方向是通信,不是存储 - Channel 不是消息队列,不是用来存数据的 - 它是两个 Goroutine 之间的「传送带」,一端发送,另一端接收,数据在两者之间流动 - 无缓冲 vs 有缓冲 - 无缓冲和有缓冲 channel 的行为差异很大,选错会导致死锁或意外的并发行为。 - 无缓冲 Channel (同步)