https://blog.csdn.net/weixin_52690231/article/details/123925880
GoLang之取地址符&、指针
注:本文以Windos系统上Go SDK1.8进行讲解

1.int取地址符&

变量是一种使用方便的占位符,用于引用计算机内存地址。
Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
以下实例演示了变量在内存中地址:

func main() {
    var a int
    fmt.Printf("变量的地址: %x\n", &a) //变量的地址: c000016098
    a = 4
    fmt.Printf("变量的地址: %x\n", &a)//变量的地址:  c000016098
}

2.指针声明

一个指针变量指向了一个值的内存地址。
类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:
*var-type 为指针类型,var_name 为指针变量名

var var_name *var-type
var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */

3.int指针使用

指针使用流程:
定义指针变量。
为指针变量赋变量。
访问指针变量中指向地址的值。

func main() {
    var a int = 20 /* 声明实际变量 */
    var ip *int    /* 声明指针变量 */

    ip = &a /* 指针变量的存储地址 */

    /* 使用指针访问变量 */
    fmt.Printf("*ip 变量的值: %d\n", *ip) //*ip 变量的值: 20

    /* 值的地址 */
    fmt.Printf("a 变量的地址是: %p\n", &a) //a 变量的地址是: 0xc000102058

    /* 指针变量里存的地址 */
    fmt.Printf("ip 变量里储存的地址: %p\n", ip) //ip 变量里储存的地址: 0xc000102058

    /* 指针变量的地址 */
    fmt.Printf("ip 变量的地址: %p\n", &ip) //ip 变量的地址: 0xc00012e018

}

4.空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil。
nil 指针也称为空指针。

func main() {
    var ptr *int
    if ptr == nil {
        fmt.Println("ptr==nil")
    } else if ptr != nil {
        fmt.Println("ptr!=nil")
    }
    fmt.Println(ptr)
    fmt.Printf("ptr 的值为 : %x\n", ptr)
    /*输出以下:
    ptr==nil
    <nil>
    ptr 的值为 : 0
    */
}

5.指向指针的指针

如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址:
指向指针的指针变量声明格式如下:

var ptr **int;

package main

import "fmt"

func main() {

   var a int
   var ptr *int
   var pptr **int

   a = 3000

   /* 指针 ptr 地址 */
   ptr = &a

   /* 指向指针 ptr 地址 */
   pptr = &ptr

   /* 获取 pptr 的值 */
   fmt.Printf("变量 a = %d\n", a )//输出:变量 a = 3000
   fmt.Printf("指针变量 *ptr = %d\n", *ptr )//输出:指针变量 *ptr = 3000
   fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)//输出:指向指针的指针变量 **pptr = 3000
}

6.三重指针

pt3 - > pto - > ptr - >变量a

package main

import "fmt"
func main(){
    var a int = 5
    //把ptr指针 指向ss所在地址
    var ptr *int = &a
    //开辟一个新的指针,指向ptr指针指向的地方
    var pts *int = ptr 
    //二级指针,指向一个地址,这个地址存储的是一级指针的地址
    var pto **int = &ptr
    //三级指针,指向一个地址,这个地址存储的是二级指针的地址,二级指针同上
    var pt3 ***int = &pto
    fmt.Println("a的地址:",&a,
                "\n 值", a, "\n\n",

                "ptr指针所在地址:",&ptr,
                "\n ptr指向的地址:",ptr,
                "\n ptr指针指向地址对应的值",*ptr,"\n\n", 

                "pts指针所在地址:",&pts,
                "\n pts指向的地址:", pts,
                "\n pts指针指向地址对应的值:",*pts,"\n\n", 

                "pto指针所在地址:",&pto,
                "\n pto指向的指针(ptr)的存储地址:",pto, 
                "\n pto指向的指针(ptr)所指向的地址: " ,*pto, 
                "\n pto最终指向的地址对应的值(a)",**pto,"\n\n",

                "pt3指针所在的地址:",&pt3,
                "\n pt3指向的指针(pto)的地址:",pt3,//等于&*pt3,
                "\n pt3指向的指针(pto)所指向的指针的(ptr)地址", *pt3, //等于&**pt3,
                "\n pt3指向的指针(pto)所指向的指针(ptr)所指向的地址(a):",**pt3, //等于&***pt3,
                "\n pt3最终指向的地址对应的值(a)", ***pt3)
}
/*
执行结果:

a的地址: 0xc00009a008 
 值 5 

 ptr指针所在地址: 0xc000092010 
 ptr指向的地址: 0xc00009a008 
 ptr指针指向地址对应的值 5 

 pts指针所在地址: 0xc000092018 
 pts指向的地址: 0xc00009a008 
 pts指针指向地址对应的值: 5 

 pto指针所在地址: 0xc000092020 
 pto指向的指针(ptr)的存储地址: 0xc000092010 
 pto指向的指针(ptr)所指向的地址:  0xc00009a008 
 pto最终指向的地址对应的值(a) 5 

 pt3指针所在的地址: 0xc000092028 
 pt3指向的指针(pto)的地址: 0xc000092020 
 pt3指向的指针(pto)所指向的指针的(ptr)地址 0xc000092010 
 pt3指向的指针(pto)所指向的指针(ptr)所指向的地址(a): 0xc00009a008 
 pt3最终指向的地址对应的值(a) 5
 */

7.引用变量

在一些开发语言中(比如 C++),对已存在的变量可以声明别名,这种别名称为引用变量。
可以看到 a、b 和 c 都指向相同的内存位置。对 a 的写操作会影响 b 和 c。

include <stdio.h>

 int main() {
         int a = 10;
         int &b = a;
         int &c = b;

         printf("%p %p %p\n", &a, &b, &c); // 0x7ffe114f0b14 0x7ffe114f0b14 0x7ffe114f0b14
         return 0;
}

8.Go语言没有引用变量

与 C++ 不同,Go 程序中定义的每个变量都占用一个惟一的内存位置。

func main() {
     var a, b, c int
     fmt.Println(&a, &b, &c) // 0x1040a124 0x1040a128 0x1040a12c
}

创建两个共享同一内存位置的变量是不可能的。但是可以创建两个指向同一内存位置的变量,不过这与两个变量共享同一内存位置是不同的。
下面这段代码,b 和 c 都具有相同的值 ,即变量 a 的地址,但 a、c 存储在内存中不同的位置。改变 b 的内容不会影响到 c。

func main() {
         var a int
        var b, c = &a, &a
        fmt.Println(b, c)   // 0x1040a124 0x1040a124
        fmt.Println(&b, &c) // 0x1040c108 0x1040c110
}

9.&使用注意事项

9.1常量不可寻址

9.2字符串变量可寻址

func main() {
    var s string = "qqqq"
    fmt.Println(&s) //0xc00005a250
}

9.3函数或方法不可寻址

9.4基本类型字面量不可寻址

10 什么情况下使用指针

package main

import "fmt"

type animal struct {
    name string
}

func (a animal) setName_v(name string) animal {
    a.name = name
    return a
}

func (a animal) setName_wrong(name *string) {
    a.name = *name //作用域只在函数内部, 垃圾回收了所有内部参数
}

func (a *animal) setName(name *string) {
    a.name = *name //作用域是外部的地址,垃圾并不会回收地址
}
func setName(p *animal, name string) {
    p.name = name //自带的数据机构 可以传 地址可以传value,看具体情况节约内存
}
func main() {
    an := animal{name: "jerry"}
    fmt.Println(an)
    setName(&an, "tommy")
    fmt.Println(an)
    strname := "zhangsan" //数据结构很大建议用地址,这样不用走栈内存。
    an.setName(&strname)
    fmt.Println(an)
    an = an.setName_v("lili")
    fmt.Println(an)
}

// 四个方法涉及了变量地址,数值 ,作用域,垃圾,内存开销。

从以上指针的详细分析中,我们可以总结出指针的两大好处:

  1. 可以修改指向数据的值;
  2. 在变量赋值,参数传值的时候可以节省内存。

不过 Go 语言作为一种高级语言,在指针的使用上还是比较克制的。它在设计的时候就对指针进行了诸多限制,比如指针不能进行运行,也不能获取常量的指针。所以在思考是否使用时,我们也要保持克制的心态。
我根据实战经验总结了以下几点使用指针的建议,供你参考:
如果接收者类型是 map、slice、channel 这类引用类型,不使用指针;
如果需要修改接收者,那么需要使用指针;
如果接收者是比较大的类型,可以考虑使用指针,因为内存拷贝廉价,所以效率高。
所以对于是否使用指针类型作为接收者,还需要你根据实际情况考虑。

  1. 不要对 map、slice、channel 这类引用类型使用指针;
  2. 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
  3. 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
  4. 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
  5. 像 int、bool 这样的小数据类型没必要使用指针;
  6. 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
  7. 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。

总结
为了使编程变得更简单,指针在高级的语言中被逐渐淡化,但是它也的确有自己的优势:修改数据的值和节省内存。所以在 Go 语言的开发中我们要尽可能地使用值类型,而不是指针类型,因为值类型可以使你的开发变得更简单,并且也是并发安全的。如果你想使用指针类型,就要参考我上面讲到的使用指针的条件,看是否满足,要在满足和必须的情况下才使用指针。

这节课到这里就要结束了,在这节课的最后同样给你留个思考题:指向接口的指针是否实现了该接口?为什么?思考后可以自己写代码验证下。

作者:秦晓川  创建时间:2024-12-08 00:37
最后编辑:秦晓川  更新时间:2025-02-22 15:31
上一篇:
下一篇: