Go:函数、方法、接口与错误
前置:类型与集合。本章把“数据怎么存”推进到“行为怎么组合”。完整例子保存为模块内唯一的 main.go,执行 go run .。Go 1.27.2;Windows/Linux 运行核对为历史记录,本轮示例未执行;macOS 仅作平台说明,本项目不执行平台运行验证。
函数参数、返回值与闭包
函数签名 func add(a, b int) int 说明两个输入都是 int,返回一个 int;函数体用 return a + b 交出结果。返回两个值常写作 (int, error),调用方要分别处理。Go 参数全部按值传递;“传指针”也是复制指针值,它们可以指向同一个对象。
最小用法:定义 func add(a, b int) int { return a + b },在 main 中用 fmt.Println(add(2, 3)),输出 5。参数名仅在函数内有效,调用方不必使用同样的名字。调用结束后用返回值继续计算,不要依赖函数修改调用方的整数参数。
可变参数 func sum(values ...int) int 在函数内得到一个切片,已有切片可用 sum(xs...) 展开。空参数时应定义结果。命名返回值进入函数时是零值,较长函数宜显式返回表达式。递归函数必须有终止条件,并考虑栈增长或数值溢出。
闭包与捕获状态
前置:函数参数与返回值。函数也能作为值保存。闭包是引用外部变量的函数:返回闭包后,被引用的状态依然存在。每次调用下面的 counter 都创建一份新的 n,但同一返回函数的多次调用共享自己的 n。
package main
import "fmt"
func counter() func() int {
n := 0
return func() int { n++; return n }
}
func main() {
first, second := counter(), counter()
fmt.Println(first(), first(), second())
}预期输出:
1 2 1闭包不是并发锁;多个 goroutine 同时调用 first 会竞争修改 n。回调应尽量捕获少量明确状态,普通业务函数不必为使用闭包而使用闭包。
结构体与指针
前置:变量赋值与数组复制。结构体将不同属性组合成一个值;指针保存值的位置。取地址用 &,通过指针修改原值用 *。
package main
import "fmt"
type Task struct { Title string; Done bool }
func main() {
task := Task{Title: "读一章"}
copied := task
pointer := &task
pointer.Done = true
fmt.Println(task.Done, copied.Done)
n := 1
p := &n
*p = 2
fmt.Println(n)
}预期输出:
true false
2copied.Done 是独立字段;pointer 指向原 task,修改对原值可见。结构体指针的字段可直接写 pointer.Done。未赋值的指针为 nil,不能解引用。结构体含切片时,复制字段不会递归复制底层元素。
new(T) 返回指向 T 零值的指针,不代表分配一定发生在堆上。嵌入字段提供组合和方法提升,不是类继承。字段间可能有对齐填充,实际大小与目标架构有关;应先测量再调整布局,不把某台机器的字节数当语言保证。包含已使用的锁的结构体不能复制。
练习:改为 pointer := &copied 后预测第一行。
参考解法
输出 false true。复制指针值可以保留对同一对象的访问,复制结构体值则获得另一份字段。
方法与接收者
结构体把相关字段放在一起;方法是带接收者的函数。指针 *Task 表示 Task 的地址,&task 取地址;p.Title 可直接访问指针指向的字段。指针接收者适合修改对象;值接收者拿到副本,但副本内的 map、slice 仍可能共享底层数据。
接收者也影响方法集:下面的 Finish 属于 *Task 的方法集,Task 值不能因此直接满足 Finisher。接口匹配的详细规则见接口与方法集。
package main
import "fmt"
type Task struct { Title string; Done bool }
func (t *Task) Finish() { t.Done = true }
type Finisher interface { Finish() }
func complete(f Finisher) { f.Finish() }
func main() {
task := Task{Title: "学习接口"}
copy := task
complete(&task)
fmt.Println(task.Done, copy.Done)
}预期输出:
true false接口与方法集
前置:上一节 Task 与 Finish 的完整程序。接口规定所需方法,类型只要满足方法集就自动实现,不需要 implements。这里只给 *Task 定义 Finish,所以 complete(task) 不满足接口,应传 &task。可寻址变量上直接写 task.Finish() 能由编译器取址,但这不等于 Task 值自动实现接口。
接口让 complete 只依赖“能完成”这个行为,不必知道调用者有哪些字段。新增一个同样具有 Finish 方法的类型即可复用 complete;不要为了每个结构体机械创建接口。使用方需要替换实现、做测试或表达最小依赖时,小接口更合适。
练习:将 complete(&task) 改为 complete(task),读编译器报告的方法集不匹配,再恢复。将 Finish 改为值接收者虽能让 Task 满足接口,却会修改副本,无法把原任务标为完成。
interface nil:动态类型仍在
接口为 nil 必须同时没有动态类型和动态值。把一个 nil 指针装进接口后,它有动态类型 *problem,所以接口本身不为 nil。返回 error 时,没有错误就显式 return nil,不要把 nil 的具体错误指针当作无错误返回。
package main
import "fmt"
type problem struct{}
func (*problem) Error() string { return "problem" }
func main() {
var pointer *problem
var err error = pointer
fmt.Println(pointer == nil, err == nil)
err = nil
fmt.Println(err == nil)
}预期输出:
true false
truevalue, ok := candidate.(Finisher) 是安全的类型断言;不确定类型时使用 ok,单值断言不匹配会 panic。业务调用优先依赖接口行为,而不是到处判断具体实现类型。
错误包装 errors.Is 与 defer 顺序
error 是普通返回值。调用方先检查 err,再用结果;给错误加上下文用 %w 保留原错误,errors.Is 判断错误链是否包含某个目标。不要比较错误文案来决定业务分支,文案变化不应该改变行为。
package main
import (
"errors"
"fmt"
)
var errEmpty = errors.New("标题为空")
func validate(title string) error {
if title == "" { return fmt.Errorf("创建任务: %w", errEmpty) }
return nil
}
func main() {
defer fmt.Println("最后释放资源")
defer fmt.Println("先完成收尾")
err := validate("")
fmt.Println(errors.Is(err, errEmpty))
fmt.Println(err)
}预期输出:
true
创建任务: 标题为空
先完成收尾
最后释放资源defer 与资源释放
前置:错误检查。上例最后两行展示释放顺序。defer 登记函数调用,所在函数返回时后登记的先执行;参数在登记时求值。打开文件成功之后才 defer Close,不能忽略 Open 的错误。循环里 defer 到整个函数结束才执行,处理大量文件时把单次操作放进小函数,及时释放资源。需要检查写入文件的 Close 错误时,应显式处理,不能只写一个忽略结果的 defer。
练习:交换上例两个 defer 的位置,先预测再运行。不要把 defer 当作独立后台任务;它在当前函数退出过程中同步执行。
panic 表示无法按正常契约继续,不用于代替输入校验;recover 仅在合适的 deferred 函数中捕获 panic。一般读文件失败、坏 JSON、标题为空都返回 error,让调用者决定提示还是重试。
recover 必须由同一个 goroutine 中的 deferred 函数直接调用,不能由 main 捕获另一个 goroutine 的 panic。恢复后不会回到触发 panic 的那一行继续执行;defer 也不能保证进程被强制结束时运行。需要提取错误链中的具体类型时用 errors.As,避免重新构造 errors.New(err.Error()) 丢掉类型和包装关系。
练习与自检
给 validate 增加去首尾空白,输入 " " 应命中 errEmpty。把 %w 改成 %v,预测 errors.Is 的变化。把 Task.Finish 改成值接收者后运行,解释为什么 task.Done 不再改变。
参考解法
导入 strings,用 strings.TrimSpace(title) == "" 判断。%v 只格式化文本,不建立包装链,因此 errors.Is 为 false。值接收者修改副本的 Done,原 task 不变;若要修改原值,保留指针接收者。接口只保证方法存在,不保证方法一定改变调用方的数据。
继续阅读
- 开发实践
- 开发实践
- 开发实践