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

Go语言指针与内存分配深度解析:从指针本质到 new、make 的底层实现

Go语言指针与内存分配深度解析:从指针本质到 newmake 的底层实现

在 Go 语言的世界里,函数是实现行为逻辑的核心载体,而指针则是打开内存管理大门的关键钥匙。

上一篇专栏预告中我们提到,本次将深入探讨 Go 语言的指针与内存分配机制。接下来,就让我们一同揭开它们的神秘面纱,从指针的本质讲起,逐步深入到 newmake 函数的底层实现。

一、指针的本质:内存地址的“代言人” 📌

指针,简单来说就是存储内存地址的变量。

在计算机中,所有的数据都存储在内存的某个位置,这个位置被称为内存地址,通常用一个十六进制的数字表示。

而指针变量就像是一个“标签”,它记录着某个数据在内存中的具体地址,通过这个“标签”我们可以找到并访问对应的数据。

比如,当我们声明一个变量 a := 10 时,计算机会在内存中开辟一块空间来存储整数 10,同时给这块空间分配一个内存地址,假设是 0xc00001a078
而如果我们声明一个指针变量 b := &a,那么 b 中存储的就是 a 的内存地址 0xc00001a078,此时我们就说 b 指向了 a
在这里插入图片描述

二、指针的基本用法:声明、取值与取地址

1. 指针的声明

在 Go 语言中,声明指针变量的语法为:var 指针变量名 *数据类型。其中 * 表示这是一个指针类型,后面的“数据类型”表示该指针指向的数据的类型。

案例 1:声明指针变量

package mainimport "fmt"func main() {var a int = 20var p *int // 声明一个指向 int 类型的指针变量 pp = &a // 将 a 的地址赋值给 pfmt.Println("a 的值:", a)fmt.Println("a 的地址:", &a)fmt.Println("指针 p 存储的地址:", p)fmt.Println("指针 p 指向的值:", *p) // 通过 * 取值
}

运行结果:

a 的值: 20
a 的地址: 0xc00000a0d8
指针 p 存储的地址: 0xc00000a0d8
指针 p 指向的值: 20

在这个案例中,我们先声明了一个 int 类型的变量 a 并赋值为 20,然后声明了一个指向 int 类型的指针变量 p,通过 &a 获取 a 的内存地址并赋值给 p。最后分别打印了 a 的值、a 的地址、指针 p 存储的地址以及指针 p 指向的值,可以看到指针 p 存储的地址与 a 的地址相同,通过 *p 可以获取到 a 的值。

2. 取地址操作(&)

& 操作符用于获取变量的内存地址,它的操作数必须是一个变量不能是常量或表达式。如上述案例中 &a 就是获取变量 a 的内存地址。

3. 取值操作(*)

* 操作符用于获取指针所指向的变量的值,称为指针解引用。在案例 1 中,*p 就是获取指针 p 所指向的变量 a 的值。

三、指针与函数:实现数据的间接修改

在 Go 语言中,函数的参数传递是值传递,即当我们将一个变量作为参数传递给函数时,函数内部会创建一个该变量的副本,对副本的修改不会影响原变量的值。而通过指针作为函数参数,可以实现对原变量的间接修改。

案例 2:指针作为函数参数修改原变量的值

package mainimport "fmt"// 通过值传递修改变量,无法改变原变量
func changeByValue(num int) {num = 100
}// 通过指针传递修改变量,可以改变原变量
func changeByPointer(num *int) {*num = 100
}func main() {a := 20fmt.Println("调用 changeByValue 前 a 的值:", a)changeByValue(a)fmt.Println("调用 changeByValue 后 a 的值:", a)fmt.Println("调用 changeByPointer 前 a 的值:", a)changeByPointer(&a)fmt.Println("调用 changeByPointer 后 a 的值:", a)
}

运行结果:

调用 changeByValue 前 a 的值: 20
调用 changeByValue 后 a 的值: 20
调用 changeByPointer 前 a 的值: 20
调用 changeByPointer 后 a 的值: 100

在这个案例中,changeByValue 函数接收一个 int 类型的参数,函数内部对参数的修改只是针对副本,原变量 a 的值没有发生变化。

changeByPointer 函数接收一个指向 int 类型的指针,通过 *num 可以获取到原变量的地址并修改其值,所以原变量 a 的值发生了改变。

四、内存分配基础:堆与栈的“分工合作” 🧠

在 Go 语言中,内存分配主要分为栈内存分配和堆内存分配

栈内存是由编译器自动管理的,它的分配和释放速度非常快。通常情况下,函数内部的局部变量、函数参数等会被分配在栈上。当函数执行结束后,这些变量所占用的栈内存会被自动释放。

堆内存的分配和释放相对复杂,它由 Go 语言的垃圾回收器负责管理。当变量需要在函数调用结束后仍然存在,或者变量的大小不确定时,变量会被分配在堆上。堆内存的分配需要向操作系统申请,而释放则由垃圾回收器在适当的时候进行回收。

案例 3:栈内存与堆内存分配示例

package mainimport "fmt"// 函数返回局部变量的指针,变量会被分配在堆上
func createNum() *int {num := 50return &num
}func main() {// 局部变量 a 分配在栈上a := 10fmt.Println("a 的值:", a)// 通过函数获取堆上变量的指针p := createNum()fmt.Println("堆上变量的值:", *p)
}

在这个案例中,变量 a 是 main 函数的局部变量,它会被分配在栈上。而 createNum 函数中的变量 num,由于函数返回了它的指针,在函数执行结束后还需要被访问,所以它会被分配在堆上。

五、new 函数:分配内存并返回指针 🔨

new 函数是 Go 语言提供的一个用于内存分配的内置函数,它的语法为:new(类型)

new 函数会为指定类型的变量分配一块内存空间,并初始化为该类型的零值,然后返回指向该内存空间的指针。

1. new 函数的基本用法

案例 4:new 函数的使用

package mainimport "fmt"func main() {// 使用 new 函数为 int 类型分配内存p := new(int)fmt.Println("new(int) 返回的指针:", p)fmt.Println("指针指向的值(零值):", *p)// 为指针指向的内存赋值*p = 100fmt.Println("赋值后指针指向的值:", *p)
}

运行结果:

new(int) 返回的指针: 0xc00001a0c0
指针指向的值(零值): 0
赋值后指针指向的值: 100

在这个案例中,new(int) 为 int 类型分配了一块内存,初始值为 0(int 类型的零值),并返回指向该内存的指针 p。我们可以通过 *p 来访问和修改这块内存的值。

2. new 函数的底层实现

从底层实现来看,new 函数的工作流程相对简单。当我们调用 new(T) 时,Go 语言的运行时系统会在堆上为类型 T 分配一块足够大的内存空间,然后将该内存空间初始化为类型 T 的零值,最后返回指向该内存空间的指针。

new 函数主要用于为基本数据类型、结构体等分配内存,它返回的永远是一个指针,指针指向的内存中的值为该类型的零值。

六、make 函数:专为引用类型分配内存 🔨

make 函数也是 Go 语言中用于内存分配的内置函数,但它与 new 函数不同,make 函数只用于为切片(slice)、映射(map)和通道(channel)这三种引用类型分配内存,并初始化它们的内部数据结构。make 函数的语法为:make(类型, 长度, 容量)(对于不同类型,参数可能有所不同)。

1. make 函数的基本用法

案例 5:make 函数创建切片

package mainimport "fmt"func main() {// 使用 make 函数创建切片,长度为 3,容量为 5s := make([]int, 3, 5)fmt.Println("切片 s 的值:", s)fmt.Println("切片 s 的长度:", len(s))fmt.Println("切片 s 的容量:", cap(s))// 向切片中添加元素s = append(s, 1, 2)fmt.Println("添加元素后切片 s 的值:", s)fmt.Println("添加元素后切片 s 的长度:", len(s))fmt.Println("添加元素后切片 s 的容量:", cap(s))
}

运行结果:

切片 s 的值: [0 0 0]
切片 s 的长度: 3
切片 s 的容量: 5
添加元素后切片 s 的值: [0 0 0 1 2]
添加元素后切片 s 的长度: 5
添加元素后切片 s 的容量: 5

在这个案例中,make([]int, 3, 5) 创建了一个 int 类型的切片,长度为 3,容量为 5。切片的初始值为 [0 0 0](int 类型的零值),通过 append 函数可以向切片中添加元素。

案例 6:make 函数创建映射

package mainimport "fmt"func main() {// 使用 make 函数创建映射m := make(map[string]int)fmt.Println("映射 m 的初始值:", m)// 向映射中添加键值对m["one"] = 1m["two"] = 2fmt.Println("添加键值对后映射 m 的值:", m)
}

运行结果:

映射 m 的初始值: map[]
添加键值对后映射 m 的值: map[one:1 two:2]

make(map[string]int) 创建了一个键为 string 类型、值为 int 类型的映射,初始为空,我们可以通过键值对的形式向其中添加数据。

案例 7:make 函数创建通道

package mainimport "fmt"func main() {// 使用 make 函数创建通道ch := make(chan int, 2)fmt.Println("通道 ch 的容量:", cap(ch))// 向通道中发送数据ch <- 1ch <- 2fmt.Println("从通道中接收数据:", <-ch)fmt.Println("从通道中接收数据:", <-ch)
}

运行结果:

通道 ch 的容量: 2
从通道中接收数据: 1
从通道中接收数据: 2

make(chan int, 2) 创建了一个能存储 int 类型数据、容量为 2 的带缓冲通道,我们可以通过 <- 操作符向通道发送和接收数据。

2. make 函数的底层实现

make 函数的底层实现比 new 函数复杂,因为它需要根据不同的引用类型来初始化内部数据结构。

  • 对于切片,make 函数会分配一个数组作为切片的底层存储,然后初始化切片的指针(指向数组的起始位置)、长度和容量。
  • 对于映射,make 函数会创建一个哈希表结构,并初始化相关的元数据,如桶数组、大小等。
  • 对于通道,make 函数会创建一个包含缓冲区、发送队列、接收队列等数据结构的通道对象。

七、new 与 make 的区别

  1. new make 二者都是用来做内存分配的。
  2. make 只用于切片(slice)、映射(map)和通道(channel)的初始化,返回的还是这三个引用类型本身。
  3. new 用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

八、总结

通过本文的讲解,我们深入了解了 Go 语言指针的本质、基本用法以及它在函数中的应用,同时也探讨了内存分配的基础概念,以及 new 和 make 函数的底层实现。指针为我们提供了间接访问内存的方式,而 new 和 make 函数则帮助我们更方便地进行内存分配,它们在 Go 语言的内存管理中都扮演着重要的角色。

下一篇专栏预告

掌握了Go 语言中的指针与内存分配的知识后,我们将进入 Go 语言中数据组织的重要部分。下一篇专栏我们将聚焦 Go 语言中的结构体,结构体是自定义数据类型的核心,它可以将不同类型的数据组合在一起,实现更复杂的数据结构。无论你是想了解结构体的定义与初始化、字段的访问与修改,还是结构体的嵌套与方法等,下一篇内容都将为你详细解读,敬请期待!😊

http://www.lryc.cn/news/622884.html

相关文章:

  • 【最后203篇系列】032 OpenAI格式调用多模型实验
  • RD-Agent for Quantitative Finance (RD-Agent(Q))
  • Spark Shuffle中的数据结构
  • 亚马逊S3的使用简记(游戏资源发布更新)
  • 后台管理系统-4-vue3之pinia实现导航栏按钮控制左侧菜单栏的伸缩
  • 二进制为什么使用记事本读取会出乱码
  • 密码学入门笔记4:分组密码常见算法1——DES
  • Custom SRP - Baked Light
  • 用Pygame开发桌面小游戏:从入门到发布
  • 搜索 AI 搜索 概率论基础教程第3章条件概率与独立性(二)
  • 概率论基础教程第3章条件概率与独立性(一)
  • 《P4180 [BJWC2010] 严格次小生成树》
  • [极客时间]LangChain 实战课 ----- 代理(上)|(12)ReAct框架,推理与行动的协同
  • Manus AI与多语言手写识别的技术突破与行业变革
  • 《Python学习之字典(一):基础操作与核心用法》
  • 【每日一题】Day5
  • 电路设计——复位电路
  • 设计模式之静态代理
  • Java 10 新特性及具体应用
  • ABB焊接机器人弧焊省气
  • 多机编队——(6)解决机器人跟踪过程中mpc控制转圈问题
  • 【轨物方案】预防性运维:轨物科技用AI+机器人重塑光伏电站价值链
  • MyBatis极速通关中篇:核心配置精讲与复杂查询实战
  • 大模型教机器人叠衣服:2025年”语言理解+多模态融合“的智能新篇
  • Tomcat架构深度解析:从Server到Servlet的全流程揭秘
  • blender制作动画导入unity两种方式
  • ENSP的简单动态路由rip协议配置
  • 广东省省考备考(第七十八天8.16)——资料分析、判断推理(强化训练)
  • Docker目录的迁移
  • GaussDB 数据库架构师修炼(十三)安全管理(3)-行级访问控制