Go语言匿名函数变量声明:两种写法的差异及注意场景
这两种写法存在关键差异,核心在于匿名函数对父作用域变量的捕获方式不同,具体区别和需要留意的场景如下:
核心差异:变量捕获方式
传参式写法(第一种)
defer func(s *Service, ID string) { err := s.Deprovision(ID) if err != nil { s.logger.Error(err) } }(service, identifier)这种写法是值传递:在声明
defer的瞬间,就会把service和identifier的当前值复制一份,作为参数传入匿名函数。后续父作用域中修改这两个变量的值,完全不会影响defer函数里使用的副本。直接引用式写法(第二种)
defer func() { err := service.Deprovision(identifier) if err != nil { service.logger.Error(err) } }()这种写法是引用捕获父作用域变量:defer函数并没有创建变量副本,而是直接绑定父作用域的变量本身。只有等到defer执行时(函数即将返回前),才会读取这两个变量的最新值。
直观示例
func demo() { service := &Service{} identifier := "initial-id" // 第一种写法:使用声明时的副本 defer func(s *Service, ID string) { fmt.Println("传参式捕获的ID:", ID) // 输出 "initial-id" }(service, identifier) // 第二种写法:使用最终的变量值 defer func() { fmt.Println("直接引用的ID:", identifier) // 输出 "updated-id" }() // 修改变量值 identifier = "updated-id" }
需要留意差异的场景
变量后续会被修改的场景
如果父作用域在声明defer后,会修改变量的值,两种写法的执行结果完全不同。如果需要确保defer使用变量在声明时的状态,必须用传参式写法。指针类型变量的特殊情况
若变量是指针类型(比如示例中的service),传参式写法传递的是指针的副本,但这个副本仍然指向原内存地址。所以如果后续修改指针指向的结构体字段,两种写法都会看到修改后的结果;但如果后续把指针重新赋值(比如service = nil),传参式写法里的s还是原来的指针,而直接引用式写法里的service会变成nil,调用方法时会触发panic。循环中使用defer的场景
这是最容易踩坑的场景:如果在for循环中用直接引用式写法捕获循环变量,所有defer函数会共享同一个变量引用,等到循环结束执行defer时,变量已经是最后一次循环的值。此时必须用传参式写法,把当前循环的变量值传入,确保每个defer都拿到对应循环的变量快照。错误示例与正确写法对比:
func loopDemo() { for i := 0; i < 3; i++ { // 错误:所有defer最终输出3 defer func() { fmt.Println(i) }() // 正确:分别输出2、1、0 defer func(num int) { fmt.Println(num) }(i) } }
内容的提问来源于stack exchange,提问作者Greg T. Wallace

