Go语言核心36讲(Go语言进阶技术九)--学习笔记 (4)

我们总是应该优先使用常规代码包中提供的 API 去编写程序,当然也可以把像reflect以及go/ast这样的代码包作为备选项。作为上层应用的开发者,请谨慎地使用unsafe包中的任何程序实体。

不过既然说到这里了,我们还是要来一探究竟的。请看下面的代码:

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类型的值,反过来也是如此。

所以,对于指针值和uintptr类型值之间的转换,必须使用unsafe.Pointer类型的值作为中转。那么,我们把指针值转换成uintptr类型的值有什么意义吗?

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

这里需要与unsafe.Offsetof函数搭配使用才能看出端倪。unsafe.Offsetof函数用于获取两个值在内存中的起始存储地址之间的偏移量,以字节为单位。

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

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

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

你可能会问,我直接用取址表达式&(dogP.name)不就能拿到这个指针值了吗?干嘛绕这么大一圈呢?你可以想象一下,如果我们根本就不知道这个结构体类型是什么,也拿不到dogP这个变量,那么还能去访问它的name字段吗?

答案是,只要有namePtr就可以。它就是一个无符号整数,但同时也是一个指向了程序内部数据的内存地址。它可能会给我们带来一些好处,比如可以直接修改埋藏得很深的内部数据。

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

即使他们不知道这些数据的结构也无所谓啊,改不好还改不坏吗?不正确地改动一定会给程序带来不可预知的问题,甚至造成程序崩溃。这可能还是最好的灾难性后果;所以我才说,使用这种非正常的编程手段会很危险。

好了,现在你知道了这种手段,也知道了它的危险性,那就谨慎对待,防患于未然吧。

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) } 总结

我们今天集中说了说与指针有关的问题。基于基本类型的指针值应该是我们最常用到的,也是我们最需要关注的,比如*Dog类型的值。怎样得到一个这样的指针值呢?这需要用到取址操作和操作符&。

不过这里还有个前提,那就是取址操作的操作对象必须是可寻址的。关于这方面你需要记住三个关键词:不可变的、临时结果和不安全的。只要一个值符合了这三个关键词中的任何一个,它就是不可寻址的。

但有一个例外,对切片字面量的索引结果值是可寻址的。那么不可寻址的值在使用上有哪些限制呢?一个最重要的限制是关于指针方法的,即:无法调用一个不可寻址值的指针方法。这涉及了两个知识点的联合运用。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgyxyy.html