指针

指针是一个指向某个确切的内存地址的值。这个内存地址可以是任何数据或代码的起始地址,比如,某个变量、某个字段或某个函数。

在 Go 语言中还有其他几样东西可以代表“指针”。其中最贴近传统意义的当属 uintptr 类型了。该类型实际上是一个数值类型,也是 Go 语言内建的数据类型之一。

根据计算架构的不同,它可以存储 32 位或 64 位的无符号整数,可以代表任何指针的位(bit)模式,也就是原始的内存地址。

Go 语言标准库中的unsafe包。unsafe包中有一个类型叫做Pointer,也代表了“指针”。

unsafe.Pointer可以表示任何指向可寻址的值的指针,同时它也是前面提到的指针值和 uintptr 值之间的桥梁。也就是说,通过它,我们可以在这两种值之上进行双向的转换。这里有一个很关键的词——可寻址的(addressable)。

不可寻址的值

  • 常量的值。

  • 基本类型值的字面量。

  • 算术操作的结果值。

  • 对各种字面量的索引表达式和切片表达式的结果值。不过有一个例外,对切片字面量的索引结果值却是可寻址的。

  • 对字符串变量的索引表达式和切片表达式的结果值。

  • 对字典变量的索引表达式的结果值。

  • 函数字面量和方法字面量,以及对它们的调用表达式的结果值。

  • 结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值。

  • 类型转换表达式的结果值。

  • 类型断言表达式的结果值。

  • 接收表达式的结果值。

常量的值总是会被存储到一个确切的内存区域中,并且这种值肯定是不可变的。

基本类型值的字面量也是一样,其实它们本就可以被视为常量,只不过没有任何标识符可以代表它们罢了。

常量的值总是会被存储到一个确切的内存区域中,并且这种值肯定是不可变的。基本类型值的字面量也是一样,其实它们本就可以被视为常量,只不过没有任何标识符可以代表它们罢了。

由于 Go 语言中的字符串值也是不可变的,所以对于一个字符串类型的变量来说,基于它的索引或切片的结果值也都是不可寻址的,因为即使拿到了这种值的内存地址也改变不了什么。

算术操作的结果值属于一种临时结果。在我们把这种结果值赋给任何变量或常量之前,即使能拿到它的内存地址也是没有任何意义的。我们可以把各种对值字面量施加的表达式的求值结果都看做是临时结果。

Go 语言中的表达式

  • 用于获得某个元素的索引表达式。

  • 用于获得某个切片(片段)的切片表达式。

  • 用于访问某个字段的选择表达式。

  • 用于调用某个函数或方法的调用表达式。

  • 用于转换值的类型的类型转换表达式。

  • 用于判断值的类型的类型断言表达式。

  • 向通道发送元素值或从通道那里接收元素值的接收表达式。

以上这些表达式施加在某个值字面量上一般都会得到一个临时结果。比如,对数组字面量和字典字面量的索引结果值,又比如,对数组字面量和切片字面量的切片结果值。它们都属于临时结果,都是不可寻址的。

例外是,对切片字面量的索引结果值是可寻址的。因为不论怎样,每个切片值都会持有一个底层数组,而这个底层数组中的每个元素值都是有一个确切的内存地址的。

对切片字面量的切片结果值为什么却是不可寻址的,因为切片表达式总会返回一个新的切片值,而这个新的切片值在被赋给变量之前属于临时结果。

如果针对的是数组类型或切片类型的变量,那么索引或切片的结果值就都不属于临时结果了,是可寻址的。因为变量的值本身就不是“临时的”。对比而言,值字面量在还没有与任何变量(或者说任何标识符)绑定之前是没有落脚点的,我们无法以任何方式引用到它们。这样的值就是“临时的”。

再说一个例外。我们通过对字典类型的变量施加索引表达式,得到的结果值不属于临时结果,可是,这样的值却是不可寻址的。原因是,字典中的每个键 - 元素对的存储位置都可能会变化,而且这种变化外界是无法感知的。

字典中总会有若干个哈希桶用于均匀地储存键 - 元素对。当满足一定条件时,字典可能会改变哈希桶的数量,并适时地把其中的键 - 元素对搬运到对应的新的哈希桶中。

在这种情况下,获取字典中任何元素值的指针都是无意义的,也是不安全的。我们不知道什么时候那个元素值会被搬运到何处,也不知道原先的那个内存地址上还会被存放什么别的东西。所以,这样的值就应该是不可寻址的。

“不安全的”操作很可能会破坏程序的一致性,引发不可预知的错误,从而严重影响程序的功能和稳定性。

函数在 Go 语言中是一等公民,所以我们可以把代表函数或方法的字面量或标识符赋给某个变量、传给某个函数或者从某个函数传出。但是,这样的函数和方法都是不可寻址的。一个原因是函数就是代码,是不可变的。另一个原因是,拿到指向一段代码的指针是不安全的。此外,对函数或方法的调用结果值也是不可寻址的,这是因为它们都属于临时结果。

不可寻址的值在使用上的限制

  • 无法使用取址操作符&获取它们的指针

  • 无法使用自增语句或自减语句(++、--)

例外:

  • 虽然对字典字面量和字典变量索引表达式的结果值都是不可寻址的,但是这样的表达式却可以被用在自增语句和自减语句中。

  • 在赋值语句中,赋值操作符左边的表达式的结果值必须可寻址的,但是对字典的索引结果值也是可以的。

  • 在带有range子句的for语句中,在range关键字左边的表达式的结果值也都必须是可寻址的,不过对字典的索引结果值同样可以被用在这里。

通过 unsafe.Pointer 操纵可寻址的值

dog := Dog{"little pig"}
dogP := &dog
dogPtr := uintptr(unsafe.Pointer(dogP))

这里先声明了一个Dog类型的变量dog,然后用取址操作符&,取出了它的指针值,并把它赋给了变量dogP。然后使用了两个类型转换,先把dogP转换成了一个unsafe.Pointer类型的值,然后紧接着又把后者转换成了一个uintptr的值,并把它赋给了变量dogPtr。这背后隐藏着一些转换规则,如下:

  • 一个指针值(比如*Dog类型的值)可以被转换为一个unsafe.Pointer类型的值,反之亦然。

  • 一个uintptr类型的值也可以被转换为一个unsafe.Pointer类型的值,反之亦然。

  • 一个指针值无法被直接转换成一个uintptr类型的值,反过来也是如此。

namePtr := dogPtr + unsafe.Offsetof(dogP.name)
nameP := (*string)(unsafe.Pointer(namePtr))

unsafe.Offsetof 函数用于获取两个值在内存中的起始存储地址之间的偏移量,以字节为单位。

这两个值一个是某个字段的值,另一个是该字段值所属的那个结构体值。我们在调用这个函数的时候,需要把针对字段的选择表达式传给它,比如dogP.name。

有了这个偏移量,又有了结构体值在内存中的起始存储地址(这里由dogPtr变量代表),把它们相加我们就可以得到dogP的name字段值的起始存储地址了。这个地址由变量namePtr代表。

我们可以再通过两次类型转换把namePtr的值转换成一个*string类型的值,这样就得到了指向dogP的name字段值的指针值。

一旦我们有意或无意地把这个内存地址泄露出去,那么其他人就能够肆意地改动dogP.name的值,以及周围的内存地址上存储的任何数据了。

最后更新于