本篇的知识点汇总
- 基本类型的指针值:需要用到取址操作
*和操作符&。- 可以使用👆1的前提是,取址操作的操作对象必须是可寻址的。
- 三个特点有其一:不可变的、临时结果or不安全的。则为不可寻址的操作对象。
- 3的表述中,存在意外,即对切片字面量的索引结果值是可寻址的。
- 对于不可寻址的值在使用上具体的限制,下面有具体案例。
- 关于
unsafe.Pointer类型和uintptr类型。unsafe.Pointer+uintptr可以突破私有成员访问 关于临时结果 各种对值字面量施加的表达式的求值结果都可以看做是临时结果。
- 用于获得某个元素的索引表达式。
- 用于获得某个切片(片段)的切片表达式。
- 用于访问某个字段的选择表达式。
- 用于调用某个函数或方法的调用表达式。
- 用于转换值的类型的类型转换表达式。
- 用于判断值的类型的类型断言表达式。
- 向通道发送元素值或从通道那里接收元素值的接收表达式。
这里一直在说针对数组值、切片值或字典值的字面量的表达式会产生临时结果。
如果针对的是数组类型或切片类型的变量,那么索引或切片的结果值就都不属于临时结果了,是可寻址的。
因为变量的值本身就不是“临时的”。
对比而言,值字面量在还没有与任何变量(或者说任何标识符)绑定之前是没有落脚点的,我们无法以任何方式引用到它们。这样的值就是“临时的”。
分析知识点1
type Dog struct {
name string
}
func (dog *Dog) SetName(name string) {
dog.name = name
}
d := Dog{"little pig"}
dogP := &d //dogP就是*Dog类型,这里属于类型推断,隐式
*Dog就是基本类型Dog的指针类型。对于Dog类型,声明该类型的变量d,若d的值不为nil,取址表达式
&d的结果就是该变量(d)的值(也就是基本值)的指针值。
func (dog *Dog) SetName(name string) {
dog.name = name
}
关于这个方法,接收者是当前基本值的指针值。我们可以通过指针值无缝地访问到基本值包含的任何字段,以及调用与之关联的任何方法。
Go语言中不可寻址的那些值(分析知识点2、3、4)
直接看demo
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 这里是为了让表达式被使用,防止warning
//_ = &num // 常量不可寻址。
//2基本类型值的字面量可以被视为常量,只是没有任何值可以代表
//_ = &(123) // 基本类型值的字面量不可寻址。
//3.算术操作的结果属于临时结果。
//_ = &(123 + 456) // 算术操作的结果值不可寻址。
num2 := 456
//_ = &(num + num2) // 算术操作的结果值不可寻址。
//4. 因为字符串值也是不可变的
var str = "abc"
//_ = &(str[0]) // 对字符串变量的索引结果值不可寻址。
//_ = &(str[0:2]) // 对字符串变量的切片结果值不可寻址。
str2 := str[0]
👉_ = &str2 // 但这样的寻址就是合法的,因为不再是临时结果,string--byte
//5.属于临时结果
//对切片字面量的索引结果值是可寻址的。
//因为不论怎样,每个切片值都会持有一个底层数组,而这个底层数组中的每个元素值都是有一个确切的内存地址的。
//对切片字面量的切片结果值是不可寻址的。
//这是因为切片表达式总会返回一个新的切片值,而这个新的切片值在被赋给变量之前属于临时结果。
//_ = &([3]int{1, 2, 3}[0]) // 对数组字面量的索引结果值不可寻址。
//_ = &([3]int{1, 2, 3}[0:2]) // 对数组字面量的切片结果值不可寻址。
👉_ = &([]int{1, 2, 3}[0]) // 对切片字面量的索引结果值却是可寻址的。
//_ = &([]int{1, 2, 3}[0:2]) // 对切片字面量的切片结果值不可寻址。
//6.不是临时结果,但是不可寻址。
//原因,字典中用哈希桶均匀存储键值对
//当满足一定条件时,字典可能会改变哈希桶的数量,并适时地把其中的键值对搬运到新的哈希桶中。并且这种变化外界无法感知。
//因此,在获取字典中任何元素值的指针都是无意义的,也是不安全的。
//不知道什么时候该元素值会被搬运到何处,也不知道原内存地址上还会被存放什么别的东西,不安全。
//_ = &(map[int]string{1: "a"}[0]) // 对字典字面量的索引结果值不可寻址。
var map1 = map[int]string{1: "a", 2: "b", 3: "c"}
//_ = &(map1[2]) // 对字典变量的索引结果值不可寻址。
//7.原因:一、函数就是代码,是不可变的。二、拿到指向一段代码的指针是不安全的。
//_ = &(func(x, y int) int {
// return x + y
//}) // 字面量代表的函数不可寻址。
//8.与7同理
//_ = &(fmt.Sprintf) // 标识符代表的函数不可寻址。
//_ = &(fmt.Sprintln("abc")) // 对函数的调用结果值不可寻址。
//9
dog := Dog{"little pig"}
//_ = &(dog.Name) // 标识符代表的函数不可寻址。
//_ = &(dog.Name()) // 对方法的调用结果值不可寻址。
//_ = &(Dog{"little pig"}.name) // 结构体字面量的字段不可寻址。
//10 临时结果
//_ = &(interface{}(dog)) // 类型转换表达式的结果值不可寻址。
dogI := interface{}(dog)
//_ = &(dogI.(Named)) // 类型断言表达式的结果值不可寻址。
named := dogI.(Named)
//_ = &(named.(Dog)) // 类型断言表达式的结果值不可寻址。
//11 临时结果
var chan1 = make(chan int, 1)
chan1 <- 1
//_ = &(<-chan1) // 接收表达式的结果值不可寻址。
}
解析知识点4
不可寻址的值在使用上有哪些限制?
- 无法使用取址操作符
&获取它们的指针,导致编译器报错。
func New(name string) Dog {
return Dog{name}
}
New("little pig").SetName("monster")
调用
New函数所得到的结果值属于临时结果,是不可寻址的。在一个基本类型的值上调用它的指针方法,这是因为 Go 语言会自动地帮我们转译。
具体地说
对于一个
Dog类型的变量dog来说,调用表达式dog.SetName("monster")会被自动地转译为(&dog).SetName("monster"),即:先取dog的指针值,再在该指针值上调用SetName方法。由于
New函数的调用结果值是不可寻址的,所以无法对它进行取址操作。因此,上边这行链式调用会让编译器报告两个错误,一个是果,即:不能在New("little pig")的结果值上调用指针方法。一个是因,即:不能取得New("little pig")的地址。
积累几个GO语言中定死的规则
Go 语言中的
++和--并不属于操作符,而分别是自增语句和自减语句的重要组成部分。虽然 Go 语言规范中的语法定义是,只要在++或--的左边添加一个表达式,就可以组成一个自增语句或自减语句,但是,它还明确了一个很重要的限制,那就是这个表达式的结果值必须是可寻址的。这就使得针对值字面量的表达式几乎都无法被用在这里。不过这有一个例外,虽然对字典字面量和字典变量索引表达式的结果值都是不可寻址的,但是这样的表达式却可以被用在自增语句和自减语句中。在赋值语句中,赋值操作符左边的表达式的结果值必须可寻址的,但是对字典的索引结果值也是可以的。
在带有
range子句的for语句中,在range关键字左边的表达式的结果值也都必须是可寻址的,不过对字典的索引结果值同样可以被用在这里。
分析知识点6
在 Go 语言中还有其他几样东西可以代表“指针”。
最贴近传统意义指针的---
uintptr类型。该类型实际上是一个数值类型,也是 Go 语言内建的数据类型之一。根据当前计算机的计算架构的不同,它可以存储 32 位或 64 位的无符号整数,可以代表任何指针的位(bit)模式,也就是原始的内存地址。Go 语言标准库中的
unsafe包中有一个类型叫做Pointer,也代表了“指针”。unsafe.Pointer可以表示任何指向可寻址的值的指针,同时它也是指针值和uintptr值之间的桥梁。通过它,可以在这两种值之上进行双向的转换。 怎样通过unsafe.Pointer操纵可寻址的值?
unsafe.Pointer是像*Dog类型的指针值和uintptr值之间的桥梁。 所以利用unsafe.Pointer的中转和uintptr的底层操作来操纵类似dog这样的值。这是一项黑科技。它可以绕过 Go 语言的编译器和其他工具的重重检查,并达到潜入内存修改数据的目的。这并不是一种正常的编程手段,使用它会很危险,很有可能造成安全隐患。
正常思路:应该优先使用常规代码包中提供的 API 去编写程序,当然也可以把像
reflect以及go/ast这样的代码包作为备选项。作为上层应用的开发者,需要谨慎地使用unsafe包中的任何程序实体。 demo
dog := Dog{"little pig"}
dogP := &dog
dogPtr := uintptr(unsafe.Pointer(dogP))
- 声明了一个
Dog类型的变量dog- 用取址操作符
&,取出了它的指针值,并把它赋给了变量dogP。- 使用两个类型转换,
dogPtounsafe.Pointer类型,unsafe.Pointer类型 touintptr类型,最后赋给变量dogPtr。 转换规则- 一个指针值(比如
*Dog类型的值)可以被转换为一个unsafe.Pointer类型的值,反之亦然。- 一个
uintptr类型的值也可以被转换为一个unsafe.Pointer类型的值,反之亦然。- 一个指针值无法被直接转换成一个
uintptr类型的值,反过来也是如此。 转换的意义
dog := Dog{"little pig"}
dogP := &dog
dogPtr := uintptr(unsafe.Pointer(dogP))
namePtr := dogPtr + unsafe.Offsetof(dogP.name)
nameP := (*string)(unsafe.Pointer(namePtr))
what
unsafe.Offsetof函数,用于获取两个值在内存中的起始存储地址之间的偏移量,以字节为单位。
一个值是某字段的值
另一个是该字段值所属的那个结构体值。 how 调用这个函数时,需要把针对字段的选择表达式传给它,比如
dogP.name。有了这个偏移量,又有了结构体值在内存中的起始存储地址(这里由
dogPtr变量代表),把它们相加我们就可以得到dogP的name字段值的起始存储地址了。这个地址由变量namePtr代表。此后,我们可以再通过两次类型转换把
namePtr的值转换成一个*string类型的值,这样就得到了指向dogP的name字段值的指针值。
你可能会问,我直接用取址表达式&(dogP.name)不就能拿到这个指针值了吗?干嘛绕这么大一圈呢?
可以想象一下,如果我们根本就不知道这个结构体类型是什么,也拿不到
dogP这个变量,那么还能去访问它的name字段吗?答案是,只要有
namePtr就可以。它就是一个无符号整数,但同时也是一个指向了程序内部数据的内存地址。它可能会给我们带来一些好处,比如可以直接修改埋藏得很深的内部数据。
但是,一旦把这个内存地址泄露出去,那么其他人就能够肆意地改动
dogP.name的值,以及周围的内存地址上存储的任何数据了。即使他们不知道这些数据的结构也无所谓,不正确地改动一定会给程序带来不可预知的问题,甚至造成程序崩溃。造成灾难性后果。
思考题
引用类型的值的指针值是有意义的吗?如果没有意义,为什么?如果有意义,意义在哪里?
从存储和传递的角度看,没有意义。因为引用类型的值已经相当于指向某个底层数据结构的指针了。
指针的指针?好像没什么使用的价值