当前位置: 首页 > news >正文

Go语言里面的堆跟栈 + new 和 make + 内存逃逸 + 闭包

在 Go 语言中,堆(Heap)和栈(Stack)是内存管理中的两个重要概念,它们在内存分配、数据存储和使用场景等方面存在明显差异。

栈(Stack)

栈是一种具有后进先出(LIFO)特性的数据结构,在程序运行时,系统会为每个线程分配一个栈空间,用于存储函数调用过程中的局部变量函数参数(形参)返回地址等(函数调用的上下文)信息。

工作原理
  • 入栈(Push):当一个函数被调用时,会在栈上为该函数的局部变量和参数分配内存空间,这个过程称为入栈操作。
  • 出栈(Pop):当函数执行完毕返回时,系统会自动释放该函数在栈上分配的内存空间,这个过程称为出栈操作。
    package main
    
    func add(a, b int) int {
        // 局部变量sum存储在栈上
        sum := a + b
        return sum
    }
    
    func main() {
        x := 1
        y := 2
        // 调用add函数时,参数a、b和局部变量sum会在栈上分配内存
        result := add(x, y)
        println(result)
    }

    注意:形参a、b是在栈上。

  • 特点
  • 自动分配和释放栈上的内存分配和释放是由系统自动完成的,函数调用结束后,栈上的内存可以立即回收。不需要程序员手动干预,因此栈的操作效率较高。
  • 内存空间连续栈上的内存空间是连续的,这使得栈的访问速度非常快
  • 大小有限:每个线程的栈空间大小是有限的,如果函数调用层次过深或者局部变量占用的内存过大,可能会导致栈溢出错误。

知识点:

形式参数(简称形参):定义函数时,函数名后面括号中的变量名。由于它不是实际存在变量,所以又称虚拟变量。

例如:当你定义函数void add(int a, int b)的时候,这里的a和b就是形参。

 实际参数(简称实参):调用函数时,函数名后面括号中的表达式。是在调用时传递给函数的参数. 实参可以是常量、变量、表达式、函数等。

例如:当你调用函数add(1 ,2)的时候,这里的1和2就是实参。 

    堆(Heap)

    堆是一块用于动态内存分配的内存区域,程序在运行时可以根据需要在堆上分配和释放内存,通常用于存储生命周期不确定的对象。在 Go 语言中,使用newmake关键字或者直接创建引用类型(如切片、映射、通道等时,会在堆上分配内存。

    堆内存的管理相对复杂,需要垃圾回收(GC)来进行清理,回收速度不如栈快。

    工作原理

    • 内存分配:当程序需要在堆上分配内存时,会向操作系统请求一块合适大小的内存空间。
    • 垃圾回收:当程序不再使用堆上的某个内存块时,Go 语言的垃圾回收器(GC)会自动回收该内存块,以便后续使用。
      package main
      
      import "fmt"
      
      func main() {
          // 使用new关键字在堆上分配内存
          ptr := new(int)
          *ptr = 10
          fmt.Println(*ptr)
      
          // 创建切片,切片的底层数组在堆上分配内存
          slice := make([]int, 3)
          slice[0] = 1
          slice[1] = 2
          slice[2] = 3
          fmt.Println(slice)
      }
      特点
    • 动态分配和释放:堆上的内存分配和释放是动态的,需要程序员手动控制或者由垃圾回收器自动处理。
    • 内存空间不连续:堆上的内存空间是不连续的,这使得堆的访问速度相对较慢。

    总结

    栈主要用于存储函数调用的上下文信息,具有自动分配和释放、内存空间连续、操作效率高但大小有限的特点;堆主要用于动态内存分配,具有动态分配和释放、内存空间不连续、大小灵活但访问速度相对较慢的特点。在 Go 语言中,编译器会根据变量的类型和使用场景自动决定将变量分配到栈上还是堆上。

    补充1:内存逃逸

    内存逃逸,就是程序在执行过程中,某些本应在栈上分配的变量,被“逃逸”到了堆上。

    原因:简单来说,就是 Go 的编译器在优化内存分配时,无法确定一个变量的生命周期和作用范围,导致它在堆上分配内存,而不是栈上。

    变量逃逸的常见原因:

    1、闭包(变量的生命周期超出函数作用域):闭包是导致内存逃逸的一个典型场景。因为闭包会持有外部函数的引用,所以即使外部函数已经返回,闭包内部的变量依然可能在堆上存活。

    func main() {
        f := func() int {
            x := 42 // 这个变量 x 会逃逸到堆上
            return x
        }
        fmt.Println(f())
    }

    这里,x 是一个局部变量,但因为 f 是一个闭包,Go 编译器不能确定 x 的生命周期是否结束,所以 x 被分配到了堆上。 

    2、指针传递:如果你将一个局部变量的指针传递给了其他函数,Go 编译器会推测这个变量的生命周期已经超出了当前作用域,从而将它分配到堆上。

    func foo(ptr *int) {
        fmt.Println(*ptr)
    }
    
    func main() {
        x := 42
        foo(&x) // 这里传递了 x 的地址,x 会被分配到堆上
    }

    在这个例子中,x 被传递给了 foo 函数,而 foo 是通过指针来访问 x 的。因为 Go 编译器无法确定 x 在 main 函数外部是否会继续使用,所以 x 被分配到了堆上。 

    3、变量大小不确定(数组或切片的引用):切片类型在传递时会导致数据逃逸。因为切片实际上是对数组的一层封装,如果你在函数外部返回了切片,就可能导致底层的数组逃逸。

    func createSlice() []int {
        arr := [3]int{1, 2, 3} // arr 会逃逸到堆上
        return arr[:]
    }
    
    func main() {
        slice := createSlice()
        fmt.Println(slice)
    }

    在这个示例中,使用 make 函数创建的切片 arr 的大小在运行时才能确定,因此 arr会逃逸到堆上。 

    4、函数返回局部变量指针

    package main
    
    func escape() *int {
        x := 10
        return &x
    }
    
    func main() {
        result := escape()
        println(*result)
    }

     在这个示例中,escape 函数返回了局部变量 x 的指针,因此 x 会逃逸到堆上。

    闭包:闭包是一个函数,它可以访问并操作其外部作用域中的变量,即使该外部作用域已经执行完毕,这些变量也不会被销毁,如下图,匿名函数且是闭包:

    // 这个例子中,outerFunction 返回的匿名函数引用了外部函数的局部变量 count,
    // 形成了闭包。每次调用闭包时,count 的值会持续增加
    func outerFunction() func() int {
        count := 0
        // 定义一个匿名函数,形成闭包
        return func() int {
            count++
            return count
        }
    }
    
    func main() {
        counter := outerFunction()
        fmt.Println(counter()) // 输出: 1
        fmt.Println(counter()) // 输出: 2
    }
    

    匿名函数:强调的是函数没有名称这一特性,常用于实现一些简单的、一次性的功能,比如作为回调函数传递给其他函数,或者在一些临时的逻辑处理中使用。

    // 下面的匿名函数没有引用任何外部作用域的变量,所以它只是一个普通的匿名函数,不是闭包。
    func main() {
        // 定义一个匿名函数并立即执行
        func() {
            fmt.Println("This is an anonymous function without closure.")
        }()
    }
    具名函数作为闭包
    // 下面,inner 是一个具名函数,它引用了外部函数的局部变量 message,形成了闭包
    func outer() func() {
        message := "Hello, Closure!"
        // 定义一个具名函数作为闭包
        func inner() {
            fmt.Println(message)
        }
        return inner
    }
    
    func main() {
        closure := outer()
        closure() // 输出: Hello, Closure!
    }

    匿名函数侧重于函数没有名称,而闭包侧重于函数对外部变量的引用和持有。匿名函数可以是闭包,也可以不是闭包;闭包可以是匿名函数,也可以是具名函数。理

    补充2:new 与 make 的使用区别:

    在 Go 语言里,new 和 make 都用于内存分配,但它们的用途和使用场景存在明显差异

    new 函数:

    new 是一个内置函数,其作用是为类型分配一片零值内存空间(只接收一个参数),并且返回指向该接收参数内存空间的指针。

    func new(Type) *Type

    这里的 Type 代表任意类型,new 函数会返回一个指向该类型零值的指针。 

     // 使用 new 函数为 int 类型分配内存
        numPtr := new(int)
        // 此时 numPtr 指向的 int 类型变量的值为 0(int 类型的零值)
        fmt.Println(*numPtr) 
    
        // 修改 numPtr 指向的变量的值
        *numPtr = 10
        fmt.Println(*numPtr) 
    
        // 使用 new 函数为结构体类型分配内存
        type Person struct {
            Name string
            Age  int
        }
        personPtr := new(Person)
        // 结构体的字段被初始化为零值
        fmt.Printf("Name: %s, Age: %d\n", personPtr.Name, personPtr.Age) 
    代码解释
    • 当调用 new(int) 时,会为 int 类型分配一块内存空间,初始值为 0,并返回指向该内存空间的指针 numPtr
    • 对于结构体类型 Person,调用 new(Person) 会为该结构体分配内存,结构体的字段会被初始化为各自类型的零值,然后可以通过指针修改这些字段的值。

    make 函数

    make 也是一个内置函数,不过它只能用于创建并初始化 slice(切片)、map(映射)和 channel(通道)这三种引用类型。

    • func make(t Type, size ...IntegerType) Type

      这里的 t 必须是 slicemap 或 channel 类型,size 是可选参数,用于指定初始大小

    package main
    
    import "fmt"
    
    func main() {
        // 创建一个长度为 3,容量为 5 的整数切片
        slice := make([]int, 3, 5)
        fmt.Println("切片长度:", len(slice)) 
        fmt.Println("切片容量:", cap(slice)) 
        fmt.Println(slice) 
    
        // 修改切片元素的值
        slice[0] = 1
        slice[1] = 2
        slice[2] = 3
        fmt.Println(slice) 
    
    
        // 创建一个字符串到整数的映射
        m := make(map[string]int)
        // 向映射中添加键值对
        m["apple"] = 1
        m["banana"] = 2
        fmt.Println(m) 
    
         // 创建一个无缓冲的整数通道
        ch := make(chan int)
        go func() {
            // 向通道发送数据
            ch <- 10
        }()
        // 从通道接收数据
        num := <-ch
        fmt.Println(num) 
    
        // 创建一个有缓冲的整数通道,缓冲区大小为 3
        ch2 := make(chan int, 3)
    }

    总结

    • new:主要用于为任意类型分配零值内存并返回指针,通常用于需要显式管理指针的场景。
    • make:专门用于创建并初始化 slicemap 和 channel 这三种引用类型,会完成必要的初始化操作,不能用于其他类型。

    相关文章:

  • URL中的特殊字符与web安全
  • uniapp封装路由管理(兼容Vue2和Vue3)
  • module ‘matplotlib‘ has no attribute ‘colormaps‘
  • phpstorm 无法重建文件
  • 统信UOS上AI辅助绘图:用DeepSeek+draw.io生成流程图
  • NET400协议网关(老款GRM300):跨品牌PLC与多协议数据整合解决方案,包含NET421,NET422,NET431等
  • 单细胞分析(22)——高效使用 Cell Ranger:安装、参数解析及 Linux 后台运行指南
  • Mysql中的常用函数
  • 系统架构设计师—数据库基础篇—数据库规范化
  • RxJava 用法封装举例
  • 初中文凭怎么成人大专-一种简单省心的方式
  • Gauss数据库omm用户无法连接处理
  • 写作思维魔方
  • 下载PyCharm 2024.3.4 (Community Edition)来开发测试python
  • 多线程或多进程或多协程部署flask服务
  • 网络安全等级保护2.0 vs GDPR vs NIST 2.0:全方位对比解析
  • linux0.11源码分析第四弹——操作系统的框架代码
  • 类和对象—多态—案例2—制作饮品
  • 笔记:如何使用XAML Styler以及在不同的开发环境中使用一致
  • 第7章 wireshark(网络安全防御实战--蓝军武器库)
  • 石材企业网站/搜索引擎优化seo名词解释
  • 汽车网站建设策划书/seo快速排名案例
  • 济南网站的优化/信息流优化师简历模板
  • 电脑网站进不去网页怎么办/快速建站教程
  • 山东省建设建设协会网站/网络营销推广方案策划书
  • 东莞网页制作免费网站制作/无锡seo优化公司