Go语言36讲笔记--15指针的使用+可寻址的概念解析

558 阅读11分钟

本篇的知识点汇总

  1. 基本类型的指针值:需要用到取址操作*和操作符&
  2. 可以使用👆1的前提是,取址操作的操作对象必须是可寻址的。
  3. 三个特点有其一:不可变的临时结果or不安全的。则为不可寻址的操作对象。
  4. 3的表述中,存在意外,即对切片字面量的索引结果值是可寻址的。
  5. 对于不可寻址的值在使用上具体的限制,下面有具体案例。
  6. 关于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

不可寻址的值在使用上有哪些限制?

  1. 无法使用取址操作符&获取它们的指针,导致编译器报错。
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语言中定死的规则

  1. Go 语言中的++--并不属于操作符,而分别是自增语句和自减语句的重要组成部分。虽然 Go 语言规范中的语法定义是,只要在++--左边添加一个表达式,就可以组成一个自增语句或自减语句,但是,它还明确了一个很重要的限制,那就是这个表达式的结果值必须是可寻址的。这就使得针对值字面量的表达式几乎都无法被用在这里。不过这有一个例外,虽然对字典字面量字典变量索引表达式的结果值都是不可寻址的,但是这样的表达式却可以被用在自增语句和自减语句中。

  2. 在赋值语句中,赋值操作符左边的表达式的结果值必须可寻址的,但是对字典的索引结果值也是可以的。

  3. 在带有range子句的for语句中,在range关键字左边的表达式的结果值也都必须是可寻址的,不过对字典的索引结果值同样可以被用在这里。


分析知识点6

在 Go 语言中还有其他几样东西可以代表“指针”。

  1. 最贴近传统意义指针的---uintptr类型。该类型实际上是一个数值类型,也是 Go 语言内建的数据类型之一。根据当前计算机的计算架构的不同,它可以存储 32 位或 64 位的无符号整数,可以代表任何指针的位(bit)模式,也就是原始的内存地址。

  2. 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))
  1. 声明了一个Dog类型的变量dog
  2. 用取址操作符&,取出了它的指针值,并把它赋给了变量dogP
  3. 使用两个类型转换,dogP to unsafe.Pointer类型, unsafe.Pointer类型 to uintptr类型,最后赋给变量dogPtr。 转换规则
  4. 一个指针值(比如*Dog类型的值)可以被转换为一个unsafe.Pointer类型的值,反之亦然。
  5. 一个uintptr类型的值也可以被转换为一个unsafe.Pointer类型的值,反之亦然。
  6. 一个指针值无法被直接转换成一个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变量代表),把它们相加我们就可以得到dogPname字段值的起始存储地址了。这个地址由变量namePtr代表。

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

你可能会问,我直接用取址表达式&(dogP.name)不就能拿到这个指针值了吗?干嘛绕这么大一圈呢?

可以想象一下,如果我们根本就不知道这个结构体类型是什么,也拿不到dogP这个变量,那么还能去访问它的name字段吗?

答案是,只要有namePtr就可以。

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

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

即使他们不知道这些数据的结构也无所谓,不正确地改动一定会给程序带来不可预知的问题,甚至造成程序崩溃。造成灾难性后果。

思考题

引用类型的值的指针值是有意义的吗?如果没有意义,为什么?如果有意义,意义在哪里?

从存储和传递的角度看,没有意义。因为引用类型的值已经相当于指向某个底层数据结构的指针了。

指针的指针?好像没什么使用的价值