GO 中的指针?

2,711 阅读9分钟

本文正在参加 「金石计划 . 瓜分6万现金大奖」

本文也主要聊聊在 GO 中的指针和内存,希望对你有点帮助

如果你学习过 C 语言,你就非常清楚指针的高效和重要性

使用 GO 语言也是一样,项目代码中,不知道你是否会看到函数参数中会传递各种 map,slice ,自定义的结构等等

这些参数数据量如果比较小的话就算了,可偏偏工作中你能看到很多这种数据量大的结构,也是这样以传值的方式就这样放到函数参数上了

而且这个数据量大的结构可能会来来回回传递很多次,就会导致同一份数据被活生生的拷贝了 N 次,对系统的内存资源真是妥妥的浪费啊

必须要学会指针的使用,能够让你写的服务性能会更加的好,那么,我们开始吧

以下分别从如下三个方面来聊聊

  • 基本的变量和指针是什么?
  • 指针的那点事

基本的变量和指针是什么?

首先,xdm 对于变量有没有一个很清晰的认知?无论我们是在写 C 语言还是 GO 语言的时候,我们都离不开定义变量

可是我们知道这些变量实际上都是对应这内存的某一个地址,这一个地址上存储了我们需要的数据

那么我们访问变量的时候,就会去找到这一个地址,然后取出数据

那么直接记录数据存放的地址不就好了吗?

咱们的内存地址是十六进制表示的,你确定你可以把每一个变量的地址都记得下来?因此才会引入一种占位符,他就是变量

对我们人来说,让你记录一个 变量 num 方便呢?还是让你去记录一个 0xFAEBF9C7 的地址方便呢?变量的好处就不言而喻了

例如,定义了一个变量 num

那么指针又是什么呢?

实际上指针他也是一种变量,只不过,他存放的是其他变量的地址,会觉得绕吗?

简单来看看有这么一块内存

例如存放了一个整型的数据:var num int = 200

这个时候有一个指针变量指向 num 的地址:var ptr * int = &num

在内存中,可能是这样的

通过上面的内容,我们就可以很清晰的知道,变量他是一个标识符,对应着实际数据的地址,让我们很方便的去拿到具体的数据

指针他也是一个变量,只不过用于存放其他变量的地址,这样能够让我们更加低成本的找到指针指向的数据

指针的那点事

那么,我们继续来细聊指针

首先结论先行

  1. 默认声明一个指针,未初始化的时候,默认零值为 nil,要使用指针的话,请初始化,或者让他指向一个变量的地址
  1. 指针一般占用的空间是 4 个字节或者 8个字节,根据你的系统是32 位的还是 64 位的,指针占用的字节也不尽相同
  1. 函数中的传参,传递指针,那也是一个拷贝,指针的拷贝而已
  1. 指针他也是一种变量类型,只不过存放的是别的变量的地址,那么指针当然也是可以存放别的指针变量的地址
  1. C 语言中,咱们可以通过操作指针偏移,去移动指针,但是 Go 中不支持
  1. Go 中的普通指针和 unsafe 包里面的 Pointer 有啥不一样

用指针为啥高效?

首先先来简单的聊聊这个高效的问题

例如还是上面的图,给一个参数中传入指针,实际上也是传入指针的拷贝,只不过这个拷贝,指向的也是原有指针指向的地址

那么对于内存消耗来说,如果是 64 位机器,拷贝的这一个指针就只占用 8 个字节

图中只是一个简单的例子,如果指针指向的是一块比较大的内存 ,例如这片内存为 M

那么如果传参的时候,不是传指针,那么就会先对这一片内存进行拷贝,传到函数中,那么这个时候,就会多占用一些内存空间了,例如总共占用 M+M

给一个未初始化的指针赋值,会 panic

虽然说使用指针方便高效,但是也要注意,使用的时候记得初始化,否则轻轻松松就会 panic

例如 给一个未初始化的指针进行赋值操作,你的程序就会崩溃 invalid memory address or nil pointer dereference

初始化可以这样做:

  • 给指针 new() 一下,分配一块内存
  • 或者直接让指针指向某一个变量的地址

事物都是有两面性的,只有我们能够看到事物的全貌,我们才能更好的理解和使用他

自然,我们也可以通过解引用的方式去修改指针指向地址上的值

func main(){
   var a int = 100
   ptr := &a
   fmt.Println("ptr == ", *ptr)

   *ptr = 200
   fmt.Println("ptr == ", *ptr)
}

此处的 *ptr 就相当于 上述的标识符 a ,给 *ptr 赋值,就相当于 给 a 赋值,是一个道理

二级指针

指针存放的是其他变量的地址,那么自然也是可以存放其他指针变量的地址的,这样的指针就可以称之为二级指针

当然,如法炮制,就会有多级指针,使用这样的指针的时候,一定要将其弄清楚,否则你会被多级指针搞的云里雾里

func main() {
   var a int = 100

   ptr := &a
   fmt.Println("*ptr == ", *ptr)
   fmt.Println("&ptr == ", &ptr)

   pptr := &ptr
   fmt.Println("**pptr == ", **pptr)
   fmt.Println("pptr == ", &pptr)

   **pptr = 200
   fmt.Println("**pptr == ", **pptr)
}

此处就可以看到,实际上一级指针和二级指针也没有啥太大的区别,仅仅是二级指针,存放的是一级指针的地址,一级指针存放的是其他变量的地址

那么如果对二级指针解引用的话,就需要先从二级指针处找到一级指针的地址,再找到具体变量的地址

因此 **ptr 也就相当于是 标识符 a,对 **pptr 赋值,就相当于是给 a 赋值

尝试指针运算??

C 语言中,我们知道,指针是可以这样玩的,例如一个指针指向的是一个 int 类型的数组,那么我们从指针的第一个元素开始偏移,就可以是 ptr+1

ptr +1 在这里表示的意思是,让 ptr 向下移动一个 int 类型的地址,而不是数值上的 +1 而已

那么,如果是 Go 语言,你就不能这么玩了

sli := []int{0,1,2,3}
ptr := &sli
fmt.Println(ptr)
ptr+1    // 很显然是不行的

可以中 GOLAND 的提示中可以看到,Go 语言中是不允许我们直接对指针这么干的,此处需要注意哈

自然 Go 语言 中不同类型的也是不可以直接赋值的,在这里就不在过多的演示了

Go 中的指针和 unsafe 包里的指针

在 C 语言中,不同类型指针是可以相互转换的,不同类型的指针也是可以进行比较的,那么你觉得你在 Go 语言里面可以吗?

显然是不行,正是因为不行,所以就避免了对指针了解不深入的初学者犯错,就可以尽量减少程序员对指针的不安全使用

因此 Go 中的指针,他是一个安全指针

但是 Go 语言中也给我们提供了一个 unsafe 包,这个包就可以让我们玩一些花的

例如,我们想将一个 int 类型的数据,转成 int64 类型的数据,显然通过直接指针赋值或者变量赋值的方式是不行的

但是我们通过 unsafe 包中的 Pointer 就可以做到

num := 200
var num64 int64

ptr := (*int64)(unsafe.Pointer(&num))
num64 = *ptr
fmt.Println("num64 == ",num64)

这里可以看到我们将 *int 转成了 unsafe.Pointer,再将 unsafe.Pointer 转成 *int64

但是 unsafe.Pointer 一样是不能直接进行数学运算的,如果我们需要让他进行数学运算,那么我们还需要将 unsafe.Pointer 转换成 uintptr

例如这样:

func main(){
    a := [3]int{1,2,3}
    res := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + unsafe.Sizeof(a[0])))
    fmt.Println(res)
}

此处我们就可以看到先是将 a 的地址 &a( 是 *int 类型) 的转换成 unsafe.Pointer ,再将 unsafe.Pointer 转换成 uintptr

此时 uintptr 就可以进行数学运算,我们偏移 a 数组中一个元素的地址

偏移之后,将 uintptr 转换成 unsafe.Pointer ,再将 unsafe.Pointer 转换成 *int ,最终对取到的指针解引用,得到一个 int 类型的数据 , 即 2 ,也就是将 a 数组从第一位向后偏移一位,得到 2 没有毛病

通过上述案例,我们可以知道,如果期望实现 C 语言那样的玩法,那么就需要进行如下转换

普通类型的指针 与 unsafe.Pointer 相互转换

unsafe.Pointer 与 uintptr 相互转换

再回过头来看 unsafe 包中的定义

type ArbitraryType int

type Pointer *ArbitraryType

实际上也非常简单,其中 ArbitraryType 表示的意思也就是任意类型

提醒一波,使用指针偏移的时候,需要注意你需要偏移的结构是什么样的

例如,结构体和数组在做偏移的时候,使用的偏移字节计算方式就不一样

可以看到,我们例子中,使用数组的方式是使用 Sizeof ,如果是结构体中的成员进行指针偏移的时候,就需要使用 Offsetof

至此,对于 golang 中的指针就聊到这里,关于 unsafe 包中的指针操作还有很多细节和知识,后续有机会可以接着聊,希望本次文章对你有帮助

感谢阅读,欢迎交流,点个赞,关注一波 再走吧

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是阿兵云原生,欢迎点赞关注收藏,下次见~

文中提到的技术点,感兴趣的可以查看这些文章: