Golang基础笔记七之指针,值类型和引用类型
本文首发于公众号:Hunter后端
本篇笔记介绍 Golang 里的指针,值类型与引用类型相关的概念,以下是本篇笔记目录:
- 指针
- 值类型与引用类型
- 内存逃逸
- 减少内存逃逸的几种方案
1、指针
在计算机内存中,每个变量都存储在特定的内存地址上,而指针是一种特殊的变量,它存储的是一个变量的内存地址。
我们可以通过指针访问变量的内存地址,也可以通过指针访问或修改这个变量的内存地址存储的值。
1. 指针的声明与初始化
使用 & 符号来获取变量的内存地址,使用 * 获取指针指向的内存地址的值:
var a int = 10
var a_ptr *int = &a
fmt.Println("a 的内存地址是: ", &a)
fmt.Println("a_ptr 的值是: ", a_ptr)
fmt.Println("根据指针获取的值是: ", *a_ptr)
2. 指针操作
使用 * 获取变量指向的内存地址的值后,可以直接使用,也可以对其进行修改,在上面操作后,我们接着操作:
*a_ptr = 20
fmt.Println("修改后 a 的值是: ", a)
可以看到,通过指针修改后,a 的值已经变成了 20。
3. 指针作为函数传参
如果我们将指针作为函数的参数传入,并且在函数内部对其进行了修改,那么会直接修改指针所指向的变量的值,下面是一个示例:
func ModityValue(ptr *int) {
*ptr = 20
}
func main() {
var a int = 10
fmt.Println("修改前, a 的值是:", a) // 修改前, a 的值是: 10
ModityValue(&a)
fmt.Println("修改后, a 的值是:", a) // 修改后, a 的值是: 20
}
2、值类型与引用类型
1. 值类型与引用类型包括的数据类型
值类型包括整型、浮点型、布尔型、字符串、数组、结构体等,值类型的变量直接存储值,内存通常分配在栈上。
引用类型包括切片、映射、通道等,引用类型的变量存储的是一个引用(内存地址),内存通常分配在堆上。
2. 栈和堆
值类型的变量通常分配在栈上,引用类型的变量通常分配在堆上,注意,这里是通常,还会有特殊情况后面再介绍。
先来介绍一下栈和堆。
1) 栈
先介绍一下栈相关的信息:
- 栈内存由编译器自动管理,在函数调用时分配,函数返回后立即释放,效率极高
- 栈上变量的生命周期严格限定在函数执行期间。函数调用开始,变量被创建并分配内存;函数调用结束,变量占用的内存会被立即回收
2) 堆
- 堆用于存储程序运行期间动态分配的内存,其分配和释放不是由函数调用的生命周期决定,而是由程序员或垃圾回收机制来管理。
- 堆上的变量生命周期不依赖于函数调用的结束,变量可以在函数调用结束后仍然存在,直到没有任何引用指向它,然后由垃圾回收机制进行回收。
3. 值类型与引用类型的内存分配
值类型变量通常具有明确的生命周期,通常与其所在的函数调用相关,函数调用结束后,这些变量占用的内存可以立即被回收,使用栈来存储值类型可以充分利用栈的高效内存管理机制。
而引用类型的变量需要动态分配内存,并且其生命周期可能超出函数调用的范围,比如切片可以动态调整大小,映射也可以增减键值对,这些操作需要在运行时进行内存的分配和释放,使用堆来存储引用类型可以更好地支持这些动态特性。
前面介绍值类型通常会被分配到栈上,但是也有可能被分配到堆上,这种情况就是内存逃逸。
内存逃逸的内容在下一个小节中再介绍。
4. 值类型和引用类型的复制
值类型的复制会复制整个数据,是深拷贝的操作,副本的修改不会影响到原始数据,比如下面的操作:
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Hunter", Age: 18}
p2 := p
p2.Name = "Tom"
fmt.Printf("p1 name is:%s, p2 name is:%s \n", p.Name, p2.Name)
// p1 name is:Hunter, p2 name is:Tom
}
而引用类型的复制则复制的是其引用,属于浅拷贝的操作,多个变量会共享底层数据,修改其中一个副本会影响原始数据,比如下面的操作:
s := []int{1, 2, 3}
s2 := s
s2[1] = 8
fmt.Println("s:", s) // s: [1 8 3]
fmt.Println("s2:", s2) // s2: [1 8 3]
5. 值类型和引用类型的函数传参
值类型和引用类型的函数传参和复制一样,值类型传递的是变量的副本,在函数内部修改不会影响原始变量,而引用类型传递的是原始数据的引用,函数内部修改会影响外部变量。
下面是值类型的函数传参的示例:
func ChangePerson(p Person) {
p.Name = "Tom"
fmt.Println("inner func p.Name is:", p.Name)
// inner func p.Name is: Tom
}
func main() {
p := Person{Name: "Hunter", Age: 18}
ChangePerson(p)
fmt.Println("outer func p.Name is:", p.Name)
// outer func p.Name is: Hunter
}
以下是引用类型传参的示例:
func ChangeSlice(s []int) {
s[2] = 9
fmt.Println("inner func slice is:", s)
// inner func slice is: [1 2 9]
}
func main() {
s := []int{1, 2, 3}
ChangeSlice(s)
fmt.Println("outer func slice is:", s)
// outer func slice is: [1 2 9]
}
对于函数传参,还有两点需要注意,一个是值类型函数传参的性能问题,一个是引用类型涉及扩容的问题。
1) 值类型函数传参的性能问题
对于值类型变量,比如一个结构体,拥有非常多的字段,当其作为函数传参,传递的会是变量的副本,也就是会将其值复制出来传递,那么当这个变量非常大的时候可能就会涉及性能问题。
为了解决这个问题,有个方法就是传递其变量的指针,但是需要注意传递指针在函数内部对其修改后,会影响到原始变量的值。
2) 引用类型函数传参扩容问题
当引用类型作为函数传参,如果在函数内部修改涉及到扩容,那么其地址就会更改,那么函数内部的修改就不会反映到其原值上了,比如下面这个是切片在函数内部修改的示例:
func ChangeSlice(s []int) {
s = append(s, []int{4, 5, 6}...)
fmt.Println("inner func slice is:", s)
// inner func slice is: [1 2 3 4 5 6]
}
func main() {
s := []int{1, 2, 3}
ChangeSlice(s)
fmt.Println("outer func slice is:", s)
// outer func slice is: [1 2 3]
}
3、内存逃逸
Golang 里编译器决定内存分配位置是在栈上还是在堆上,这个就是逃逸分析,这个过程发生在编译阶段。
1. 逃逸分析的方法
我们可以使用下面的命令来查看逃逸分析的结果:
go build -gcflags="-m" main.go
2. 内存逃逸的场景
内存逃逸可能会存在于以下这些情况,比如函数返回一个值类型变量的指针,或者闭包引用局部变量等。
1) 函数返回局部变量的指针
如果一个函数返回值是变量的指针,那么该局部变量会逃逸到堆上:
func CreateInt() *int {
x := 1
return &x
}
func main() {
_ = CreateInt()
}
使用逃逸分析的命令:
go build -gcflags="-m" main.go
可以看到输出如下:
# command-line-arguments
./main.go:14:2: moved to heap: x
说明 x 这个变量会逃逸到堆上。
2) 闭包引用局部变量
如果闭包引用了函数的局部变量,这些局部变量会逃逸到堆上,因为闭包可能在函数调用结束后继续存在并访问这些变量:
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
_ = counter()
}
对此使用逃逸分析的命令,输出结果如下:
# command-line-arguments
./main.go:14:2: moved to heap: count
./main.go:15:9: func literal escapes to heap
3) 向接口类型变量赋值
当我们将值赋给接口类型的变量,因为接口类型需要再运行时才能确定具体的类型,所以这个值也会逃逸到堆上,最常见的一个例子就是 fmt.Println():
func main() {
s := "hello world"
fmt.Println(s)
}
其逃逸分析结果如下:
# command-line-arguments
./main.go:25:13: ... argument does not escape
./main.go:25:14: s escapes to heap
除此之外,还有一些原因也可能造成内存逃逸,比如大对象超出了栈容量限制,被强制分配到堆、发送变量到 channel 等。
3. 逃逸分析的意义
内存逃逸就是原本分配在栈上的变量被分配到了堆上,而分配到堆上的变量在函数调用结束后仍然存在,直到没有任何引用指向它,然后由垃圾回收机制进行回收。
所以通过逃逸分析,我们可以减轻GC(垃圾回收)的压力。
4、减少内存逃逸的几种方案
- 减少堆分配,避免函数不必要的指针返回,优先通过返回值传递小对象
- 避免闭包引用局部变量
- 减少使用向接口类型赋值,如 fmt.Println() 这种
- 避免大对象超出栈容量限制
Golang基础笔记七之指针,值类型和引用类型的更多相关文章
- C#基础知识1-深入理解值类型和引用类型
C#值类型和引用类型这个概念在刚学习的时候应该就知道了.但是我们并没有深入的去理解它.越是基础知识其实才是最有用的.对代码的优化,代码质量的提升都有帮助.通过整理本文章,对很多知识也起到了巩固的作用吧 ...
- C#学习笔记(基础知识回顾)之值类型和引用类型
一:C#把数据类型分为值类型和引用类型 1.1:从概念上来看,其区别是值类型直接存储值,而引用类型存储对值的引用. 1.2:这两种类型在内存的不同地方,值类型存储在堆栈中,而引用类型存储在托管对上.存 ...
- C#基础知识系列二(值类型和引用类型、可空类型、堆和栈、装箱和拆箱)
前言 之前对几个没什么理解,只是简单的用过可空类型,也是知道怎么用,至于为什么,还真不太清楚,通过整理本文章学到了很多知识,也许对于以后的各种代码优化都有好处. 本文的重点就是:值类型直接存储其值,引 ...
- c#基础系列1---深入理解值类型和引用类型
"大菜":源于自己刚踏入猿途混沌拾起,自我感觉不是一般的菜,因而得名"大菜",于自身共勉. 不知不觉已经踏入坑已10余年之多,对于c#多多少少有一点自己的认识, ...
- C#学习笔记(基础知识回顾)之值类型与引用类型转换(装箱和拆箱)
一:值类型和引用类型的含义参考前一篇文章 C#学习笔记(基础知识回顾)之值类型和引用类型 1.1,C#数据类型分为在栈上分配内存的值类型和在托管堆上分配内存的引用类型.如果int只不过是栈上的一个4字 ...
- c# 值类型和引用类型 笔记
参考以下博文,我这里只是笔记一下,原文会更加详细 c#基础系列1---深入理解值类型和引用类型 堆栈和托管堆c# 值类型和引用类型 红色表示——“这啥?”(真实1个问题引出3个问题) CLR支持的两种 ...
- Net基础篇_学习笔记_第十二天_面向对象继承(命名空间 、值类型和引用类型)
命名空间可以认为类是属于命名空间的. 解决类的重名问题,可以看做类的“文件夹”如果在当前项目中没有这个类的命名空间,需要我们手动的导入这个类所在的命名空间.1).用鼠标去点2).alt+shift+F ...
- [No0000B9]C# 类型基础 值类型和引用类型 及其 对象复制 浅度复制vs深度复制 深入研究2
接上[No0000B5]C# 类型基础 值类型和引用类型 及其 对象判等 深入研究1 对象复制 有的时候,创建一个对象可能会非常耗时,比如对象需要从远程数据库中获取数据来填充,又或者创建对象需要读取硬 ...
- [No0000B5]C# 类型基础 值类型和引用类型 及其 对象判等 深入研究1
引言 本文之初的目的是讲述设计模式中的 Prototype(原型)模式,但是如果想较清楚地弄明白这个模式,需要了解对象克隆(Object Clone),Clone其实也就是对象复制.复制又分为了浅度复 ...
- Golang的值类型和引用类型的范围、存储区域、区别
常见的值类型和引用类型分别有哪些? 值类型:基本数据类型 int 系列, float 系列, bool, string .数组和结构体struct,使用这些类型的变量直接指向存在内存中的值,值类型的变 ...
随机推荐
- DFS 2025/1/15
DFS & DFS 剪枝优化 Basic 01 先搜节点少的分支 如果搜进来一个大分支而答案不在此分支就会浪费大量时间 02 可行性剪枝 已经白扯了就 return 判断当前是否合法 03 最 ...
- shared_ptr的线程安全性与再论cmu15445 project0的COW线程安全字典树
shared_ptr的线程安全性 近期在网上冲浪时看到一篇boost的文章,里面聊到了shared_ptr的线程安全性 https://www.boost.org/doc/libs/1_87_0/li ...
- md语法练手随笔
代码 单行代码:代码之间分别用一个反引号包起来 代码块: 代码之间分别用三个反引号包起来, 且两边的反引号单独占一行 这是一级标题 这是六级标题 这是加粗的文字 这是倾斜的文字 这是斜体加粗的文字 这 ...
- SRAM的读、写操作、信息保持原理
\(Vcc\)会使得\(T_3\)和\(T_4\)导通,但是哪个先导通是随机的,那么当\(T3\)先导通的时候,\(a\)点变为高电平,此时电流经由 \(a\) 点导通\(T2\),\(T2\)导通, ...
- java基础之数据结构
一.栈:stack,又称堆栈[出口和入口在同一侧],特点:先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素) 例子:子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当 ...
- C#使用Blazor编译WebAssembly供前端调用(一),关于SkiaSharp相关问题
目前信创热潮开始掀起,而C#很多行业开发的都是桌面端,迁移到网页端常常会因为很多库不支持或者不友好导致项目一直卡着. 最近一直在网上找灵感,偶然发现Web Assembly,一开始我还没不知道这是什么 ...
- FreeSWITCH中SIP网关(Gateway)操作
freeswitch是一款简单好用的VOIP开源软交换平台. 以下是一篇关于FreeSWITCH中SIP网关(Gateway)操作的技术指南,基于提供的官方文档内容整理: 一.网关生命周期管理 1. ...
- CSS横向滚动
Flex版本 .super { display: flex; width: 100%; overflow-x: scroll; white-space: nowrap; } .sub { width: ...
- 聊聊AI浏览器
提供AI咨询+AI项目陪跑服务,有需要回复1 大模型一直有个难以解决的问题:系统的知识是过时的,他们难以跟进最新的信息. 基于这个原因,ChatGPT以及DeepSeek都提出了联网功能,只不过效果嘛 ...
- SpringBoot——yaml配置文件
yaml简介 YAML 是 "YAML Ain't a Markup Language"(YAML 不是一种标记语言).在开发的这种语言时,YAML 的意思其实是:"Ye ...