内容简介:之前已经用到过很多次指针了,不过大多数时候是指指针类型及其对应的指针值。这里要讲更为深入的内容。从传统意义上说,指针是一个指向某个确切的内存地址的值。这个内存地址可以是任何数据或代码的起始地址,比如,某个变量、某个字段或某个函数。uintptr在Go语言中还有其他几样东西可以代表“指针”。其中最贴近传统意义的当属uintptr类型了。该类型实际上是一个数值类型,也是Go语言内建的数据类型之一。根据当前计算机的计算架构的不同,它可以存储32位或64位的无符号整数,可以代表任何指针的位(bit)模式,也就是原始
指针
之前已经用到过很多次指针了,不过大多数时候是指指针类型及其对应的指针值。这里要讲更为深入的内容。
其他指针
从传统意义上说,指针是一个指向某个确切的内存地址的值。这个内存地址可以是任何数据或代码的起始地址,比如,某个变量、某个字段或某个函数。
uintptr在 Go 语言中还有其他几样东西可以代表“指针”。其中最贴近传统意义的当属uintptr类型了。该类型实际上是一个数值类型,也是Go语言内建的数据类型之一。根据当前计算机的计算架构的不同,它可以存储32位或64位的无符号整数,可以代表任何指针的位(bit)模式,也就是原始的内存地址。
unsafe.Pointer在Go语言标准库中的unsafe包。unsafe包中有一个类型叫做Pointer,也代表了“指针”。unsafe.Pointer可以表示任何指向可寻址的值的指针,同时它也是前面提到的指针值和uintptr值之间的桥梁。也就是说,通过它,我们可以在这两种值之上进行双向的转换。这里有一个很关键的词——可寻址的(addressable)。在我们继续说unsafe.Pointer之前,需要先要搞清楚这个词的确切含义。
不可寻址的值
一下的值都是不可寻址的:
- 常量的值
- 基本类型值的字面量
- 算数操作的结果值
- 对各种字面量的索引表达式和切片表达式的结果值。例外,切片字面量的索引结果值是可寻址的
- 对字符串变量的索引表达式和切片表达式的结果值
- 对字典变量的索引表达式的结果值
- 函数字面量和方法字面量,以及对他们的调用表达式的结果值
- 结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值
- 类型转换表达式的结果值
- 类型断言表达式的结果值
- 接收表达式的结果值
上面一堆术语,看看在代码里具体指的是哪些类型:
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) // 接收表达式的结果值不可寻址。 }
总结一个不可寻址的值的特点:
- 不可变的 值不可寻址。常量、基本类型的值字面量、字符串变量的值、函数以及方法的字面量都是如此。其实这样规定也有安全性方面的考虑。
- 绝大多数被视为 临时结果 的值都是不可寻址的。算术操作的结果值属于临时结果,针对值字面量的表达式结果值也属于临时结果。但有一个例外,对切片字面量的索引结果值虽然也属于临时结果,但却是可寻址的。
- 若拿到某值的指针可能会破坏程序的一致性,那么就是 不安全的 ,该值就不可寻址。由于字典的内部机制,对字典的索引结果值的取址操作都是不安全的。另外,获取由字面量或标识符代表的函数或方法的地址显然也是不安全的。
最后,如果把临时结果赋值给一个变量,那么它就是可寻址的了。
不可寻址的值的限制无法使用取址操作符&获取他们的指针。如果尝试取址会是编译器报错,所以不用太担心。这里再看个小问题:
package main import "fmt" type Dog struct { name string } func (d *Dog) SetName (name string) { d.name = name } func New(name string) Dog { return Dog{name} } func main() { obj := New("Snoopy") obj.SetName("Goofy") fmt.Println(obj.name) // New("Snoopy").SetName("Wishbone") // }
这里写了一个New函数,用于获取Dog的结构体。返回的是结构体的值类型。还有一个指针方法,这里直接对值类型调用指针方法是没有问题的。因为会被自动转译成 (&dog).SetName("Goofy")
。但是New函数的调用结果值是不可寻址的,所以最后一行尝试直接以链式的方法调用就会有编译问题。这个不可取址的情况应该是属于临时结果,所以把结果赋值给一个变量,再调用指针方法是没有问题的。
另外,在Go语言中++和--不属于操作符,而是自增语句或自减语句的组成部分。只要在++或--的左边添加一个表达式,就组成了一个自增语句或自减语句,但是表达式的结果值必须是可寻址的。比如值字面的表达式就是无法自增的 1++
。
这里也有例外,字典字面量和字典变量索引表达式的结果值都是不可寻址的,但是可以自增、自减。
类似的规则还有两个:
- 赋值语句,赋值操作符左边的表达式的结果值必须是可寻址的。但是对字典的索引结果值也是赋值
- 带有range子句的for语句中,在range关键字左边的表达式的结果值也必须是可寻址的。还是对字典的索引结果值例外。
unsafe.Pointer黑科技
下面讲的方法,可以绕过Go语言的编译器和其他 工具 的重重检查,并达到潜入内存修改数据的目的。这不是一种正常的手段,使用它会很危险,还很可能造成安全隐患。我们总是应该优先使用常规代码包中提供的API去编写程序,当然也可以把像reflect以及go/ast这样的代码包作为备选项。
指针值、unsafe.Pointer、uintptr有如下的转换规则:
- 指针值和unsafe.Pointer可以互相转换
- uintptr和unsafe.Pointer也可以互相转换
- 指针值和uintptr无法直接互相转换
所以说unsafe.Pointer是指针值和uintptr值之间的桥梁。到这一步,我们现在已经可以获取到变量的uintptr类型的值了:
s := student{} sP := &s sPtr := uintptr(unsafe.Pointer(sP))
unsafe.Offsetof 的使用unsafe.Offsetof函数返回变量(struct类型)指定属性的偏移量,以字节为单位。如下使用就可以获取到结构体的属性相对于结构体的偏移量了:
func main() { type student struct { name string age int } s1 := student{} p1 := unsafe.Offsetof((&s1).name) // 结构体的第一个变量,偏移量是0 p2 := unsafe.Offsetof((&s1).age) // 这里就会有偏移量了 fmt.Println(p1, p2) }
搭配使用获取属性的地址简单的把结构体的地址和属性的偏移量相加,就能获得属性的地址了。获取到了属性的地址后,如果再对这个地址做两次地址转换,就变回属性的指针值了:
package main import ( "unsafe" "fmt" ) func main() { type student struct { name string age int } s1 := student{"Adam", 18} s1P := &s1 s1Ptr := uintptr(unsafe.Pointer(s1P)) // 结构体的地址 fmt.Println(s1Ptr) namePtr := s1Ptr + unsafe.Offsetof(s1P.name) // name属性的地址 agePtr := s1Ptr + unsafe.Offsetof(s1P.age) // age属性的地址 fmt.Println(namePtr, agePtr) nameP := (*string)(unsafe.Pointer(namePtr)) // 获取到属性的指针 ageP := (*int)(unsafe.Pointer(agePtr)) fmt.Println(*nameP, *ageP) // 取值获取到属性指针的值 }
上面的方法,饶了一大圈就是为了获取到结构体里属性的地址。有了地址就可以对操作,也就可以直接修改埋藏的很深的内部数据了。比如可以直接结果别的包里的结构体内的不可导出的属性值。
修改结构体不可导出的属性值知识点都在上面了,这里直接试着修改别的包的结构体内的不可导出的属性的值:
// article15/example06/model/s.go package model // 结构体属性全小写 type Student struct { name string age int } // article15/example06/main.go package main import ( "Go36/article15/example06/model" "fmt" "unsafe" ) func main() { s1 := model.Student{} s1P := &s1 s1Ptr := uintptr(unsafe.Pointer(s1P)) namePtr := s1Ptr + 0 agePtr := s1Ptr + 16 nameP := (*string)(unsafe.Pointer(namePtr)) ageP := (*int)(unsafe.Pointer(agePtr)) *nameP = "Adam" *ageP = 22 fmt.Println(s1) }
这里unsafe.Pointer类型和uintptr类型所代表指针更贴近于底层和内存,理论上可以利用它们去访问或修改一些内部数据。但是这么用会带来安全隐患,在很多时候,使用它们操纵数据是弊大于利的。总之知道就行了,别这么用。
以上所述就是小编给大家介绍的《Go36-15-指针》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- NULL 指针、零指针、野指针
- 将数组和矩阵传递给函数,作为C中指针的指针和指针
- C语言指针数组和数组指针
- python(函数指针和类函数指针)
- C++ 基类指针和派生类指针之间的转换
- golang的值类型,指针类型和引用类型&值传递&指针传递
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
未来世界的幸存者
阮一峰 / 人民邮电出版社 / 2018-6-1 / 39.00 元
本书为阮一峰博客文集,主要收录的是作者对技术变革的影响的一些思考,希望能够藉此书让读者意识到世界正在剧烈变化,洪水就在不远处,从而早早准备出路。本书适合所有乐于思考的读者。一起来看看 《未来世界的幸存者》 这本书的介绍吧!