首页 > 编程笔记 > Go语言笔记 阅读:3

Go语言指针的用法(非常详细)

Go 语言程序中,使用指针可以达到将数据从程序边界的一侧共享给另一侧的效果。如果不需要共享,则无须使用指针,只需要按值传递数据即可。示例代码如下:
func main() {
    counter := 10
    fmt.Println("counter 的值是 [", counter, "],地址是 [", &counter, "]")
    increment(&counter)
    fmt.Println("counter 的值是 [", counter, "],地址是 [", &counter, "]")
}
 
//go:noinline
func increment(inc *int) {
    *inc++
    fmt.Println("inc 的值是[", inc, "],地址是 [", &inc, "]")
}
 
//输出
counter 的值是 [ 10 ],地址是 [ 0xc000022088 ]
inc     的值是[ 0xc000022088 ],地址是 [ 0xc00000e030 ]
counter 的值是 [ 11 ],地址是 [ 0xc000022088 ]
在上述代码中,传递给 increment() 函数的是变量 counter 的地址。可能有读者会说,这是引用传递。事实并非如此,Go 语言只有值传递,也就是所见即所得,这一侧传进去的是什么,另一侧收到的就是什么。因此,传到边界另一侧的是原数据的副本,只不过这里的原数据是“存储counter值的地址”。

这个地址的副本怎么保存呢?这正是指针变量的用途。指针变量用来存储地址或者地址格式的数据。

声明 increment() 函数时,我们在 inc 类型的左边加了一个 * 号,这个 * 号用于声明指针变量。我们可以在任意现有类型的左边加上 * 号,从而构成指针类型。添加 * 号后,inc 就从整数类型变成了保存地址的指针类型。

指针变量存储地址的目的就是操作所存地址指向的内存,也就是读取或写入数据到该地址所指向的那块内存中,而我们也正是通过这个特性来修改想要操作的内容的。

事实上,我们还可以在指针变量的左边加 * 号,以访问该指针变量所指向的内存地址中的值,这称为间接读取或写入内存。可见,*inc++ 实际上就是通过指针间接读、写内存的。

在调用 increment() 函数后,活动帧切换到 increment() 函数上,此刻的 G 就只能在 increment() 函数的这个沙盒内活动了。如果 G 想要访问沙盒外的数据,必须先读取那份数据所在的地址。

当然,G 只能在当前帧内寻找。所以,想让 G 访问当前帧以外的数据,就必须先把对应数据的地址分享到这一帧来,这种能力正是指针赋予的。

Go语言指针类型

内存是一段连续的存储空间,每次声明变量时,都会在内存中为变量分配地址,这里的地址是指存储变量的空间起始地址,这个地址被称为指向变量的指针。

例如,int 类型是一个 8 位的数值类型,因此,两个相邻的 int 类型变量 a 和 b 的地址值就相差 8。

Go 语言对指针变量的声明方式为:
var name *varType
其中 varType 是指针类型,name 是指针变量名,* 号用于声明指针变量。指针默认的零值为 nil。

我们常说的指针其实是指针类型,是内存中存储单元的地址。如果变量是指针类型,则它存储的就是一个地址,这个地址指向一段内存,内存中存储的才是值。它们之间的关系如下图所示。


图 1 变量、变量地址与指针的关系

图 1 中,指针变量 ptr 用于存储变量 a 的地址,其示意图如下图所示:


图 2 指针变量ptr存储变量a的地址

这里访问存储单元 0xc00000a368(分配给变量 a 的内存地址)的方法有两种。第一种是通过变量 a 直接访问;第二种是通过指向变量 a 的指针变量 ptr 间接访问。

我们可以把连接指针变量 ptr 和变量 a 的线看作指针。变量有值和地址,指针变量也不例外。指针变量 ptr 的值 0xc00000a368 是变量 a 的地址,而指针变量 ptr 的地址则为 0xc000006038。

Go 语言的指针相较于 C语言的指针已被弱化,比如 Go语言中不支持对指针变量的值进行运算,指针自增操作 ptrA++ 和指针偏移操作 ptrA+2 在 Go语言中并未被支持。

Go 语言中的值类型常会使用 & 符号返回地址。除了使用 & 符号返回地址,值类型还可以使用“new(类型)”初始化变量,这时返回的是对应类型的指针,相当于 &{}。

切片、映射、通道等引用类型,通常不涉及取址操作。因为它们的底层实现是一个结构体,其指针类型成员指向了实际内容。值类型使用关键字 new 初始化变量,引用类型则使用关键字 make 初始化。

使用指针时注意以下事项:

Go语言指针实际应用

下面是一个实例程序,展示了如何在 Go 语言中使用指针来修改数组中的元素:
package main

import "fmt"

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println("原始数组:", numbers)

    // 传递数组的指针给函数
    increment(&numbers)

    fmt.Println("修改后的数组:", numbers)
}

// increment 函数接收一个指向切片的指针,并增加每个元素的值
func increment(s *[]int) {
    for i := range *s {
        (*s)[i]++
    }
    fmt.Println("函数内数组:", *s)
}
运行结果为:

原始数组: [1 2 3 4 5]
函数内数组: [2 3 4 5 6]
修改后的数组: [2 3 4 5 6]

这个程序展示了如何通过指针修改切片中的元素。在 increment() 函数中,我们通过解引用操作 (*s)[i] 来访问和修改切片中的元素。由于 s 是指向切片的指针,我们可以直接修改切片的内容,而这些修改在函数外部也是可见的。

Go语言nil指针

使用指针时,要避免出现 nil 指针,因为如果操作没有指向合法的内存,则会报错,示例代码如下:
var p *int
*p = 100 //错误,因为p没有合法的指向
上述代码执行时的报错信息为 panic: runtime error: invalid memory address or nil pointer dereference。

如果将指针变量的地址指向一个零值的地址,并不会报错,示例代码如下:
var a int
var p *int
p = &a //p指向a
*p = 100 //这一步相当于 a=100
fmt.Println("a = ", a) //输出:a =  100

Go语言指针数组与数组指针

指针数组与数组指针是两个容易混淆的概念,来看看它们的区别:
指针数组和数组指针的示例代码如下:
//定义了长度为 5 的整型数组的指针
a := [...]int{100, 200, 300, 400, 500}
var p *[5]int = &a
 
//定义了长度为 5 的整型指针数组
var p2 [5]*int
要区分指针数组和数组指针,必须注意 * 是与谁组合的。在 p *[5]int 中,* 与数组结合,说明是数组指针。在 p [5]*int 中,* 与 int 结合,说明这个数组的元素都是int类型的指针,所以是指针数组。

关于指针的补充说明:
下图针对指针的使用进行了总结:


图 3 指针的使用总结

我们使用 & 符号返回变量的地址,这里的地址其实是一种抽象的表示。

举个例子,我们的住所、所上的大学可以看作是变量的值,而它们的门牌号就是变量的地址。想象一下,如果我们传递的值是大学这个对象而非大学的门牌号,那这将是多么耗费资源的操作,这也是在传递大对象时,会优先考虑使用指针的原因。

相关文章