学过 C 的朋友应该知道,有一种类型是指针类型,指针类型存储的是一个内存地址,经过这个内存地址能够找到它指向的变量。 go 虽然是一种高级言语,可是也仍是给开发者供给了指针的类型 unsafe.Pointer,咱们能够经过它来直接读写变量的内存。 正由于如此,假如咱们操作不当,极有或许会导致程序崩溃。今天就来了解一下 unsafe 里所能供给的关于指针的一些功能, 以及运用 unsafe.Pointer 的一些留意事项。

内存里边的二进制数据表明什么?

咱们知道,计算机存储数据的时分是以二进制的办法存储的,当然,内存里边存储的数据也是二进制的。二进制的 01 自身其实并没有什么特殊的含义。

它们的具体含义完全取决于咱们怎样去了解它们,比方 0010 0000,假如咱们将其看作是一个十进制数字,那么它便是 32, 假如咱们将其看作是字符,那么他便是一个空格(具体可参阅 ASCII 码表)。

对应到编程言语层面,其实咱们的变量存储在内存里边也是 01 表明的二进制,这些二进制数表明是什么类型都是言语层面的事, 更准确来说,是编译器来处理的,咱们写代码的时分将变量声明为整数,那么咱们取出来的时分也会表明成一个整数。

这跟本文有什么关系呢?咱们下面会讲到许多关于类型转化的内容,假如咱们了解了这一节说的内容,下面的内容会更容易了解

在咱们做类型转化的时分,实践上底层的二进制表明是没有变的,变的仅仅咱们所看到的外表的东西。

内存布局

有点想直接开端讲 unsafe 里的 Pointer 的,可是假如读者对计算机内存怎样存储变量不太熟悉的话, 看起来或许会比较隐晦,所以在文章最初会花比较大的篇幅来叙述计算机是怎样存储数据的, 相信读完会再阅览后边的内容(比方指针的算术运算、经过指针修正结构体字段)会没有那么多障碍。

变量在内存中是怎样的?

咱们先来看一段代码:

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var a int8 = 1
	var b int16 = 2
	// unsafe.Sizeof() 能够获取存储变量需求的内存巨细,单位为字节
	// 输出:1 2
	// int8 意味着,用 8 位,也便是一个字节来存储整型数据
	// int16 意味着,用 16 位,也便是两个字节来存储整型数据
	fmt.Println(unsafe.Sizeof(a), unsafe.Sizeof(b))
}

在这段代码中咱们界说了两个变量,占用一个字节的 a 和占用两个字节的 b,在内存中它们大约如下图:

深入理解 go unsafe

咱们能够看到,在图中,a 存储在低地址,占用一个字节,而 b 存储在 a 相邻的当地,占用两个字节。

结构体在内存中是怎样的?

咱们再来看看结构体在内存中的存储:

package main
import (
	"fmt"
	"unsafe"
)
type Person struct {
	age   int8
	score int8
}
func main() {
	var p Person
	// 输出:2 1 1
	// 意味着 p 占用两个字节,
	// 其间 age 占用一个字节,score 占用一个字节
	fmt.Println(unsafe.Sizeof(p), unsafe.Sizeof(p.age), unsafe.Sizeof(p.score))
}

这段代码中,咱们界说了一个 Person 结构体,其间两个字段 agescore 都是 int8 类型,都是只占用一个字节的,它的内存布局大约如下图:

深入理解 go unsafe

咱们能够看到,在内存中,结构体字段是占用了内存中接连的一段存储空间的,具体来说是占用了接连的两个字节。

指针在内存中是怎样存储的?

在下面的代码中,咱们界说了一个 a 变量,巨细为 1 字节,然后咱们界说了一个指向 a 的指针 p

需求先阐明的是,下面有两个操作符,一个是 &,这个是取地址的操作符,var p = &a 意味着,获得 a 的内存地址,将其存储在变量 p 中, 另一个操作符是 *,这个操作符的意思是解指针,*p 便是经过 p 的地址获得 p 指向的内容(也便是 a)然后进行操作。 *p = 4 意味着,将 p 指向的 a 修正为 4。

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var a int8 = 3
	// ... 其他变量
	var p = &a
	fmt.Println(unsafe.Sizeof(p))
	fmt.Println(*p) // 3
	*p = 4
	fmt.Println(a) // 4
}

深入理解 go unsafe

需求留意的是,这儿边不再是一个单元格一个字节了,p(指针变量)是要占用 8 个字节的(这个跟机器有关,我的是 64 位的 CPU,所以是 8 个字节)。

从这个图,咱们能够得知,指针实践上存储的是一个内存地址,经过这个地址咱们能够找到它实践存储的内容。

结构体的内存布局真的是咱们上面说的那样吗?

上面咱们说了,下面这个结构体占用了两个字节,结构体里边的一个字段占用一个字节:

type Person struct {
	age   int8
	score int8
}

然后咱们再来看看下面这个结构体,它会占用多少字节呢?

type Person struct {
	age   int8
	score int16 // 类型由 int8 改为了 int16
}

也许咱们这个时分现已算好了 1 + 2 = 3,3 个字节不是吗?说实话,真的不是,它会占用 4 个字节, 这或许会有点反常理,可是这跟计算机的体系结构有着亲近的关系,先看具体的运转成果:

package main
import (
	"fmt"
	"unsafe"
)
type Person struct {
	age   int8
	score int16
}
func main() {
	var p Person
	// 输出:4 1 2
	// 意味着 p 占用 4 个字节,
	// 其间 age 占用 2 个字节,score 占用 2 个字节
	fmt.Println(unsafe.Sizeof(p), unsafe.Sizeof(p.age), unsafe.Sizeof(p.score))
}

为什么会这样呢?由于 CPU 运转的时分,需求从内存读取数据,而从内存取数据的进程是按字读取的,假如咱们数据的内存没有对齐, 则或许会导致 CPU 原本一次能够读取完的数据现在需求屡次读取,这样就会形成功率的下降。

关于内存对齐,是一个比较庞大的论题,这儿不展开了,咱们需求清晰的是,go 编译器会对咱们的结构体字段进行内存对齐。

内存对咱们的影响便是,它或许会导致结构体所占用的空间比它字段类型所需求的空间大(所以咱们做指针的算术运算的时分需求十分留意), 具体大多少其实咱们其实不需求知道,由于有办法能够知道,哪便是 unsafe.Offsetof,下面会说到。

uintptr 是什么意思?

在开端下文之前,仍是得烦琐一句,uintptr 这种命名办法是 C 言语里边的一种类型命名的常规, u 前缀表明是无符号数(unsigned),ptr 是指针(pointer)的缩写,这个 uintptr 按这个命名常规解析的话,便是一个指向无符号整数的指针。

别的,还有别的一种命名常规,便是在整型类型的后边加上一个表明占用 bit 数的数字,(1字节=8bit) 比方 int8 表明一个占用 8 位的整数,只能够存储 1 个字节的数据,然后 int64 表明的是一个 8 字节数(64位)。

unsafe 包界说的三个新类型

ArbitraryType

type ArbitraryType int,这个类型实践上是一个 int 类型,可是从名字上咱们能够看到,它被命名为恣意类型, 也便是说,他会被咱们用来表明恣意的类型,具体怎样用,是下面说的 unsafe.Pointer 用的。

IntegerType

type IntegerType int,它表明的是一个恣意的整数,在 unsafe 包中它被用来作为表明切片或许指针加减的长度。

Pointer

type Pointer *ArbitraryType,这个便是咱们上一节提到的指针了,它能够指向任何类型的数据(*ArbitraryType)。

内存地址实践上便是计算机内存的编号,是一个整数,所以咱们才能够运用 int 来表明指针。

unsafe 包计算内存的三个办法

这几个办法在咱们对内存进行操作的时分会十分有帮助,由于依据这几个办法,咱们才能够得知底层数据类型的实践巨细。

Sizeof

计算 x 所需求的内存巨细(单位为字节),假如其间包含了引证类型,Sizeof 不会计算引证指向的内容的巨细。

有几种常见的状况(没有涵盖悉数状况):

  • 根本类型,如 int8intSizeof 回来的是这个类型自身的巨细,如 unsafe.Sizeof(int8(x)) 为 1,由于 int8 只占用一个字节。
  • 引证类型,如 var x *intSizeof(x) 会回来 8(在我的机器上,不同机器或许不相同),别的就算引证指向了一个复合类型,比方结构体,回来的仍是 8(由于变量自身存储的仅仅内存地址)。
  • 结构体类型,假如是结构体,那么 Sizeof 回来的巨细包含了用于内存对齐的内存(所以或许会比结构体底层类型所需求的实践巨细要大)
  • 切片,Sizeof 回来的是 24(回来的是切片这个类型所需求占用空间的巨细,咱们需求知道,切片底层是 slice 结构体,里边三个字段分别是 array unsafe.Pointerlen intcap int,这三个字段所需求的巨细为 24)
  • 字符串,跟切片相似,Sizeof 会回来 16,由于字符串底层是一个用来存储字符串内容的 unsafe.Pointer 指针和一个表明长度的 int,所以是 16。

这个办法回来的巨细跟机器亲近相关,但一般开发者的电脑都是 64 位的,调用这个函数的值应该跟我的机器上得到的相同。

比方:

package main
import (
	"fmt"
	"unsafe"
)
type Person struct {
	age   int8
	score int16
}
type School struct {
	students []Person
}
func main() {
	var x int8
	var y int
	// 1 8
	// int8 占用 1 个字节,int 占用 8 个字节
	fmt.Println(unsafe.Sizeof(x), unsafe.Sizeof(y))
	var p *int
	// 8
	// 指针变量占用 8 个字节
	fmt.Println(unsafe.Sizeof(p))
	var person Person
	// 4
	// age 内存对齐需求 2 个字节
	// score 也需求两个字节
	fmt.Println(unsafe.Sizeof(person))
	var school School
	// 24
	// 只要一个切片字段,切片需求 24 个字节
	// 不论这个切片里边有多少数据,school 所需求占用的内存空间都是 24 字节
	fmt.Println(unsafe.Sizeof(school))
	var s string
	// 16
	// 字符串底层是一个 unsafe.Pointer 和一个 int
	fmt.Println(unsafe.Sizeof(s))
}

Offsetof 办法

这个办法用于计算结构体字段的内存地址相关于结构体内存地址的偏移。具体来说便是,咱们能够经过 &(取地址)操作符获取结构体地址。

实践上,结构体地址便是结构体中榜首个字段的地址。

拿到了结构体的地址之后,咱们能够经过 Offsetof 办法来获取结构体其他字段的偏移量,下面是一个比方:

package main
import (
	"fmt"
	"unsafe"
)
type Person struct {
	age   int8
	score int16
}
func main() {
	var person Person
	// 0 2
	// person.age 是榜首个字段,所以是 0
	// person.score 是第二个字段,由于需求内存对齐,实践上 age 占用了 2 个字节,
	// 因而 unsafe.Offsetof(person.score) 是 2,也便是说从第二个字节开端才是 person.score
	fmt.Println(unsafe.Offsetof(person.age), unsafe.Offsetof(person.score))
}

咱们上面也说了,编译器会对结构体做一些内存对齐的操作,这会导致结构体底层字段占用的内存巨细会比实践需求的巨细要大。 因而,咱们在取结构体字段地址的时分,最好是经过结构体地址加上 unsafe.Offsetof(x.y) 拿到的地址来操作。如下:

package main
import (
	"fmt"
	"unsafe"
)
type Person struct {
	age   int8
	score int16
}
func main() {
	var person = Person{
		age:   10,
		score: 20,
	}
	// {10 20}
	fmt.Println(person)
	// 获得 score 字段的指针
	// 经过结构体地址,加上 score 字段的偏移量,得到 score 字段的地址
	score := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&person)) + unsafe.Offsetof(person.score)))
	*score = 30
	// {10 30}
	fmt.Println(person)
}

这个比方看起来有点杂乱,可是不要紧,后边会具体展开的,这儿首要要阐明的是:

咱们经过 unsafe.Pointer 来操作结构体底层字段的时分,咱们是经过 unsafe.Offsetof 来获取结构体字段地址偏移量的, 由于咱们看到的类型巨细并不是内存实践占用的巨细,经过 Offsetof 拿到的成果是现已将内存对齐等因素考虑在内的了。 (假如咱们过错的认为 age 只占用一个字节,然后将 unsafe.Offsetof(person.score) 替换为 1,那么咱们就修正不了 score 字段了)

Alignof 办法

这个办法用以获取某一个类型的对齐系数,便是对齐一个类型的时分需求多少个字节。 这个对开发者而言意义不是十分大,go 里边只要 WaitGroup 用到了一下, 没有看到其他当地有用到这个办法,所以本文不展开了,有爱好的自行了解。

unsafe.Pointer 是什么?

让咱们再来回顾一下,Pointer 的界说是 type Pointer *ArbitraryType,也便是一个指向恣意类型的指针类型。 首要它是指针类型,所以咱们初始化 unsafe.Pointer 的时分,需求经过 & 操作符来将变量的地址传递进去。咱们能够将其幻想为指针类型的包装类型。

比方:

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var a int
	// 打印出 a 的地址:0xc0000240a8
	fmt.Println(unsafe.Pointer(&a))
}

unsafe.Pointer 类型转化

在运用 unsafe.Pointer 的时分,往往需求另一个类型来配合,那便是 uintptr,这个 uintptr 在文档里边的描述是: uintptr 是一种整数类型,其巨细足以包容任何指针的位方式。这儿的要害是 “任何指针”, 也便是说,它规划出来是被用来存储指针的,并且其巨细确保能存储下任何指针。

而咱们知道 unsafe.Pointer 也是表明指针,那么 uintptrunsafe.Pointer 有什么区别呢?

只需求记住最要害的一点,uintptr 是内存地址的整数表明,并且能够进行算术运算,而 unsafe.Pointer 除了能够表明一个内存地址之外,还能确保其指向的内存不会被废物收回器收回,可是 uintptr 这个地址不能确保其指向的内存不被废物收回器收回。

咱们先来看看与 unsafe.Pointer 相关的几种类型转化,这在咱们下文简直所有当地都会用到:

  • 任何类型的指针值都能转化为 unsafe.Pointer
  • unsafe.Pointer 能够转化为一个指向任何类型的指针值
  • unsafe.Pointer 能够转化为 uintptr
  • uintptr 能够转化为 unsafe.Pointer

比方(下面这个比方中输出的地址都是变量 a 所在的内存地址,都是相同的地址):

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var a int
	var p = &a
	// 1. int 类型指针转化为 unsafe.Pointer
	fmt.Println(unsafe.Pointer(p)) // 0xc0000240a8
	// 2. unsafe.Pointer 转化为一般类型的指针
	pointer := unsafe.Pointer(&a)
	var pp *int = (*int)(pointer) // 0xc0000240a8
	fmt.Println(pp)
	// 3. unsafe.Pointer 能够转化为 uintptr
	var p1 = uintptr(unsafe.Pointer(p))
	fmt.Printf("%x\n", p1) // c0000240a8,没有 0x 前缀
	// 4. uintptr 能够转化为 unsafe.Pointer
	p2 := unsafe.Pointer(p1)
	fmt.Println(p2) // 0xc0000240a8
}

怎么正确地运用指针?

指针答应咱们忽略类型体系而对恣意内存进行读写,这是十分风险的,所以咱们在运用指针的时分要分外的当心。

咱们运用 Pointer 的方式有以下几种,假如咱们不是依照以下方式来运用 Pointer 的话,那运用的办法很或许是无效的, 或许在将来变得无效,但就算是下面的几种运用方式,也有需求留意的当地。

运转 go vet 能够帮助查找不符合这些方式的 Pointer 的用法,但 go vet 没有正告也并不能确保代码有用。

以下咱们就来具体学习一下运用 Pointer 的几种正确的方式:

1. 将 *T1 转化为指向 *T2Pointer

前提条件:

  • T2 类型所需求的巨细不大于 T1 类型的巨细。(巨细大的类型转化为占用空间更小的类型)
  • T1T2 的内存布局相同。

这是由于假如直接将占用空间小的类型转化为占用空间更大的类型的话,多出来的部分是不确定的内容,当然咱们也能够经过 unsafe.Pointer 来修正这部分内容。

这种转化答应将一种类型的数据重新解释为别的一种数据类型。下面是一个比方(为了方便演示用了 int32int8 类型):

在这个比方中,int8 类型不大于 int32 类型,并且它们的内存布局是相同的,所以能够转化。

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var a int32 = 2
	// p 是 *int8 类型,由 *int32 转化而来
	var p = (*int8)(unsafe.Pointer(&a))
	var b int8 = *p
	fmt.Println(b) // 2
}

unsafe.Pointer(&a) 是指向 aunsafe.Pointer(本质上是指向 int32 的指针),(*int8) 表明类型转化,将这个 unsafe.Pointer 转化为 (*int8) 类型。

觉得代码欠好了解的能够看下图:

深入理解 go unsafe

在上图,咱们实践上是创建了一个指向了 a 最低位那 1 字节的指针,然后取出了这个字节里边存储的内容,将其存入了 b 中。

上面提到有一个比较重要的当地,那便是:转化的时分是占用空间大的类型,转化为占用空间小的类型,比方 int32int8 便是符合这个条件的, 那么假如咱们将一个小的类型转化为大的类型会发生什么呢?咱们来看看下面这个比方:

package main
import (
	"fmt"
	"unsafe"
)
type A struct {
	a int8
}
type B struct {
	b int8
	c int8
}
func main() {
	var a = A{1}
	var b = B{2, 3}
	// 1. 大转小
	var pa = (*A)(unsafe.Pointer(&b))
	fmt.Println(*pa) // {2}
	// 2. 过错示例:小转大(风险,A 里边 a 后边的内存其实是未知的)
	var pb = (*B)(unsafe.Pointer(&a))
	fmt.Println(*pb) // {1 2}
}

大转小:*B 转化为 *A 的具体转化进程能够表明为下图:

深入理解 go unsafe

在这个进程中,其实 ab 都没有改动,本质上咱们仅仅创建了一个 A 类型的指针, 这个指针指向变量 b 的地址(可是 *pa 会被看作是 A 类型),所以 pa 实践上是跟 b 共享了内存。 咱们能够尝试修正 (*pa).a = 3,咱们就会发现 b.b 也变成了 3。

也便是说,终究的内存布局是下图这样的:

深入理解 go unsafe

小转大:*A 转化为 *B 的具体转化进程能够表明为下图:

深入理解 go unsafe

留意:这是过错的用法。(当然也不是完全不行)

*A 转化为 *B 的进程中,由于 B 需求 2 个字节空间,所以咱们拿到的 pb 实践上是包含了 a 后边的 1 个字节, 可是这个字节原本是属于 b 变量的,这个时分 b*pb 都引证了第 2 个字节,这样依赖它们在修正这个字节的时分, 会相互影响,这或许不是咱们想要的成果,并且这种操作十分风险。

2. 将 Pointer 转化为 uintptr(但不转化回 Pointer

Pointer 转化为 uintptr 会得到 Pointer 指向的内存地址,是一个整数。这种 uintptr 的一般用途是打印它。

可是,uintptr 转化回 Pointer 一般无效uintptr 是一个整数,而不是一个引证。将指针转化为 uintptr 会创建一个没有指针语义的整数值。 即便 uintptr 持有某个目标的地址,假如该目标移动,废物搜集器也不会更新该 uintotr 的值, 也不会阻挠该目标被收回。

如下面这种,咱们获得了变量的地址 p,然后做了一些其他操作,最终再从这个地址里边读取数据:

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var a int = 10
	var p = uintptr(unsafe.Pointer(&a))
	// ... 其他代码
	// 下面这种转化是风险的,由于有或许 p 指向的目标现已被废物收回器收回
	fmt.Println(*(*int)(unsafe.Pointer(p)))
}

具体如下图:

深入理解 go unsafe

只要下面的方式中转化 uintptrPointer 是有用的。

3. 运用算术运算将 Pointer 转化为 uintptr 并转化回去

假如 p 指向一个已分配的目标,咱们能够将 p 转化为 uintptr 然后加上一个偏移量,再转化回 Pointer。如:

p = unsafe.Pointer(uintptr(p) + offset)

这种方式最常见的用法是拜访结构体或许数组元素中的字段:

// 等价于 f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))
// 等价于 e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + i*unsafe.Sizeof(x[0]))

关于榜首个比方,完好代码如下:

package main
import (
	"fmt"
	"unsafe"
)
type S struct {
	d int8
	f int8
}
func main() {
	var s = S{
		d: 1,
		f: 2,
	}
	f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))
	fmt.Println(*(*int8)(f)) // 2
}

终究的内存布局如下图(s 的两个字段都是 1 字节,所以图中 df 都是 1 字节):

深入理解 go unsafe

具体阐明一下:

榜首小节咱们说过了,结构体字段的内存布局是接连的。上面没有说的是,其实数组的内存布局也是接连的。这对了解下面的内容很有帮助。

  • &s 获得了结构体 s 的地址
  • unsafe.Pointer(&s) 转化为 Pointer 目标,这个指针目标指向的是结构体 s
  • uintptr(unsafe.Pointer(&s)) 获得 Pointer 目标的内存地址(整数)
  • unsafe.Offsetof(s.f) 获得了 f 字段的内存偏移地址(相对地址,相关于 s 的地址)
  • uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) 便是 s.f 的实践内存地址了(肯定地址)
  • 最终转化回 unsafe.Pointer 目标,这个目标指向的地址是 s.f 的地址

终究 f 指向的地址是 s.f,然后咱们能够经过 (*int8)(f)unsafe.Pointer 转化为 *int8 类型指针,最终经过 * 操作符获得这个指针指向的值。

关于第二个比方,完好代码如下:

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var x = [3]int8{4, 5, 6}
	e := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + 2*unsafe.Sizeof(x[0]))
	fmt.Println(*(*int8)(e)) // 6
}

终究的内存布局如下图,e 指向了数组的第 3 个元素(下标从 0 开端算的):

深入理解 go unsafe

代码中的 2 能够是其他任何有用的数组下标。

  • &s 获得了数组 x 的地址
  • unsafe.Pointer(&x) 转化为 Pointer 目标,这个指针目标指向的是数组 x
  • uintptr(unsafe.Pointer(&x)) 获得 Pointer 目标的内存地址(也便是 0xab
  • unsafe.Sizeof(x[0]) 是数组 x 里边每一个元素所需求的内存巨细,乘以 2 表明是元素 x[2] 的地址偏移量(相对地址,相关于 x[0] 的地址)
  • uintptr(unsafe.Pointer(&x)) + 2*unsafe.Sizeof(x[0]) 表明的是数组元素 x[2] 的实践内存地址(肯定地址)
  • 最终转化回 unsafe.Pointer 目标,这个目标指向的地址是 x[2] 的地址(也便是 0xab + 2)。

终究,咱们能够经过 (*int8)e 转化为 *int8 类型的指针,最终经过 * 操作符获取其指向的内容,也便是 6。

以这种办法对指针进行加减偏移量的运算都是有用的。(em…这儿说的是写在同一行的这种办法)。这种状况下运用 &^ 这两个操作符也是有用的(一般用于内存对齐)。 在所有状况下,得到的成果有必要指向原始分配的目标。

不像 C 言语,将指针加上一个超出其原始分配的内存区域的偏移量是无效的:

// 无效: end 指向了分配的空间以外的区域
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))

深入理解 go unsafe

下面临切片的这种操作也跟上图相似。

// 无效: end 指向了分配的空间以外的区域
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))

这是由于,内存的地址范围是 [start, end),是不包含终点的那个地址的,上面的 end 都指向了地址的鸿沟,这是无效的。 当然,除了鸿沟上,鸿沟以外都是无效的。(end 指向的内存不是属于那个变量的)

留意:两个转化(Pointer => uintptr, uintptr => Pointer)有必要出现在同一个表达式中,只要中心的算术运算:

// 无效: uintptr 在转化回 Pointer 之前不能存储在变量中
// 原因上面也说过了,便是 p 指向的内容或许会被废物收回器收回。
u := uintptr(p)
p = unsafe.Pointer(u + offset)

留意:指针有必要指向已分配的目标,因而它不能是 nil

// 无效: nil 指针转化
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)

4. 调用 syscall.Syscall 时将指针转化为 uintptr

觉得文字太烦琐能够直接看图:

深入理解 go unsafe

syscall 包中的 Syscall 函数将其 uintptr 参数直接传递给操作体系,然后操作体系能够依据调用的细节将其间一些参数重新解释为指针。 也便是说,体系调用完结隐式地将某些参数从 uintptr 转化回指针。

假如有必要将指针参数转化为 uintptr 以用作参数,则该转化有必要出现在调用表达式自身中:

syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

编译器经过组织被引证的分配目标(假如有的话)被保存,并且在调用完结之前不移动,来处理在调用程序会集完结的函数的参数列表中转化为 uintptr 的指针, 即便仅从类型来看,在调用期间好像不再需求目标。

为了使编译器辨认该方式,转化有必要出现在参数列表中:

// 无效:在体系调用期间隐式转化回指针之前,
// uintptr 不能存储在变量中。
u := uintptr(unsafe.Pointer(p))
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))

5. 将 reflect.Value.Pointerreflect.Value.UnsafeAddr 的成果从 uintptr 转化为 Pointer

reflect.ValuePointerUnsafeAddr 办法回来类型 uintptr 而不是 unsafe.Pointer, 从而防止调用者在未导入 unsafe 包的状况下将成果更改为恣意类型。(这是为了防止开发者对 Pointer 的误操作。) 可是,这也意味着这个回来的成果是软弱的,咱们有必要在调用之后当即转化为 Pointer(假如咱们确切的需求一个 Pointer):

其实便是为了让开发者清晰自己知道在干啥,要不然写出了 bug 都不知道。

// 在调用了 reflect.Value 的 Pointer 办法后,
// 当即转化为 unsafe.Pointer。
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

与上述状况相同,在转化之前存储成果是无效的:

// 无效: uintptr 在转化回 Pointer 之前不能保存在变量中
u := reflect.ValueOf(new(int)).Pointer() // uintptr 保存到了 u 中
p := (*int)(unsafe.Pointer(u))

原因上面也说了,由于 u 指向的内存是不受维护的,或许会被废物收回器搜集。

6. 将 reflect.SliceHeaderreflect.StringHeaderData 字段跟 Pointer 相互转化

与前面的状况相同,反射数据结构 SliceHeaderStringHeader 将字段 Data 声明为 uintptr, 以防止调用者在不首要导入 unsafe 的状况下将成果更改为恣意类型。 可是,这意味着 SliceHeaderStringHeader 仅在解析实践切片或字符串值的内容时有用。

咱们先来看看这两个结构体的界说:

// SliceHeader 是切片的运转时表明(内存布局跟切片共同)
// 它不能安全或可移植地运用,其表明方式或许会在以后的版本中更改。
// 此外,Data 字段不足以确保它引证的数据不会被废物收回器搜集,
// 因而程序有必要保存一个指向底层数据的单独的、正确类型的指针。
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}
// StringHeader 字符串的运转时表明(内存布局跟字符串共同)
// ... 其他留意事项跟 SliceHeader 相同
type StringHeader struct {
    Data uintptr
    Len  int
}

运用示例:

// 将字符串的内容修正为 p 指向的内容
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n

这种转化是有用的,由于 SliceHeader 的内存布局和 StringHeader 的内存布局共同,并且 SliceHeader 所占用的内存空间比 StringHeader 所占用内存空间大,也便是说,这是一种巨细更大的类型转化为巨细更小的类型,这会丢掉 SliceHeader 的一部分数据, 可是丢掉的那部分对咱们程序正常运转是没有任何影响的。

在这个用法中,hdr.Data 实践上是引证字符串头中的根底指针的另一种办法,而不是 uintptr 变量自身。 (咱们这儿也是运用了 uintptr 表达式,而不是一个存储了 uintptr 类型的变量)

一般来说,reflect.SliceHeaderreflect.StringHeader 一般用在指向实践切片或许字符串的 *reflect.SliceHeader*reflect.StringHeader永远不会被当作一般结构体运用。 程序不应该声明或许分配这些结构体类型的变量,下面的写法是有风险的。

// 无效: 直接声明的 Header 不会将 Data 作为引证
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // p 或许现已丢掉

Add 函数

函数原型是:func Add(ptr Pointer, len IntegerType) Pointer

这个函数的作用是,能够将 unsafe.Pointer 类型加上一个偏移量得到一个指向新地址的 unsafe.Pointer。 简略点来说,便是对 unsafe.Pointer 做算术运算的,上面咱们说过 unsafe.Pointer 是不能直接进行算术运算的, 因而需求先转化为 uintptr 然后再进行算术运算,算完再转化回 unsafe.Pointer 类型,所以会很繁琐。 有了 Add 办法,咱们能够写得简略一些,不必做 uintptr 的转化。

有了 Add,咱们能够简化一下上面那个经过数组指针加偏移量的比方,示例:

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var x = [3]int8{4, 5, 6}
	//e := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + 2*unsafe.Sizeof(x[0]))
	e := unsafe.Add(unsafe.Pointer(&x), 2 * unsafe.Sizeof(x[0]))
	fmt.Println(*(*int8)(e)) // 6
}

在这个比方中,咱们先是经过 unsafe.Pointer(&x) 获取到了一个指向 xunsafe.Pointer 目标, 然后经过 unsafe.Add 加上了 2 个 int8 类型巨细的偏移量,终究得到的是一个指向 x[2]unsafe.Pointer

Add 办法能够简化咱们对指针的一些操作。

Slice 函数

Slice 函数的原型是:func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

函数 Slice 回来一个切片,其底层数组以 ptr 最初,长度和容量为 len

unsafe.Slice(ptr, len) 等价于:

(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]

除了这个,作为一种特殊状况,假如 ptrnillen 为零,则 Slice 回来 nil

示例:

package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var x = [6]int8{4, 5, 6, 7, 8, 9}
	// 这儿取了数组榜首个元素 x[1] 的地址,
	// 从这个地址开端取了 3 个元素作为新的切片底层数组,
	// 回来这个新的切片
	s := unsafe.Slice(&x[1], 3)
	fmt.Println(s) // [5 6 7]
}

需求十分留意的是,榜首个参数实践上隐含传递了该地址对应的类型信息,上面用了 &x[1],传递的类型实践上是 int8

假如咱们依照下面这样写,得到的成果便是过错的,由于它隐式传递的类型是 [6]int8(这是一个数组),而不是 int8

// 过错示例:
package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var x = [6]int8{4, 5, 6, 7, 8, 9}
	// unsafe.Slice 榜首个参数接收到的类型是 [6]int,
	// 所以终究回来了一个切片,这个切片有三个元素,
	// 每一个元素都是长度为 6 数据类型为 int8 的数组。
	// 也即形如 [[6]int8, [6]int8, [6]int8] 的切片
	s := unsafe.Slice(&x, 3)
	// [[4 5 6 7 8 9] [91 91 52 32 53 32] [54 32 4 5 6 7]]
	fmt.Println(s)
}

这样显然不是咱们想要的成果,由于它读取到了一部分未知的内存,假如咱们修正这部分内存,或许会形成程序崩溃。

一个很常见的用法

在实践应用中,许多结构为了进步功能,在做 []bytestring 的切换的时分,往往会运用 unsafe.Pointer 来完结(比方 gin 结构):

下面这个比方完结了 []bytestring 的转化,并且避免了内存分配。这是由于,切片和字符串的内存布局是共同的,只不过切片比字符串占用 的空间多了一点,还有一个 cap 容量字段,用来表明切片的容量是多少。具体咱们能够再看看上面的 reflect.SliceHeaderreflect.StringHeader, 在下面这个字节切片到字符串的转化进程中,是从占用空间更大的类型转化为占用空间更小的类型,所以是安全的,丢掉的那个 cap 对咱们程序正常运转无影响。

先看看 []bytestring 的类型底层界说:

// 字符串
type stringStruct struct {
	str unsafe.Pointer
	len int
}
// 切片,比 string 的结构体多了一个 cap 字段,可是前面的两个字段是相同的
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

[]byte 转字符串的示例:

func BytesToString(b []byte) string {
	// 将 b 解析为字符串
	return *(*string)(unsafe.Pointer(&b))
}

这个操作如下图:

深入理解 go unsafe

在这个转化进程中,其实仅仅将 b 表明的类型转由 []byte 转化为了 string,之所以能够这么转, 是由于 []byte 的内存布局跟 string 的内存布局是相同的, 可是由于字符串实践占用空间比切片类型要小(不包括其底层指针指向的内容), 所以在转化进程中,cap 字段丢掉了,可是 strin 也不需求这个字段,所以对程序运转没影响。

同时字符串长度是依照字节计算的,所以字节切片和字符串的 len 字段是相同的,不需求做额外处理。

字符串转 []byte 的示例:

func StringToBytes(s string) []byte {
	return *(*[]byte)(unsafe.Pointer(
		// 界说匿名结构体变量,内存布局跟 []byte 共同,
		// 这样就能够转化为 []byte 了。
		&struct {
			string
			Cap int
		}{s, len(s)},
	))
}

这个操作如下图:

深入理解 go unsafe

这个进程仅仅需求分配很小一部分内存就能够完结了,功率比 go 自带的转化高。

go 里边字符串是不可变的,但 go 为了保持字符串不可变的特性,在字符串和字节切片之间转化一般都是经过数据拷贝的办法完结的。 由于这样就不会影响到本来的字符串或许字节切片了,可是这样做的功能会十分低。 具体可参阅 slicebytetostringstringtoslicebyte 函数,这两个函数坐落 runtime/string.go 中。

总结

本文首要讲了如下内容:

  • 内存布局:结构体的字段存储是占用了接连的一段内存,并且结构体或许会占用比实践需求空间更大的内存,由于需求对齐内存。
  • 指针存储了指向变量的地址,对这个地址运用 * 操作符能够获取这个地址指向的内容。
  • uintptr 是 C 里边的一种命名常规,u 前缀的意思是 unsignedint 表明是 int 类型,ptr 表明这个类型是用来表明指针的。
  • unsafe 界说的 Pointer 类型是一种能够指向任何类型的指针,ArbitraryType 可用于表明恣意类型。
  • 咱们经过 unsafe.Pointer 修正结构体字段的时分,要运用 unsafe.Offsetof 获取结构体的偏移量。
  • 经过 unsafe.Sizeof 能够获得某一种类型所需求的内存空间巨细(其间包括了用于内存对齐的内存)。
  • unsafe.Pointeruintptr 之间的类型转化。
  • 几种运用 unsafe.Pointer 的方式:
    • *T1*T2 的转化
    • unsafe.Pointer 转化为 uintptr
    • 运用算术运算将 unsafe.Pointer 转化为 uintptr 并转化回去(需求留意不能运用中心变量来保存 uintptr(unsafe.Pointer(p))
    • 调用 syscall.Syscall 时将指针转化为 uintptr
    • reflect.ValuePointerUnsafeAddr 的成果从 uintptr 转化为 unsafe.Pointer
    • reflect.SliceHeaderreflect.StringHeaderData 字段跟 Pointer 相互转化
  • Add 函数能够简化指针的算术运算,不必来回转化类型(比方 unsafe.Pointer 转化为 uintptr,然后再转化为 unsafe.Pointer)。
  • Slice 函数能够获取指针指向内存的一部分。
  • 最终介绍了 string[]byte 之间经过 unsafe.Pointer 完结高效转化的办法。