指针示例:编程
type Dog struct { name string } func (dog *Dog) SetName(name string) { dog.name = name }
对于基本类型Dog来讲,*Dog就是它的指针类型。而对于一个Dog类型,值不为nil的变量dog,取址表达式&dog的结果就是该变量的值(也就是基本值)的指针值。数组
若是一个方法的接收者是*Dog类型的,那么该方法就是基本类型Dog的指针方法。安全
从传统意义上说,指针是一个指向某个确切的内存地址的值。这个内存地址能够是任何数据或代码的起始地址,好比,某个变量、某个字段或某个函数。ide
在 Go 语言中还有其余几样东西能够表明“指针”。其中最贴近传统意义的当属uintptr类型了。该类型其实是一个数值类型,也是 Go 语言内建的数据类型之一。函数
再来看 Go 语言标准库中的unsafe包。unsafe包中有一个类型叫作Pointer,也表明了“指针”。工具
unsafe.Pointer能够表示任何指向可寻址的值的指针,同时它也是前面提到的指针值和uintptr值之间的桥梁。也就是说,经过它,咱们能够在这两种值之上进行双向的转换。这里有一个很关键的词——可寻址的(addressable)。在咱们继续说unsafe.Pointer以前,须要先要搞清楚这个词的确切含义。ui
你能列举出 Go 语言中的哪些值是不可寻址的吗?指针
常量的值。
基本类型值的字面量。
算术操做的结果值。
对各类字面量的索引表达式和切片表达式的结果值。
不过有一个例外,对切片字面量的索引结果值倒是可寻址的。
对字符串变量的索引表达式和切片表达式的结果值。
对字典变量的索引表达式的结果值。函数字面量和方法字面量,以及对它们的调用表达式的结果值。结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值。类型转换表达式的结果值。类型断言表达式的结果值。接收表达式的结果值。code
常量的值老是会被存储到一个确切的内存区域中,而且这种值确定是不可变的。基本类型值的字面量也是同样,其实它们本就能够被视为常量,只不过没有任何标识符能够表明它们罢了。
第一个关键词:不可变的。因为 Go 语言中的字符串值也是不可变的,因此对于一个字符串类型的变量来讲,基于它的索引或切片的结果值也都是不可寻址的,由于即便拿到了这种值的内存地址也改变不了什么
第二个关键词:临时结果。这个关键词能被用来解释不少现象。咱们能够把各类对值字面量施加的表达式的求值结果都看作是临时结果。
Go 语言中的表达式有不少种,其中经常使用的包括如下几种。
用于得到某个元素的索引表达式。
用于得到某个切片(片断)的切片表达式。
用于访问某个字段的选择表达式。
用于调用某个函数或方法的调用表达式。
用于转换值的类型的类型转换表达式。
用于判断值的类型的类型断言表达式。
向通道发送元素值或从通道那里接收元素值的接收表达式。咱们把以上这些表达式施加在某个值字面量上通常都会获得一个临时结果。好比,对数组字面量和字典字面量的索引结果值,又好比,对数组字面量和切片字面量的切片结果值。它们都属于临时结果,都是不可寻址的。
一个须要特别注意的例外是,对切片字面量的索引结果值是可寻址的。由于不论怎样,每一个切片值都会持有一个底层数组,而这个底层数组中的每一个元素值都是有一个确切的内存地址的。
我一直在说针对数组值、切片值或字典值的字面量的表达式会产生临时结果。若是针对的是数组类型或切片类型的变量,那么索引或切片的结果值就都不属于临时结果了,是可寻址的。
这主要由于变量的值自己就不是“临时的”。对比而言,值字面量在尚未与任何变量(或者说任何标识符)绑定以前是没有落脚点的,咱们没法以任何方式引用到它们。这样的值就是“临时的”。
再说一个例外。咱们经过对字典类型的变量施加索引表达式,获得的结果值不属于临时结果,但是,这样的值倒是不可寻址的。缘由是,字典中的每一个键 - 元素对的存储位置均可能会变化,并且这种变化外界是没法感知的。典中总会有若干个哈希桶用于均匀地储存键 - 元素对。当知足必定条件时,字典可能会改变哈希桶的数量,并适时地把其中的键 - 元素对搬运到对应的新的哈希桶中。
在这种状况下,获取字典中任何元素值的指针都是无心义的,也是不安全的。咱们不知道何时那个元素值会被搬运到何处,也不知道原先的那个内存地址上还会被存放什么别的东西。因此,这样的值就应该是不可寻址的。
第三个关键词:不安全的。“不安全的”操做极可能会破坏程序的一致性,引起不可预知的错误,从而严重影响程序的功能和稳定性。
函数在 Go 语言中是一等公民,因此咱们能够把表明函数或方法的字面量或标识符赋给某个变量、传给某个函数或者从某个函数传出。可是,这样的函数和方法都是不可寻址的。一个缘由是函数就是代码,是不可变的。
另外一个缘由是,拿到指向一段代码的指针是不安全的。此外,对函数或方法的调用结果值也是不可寻址的,这是由于它们都属于临时结果。
总结一下
不可变的值不可寻址。常量、基本类型的值字面量、字符串变量的值、函数以及方法的字面量都是如此。其实这样规定也有安全性方面的考虑。
绝大多数被视为临时结果的值都是不可寻址的。算术操做的结果值属于临时结果,针对值字面量的表达式结果值也属于临时结果。但有一个例外,对切片字面量的索引结果值虽然也属于临时结果,但倒是可寻址的。
若拿到某值的指针可能会破坏程序的一致性,那么就是不安全的,该值就不可寻址。因为字典的内部机制,对字典的索引结果值的取址操做都是不安全的。另外,获取由字面量或标识符表明的函数或方法的地址显然也是不安全的。
最后,若是咱们把临时结果赋给一个变量,那么它就是可寻址的了。如此一来,取得的指针指向的就是这个变量持有的那个值了。
package main type Named interface { // Name 用于获取名字。 Name() string } type Dog struct { name string } func (dog *Dog) SetName(name string) { dog.name = name } func (dog Dog) Name() string { return dog.name } func main() { // 示例1。 const num = 123 //_ = &num // 常量不可寻址。 //_ = &(123) // 基本类型值的字面量不可寻址。 var str = "abc" _ = str //_ = &(str[0]) // 对字符串变量的索引结果值不可寻址。 //_ = &(str[0:2]) // 对字符串变量的切片结果值不可寻址。 str2 := str[0] _ = &str2 // 但这样的寻址就是合法的。 //_ = &(123 + 456) // 算术操做的结果值不可寻址。 num2 := 456 _ = num2 //_ = &(num + num2) // 算术操做的结果值不可寻址。 //_ = &([3]int{1, 2, 3}[0]) // 对数组字面量的索引结果值不可寻址。 //_ = &([3]int{1, 2, 3}[0:2]) // 对数组字面量的切片结果值不可寻址。 _ = &([]int{1, 2, 3}[0]) // 对切片字面量的索引结果值倒是可寻址的。 //_ = &([]int{1, 2, 3}[0:2]) // 对切片字面量的切片结果值不可寻址。 //_ = &(map[int]string{1: "a"}[0]) // 对字典字面量的索引结果值不可寻址。 var map1 = map[int]string{1: "a", 2: "b", 3: "c"} _ = map1 //_ = &(map1[2]) // 对字典变量的索引结果值不可寻址。 //_ = &(func(x, y int) int { // return x + y //}) // 字面量表明的函数不可寻址。 //_ = &(fmt.Sprintf) // 标识符表明的函数不可寻址。 //_ = &(fmt.Sprintln("abc")) // 对函数的调用结果值不可寻址。 dog := Dog{"little pig"} _ = dog //_ = &(dog.Name) // 标识符表明的函数不可寻址。 //_ = &(dog.Name()) // 对方法的调用结果值不可寻址。 //_ = &(Dog{"little pig"}.name) // 结构体字面量的字段不可寻址。 //_ = &(interface{}(dog)) // 类型转换表达式的结果值不可寻址。 dogI := interface{}(dog) _ = dogI //_ = &(dogI.(Named)) // 类型断言表达式的结果值不可寻址。 named := dogI.(Named) _ = named //_ = &(named.(Dog)) // 类型断言表达式的结果值不可寻址。 var chan1 = make(chan int, 1) chan1 <- 1 //_ = &(<-chan1) // 接收表达式的结果值不可寻址。 }
固然是没法使用取址操做符&获取它们的指针了
结构体类型Dog为例
为它编写一个函数New。这个函数会接受一个名为name的string类型的参数,并会用这个参数初始化一个Dog类型的值,最后返回该值。我如今要问的是:若是我调用该函数,并直接以链式的手法调用其结果值的指针方法SetName,那么能够达到预期的效果吗?
若是你还记得我在前面讲述的内容,那么确定会知道调用New函数所获得的结果值属于临时结果,是不可寻址的。
别忘了,我在讲结构体类型及其方法的时候还说过,咱们能够在一个基本类型的值上调用它的指针方法,这是由于 Go 语言会自动地帮咱们转译。
更具体地说,对于一个Dog类型的变量dog来讲,调用表达式dog.SetName("monster")会被自动地转译为(&dog).SetName("monster"),即:先取dog的指针值,再在该指针值上调用SetName方法。
于New函数的调用结果值是不可寻址的,因此没法对它进行取址操做。所以,上边这行链式调用会让编译器报告两个错误,一个是果,即:不能在New("little pig")的结果值上调用指针方法。一个是因,即:不能取得New("little pig")的地址。
咱们都知道,Go 语言中的++和--并不属于操做符,而分别是自增语句和自减语句的重要组成部分。
Go 语言规范中的语法定义是,只要在++或--的左边添加一个表达式,就能够组成一个自增语句或自减语句,可是,它还明确了一个很重要的限制,那就是这个表达式的结果值必须是可寻址的。这就使得针对值字面量的表达式几乎都没法被用在这里。
不过这有一个例外,虽然对字典字面量和字典变量索引表达式的结果值都是不可寻址的,可是这样的表达式却能够被用在自增语句和自减语句中。
与之相似的规则还有两个。一个是,在赋值语句中,赋值操做符左边的表达式的结果值必须可寻址的,可是对字典的索引结果值也是能够的。
另外一个是,在带有range子句的for语句中,在range关键字左边的表达式的结果值也都必须是可寻址的,不过对字典的索引结果值一样能够被用在这里。以上这三条规则咱们合并起来记忆就能够了。
与这些定死的规则相比,我刚刚讲到的那个与指针方法有关的问题,你须要好好理解一下,它涉及了两个知识点的联合运用。起码在我面试的时候,它是一个可选择的考点。
package main type Dog struct { name string } func New(name string) Dog { return Dog{name} } func (dog *Dog) SetName(name string) { dog.name = name } func (dog Dog) Name() string { return dog.name } func main() { // 示例1。 //New("little pig").SetName("monster") // 不能调用不可寻址的值的指针方法。 // 示例2。 map[string]int{"the": 0, "word": 0, "counter": 0}["word"]++ map1 := map[string]int{"the": 0, "word": 0, "counter": 0} map1["word"]++ }
前边的基础知识很重要。不过如今让咱们再次关注指针的用法。我说过,unsafe.Pointer是像*Dog类型的值这样的指针值和uintptr值之间的桥梁,那么咱们怎样利用unsafe.Pointer的中转和uintptr的底层操做来操纵像dog这样的值呢?
这是一项黑科技。它能够绕过 Go 语言的编译器和其余工具的重重检查,并达到潜入内存修改数据的目的。这并非一种正常的编程手段,使用它会很危险,颇有可能形成安全隐患。
咱们老是应该优先使用常规代码包中提供的 API 去编写程序,固然也能够把像reflect以及go/ast这样的代码包做为备选项。做为上层应用的开发者,请谨慎地使用unsafe包中的任何程序实体。
我先声明了一个Dog类型的变量dog,而后用取址操做符&,取出了它的指针值,并把它赋给了变量dogP。
最后,我使用了两个类型转换,先把dogP转换成了一个unsafe.Pointer类型的值,而后紧接着又把后者转换成了一个uintptr的值,并把它赋给了变量dogPtr。这背后隐藏着一些转换规则,以下:
一个指针值(好比*Dog类型的值)能够被转换为一个unsafe.Pointer类型的值,反之亦然。
一个uintptr类型的值也能够被转换为一个unsafe.Pointer类型的值,反之亦然。
一个指针值没法被直接转换成一个uintptr类型的值,反过来也是如此。
因此,对于指针值和uintptr类型值之间的转换,必须使用unsafe.Pointer类型的值做为中转。那么,咱们把指针值转换成uintptr类型的值有什么意义吗?
这里须要与unsafe.Offsetof函数搭配使用才能看出端倪。unsafe.Offsetof函数用于获取两个值在内存中的起始存储地址之间的偏移量,以字节为单位。
这两个值一个是某个字段的值,另外一个是该字段值所属的那个结构体值。咱们在调用这个函数的时候,须要把针对字段的选择表达式传给它,好比dogP.name。
有了这个偏移量,又有告终构体值在内存中的起始存储地址(这里由dogPtr变量表明),把它们相加咱们就能够获得dogP的name字段值的起始存储地址了。这个地址由变量namePtr表明。
此后,咱们能够再经过两次类型转换把namePtr的值转换成一个*string类型的值,这样就获得了指向dogP的name字段值的指针值。
你可能会问,我直接用取址表达式&(dogP.name)不就能拿到这个指针值了吗?干吗绕这么大一圈呢?你能够想象一下,若是咱们根本就不知道这个结构体类型是什么,也拿不到dogP这个变量,那么还能去访问它的name字段吗?
答案是,只要有namePtr就能够。它就是一个无符号整数,但同时也是一个指向了程序内部数据的内存地址。它可能会给咱们带来一些好处,好比能够直接修改埋藏得很深的内部数据。
可是,一旦咱们有意或无心地把这个内存地址泄露出去,那么其余人就可以肆意地改动dogP.name的值,以及周围的内存地址上存储的任何数据了。
即便他们不知道这些数据的结构也无所谓啊,改很差还改不坏吗?不正确地改动必定会给程序带来不可预知的问题,甚至形成程序崩溃。这可能仍是最好的灾难性后果;因此我才说,使用这种非正常的编程手段会很危险。
好了,如今你知道了这种手段,也知道了它的危险性,那就谨慎对待
总结
咱们今天集中说了说与指针有关的问题。基于基本类型的指针值应该是咱们最经常使用到的,也是咱们最须要关注的,好比*Dog类型的值。怎样获得一个这样的指针值呢?这须要用到取址操做和操做符&。
不过这里还有个前提,那就是取址操做的操做对象必须是可寻址的。关于这方面你须要记住三个关键词:不可变的、临时结果和不安全的。只要一个值符合了这三个关键词中的任何一个,它就是不可寻址的。
但有一个例外,对切片字面量的索引结果值是可寻址的。那么不可寻址的值在使用上有哪些限制呢?一个最重要的限制是关于指针方法的,即:没法调用一个不可寻址值的指针方法。这涉及了两个知识点的联合运用。
相比于刚说到的这些,unsafe.Pointer类型和uintptr类型的重要性好像就没那么高了。它们的值一样能够表明指针,而且比前面说的指针值更贴近于底层和内存。
虽然咱们能够利用它们去访问或修改一些内部数据,并且就灵活性而言,这种要比通用的方式高不少,可是这每每也会带来不容小觑的安全隐患。
package main import ( "fmt" "unsafe" ) type Dog struct { name string } func (dog *Dog) SetName(name string) { dog.name = name } func (dog Dog) Name() string { return dog.name } func main() { // 示例1。 dog := Dog{"little pig"} dogP := &dog dogPtr := uintptr(unsafe.Pointer(dogP)) namePtr := dogPtr + unsafe.Offsetof(dogP.name) nameP := (*string)(unsafe.Pointer(namePtr)) fmt.Printf("nameP == &(dogP.name)? %v\n", nameP == &(dogP.name)) fmt.Printf("The name of dog is %q.\n", *nameP) *nameP = "monster" fmt.Printf("The name of dog is %q.\n", dogP.name) fmt.Println() // 示例2。 // 下面这种不匹配的转换虽然不会引起panic,可是其结果每每不符合预期。 numP := (*int)(unsafe.Pointer(namePtr)) num := *numP fmt.Printf("This is an unexpected number: %d\n", num) }
go run demo37.go nameP == &(dogP.name)? true The name of dog is "little pig". The name of dog is "monster". This is an unexpected number: 17596893