主题
03 - 函数、接口与错误处理
本节全景图
┌─────────────────────────────────────────────────────────┐
│ 函数 & 接口 & 错误处理 │
├───────────────┬──────────────────┬──────────────────────┤
│ 函数 │ 接口 │ 错误处理 │
│ │ │ │
│ 多返回值 │ 隐式实现 │ error 接口 │
│ 一等公民 │ 鸭子类型 │ 多返回值模式 │
│ 闭包 │ 空接口 any │ defer/panic/recover │
│ 可变参数 │ 类型断言 │ 错误包装 %w │
└───────────────┴──────────────────┴──────────────────────┘1. 函数(Function)
1.1 基本语法
go
package main
import "fmt"
// 基本函数
func add(a int, b int) int {
return a + b
}
// 参数类型相同可以简写
func multiply(a, b int) int {
return a * b
}
// 多返回值(Go 最重要的特性之一!)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为零")
}
return a / b, nil
}
// 命名返回值
func stats(numbers []int) (min, max, sum int) {
min = numbers[0]
max = numbers[0]
for _, n := range numbers {
if n < min { min = n }
if n > max { max = n }
sum += n
}
return // 裸 return,自动返回命名的变量
}
func main() {
fmt.Println(add(3, 5))
result, err := divide(10, 3)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Printf("结果: %.2f\n", result)
lo, hi, total := stats([]int{3, 1, 4, 1, 5, 9})
fmt.Printf("min=%d max=%d sum=%d\n", lo, hi, total)
}多返回值 —— Go vs Java:
Java: Go:
┌────────────────────────┐ ┌──────────────────────────┐
│ // 只能返回一个值 │ │ // 可以返回多个值 │
│ Result divide(a, b) { │ │ func divide(a, b float64)│
│ if (b == 0) │ │ (float64, error) { │
│ throw Exception(); │ │ if b == 0 { │
│ return a / b; │ │ return 0, err │
│ } │ │ } │
│ │ │ return a/b, nil │
│ // 调用端: │ │ } │
│ try { │ │ │
│ r = divide(10, 0); │ │ // 调用端: │
│ } catch (Exception e) {│ │ r, err := divide(10, 0) │
│ // 处理 │ │ if err != nil { │
│ } │ │ // 处理 │
│ │ │ } │
└────────────────────────┘ └──────────────────────────┘
Go 用多返回值替代了异常机制
(value, error) 是 Go 中最经典的模式1.2 函数是一等公民
go
package main
import "fmt"
// 函数类型
type MathFunc func(int, int) int
// 高阶函数:接受函数作为参数
func apply(a, b int, fn MathFunc) int {
return fn(a, b)
}
// 返回函数
func makeMultiplier(factor int) func(int) int {
return func(x int) int {
return x * factor // 闭包:捕获了外部变量 factor
}
}
func main() {
// 函数赋值给变量
add := func(a, b int) int { return a + b }
sub := func(a, b int) int { return a - b }
fmt.Println(apply(10, 3, add)) // 13
fmt.Println(apply(10, 3, sub)) // 7
// 闭包
double := makeMultiplier(2)
triple := makeMultiplier(3)
fmt.Println(double(5)) // 10
fmt.Println(triple(5)) // 15
// 匿名函数立即执行
result := func(x int) int {
return x * x
}(5)
fmt.Println(result) // 25
}闭包(Closure) 的内存示意:
makeMultiplier(2) 返回的函数:
┌─────────────────────────────┐
│ 闭包 = 函数代码 + 环境 │
│ ┌─────────────────────┐ │
│ │ func(x int) int { │ │
│ │ return x * factor │ │
│ │ } │ │
│ └──────────┬──────────┘ │
│ │ 引用 │
│ ┌──────────▼──────────┐ │
│ │ factor = 2 │ │ ← 被闭包捕获,不会被 GC
│ └─────────────────────┘ │
└─────────────────────────────┘
double(5) → 5 * 2 = 10
triple(5) → 5 * 3 = 15 ← triple 捕获的 factor = 3
每次调用 makeMultiplier 都会创建一个新的 factor
各闭包之间互不影响1.3 可变参数
go
package main
import "fmt"
// ...int 表示接收任意数量的 int 参数
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
// 传入切片时用 ... 展开
numbers := []int{10, 20, 30}
fmt.Println(sum(numbers...)) // 60
}2. 接口(Interface)
2.1 接口的核心理念
Go 接口 = 隐式实现(鸭子类型)
"If it walks like a duck and quacks like a duck, it's a duck"
"如果它走路像鸭子、叫声像鸭子,那它就是鸭子"
Java: Go:
┌──────────────────────┐ ┌──────────────────────┐
│ interface Animal { │ │ type Animal interface│
│ void speak(); │ │ { │
│ } │ │ Speak() string │
│ │ │ } │
│ class Dog │ │ │
│ implements Animal {│ ← 显式声明 │ type Dog struct{} │
│ void speak() {...} │ │ func (d Dog) Speak() │
│ } │ │ string { ... } │
└──────────────────────┘ └──────────────────────┘
↑ 没有 implements!
只要有 Speak() 方法就自动满足
优势:
┌────────────────────────────────────────────────┐
│ 1. 解耦:定义接口的包不需要知道实现者 │
│ 2. 灵活:第三方的类型也能满足你的接口 │
│ 3. 小接口:Go 鼓励 1-2 个方法的小接口 │
└────────────────────────────────────────────────┘2.2 定义和使用
go
package main
import (
"fmt"
"math"
)
// 定义接口:只描述行为,不描述数据
type Shape interface {
Area() float64
Perimeter() float64
}
// --- Circle 实现了 Shape(隐式) ---
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// --- Rectangle 也实现了 Shape(隐式) ---
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// 接受接口类型的函数 → 多态!
func printShape(s Shape) {
fmt.Printf("面积=%.2f 周长=%.2f\n", s.Area(), s.Perimeter())
}
func main() {
c := Circle{Radius: 5}
r := Rectangle{Width: 3, Height: 4}
printShape(c) // 面积=78.54 周长=31.42
printShape(r) // 面积=12.00 周长=14.00
// 接口切片:多态集合
shapes := []Shape{c, r, Circle{Radius: 1}}
for _, s := range shapes {
printShape(s)
}
}接口满足关系的可视化:
type Shape interface {
Area() float64
Perimeter() float64
}
Circle: Rectangle:
┌──────────────────┐ ┌──────────────────┐
│ Radius float64 │ │ Width float64 │
│ │ │ Height float64 │
│ Area() ✓ │ │ Area() ✓ │
│ Perimeter() ✓ │ │ Perimeter() ✓ │
└────────┬─────────┘ └────────┬──────────┘
│ │
│ 满足 Shape │ 满足 Shape
▼ ▼
┌──────────────────────────────────────────────┐
│ Shape 接口 │
│ 可以放 Circle 也可以放 Rectangle │
└──────────────────────────────────────────────┘2.3 标准库中的经典接口
go
package main
import (
"fmt"
"strings"
)
// io.Reader — 只有一个方法的接口(Go 推崇小接口)
// type Reader interface {
// Read(p []byte) (n int, err error)
// }
// fmt.Stringer — Go 的 toString()
// type Stringer interface {
// String() string
// }
type User struct {
Name string
Age int
}
// 实现 Stringer 接口
func (u User) String() string {
return fmt.Sprintf("%s (age %d)", u.Name, u.Age)
}
// error 接口 — Go 错误处理的基础
// type error interface {
// Error() string
// }
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
func main() {
u := User{Name: "Alice", Age: 30}
fmt.Println(u) // "Alice (age 30)" — 自动调用 String()
r := strings.NewReader("Hello")
buf := make([]byte, 5)
r.Read(buf)
fmt.Println(string(buf)) // "Hello"
}Go 标准库经典小接口:
┌────────────────────────────────────────────────┐
│ 接口名 方法 用途 │
├────────────────────────────────────────────────┤
│ io.Reader Read([]byte)(int,error) 读数据 │
│ io.Writer Write([]byte)(int,error) 写数据 │
│ io.Closer Close() error 关闭 │
│ fmt.Stringer String() string 打印 │
│ error Error() string 错误 │
│ sort.Interface Len/Less/Swap 排序 │
│ http.Handler ServeHTTP(w,r) HTTP │
└────────────────────────────────────────────────┘
Go 格言:"接口越小,抽象越有用"
io.Reader 只有一个方法,但整个标准库都围绕它构建2.4 空接口和类型断言
go
package main
import "fmt"
func main() {
// any (= interface{}) 可以存放任何类型的值
var anything any
anything = 42
anything = "hello"
anything = []int{1, 2, 3}
// 类型断言:从 any 中取出具体类型
str, ok := anything.([]int)
if ok {
fmt.Println("是切片:", str)
}
// 类型 switch
describe(42)
describe("hello")
describe(true)
describe([]int{1, 2})
}
func describe(val any) {
switch v := val.(type) {
case int:
fmt.Printf("整数: %d\n", v)
case string:
fmt.Printf("字符串: %q (长度=%d)\n", v, len(v))
case bool:
fmt.Printf("布尔: %t\n", v)
default:
fmt.Printf("其他类型: %T = %v\n", v, v)
}
}类型断言的工作原理:
一个 interface 变量内部存了两样东西:
var s Shape = Circle{Radius: 5}
┌──────────────────────────────┐
│ interface 变量 s │
│ ┌───────────────────────┐ │
│ │ type: *Circle 的类型信息│ │
│ ├───────────────────────┤ │
│ │ value: Circle{R: 5} │ │ ← 实际数据
│ └───────────────────────┘ │
└──────────────────────────────┘
类型断言 s.(Circle):
→ 检查 type 是不是 Circle
→ 是:返回 value + true
→ 否:返回零值 + false
类型 switch:
→ 依次检查 type,执行匹配的分支3. 错误处理
3.1 Go 的错误处理哲学
Go 没有 try-catch-finally!
Java: Go:
┌─────────────────────┐ ┌─────────────────────────┐
│ try { │ │ result, err := doSth() │
│ result = doSth(); │ │ if err != nil { │
│ } catch (Ex e) { │ │ // 处理错误 │
│ // 处理 │ │ return err │
│ } finally { │ │ } │
│ // 清理 │ │ defer cleanup() │
│ } │ │ // 继续正常逻辑 │
└─────────────────────┘ └─────────────────────────┘
Go 的理念:
┌──────────────────────────────────────────────────┐
│ 错误是值(value),不是异常(exception) │
│ 错误要在产生的地方立即处理,不要抛来抛去 │
│ 代码会有很多 if err != nil,这是 Go 的风格 │
└──────────────────────────────────────────────────┘3.2 错误处理实战
go
package main
import (
"errors"
"fmt"
"os"
)
// 自定义错误类型
type NotFoundError struct {
Name string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s not found", e.Name)
}
// 哨兵错误(预定义的全局错误值)
var ErrPermission = errors.New("permission denied")
func readConfig(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
// 错误包装:用 %w 保留原始错误链
return "", fmt.Errorf("读取配置文件失败: %w", err)
}
if len(data) == 0 {
return "", &NotFoundError{Name: path}
}
return string(data), nil
}
func main() {
content, err := readConfig("/nonexistent/config.yaml")
if err != nil {
// 检查是否是特定类型的错误
var nfe *NotFoundError
if errors.As(err, &nfe) {
fmt.Println("文件不存在:", nfe.Name)
return
}
// 检查是否是特定的哨兵错误
if errors.Is(err, os.ErrNotExist) {
fmt.Println("路径不存在")
return
}
fmt.Println("其他错误:", err)
return
}
fmt.Println(content)
}错误链 (Error Chain):
底层错误: os.ErrNotExist
│
│ 被包装
▼
中间层: fmt.Errorf("open %s: %w", path, err)
│
│ 被包装
▼
业务层: fmt.Errorf("读取配置失败: %w", err)
最终错误信息: "读取配置失败: open /xxx: file does not exist"
errors.Is(err, os.ErrNotExist) → 沿着链查找
errors.As(err, &target) → 沿着链找匹配的类型
┌──────────────────────────────────────────┐
│ errors.Is → 这个错误(或它包装的错误) │
│ 是不是某个特定值? │
│ │
│ errors.As → 这个错误(或它包装的错误) │
│ 能不能转成某个特定类型? │
└──────────────────────────────────────────┘3.3 defer — 延迟执行
go
package main
import (
"fmt"
"os"
)
func main() {
// defer 会在函数返回前执行(类似 Java 的 finally)
fmt.Println("开始")
defer fmt.Println("3 - 最后执行")
defer fmt.Println("2 - 倒数第二")
defer fmt.Println("1 - 倒数第一")
fmt.Println("结束")
// 输出:开始 → 结束 → 1 → 2 → 3(LIFO 栈顺序)
// 最常见用途:确保资源被关闭
content, err := readFile("test.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(content)
}
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() // 无论后面发生什么,f 一定会被关闭
buf := make([]byte, 1024)
n, err := f.Read(buf)
if err != nil {
return "", err // 即使这里 return,defer 的 Close 也会执行
}
return string(buf[:n]), nil
}defer 执行顺序(LIFO 栈):
func example() {
defer A() ←─┐
defer B() ←─┤ 入栈
defer C() ←─┘
// ... 函数逻辑
return ←── 触发
} │
▼
执行顺序: C() → B() → A() (后进先出)
┌─────────┐
│ C() │ ← 栈顶,先出
├─────────┤
│ B() │
├─────────┤
│ A() │ ← 栈底,最后
└─────────┘
经典用法:
f, _ := os.Open(path)
defer f.Close() // 保证关闭
mu.Lock()
defer mu.Unlock() // 保证解锁
tx := db.Begin()
defer tx.Rollback() // 保证回滚(commit 后 rollback 是 no-op)3.4 panic 和 recover(很少用)
go
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
// recover 必须在 defer 中调用
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("caught panic: %v", r)
}
}()
// 这会触发 panic(除以零)
return a / b, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println(result)
}panic/recover 使用指南:
┌───────────────────────────────────────────────┐
│ panic 何时用? │
│ ✗ 不要用 panic 代替 error return │
│ ✗ 不要用 panic 处理业务逻辑错误 │
│ ✓ 程序初始化失败(必须停止) │
│ ✓ 真正不可恢复的情况 │
│ │
│ recover 何时用? │
│ ✓ HTTP 框架的中间件(防止一个请求崩溃整个服务) │
│ ✓ goroutine 的顶层保护 │
│ │
│ 99% 的情况用 error 就够了 │
└───────────────────────────────────────────────┘4. 小结
┌─────────────────────────────────────────────────────┐
│ 函数 & 接口 & 错误处理 速查 │
├─────────────────────────────────────────────────────┤
│ │
│ 函数: │
│ ├── func name(a T, b T) (T, error) 多返回值 │
│ ├── func(a T) T 匿名函数 │
│ ├── func(args ...T) 可变参数 │
│ └── 函数选项模式 构造复杂对象 │
│ │
│ 接口: │
│ ├── type X interface { Method() } 定义行为 │
│ ├── 隐式实现(不需要 implements) 鸭子类型 │
│ ├── any = interface{} 空接口 │
│ └── val.(Type) / switch val.(type) 类型断言 │
│ │
│ 错误: │
│ ├── if err != nil { return err } 基本模式 │
│ ├── fmt.Errorf("...: %w", err) 错误包装 │
│ ├── errors.Is / errors.As 错误检查 │
│ └── defer / panic / recover 延迟与恢复 │
│ │
└─────────────────────────────────────────────────────┘下一节: 04 - 并发编程 — Goroutine、Channel、GMP 模型(重点!)