本文也主要聊聊在 GO 中的指针和内存,希望对你有点帮助
如果你学习过 C 语言,你就非常清楚指针的高效和重要性
使用 GO 语言也是一样,项目代码中,不知道你是否会看到函数参数中会传递各种 map,slice ,自定义的结构等等
这些参数数据量如果比较小的话就算了,可偏偏工作中你能看到很多这种数据量大的结构,也是这样以传值的方式就这样放到函数参数上了
而且这个数据量大的结构可能会来来回回传递很多次,就会导致同一份数据被活生生的拷贝了 N 次,对系统的内存资源真是妥妥的浪费啊
必须要学会指针的使用,能够让你写的服务性能会更加的好,那么,我们开始吧
以下分别从如下三个方面来聊聊
首先,xdm 对于变量有没有一个很清晰的认知?无论我们是在写 C 语言还是 GO 语言的时候,我们都离不开定义变量
可是我们知道这些变量实际上都是对应这内存的某一个地址,这一个地址上存储了我们需要的数据
那么我们访问变量的时候,就会去找到这一个地址,然后取出数据
那么直接记录数据存放的地址不就好了吗?
咱们的内存地址是十六进制表示的,你确定你可以把每一个变量的地址都记得下来?因此才会引入一种占位符,他就是变量
对我们人来说,让你记录一个 变量 num 方便呢?还是让你去记录一个 0xFAEBF9C7 的地址方便呢?变量的好处就不言而喻了
例如,定义了一个变量 num
那么指针又是什么呢?
实际上指针他也是一种变量,只不过,他存放的是其他变量的地址,会觉得绕吗?
简单来看看有这么一块内存
例如存放了一个整型的数据:var num int = 200
这个时候有一个指针变量指向 num 的地址:var ptr * int = &num
在内存中,可能是这样的
通过上面的内容,我们就可以很清晰的知道,变量他是一个标识符,对应着实际数据的地址,让我们很方便的去拿到具体的数据
指针他也是一个变量,只不过用于存放其他变量的地址,这样能够让我们更加低成本的找到指针指向的数据
那么,我们继续来细聊指针
首先结论先行
首先先来简单的聊聊这个高效的问题
例如还是上面的图,给一个参数中传入指针,实际上也是传入指针的拷贝,只不过这个拷贝,指向的也是原有指针指向的地址
那么对于内存消耗来说,如果是 64 位机器,拷贝的这一个指针就只占用 8 个字节
图中只是一个简单的例子,如果指针指向的是一块比较大的内存 ,例如这片内存为 M
那么如果传参的时候,不是传指针,那么就会先对这一片内存进行拷贝,传到函数中,那么这个时候,就会多占用一些内存空间了,例如总共占用 M+M
虽然说使用指针方便高效,但是也要注意,使用的时候记得初始化,否则轻轻松松就会 panic
例如 给一个未初始化的指针进行赋值操作,你的程序就会崩溃 invalid memory address or nil pointer dereference
初始化可以这样做:
事物都是有两面性的,只有我们能够看到事物的全貌,我们才能更好的理解和使用他
自然,我们也可以通过解引用的方式去修改指针指向地址上的值
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 语言 中不同类型的也是不可以直接赋值的,在这里就不在过多的演示了
在 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 包中的指针操作还有很多细节和知识,后续有机会可以接着聊,希望本次文章对你有帮助
感谢阅读,欢迎交流,点个赞,关注一波 再走吧
朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是阿兵云原生,欢迎点赞关注收藏,下次见~
文中提到的技术点,感兴趣的可以查看这些文章:
可以进入地址进行体验和学习:https://xxetb.xet.tech/s/3lucCI