Go中使用defer时更新传入函数切片,除指针外有何替代方案?
在Go中使用defer时获取切片更新的惯用方案
你想在Go里用defer的同时,捕获后续对切片的更新,但又不想用record3那种传递切片指针的写法(不符合Go惯用风格)。先看你的测试代码和执行结果:
测试代码
package main import ( "fmt" "time" ) func record(start time.Time, l []int) { fmt.Printf("Record: Time: %v, List: %v\n", time.Since(start), l) } func record2(start time.Time, l ...int) { fmt.Printf("Record2: Time: %v, List: %v\n", time.Since(start), l) } func record3(start time.Time, l *[]int) { fmt.Printf("Record3: Time: %v, List: %v\n", time.Since(start), *l) } func main() { l := []int{1, 2, 3} defer func(t0 time.Time) { record(t0, l) }(time.Now()) // 能拿到切片更新后的值 defer record2(time.Now(), l...) // 只能拿到初始切片值 defer record3(time.Now(), &l) // 能拿到更新值但不符合惯用写法 l = append(l, 4) time.Sleep(1 * time.Second) return }
执行输出
Record3: Time: 1.001027459s, List: [1 2 3 4] Record2: Time: 1.001204583s, List: [1 2 3] Record: Time: 1.001209792s, List: [1 2 3 4]
原因分析
record2用了可变参数,defer注册时就把当前l的切片值拷贝了一份,后续append如果触发底层数组扩容,l会被赋值为新的切片结构体,record2拿到的还是旧的拷贝值。record3通过指针传递切片,虽然能获取到更新,但正如《Effective Go》提到的,这种写法不符合Go的惯用风格——切片本身就是带指针的结构体,除非要修改切片的长度、容量或者让它指向新数组,否则不需要传指针。
惯用替代方案
最符合Go风格的方式就是你已经用到的闭包捕获切片变量,也就是第一个defer的写法。核心是利用闭包延迟获取切片的最新值,而不是在defer注册时就传递参数。
简化优化版
package main import ( "fmt" "time" ) func record(start time.Time, l []int) { fmt.Printf("Record: Time: %v, List: %v\n", time.Since(start), l) } func main() { l := []int{1, 2, 3} t0 := time.Now() // 闭包捕获l,defer执行时读取l的最新值 defer func() { record(t0, l) }() l = append(l, 4) time.Sleep(1 * time.Second) return }
为什么这是惯用写法?
切片是引用类型,但它本身是包含指针、长度、容量的结构体。当append触发扩容时,l会被重新赋值为新的切片结构体。闭包捕获的是l这个变量本身,所以在defer执行时,会读取l的最新状态,自然就能拿到更新后的切片。
如果需要复用这种延迟记录逻辑,可以封装成高阶函数,避免重复写闭包:
封装高阶函数示例
func deferRecord(start time.Time, getList func() []int) { defer func() { record(start, getList()) }() } func main() { l := []int{1, 2, 3} t0 := time.Now() // 传递一个获取切片的函数,延迟到defer执行时获取最新值 deferRecord(t0, func() []int { return l }) l = append(l, 4) time.Sleep(1 * time.Second) }
这种方式同样不需要传递切片指针,保持了Go的惯用风格,同时让代码更易于复用。
内容的提问来源于stack exchange,提问作者lexhuismans
相关产品推荐
相关产品推荐

