Skip to content

函数嵌套与闭包

作者: ryan 发布于: 2025/7/16 更新于: 2025/7/16 字数: 0 字 阅读: 0 分钟

在 Go 语言中,函数是支持嵌套定义的,嵌套函数指的是在一个函数内部定义另一个函数。

通过嵌套函数,可以实现较高的代码封装性,并且内层函数可以访问外层函数的局部变量,但是外层函数不能访问内层函数的局部变量

go
package main  
  
import "fmt"  
  
func outer() {  
    c := 99 //局部变量  
    var inner = func() {  
       fmt.Printf("1,%d,%[1]c\n", c)  
    }  
    inner()  
    fmt.Printf("2,%d,%[1]c\n", c)  
}  
  
func main() {  
    outer()  
}

//1,99,c
//2,99,c

此时c分别是多少?

go

package main  
  
import "fmt"  
  
func outer() {  
    c := 99 //局部变量  
    var inner = func() {  
       c = 100  
       fmt.Printf("1,%d,%[1]c\n", c)  
    }  
    inner()  
    fmt.Printf("2,%d,%[1]c\n", c)  
}  
  
func main() {  
    outer()  
}


//1,100,d
//2,100,d
go
package main  
  
import "fmt"  
  
func outer() {  
    c := 99 //局部变量  
    var inner = func() {  
       c = 100  
       fmt.Printf("1,%d,%[1]c\n", c)  
       c := c + 1  
       fmt.Printf("3,%d,%[1]c\n", c)  
    }  
    inner()  
    fmt.Printf("2,%d,%[1]c\n", c)  
}  
  
func main() {  
    outer()  
}

//1,100,d
//3,101,e
//2,100,d
go
package main  
  
import "fmt"  
  
func outer() {  
    c := 99 //局部变量  
    fmt.Printf("0,%d,%[1]c,%p\n", c, &c)  
    var inner = func() {  
       c = 100  
       fmt.Printf("1,%d,%[1]c,%p\n", c, &c)  
       c := c + 1  
       fmt.Printf("3,%d,%[1]c,%p\n", c, &c)  
    }  
    inner()  
    fmt.Printf("2,%d,%[1]c,%p\n", c, &c)  
}  
  
func main() {  
    outer()  
}

//0,99,c,0x100a108
//1,100,d,0x100a108
//3,101,e,0x100a128
//2,100,d,0x100a108

请问在outer调用之前,请问inner函数存在吗?

不存在,inner作为函数类型的局部变量,只有调用outer函数时才在栈帧上才创建出来。

image.png

过程:main函数压栈->outer函数 c和inner都是变量分别在栈帧上被创建,c的值是普通类型因此存放在栈帧中,而inner是函数引用类型则创建函数的指针,指向内存中的某个位置也就是堆上开辟了内存空间存放了这个函数指令。

image.png

当inner 函数 和 outer 函数都执行完了栈帧出栈后,原本inner函数指向堆里面的函数指令没有人指向了,在适当的适合GC就会进行回收垃圾。

函数调用是一个压栈的过程。在调用一个函数时,程序会将该函数的局部变量、参数以及执行现场信息压入栈中。

栈中的数据会在函数执行结束后弹出,从而释放相关内存空间。

压栈的过程确保了每个函数都有独立的作用域和执行环境。

闭包

闭包是指内层函数能够访问外层函数的局部变量,并且可以在外层函数返回后依然保持对这些变量的引用。

闭包(Closure) 是函数与其相关的引用环境(lexical environment)的组合体。简单来说:

闭包 = 函数 + 被捕获的变量环境

核心特性

  1. 函数嵌套:闭包通常是函数内部定义的函数。
  2. 变量捕获:内部函数可以访问外部函数的局部变量(即使外部函数已执行完毕)。
  3. 状态持久化:被捕获的变量会与闭包函数绑定,形成“私有状态”。

变量引用

闭包捕获的是变量的引用(内存地址),而非值拷贝。

go
func outer() {
    x := 10
    inner := func() {
        fmt.Println(x) // 直接操作外部x的内存地址
        x++            // 修改外部变量
    }
    inner() // 输出10,x变为11
    inner() // 输出11,x变为12(状态保留)
}

变量遮蔽

在闭包内部使用短声明 := 会创建同名局部变量,切断对外部变量的访问

go
func outer() {
    x := 10
    inner := func() {
        x := 20 // 新建局部变量,遮蔽外部x
        fmt.Println(x) // 20(局部变量)
    }
    inner()
    fmt.Println(x) // 10(外部变量未变)
}

封装私有状态

当执行c := counter()时调用counter()函数,返回一个匿名函数(闭包),该函数引用并持有count变量。返回的闭包被赋值给变量c 此时闭包已绑定一个专属的count变量,该变量不会随counter()函数结束而销毁,而是由闭包长期持有。

go
func counter() func() int {
    count := 0
    return func() int {
        count++ // 这个count被闭包私有持有
        return count
    }
}
// 使用:
c := counter()
c() // 1
c() // 2 (状态独立保存)

外层函数outer()返回后依然保持对变量的引用

go
package main
import "fmt"

func outer() func() {
    c := 99 // 外层函数的局部变量
    return func() {
        fmt.Println(c) // 内层函数访问外层函数的局部变量
    }
}

func main() {
    fn := outer() // 获取返回的闭包函数
    fn() // 调用闭包,输出:99
}

从函数调用时栈帧变化和闭包底层实现的角度,分析闭包变量私有化的完整过程:

普通函数调用时的栈帧生命周期

go
func outer() {
    c := 99 // 局部变量存储在栈帧中
}
func main() {
    outer() 
}
  1. main调用outer : 系统为outer创建栈帧(Stack Frame),局部变量c=99存储在此栈帧内
  2. outer执行完毕后: outer栈帧被销毁(出栈),变量c随之被释放回到main栈帧,无法再访问c

闭包场景下的栈帧变化与变量捕获

go
func outer() func() {
    c := 99          // Step1: c被提升至堆
    return func() {   // Step2: 返回闭包对象(含c指针)
        fmt.Println(c) 
    }
}

fn1 := outer() // 创建闭包实例1(c1=99)
fn2 := outer() // 创建闭包实例2(c2=99)→ 独立于c1

fn1()          // 输出99(访问c1)
fn2()          // 输出99(访问c2)
  1. 阶段1:调用outer()

创建outer栈帧,局部变量c=99存放其中,定义匿名函数时,编译器检测到内部引用外部变量c 此时将c从栈内存提升至堆内存(escape to heap) 生成闭包对象(funcval结构体),包含:

  • 匿名函数的入口地址
  • 捕获变量c的堆内存指针
  1. 阶段2:outer返回后

outer栈帧被销毁,但堆上的闭包对象保留,变量c因其被闭包引用,生命周期延续(不再依赖栈帧) 返回的闭包函数赋值给main中的fn变量,此时fn实际指向堆中的闭包对象

  1. 阶段3:调用fn()
go
fn() // 实际执行闭包内的匿名函数

为匿名函数创建新的栈帧,通过闭包对象获取c的堆内存地址,执行fmt.Println(*c_ptr),输出99

若在闭包内修改变量(如c=100): 通过指针修改堆内存中的值(*c_ptr = 100) 多个闭包共享同一变量时,修改对所有闭包可见

总结

机制说明
堆内存逃逸被捕获的局部变量从栈移至堆,突破栈帧的生命周期限制 79
闭包对象封装funcval结构体存储函数指针和捕获变量指针,构成闭包实例 710
指针间接访问闭包函数通过存储在堆中的指针访问变量,而非直接操作栈内存 69
状态隔离每次调用外部函数会创建独立的闭包对象,实现变量私有化

Go闭包的变量私有化本质是通过堆内存逃逸 + 闭包对象封装实现。编译器将捕获变量移至堆中,闭包函数通过指针间接访问,使变量生命周期独立于原栈帧,同时支持状态隔离。