Golang基础笔记七之指针,值类型和引用类型

本文首发于公众号:Hunter后端

原文链接:Golang基础笔记七之指针,值类型和引用类型

本篇笔记介绍 Golang 里的指针,值类型与引用类型相关的概念,以下是本篇笔记目录:

  1. 指针

  2. 值类型与引用类型

  3. 内存逃逸

  4. 减少内存逃逸的几种方案

1、指针

在计算机内存中,每个变量都存储在特定的内存地址上,而指针是一种特殊的变量,它存储的是一个变量的内存地址。

我们可以通过指针访问变量的内存地址,也可以通过指针访问或修改这个变量的内存地址存储的值。

1. 指针的声明与初始化

使用 & 符号来获取变量的内存地址,使用 * 获取指针指向的内存地址的值:

go 复制代码
var a int = 10

var a_ptr *int = &a

fmt.Println("a 的内存地址是: ", &a)

fmt.Println("a_ptr 的值是: ", a_ptr)

fmt.Println("根据指针获取的值是: ", *a_ptr)

2. 指针操作

使用 * 获取变量指向的内存地址的值后,可以直接使用,也可以对其进行修改,在上面操作后,我们接着操作:

go 复制代码
*a_ptr = 20

fmt.Println("修改后 a 的值是: ", a)

可以看到,通过指针修改后,a 的值已经变成了 20。

3. 指针作为函数传参

如果我们将指针作为函数的参数传入,并且在函数内部对其进行了修改,那么会直接修改指针所指向的变量的值,下面是一个示例:

go 复制代码
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) 栈

先介绍一下栈相关的信息:

  1. 栈内存由编译器自动管理,在函数调用时分配,函数返回后立即释放,效率极高

  2. 栈上变量的生命周期严格限定在函数执行期间。函数调用开始,变量被创建并分配内存;函数调用结束,变量占用的内存会被立即回收

2) 堆
  1. 堆用于存储程序运行期间动态分配的内存,其分配和释放不是由函数调用的生命周期决定,而是由程序员或垃圾回收机制来管理。

  2. 堆上的变量生命周期不依赖于函数调用的结束,变量可以在函数调用结束后仍然存在,直到没有任何引用指向它,然后由垃圾回收机制进行回收。

3. 值类型与引用类型的内存分配

值类型变量通常具有明确的生命周期,通常与其所在的函数调用相关,函数调用结束后,这些变量占用的内存可以立即被回收,使用栈来存储值类型可以充分利用栈的高效内存管理机制。

而引用类型的变量需要动态分配内存,并且其生命周期可能超出函数调用的范围,比如切片可以动态调整大小,映射也可以增减键值对,这些操作需要在运行时进行内存的分配和释放,使用堆来存储引用类型可以更好地支持这些动态特性。

前面介绍值类型通常会被分配到栈上,但是也有可能被分配到堆上,这种情况就是内存逃逸。

内存逃逸的内容在下一个小节中再介绍。

4. 值类型和引用类型的复制

值类型的复制会复制整个数据,是深拷贝的操作,副本的修改不会影响到原始数据,比如下面的操作:

go 复制代码
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

}

而引用类型的复制则复制的是其引用,属于浅拷贝的操作,多个变量会共享底层数据,修改其中一个副本会影响原始数据,比如下面的操作:

go 复制代码
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. 值类型和引用类型的函数传参

值类型和引用类型的函数传参和复制一样,值类型传递的是变量的副本,在函数内部修改不会影响原始变量,而引用类型传递的是原始数据的引用,函数内部修改会影响外部变量。

下面是值类型的函数传参的示例:

go 复制代码
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

}

以下是引用类型传参的示例:

go 复制代码
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) 引用类型函数传参扩容问题

当引用类型作为函数传参,如果在函数内部修改涉及到扩容,那么其地址就会更改,那么函数内部的修改就不会反映到其原值上了,比如下面这个是切片在函数内部修改的示例:

go 复制代码
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 复制代码
 go build -gcflags="-m" main.go

2. 内存逃逸的场景

内存逃逸可能会存在于以下这些情况,比如函数返回一个值类型变量的指针,或者闭包引用局部变量等。

1) 函数返回局部变量的指针

如果一个函数返回值是变量的指针,那么该局部变量会逃逸到堆上:

go 复制代码
func CreateInt() *int {

    x := 1

    return &x

}

func main() {

    _ = CreateInt()

}

使用逃逸分析的命令:

go 复制代码
 go build -gcflags="-m" main.go

可以看到输出如下:

go 复制代码
# command-line-arguments

./main.go:14:2: moved to heap: x

说明 x 这个变量会逃逸到堆上。

2) 闭包引用局部变量

如果闭包引用了函数的局部变量,这些局部变量会逃逸到堆上,因为闭包可能在函数调用结束后继续存在并访问这些变量:

go 复制代码
func counter() func() int {

    count := 0

    return func() int {

        count++

        return count

    }

}

func main() {

    _ = counter()

}

对此使用逃逸分析的命令,输出结果如下:

go 复制代码
# command-line-arguments

./main.go:14:2: moved to heap: count

./main.go:15:9: func literal escapes to heap
3) 向接口类型变量赋值

当我们将值赋给接口类型的变量,因为接口类型需要在运行时才能确定具体的类型,所以这个值也会逃逸到堆上,最常见的一个例子就是 fmt.Println():

go 复制代码
func main() {

    s := "hello world"

    fmt.Println(s)

}

其逃逸分析结果如下:

go 复制代码
# command-line-arguments

./main.go:25:13: ... argument does not escape

./main.go:25:14: s escapes to heap

除此之外,还有一些原因也可能造成内存逃逸,比如大对象超出了栈容量限制,被强制分配到堆、发送变量到 channel 等。

3. 逃逸分析的意义

内存逃逸就是原本分配在栈上的变量被分配到了堆上,而分配到堆上的变量在函数调用结束后仍然存在,直到没有任何引用指向它,然后由垃圾回收机制进行回收。

所以通过逃逸分析,我们可以减轻GC(垃圾回收)的压力。

4、减少内存逃逸的几种方案

  1. 减少堆分配,避免函数不必要的指针返回,优先通过返回值传递小对象

  2. 避免闭包引用局部变量

  3. 减少使用向接口类型赋值,如 fmt.Println() 这种

  4. 避免大对象超出栈容量限制

相关推荐
舒一笑7 分钟前
我的开源项目-PandaCoder迎来史诗级大更新啦
后端·程序员·intellij idea
@昵称不存在1 小时前
Flask input 和datalist结合
后端·python·flask
zhuyasen2 小时前
Go 分布式任务和定时任务太难?sasynq 让异步任务从未如此简单
后端·go
东林牧之2 小时前
Django+celery异步:拿来即用,可移植性高
后端·python·django
超浪的晨3 小时前
Java UDP 通信详解:从基础到实战,彻底掌握无连接网络编程
java·开发语言·后端·学习·个人开发
AntBlack3 小时前
从小不学好 ,影刀 + ddddocr 实现图片验证码认证自动化
后端·python·计算机视觉
Pomelo_刘金3 小时前
Clean Architecture 整洁架构:借一只闹钟讲明白「整洁架构」的来龙去脉
后端·架构·rust
双力臂4043 小时前
Spring Boot 单元测试进阶:JUnit5 + Mock测试与切片测试实战及覆盖率报告生成
java·spring boot·后端·单元测试
midsummer_woo5 小时前
基于spring boot的医院挂号就诊系统(源码+论文)
java·spring boot·后端
Olrookie6 小时前
若依前后端分离版学习笔记(三)——表结构介绍
笔记·后端·mysql