美文网首页
GO 中的指针?

GO 中的指针?

作者: 阿兵云原生 | 来源:发表于2023-10-03 23:30 被阅读0次

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    例如,定义了一个变量 num

    [图片上传失败...(image-e33b24-1696433295970)]

    那么指针又是什么呢?

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

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

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

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

    在内存中,可能是这样的

    [图片上传失败...(image-85bc56-1696433295970)]

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

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

    指针的那点事

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

    首先结论先行

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

    用指针为啥高效?

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

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

    [图片上传失败...(image-72ba62-1696433295970)]

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

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

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

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

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

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

    [图片上传失败...(image-eb6d-1696433295970)]

    初始化可以这样做:

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

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

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

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

    [图片上传失败...(image-c55721-1696433295970)]

    此处的*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)
    }
    

    [图片上传失败...(image-81be64-1696433295970)]

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

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

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

    尝试指针运算??

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

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

    [图片上传失败...(image-3c52da-1696433295970)]

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

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

    [图片上传失败...(image-828aec-1696433295970)]

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

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

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

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

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

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

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

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

    [图片上传失败...(image-23b975-1696433295970)]

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

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

    [图片上传失败...(image-bb1542-1696433295970)]

    这里可以看到我们将*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)
    }
    

    [图片上传失败...(image-427899-1696433295970)]

    此处我们就可以看到先是将 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 相互转换

    [图片上传失败...(image-958494-1696433295970)]

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

    type ArbitraryType int
    
    type Pointer *ArbitraryType
    

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

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

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

    [图片上传失败...(image-ba1c88-1696433295970)]

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

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

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

    欢迎点赞,关注,收藏

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

    [图片上传失败...(image-89083f-1696433295970)]

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

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

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

    相关文章

      网友评论

          本文标题:GO 中的指针?

          本文链接:https://www.haomeiwen.com/subject/eycybdtx.html