在前面的文章中,我们已经提到过很多次“指针”了,你应该已经比较熟悉了。不过,我们那时大多指的是指针类型及其对应的指针值,今天我们讲的则是更为深入的内容。
让我们先来复习一下。
type Dog struct {name string
}func (dog *Dog) SetName(name string) {dog.name = name
}
对于基本类型Dog来说,*Dog就是它的指针类型。而对于一个Dog类型,值不为nil的变量dog,取址表达式&dog的结果就是该变量的值(也就是基本值)的指针值。
如果一个方法的接收者是*Dog类型的,那么该方法就是基本类型Dog的指针方法。
在这种情况下,这个方法的接收者,实际上就是当前的基本值的指针值。
我们可以通过指针值无缝地访问到基本值包含的任何字段,以及调用与之关联的任何方法。这应该就是我们在编写 Go 程序的过程中,用得最频繁的“指针”了。
从传统意义上说,指针是一个指向某个确切的内存地址的值。这个内存地址可以是任何数据或代码的起始地址,比如,某个变量、某个字段或某个函数。
我们刚刚只提到了其中的一种情况,在 Go 语言中还有其他几样东西可以代表“指针”。其中最贴近传统意义的当属uintptr类型了。该类型实际上是一个数值类型,也是 Go 语言内建的数据类型之一。
根据当前计算机的计算架构的不同,它可以存储 32 位或 64 位的无符号整数,可以代表任何指针的位(bit)模式,也就是原始的内存地址。
再来看 Go 语言标准库中的unsafe包。unsafe包中有一个类型叫做Pointer,也代表了“指针”。
unsafe.Pointer可以表示任何指向可寻址的值的指针,同时它也是前面提到的指针值和uintptr值之间的桥梁。也就是说,通过它,我们可以在这两种值之上进行双向的转换。这里有一个很关键的词——可寻址的(addressable)。在我们继续说unsafe.Pointer之前,需要先要搞清楚这个词的确切含义。
今天的问题是:你能列举出 Go 语言中的哪些值是不可寻址的吗?
这道题的典型回答是以下列表中的值都是不可寻址的。
- 常量的值。
- 基本类型值的字面量。
- 算术操作的结果值。
- 对各种字面量的索引表达式和切片表达式的结果值。不过有一个例外,对切片字面量的索引结果值却是可寻址的。
- 对字符串变量的索引表达式和切片表达式的结果值。
- 对字典变量的索引表达式的结果值。
- 函数字面量和方法字面量,以及对它们的调用表达式的结果值。
- 结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值。
- 类型转换表达式的结果值。
- 类型断言表达式的结果值。
- 接收表达式的结果值。
问题解析
初看答案中的这些不可寻址的值好像并没有什么规律。不过别急,我们一起来梳理一下。你可以对照着 demo35.go 文件中的代码来看,这样应该会让你理解起来更容易一些。
常量的值总是会被存储到一个确切的内存区域中,并且这种值肯定是不可变的。基本类型值的字面量也是一样,其实它们本就可以被视为常量,只不过没有任何标识符可以代表它们罢了。
第一个关键词:不可变的。由于 Go 语言中的字符串值也是不可变的,所以对于一个字符串类型的变量来说,基于它的索引或切片的结果值也都是不可寻址的,因为即使拿到了这种值的内存地址也改变不了什么。
算术操作的结果值属于一种临时结果。在我们把这种结果值赋给任何变量或常量之前,即使能拿到它的内存地址也是没有任何意义的。
第二个关键词:临时结果。这个关键词能被用来解释很多现象。我们可以把各种对值字面量施加的表达式的求值结果都看做是临时结果。
我们都知道,Go 语言中的表达式有很多种,其中常用的包括以下几种。
- 用于获得某个元素的索引表达式。
- 用于获得某个切片(片段)的切片表达式。
- 用于访问某个字段的选择表达式。
- 用于调用某个函数或方法的调用表达式。
- 用于转换值的类型的类型转换表达式。
- 用于判断值的类型的类型断言表达式。
- 向通道发送元素值或从通道那里接收元素值的接收表达式。
我们把以上这些表达式施加在某个值字面量上一般都会得到一个临时结果。比如,对数组字面量和字典字面量的索引结果值,又比如,对数组字面量和切片字面量的切片结果值。它们都属于临时结果,都是不可寻址的。
一个需要特别注意的例外是,对切片字面量的索引结果值是可寻址的。因为不论怎样,每个切片值都会持有一个底层数组,而这个底层数组中的每个元素值都是有一个确切的内存地址的。
你可能会问,那么对切片字面量的切片结果值为什么却是不可寻址的?这是因为切片表达式总会返回一个新的切片值,而这个新的切片值在被赋给变量之前属于临时结果。
你可能已经注意到了,我一直在说针对数组值、切片值或字典值的字面量的表达式会产生临时结果。如果针对的是数组类型或切片类型的变量,那么索引或切片的结果值就都不属于临时结果了,是可寻址的。
再说一个例外。我们通过对字典类型的变量施加索引表达式,得到的结果值不属于临时结果,可是,这样的值却是不可寻址的。原因是,字典中的每个键 - 元素对的存储位置都可能会变化,而且这种变化外界是无法感知的。
我们都知道,字典中总会有若干个哈希桶用于均匀地储存键 - 元素对。当满足一定条件时,字典可能会改变哈希桶的数量,并适时地把其中的键 - 元素对搬运到对应的新的哈希桶中。
在这种情况下,获取字典中任何元素值的指针都是无意义的,也是不安全的。我们不知道什么时候那个元素值会被搬运到何处,也不知道原先的那个内存地址上还会被存放什么别的东西。所以,这样的值就应该是不可寻址的。