概念

Go指针使用上有很多限制,如不支持算数运算;对于任意两个指针值,可能不能转换到对方的类型。这种指针称为类型安全指针

类型安全指针有助于我们轻松写出安全的代码,但是施加在上面的限制可能让我们不容易写出高效的代码。

Go支持限制较少的非类型安全指针,和C指针相似,他们都很强大,但是使用上都很危险。

使用非类型安全指针可以写出效率更高的代码,但是也会导致风险:

  • 可能会写出不安全的代码,并且很难发现
  • Go不保证非类型安全指针机制的版本兼容性。使用非类型安全指针的代码在今后的Go版本上可能无法编译,或者运行时行为发生变化。

非类型安全指针相关的类型转换

Go1.17支持的非类型安全指针相关的类型转换:

  • 一个类型安全指针可以和一个非类型安全指针类型相互显式转换
  • 一个uintptr值可以和一个非类型安全指针类型相互显式转换

注意,一个nil非类型安全指针类型不应该被转换为uintptr并进行算术运算后再转换回来。

通过使用这些转换规则,可以将任意两个类型安全指针转换为对方的类型,也可以将一个安全指针值和一个uintptr值转换为对方的类型。

然而,尽管这些转换在编译时是合法的,但是它们中一些在运行时并非是合法和安全的。 我们必须遵循一些用法指示来使用非类型安全指针才能让写出的代码合法并且安全。

unsafe标准库包

unsafe.Pointer

unsafe.Pointer即非类型安全指针,在unsafe包中的声明为

1
2
type Pointer *ArbitraryType
type ArbitraryType int

这里的 ArbitraryType 仅仅是暗示 unsafe.Pointer 类型值可以被转换为任意类型安全指针(反之亦然)。

unsafe 包的危险性基本上来自于非类型安全指针。它和C指针一样危险,这是Go安全指针千方百计设法去避免的。

unsafe.Alignof/Offsetof/Sizeof

  • func Alignof(v ArbitraryType) uintptr
    • 用来获取一个值在内存中的地址对齐保证(address alignment guarantee)。
    • 同一个类型的值做为结构体字段和非结构体字段时地址对齐保证可能是不同的,这和具体编译器的实现有关。
    • 对于目前的标准编译器,同一个类型的值做为结构体字段和非结构体字段时的地址对齐保证总是相同的;
    • gccgo编译器对这两种情形是区别对待的。
  • func Offsetof(selector ArbitraryType) uintptr
    • 用来取得一个结构体值的某个字段的地址相对于此结构体值的地址的偏移。
    • 在一个程序中,对于同一个结构体类型的不同值的对应相同字段,此函数的返回值总是相同的。
  • func Sizeof(variable ArbitraryType) uintptr
    • 用来取得一个值的尺寸(值的类型的尺寸)。
    • 在一个程序中,对于同一个类型的不同值,此函数的返回值总是相同的。
    • 以string类型为例,虽然不同的字符串包含的内容不一长短不同,但是string类型的尺寸固定为16字节,包含8B指针指向底层数据+8B的int类型表示字节长度。

这三个函数都是在编译时被估值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"unsafe"
)

func main() {
	var foo = Foo{}
	print(unsafe.Alignof(foo.b))  // 8
	print(unsafe.Alignof(foo))    // 8
	print(unsafe.Alignof(""))     // 8
	print(unsafe.Sizeof(foo.b))   // 16
	print(unsafe.Offsetof(foo.b)) // 8
}

type Foo = struct {
	a int
	b string
}

unsafe.Add/Slice

这两个函数在Go1.17引入。

  • func Add(ptr Pointer, len IntegerType) Pointer
    • 在一个非安全指针表示的地址上添加一个偏移量,然后返回表示新地址的一个指针。
    • 此函数以一种更正规的形式部分地覆盖了下面将要介绍的使用模式3中展示的合法用法。
  • func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
    • 从一个任意(安全)指针派生出一个指定长度的切片
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	a := [16]int{3: 3, 9: 9, 11: 11}
	fmt.Println(a)
	eleSize := int(unsafe.Sizeof(a[0]))
	p9 := &a[9]
	up9 := unsafe.Pointer(p9)
	p3 := (*int)(unsafe.Add(up9, -6*eleSize))
	fmt.Println(*p3) // 3
	s := unsafe.Slice(p9, 5)[:3]
	fmt.Println(s)              // [9 0 11]
	fmt.Println(len(s), cap(s)) // 3 5

	t := unsafe.Slice((*int)(nil), 0)
	fmt.Println(t == nil) // true

	// 下面是两个不正确的调用。因为它们
	// 的返回结果引用了未知的内存块。
	_ = unsafe.Add(up9, 7*eleSize)
	_ = unsafe.Slice(p9, 8)
}

reflect包的SliceHeader/StringHeader

unsafe包的例子中,经常涉及string和slice的底层存储结构,这里提前展示一下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// package reflect
// SliceHeader is the runtime representation of a slice.
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}
// StringHeader is the runtime representation of a string.
type StringHeader struct {
	Data uintptr
	Len  int
}

几点事实

事实1:非类型安全指针是指针,uintptr是整数值

  • 一个非零安全或非安全指针值都是指针,他们引用者一个内存地址。
  • uintptr是一个整数值,保存了某个内存的地址值,但并不引用它;uintptr可以进行算数运算。

他们的不同导致了在GC时的处理不同。某个内存地址保存在uinptr中并不会阻碍这块内存被回收,因为uintptr不是指针。

事实2:不再被使用的内存块的回收时间点是不确定的

运行时的GC可能在一个不确定的时间点启动,并且需要一段不确定的时间才能完成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "unsafe"

// 假设此函数不会被内联(inline)。
//go:noinline
func createInt() *int {
	return new(int)
}

func main() {
	p0, y, z := createInt(), createInt(), createInt()
	var p1 = unsafe.Pointer(y) // 和y一样引用着同一个值
	var p2 = uintptr(unsafe.Pointer(z))

	// 即使z指针值所引用的int值的地址仍旧存储在p2值中,但是此int值已经不再被使用了,
	// 所以垃圾回收器认为可以回收它所占据的内存块了。
	// 另一方面,p0和p1各自所引用的int值仍旧将在下面被使用。

	// uintptr值可以参与算术运算。
	p2 += 2
	p2--
	p2--

	*p0 = 1                         // okay
	*(*int)(p1) = 2                 // okay
	*(*int)(unsafe.Pointer(p2)) = 3 // 危险操作!
	// z指针指向的内存空间可能已经回收,甚至分配给其它值使用了
}

事实3:一个值的地址在程序运行中可能改变

当一个协程的栈大小改变时,开辟在栈上的内存块需要移动,从而相应的值的地址会改变。

事实4:一个值的生命范围可能并没有代码中看上去的大

下面例子中,值t仍然在使用,并不能保证t.y所引用的值仍然在被使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "unsafe"

type T struct {
	x int
	y *[1 << 23]byte
}

func main() {
	t := T{y: new([1 << 23]byte)}
	p := uintptr(unsafe.Pointer(&t.y[0]))

	// ... 使用t.x和t.y

	// 一个聪明的编译器能够觉察到值t.y将不会再被用到,
	// 所以认为t.y值所占的内存块可以被回收了。

	*(*byte)(unsafe.Pointer(p)) = 1 // 危险操作!
	// t.y的内存可能已经被释放

	println(t.x) // ok。继续使用值t,但只使用t.x字段。
}

事实5:*unsafe.Pointer是一个类型安全指针类型

类型 *unsafe.Pointer 是一个类型安全指针类型。 它的基类型为unsafe.Pointer

既然它是一个类型安全指针类型,根据上面列出的类型转换规则,它的值可以转换为类型unsafe.Pointer ,反之亦然。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "unsafe"

func main() {
	x := 123                // 类型为int
	p := unsafe.Pointer(&x) // 类型为unsafe.Pointer
	pp := &p                // 类型为*unsafe.Pointer
	p = unsafe.Pointer(pp)
	pp = (*unsafe.Pointer)(p)
}

非类型安全指针的使用模式

unsafe 标准库包的文档中列出了六种非类型安全指针的使用模式

模式1:将*T1的值转为非类型安全指针值,再转为*T2

利用前面提到的非类型安全指针相关的转换规则,可以将一个*T1值转换为*T2值,T1和T2为两个任意类型。

前提:只有在T1类型的尺寸不小于T2并且转换有意义的时候才应该实施这样的转换。(如果T1类型尺寸太小,会导致引用到不安全的内存)

例子math.Float32bits

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func bytes2string1(bytes []byte) string {
	return string(bytes)
}

func bytes2string2(bytes []byte) string {
	return *(*string)(unsafe.Pointer(&bytes))
}

func main() {
	bytes := []byte{'a', 'b', 'c'}
	fmt.Println(bytes2string1(bytes))
	fmt.Println(bytes2string2(bytes))
}

第一种方法,涉及byte切片的值拷贝,会导致内存申请和赋值。

第二种方法,使用unsafe.Pointer进行类型转换,共用内存空间,转换后对[]byte的修改会反应到string,使string“mutable”。

这种使用unsafe的方式将字节切片转为string的方法是安全的,因为切片类型的尺寸(24B)比字符串类型的尺寸(16B)大

  • 切片的底层结构包括三个字段:指向底层数组的指针、长度、容量
  • 字符串的底层结构包括两个字段:指向底层字节的指针、长度

下面这种转换是不安全的,因为字符串类型的尺寸比切片类型的尺寸小,不过不会引起编译错误

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
	"fmt"
	"unsafe"
)

func str2Bytes(sp *string) []byte {
	return *(*[]byte)(unsafe.Pointer(sp)) // 错误用法,不安全
}

func main() {
	str := "abc"
	bs := str2Bytes(&str)
	fmt.Println(cap(bs), string(bs)) // 0 abc
}

切片类型和字符串类型的内部结构

如果使用上面的方法,会导致切片的容量值错误,可能会引用到非法内存地址。

1
2
3
4
5
6
7
8
9
type _slice struct {
    elements unsafe.Pointer // 引用着底层的元素
    len int // 当前的元素个数
    cap int // 切片的容量
}
type _string struct {
    elements *byte // 引用着底层的byte元素
    len int // 字符串的长度
}

文章最后的例子中会使用合法的方式在无需复制底层数组的前提下将字符串转为字节切片

模式2:将一个非类型安全指针转为一个uintptr值,然后使用这个值

此模式不是很有用。一般我们将转换结果uintptr值输出到日志中用来调试,但是有很多其它安全且简洁的途径也可以实现此目的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	type T struct {
		a int
	}
	var t T
	fmt.Printf("%x\n", uintptr(unsafe.Pointer(&t))) // c00001e088
	fmt.Printf("%p\n", &t)                          // 0xc00001e088
}

模式3:将一个非类型安全指针转为一个uintptr值,经过算数运算后转回非类型安全指针

要求转换前后的非类型安全指针必须指向同一个内存块,这样转换才是合法安全的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"fmt"
	"unsafe"
)

type T struct {
	a bool
	b [3]int16
}

const offset = unsafe.Offsetof(T{}.b)
const size = unsafe.Sizeof(T{}.b[0])

func main() {
	t := T{
		a: true,
		b: [...]int16{10, 11, 12},
	}
	p := unsafe.Pointer(&t)
	// uintptr 参与运算后再转回unsafe.Pointer,需要在同一行,防止GC回收对象后导致ptr指向的地址失效
	val := *(*int16)(unsafe.Pointer(uintptr(p) + offset + size*2))
	fmt.Println(val) // 12
}

uintptr 参与运算后再转回unsafe.Pointer,需要在同一行,防止GC回收对象后导致ptr指向的地址失效

下面的用法是错误的有危险的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
	t := T{y: [3]int16{123, 456, 789}}
	p := unsafe.Pointer(&t)
	// ty2 := (*int16)(unsafe.Pointer(uintptr(p)+N+M+M))
	addr := uintptr(p) + N + M + M
	
	// ...(一些其它操作)
	
	// 从这里到下一行代码执行之前,t值将不再被任何值引用,所以垃圾回收器认为它可以被回收了。
        // 一旦它真地被回收了,下面继续使用t.y[2]值的曾经的地址是非法和危险的!
        // 另一个危险的原因是t的地址在执行下一行之前可能改变(见事实3)。
	// 另一个潜在的危险是:如果在此期间发生了一些操作导致协程堆栈大小改变的情况,则记录在addr中的地址将失效。
	ty2 := (*int16)(unsafe.Pointer(addr))
	fmt.Println(*ty2)
}

这样的bug是非常微妙和很难被觉察到的,并且爆发出来的几率是相当得低。这也是使用非类型安全指针被认为是危险操作的原因之一。

模式4:将非类型安全指针转换为uintptr值再传递给syscall调用

从上一个模式看出,下面这种用法是危险的,不能保证addr地址的内存没有被GC回收

1
2
3
4
// 假设函数不会被内联
func DoSomething(addr uintptr) {
    // 对addr地址进行读写
}  

然而,syscall标准库的Syscall函数原型

1
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

因为编译器对syscall.Syscall做了特殊处理,可以保证函数执行过程中参数a1,a2,a3处的内存不会被回收和移动。而自定义函数不会有这样的处理。

另外,在调用syscall函数时,实参必须是uintptr(anUnsafePointer)这种形式

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

模式5:将reflect.Value.Pointer/reflect.Value.UnsafeAddr方法的uintptr返回值立即转换为非类型安全指针

reflect 标准库包中的 Value 类型的 Pointer()UnsafeAddr() 方法都返回一个 uintptr 值,而不是一个 unsafe.Pointer 值。

这样设计的目的是避免用户不引用unsafe标准库包就可以将这两个方法的返回值(如果是 unsafe.Pointer 类型)转换为任何类型安全指针类型。

同前面两种模式,这样的设计需要我们将这两个方法返回的 uintptr 结果立即转换为非类型安全指针。 否则,将出现 一个短暂的可能导致处于返回的地址处的内存块被回收掉的时间窗。

1
2
3
4
5
6
7
// Safe.
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

// Danger.
u := reflect.ValueOf(new(int)).Pointer() // 返回uintptr类型
// 这时,处于存储在u中的地址处的内存块可能会被回收掉。
p := (*int)(unsafe.Pointer(u))

模式6:将reflect.SliceHeader/reflect.StringHeader 值的 Data 字段转换为非类型安全指针,及其逆转换。

和上一小节中提到的同样的原因, reflect标准库包中的 SliceHeaderStringHeader 类型的 Data 字段的类型被指定为 uintptr ,而不是 unsafe.Pointer

可以将一个字符串的指针值转换为一个 *reflect.StringHeader 指针值,从而可以对此字符串的内部进行修改。 类似地,我们可以将一个切片的指针值转换为一个 *reflect.SliceHeader 指针值, 从而可以对此切片的内部进行修改

reflect.StringHeader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	a := [...]byte{'G', 'o', 'l', 'a', 'n', 'g'}
	s := "Java"
	hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
	hdr.Data = uintptr(unsafe.Pointer(&a))
	hdr.Len = len(a)
	fmt.Println(s) // Golang
	// 现在,字符串s和切片a共享着底层的byte字节序列
	a[2], a[3], a[4], a[5] = 'o', 'g', 'l', 'e'
	fmt.Println(s) // Google
}

reflect.SliceHeader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	a := [6]byte{'G', 'o', '1', '0', '1'}
	bs := []byte("Golang")
	hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
	hdr.Data = uintptr(unsafe.Pointer(&a))

	hdr.Len = 2
	hdr.Cap = len(a)
	fmt.Printf("%s\n", bs) // Go
	bs = bs[:cap(bs)]
	fmt.Printf("%s\n", bs) // Go101
}

一般说来,只应该从一个已经存在的字符串值得到一个 *reflect.StringHeader 指针, 或者从一个已经存在的切片值得到一个 *reflect.SliceHeader指针, 而不应该从一个 StringHeader值生成一个字符串,或者从一个 SliceHeader 值生成一个切片。

下面的代码是不安全的:

1
2
3
4
5
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(new([5]byte)))
// 此时,上一行代码中刚开辟的数组内存块已经不再被任何值所引用,所以它可以被回收了。
hdr.Len = 5
s := *(*string)(unsafe.Pointer(&hdr)) // 危险

应用

非复制的方式实现string和[]byte转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
	"fmt"
	"reflect"
	"strings"
	"unsafe"
)

func String2ByteSlice(str string) (bs []byte) {
	strHdr := (*reflect.StringHeader)(unsafe.Pointer(&str))
	sliceHdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
	sliceHdr.Data = strHdr.Data
	sliceHdr.Cap = strHdr.Len
	sliceHdr.Len = strHdr.Len
	return
}
func ByteSlice2String(bs []byte) (str string) {
	sliceHdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
	strHdr := (*reflect.StringHeader)(unsafe.Pointer(&str))
	strHdr.Data = sliceHdr.Data
	strHdr.Len = sliceHdr.Len
	return
	// 此实现比模式1中展示的方法略为安全一些(但是也更慢一些)。
}

func main() {
	// str := "Golang"
	// 对于官方标准编译器来说,上面这行将使str中的字节开辟在不可修改内存区。
	// 所以这里我们使用下面这行。
	str := strings.Join([]string{"Go", "land"}, "")
	s := String2ByteSlice(str)
	fmt.Printf("%s\n", s) // Goland
	s[5] = 'g'
	fmt.Println(str) // Golang
}

注意,这里有个疑问,为什么`String2ByteSlice(str string)方法参数声明是string``类型,而不是 *string 类型?Go不是值传递吗?

方法里获得的应该只是形参的地址呀?main方法中字符串的修改不应该反应到str的值才对呀?

string类型参数的形参,到底传了什么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	var s = "abc"
	fmt.Printf("%p\n", &s)                                        // 0xc000010230
	fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&s)).Data) // 17445721

	sub(s)
	fmt.Printf("%p\n", &s)                                        // 0xc000010230
	fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&s)).Data) // 17445721

	fmt.Println(s) // abc
}

func sub(s string) {
	fmt.Printf("%p\n", &s)                                        // 0xc000010240
	fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&s)).Data) // 17445721
	// 在此之前,Data仍指向父函数实参.Data的地址

	s = "ABCD"                                                    // 写时复制,指向新的地址
	fmt.Printf("%p\n", &s)                                        // 0xc000010240
	fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&s)).Data) // 17445844
}

string类型的底层对应reflect.StringHeader类型,当string类型作为参数进行值传递时,传递的实际是这个StringHeader类型的拷贝(具有相同的Data和Len值)

对string类型用取地址符取地址时,获取到的也可以认为是StringHeader类型的地址

如果子函数内不对string做修改,则形参的StringHeader.Data永远是父函数实参StringHeader.Data的值(即上例的情况);如果子函数做了修改,形参的Data将指向新的地址,而实参不变,类似于写时复制

两个字符串共享一个底层存储

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"fmt"
	"reflect"
	"strings"
	"unsafe"
)

func main() {
	str := strings.Join([]string{"a", "b", "c"}, "")
	var str2 string

	// 两个字符串共享底层存储
	hdr := (*reflect.StringHeader)(unsafe.Pointer(&str2)) // 模式1
	hdr.Data = (*reflect.StringHeader)(unsafe.Pointer(&str)).Data
	hdr.Len = (*reflect.StringHeader)(unsafe.Pointer(&str)).Len
	fmt.Println(str2) // abc

	h := (*reflect.StringHeader)(unsafe.Pointer(&str2))
	*(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(h.Data)) + 1)) = 'B' // 模式3
	// 定义时如果使用str := "abc",会导致str分配在常量区,无法修改内容。
        // 所以需要用strings.Join分配到可修改区域
	
	fmt.Println(str2, str) // aBc aBc
}

References

go101