Go指针剖析~

思维导图

指针的概念

在 Go 中,指针是一种存储变量内存地址的变量。在指针类型变量中存储的是另一个变量的地址,而不是该变量的值本身。

Go指针类型允许对这个指针类型指向的数据进行修改,传递数据可以通过传递指针来实现,无须拷贝数据,并且指针类型不能够进行偏移和运算,因此Go中的指针类型变量拥有指针高效访问的特点,且不会发生指针偏移,避免了非法修改指针从而导致非法内存访问和指针溢出等问题,提高代码安全性。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

当声明并初始化一个变量时:

go 复制代码
var num int = 10

上述声明并初始化时,在内存中开辟了一块空间,该空间存放着数值10,此时该空间有一个唯一的地址来进行标识,此时指向这个地址的变量称为指针变量(指针)。

当一个指针变量被定义后没有指向到任何变量时,它的默认值为 nil

go 复制代码
func main() {
    var num *int
    fmt.Println(num)
}

// 执行结果
<nil>

指针地址

Go程序中,每个变量在运行时都拥有一个在内存分配的地址来表示变量在内存中的位置。

Go语言中使用&字符放在变量前面对变量进行"取地址"操作。

取变量指针的语法如下:

go 复制代码
ptr := &v    // v的类型为T
  • v: 代表被取地址的变量,类型为T
  • ptr: 用于接收地址的变量,ptr的类型就为*T,其中T为指针的类型。*代表指针。
go 复制代码
func main() {
    num := 10
    address := &num
    fmt.Printf("nums: %d ptr: %p\n", num, &num)
    fmt.Printf("address: %p type: %T\n", address, address)
    fmt.Println(&address)
}

// 执行结果
nums: 10 ptr: 0xc0000aa058
address: 0xc0000aa058 type: *int
0xc0000ce018

指针类型

Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int*int64*string等。

go 复制代码
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    var x int = 255
    var ptrInt *int = &x
    fmt.Printf("int pointer: %T\n", ptrInt)

    var str string = "hello"
    var ptrStr *string = &str
    fmt.Printf("string pointer: %T\n", ptrStr)

    var b bool = true
    var ptrBool *bool = &b
    fmt.Printf("bool pointer: %T\n", ptrBool)

    var f float64 = 3.14
    var ptrFloat64 *float64 = &f
    fmt.Printf("float64 pointer: %T\n", ptrFloat64)

    var arr [3]int = [3]int{1, 2, 3}
    var ptrArr *[3]int = &arr
    fmt.Printf("[3]int pointer: %T\n", ptrArr)

    var p Person = Person{"Tom", 20}
    var ptrP *Person = &p
    fmt.Printf("Person pointer: %T\n", ptrP)
}

// 执行结果
int pointer: *int
string pointer: *string
bool pointer: *bool
float64 pointer: *float64
[3]int pointer: *[3]int
Person pointer: *main.Person

指针取值

使用&操作符对变量进行取地址操作后会得到该变量的指针,对指针使用*操作,可以获得该指针所指向的值,即指针取值

go 复制代码
func main() {
    num := 256
    addr := &num
    fmt.Printf("type of addr: %T\n", addr)
    value := *addr
    fmt.Printf("type of value: %T\n", value)
    fmt.Printf("value: %v\n", value)
}

// 执行结果
type of addr: *int
type of value: int
value: 256

通过取地址操作符&获取到变量的地址,通过取值操作符*可以获取到地址指向的值。

指针使用

使用指针在函数或者方法传参时:

  • 使用指针,可以在函数内部修改实参的值
  • 使用指针,可以避免参数副本的内存消耗

在 Go 函数中,函数参数的传递方式是值传递(Pass by Value),会将参数值(变量值)进行拷贝并传入到函数中,即传递的是参数值的副本。

在函数内部,对参数副本(形参)的修改并不会影响到函数外的实参值。

go 复制代码
func main() {
    num := 255
    fmt.Printf("modify before main() num address: %p\n", &num)
    fmt.Printf("modify before main() num value: %d\n", num)
    modify(num)
    fmt.Printf("modify after main() num address: %p\n", &num)
    fmt.Printf("modify after main() num value: %d\n", num)
}

func modify(num int) {
    num = 255 * 2
    fmt.Printf("modify() num address: %p\n", &num)
    fmt.Printf("modify() num value: %d\n", num)
}

// 执行结果
modify before main() num address: 0xc00001c098
modify before main() num value: 255
modify() num address: 0xc00001c0c0
modify() num value: 510
modify after main() num address: 0xc00001c098
modify after main() num value: 255

若使用指针传递,则可以实现函数内修改实参的值

go 复制代码
func main() {
    num := 255
    fmt.Printf("modify before main() num address: %p\n", &num)
    fmt.Printf("modify before main() num value: %d\n", num)
    modify(&num)
    fmt.Printf("modify after main() num address: %p\n", &num)
    fmt.Printf("modify after main() num value: %d\n", num)
}

func modify(ptr *int) {
    *ptr = 255 * 2
    fmt.Printf("modify() ptr address: %p\n", ptr)
    fmt.Printf("modify() ptr address: %d\n", *ptr)
}

// 执行结果
modify before main() num address: 0xc00001c098
modify before main() num value: 255          
modify() ptr address: 0xc00001c098           
modify() ptr address: 510                    
modify after main() num address: 0xc00001c098
modify after main() num value: 510    

上述代码中,将变量num通过取地址操作符&num的地址传入函数modify,该函数的参数为一个指针形参ptr,传入函数时,会将变量num地址拷贝一份到函数modifyptr指针变量中,此时modify的指针变量ptr指向main中的num变量,在modify函数中,*ptr = 255 * 2修改了指针变量指向的值,进而对函数外的变量(原始参数)进行修改。

另外,当函数需要对较大的数据结构进行操作时,使用指针类型参数可以避免因实参拷贝到函数的形参而导致内存消耗

new与make

new

new是Go中的一个内置函数,函数签名如下:

go 复制代码
func new(Type) *Type

上述函数签名中,参数Type表示类型,new函数接收一个类型参数。返回值为*Type,表示返回一个指向该类型的内存地址的指针,即Type类型的指针。

go 复制代码
func main() {
    num := new(int)
    b := new(bool)
    fmt.Printf("%T\n", num) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Println(*num) // 0
    fmt.Println(*b) // false
}

上述代码中,使用new函数获得传入参数中的类型的指针,并且该指针对应的值为该类型的初始值 。与var num *int的不同之处在于使用var num *int只是声明了指针变量num但并没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值 。而使用内置的new函数返回的指针变量则进行了初始化,可以正常对其赋值。

make

make函数同样由于内存分配,不同于newmake函数用于slicemapchannel类型的内存创建,并且返回的类型就是其本身,不是指针类型,因为slicemapchannel类型就是引用类型,无须返回其类型的指针。

make函数签名

go 复制代码
func make(t Type, size ...IntegerType) Type

在go中,使用slicemap以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

go 复制代码
func main() {
    var m map[string]int
    m = make(map[string]int, 10)
    m["学习"] = 100
    fmt.Println(m) // map[学习:100]
}

new与make的区别

  • 二者都用于内存分配;
  • make只用于slicemapchannel的初始化,并且这三者本身是引用类型,因此返回这三者本身;
  • new用于类型的内存分配,返回指定类型的指针,并在内存初始化对应的类型零值;
相关推荐
渣哥15 小时前
面试高频:Spring 事务传播行为的核心价值是什么?
javascript·后端·面试
调试人生的显微镜15 小时前
iOS 代上架实战指南,从账号管理到使用 开心上架 上传IPA的完整流程
后端
本就一无所有 何惧重新开始15 小时前
Redis技术应用
java·数据库·spring boot·redis·后端·缓存
低音钢琴15 小时前
【SpringBoot从初学者到专家的成长11】Spring Boot中的application.properties与application.yml详解
java·spring boot·后端
越千年16 小时前
用Go实现类似WinGet风格彩色进度条
后端
淳朴小学生16 小时前
静态代理和动态代理
后端
渣哥16 小时前
数据一致性全靠它!Spring 事务传播行为没搞懂=大坑
javascript·后端·面试
Mgx16 小时前
高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
go
三七互娱后端团队16 小时前
Serena语义检索在AI CodeReview中的应用
后端