golang之unsafe包

环境

go version go1.14.4 linux/amd64

go语言中指针类型的限制

要说unsafe.Pointer, 不得不说这个话题.
学过c语言的同学都知道, p++, p--, p1 == p2这种代码在c语言太过常见.
但是go中的指针有很多的限制:

  • 不能进行数学运算
    i := 3
    p := &i
    p++ // Invalid operation: p++ (non-numeric type *int) 
    p = &i + 5 // Invalid operation: &i + 5 (mismatched types *int and untyped int) 
    
  • 不同类型的指针不能相互转换
    i := 3
    var f *float64
    f = &i // Cannot use '&i' (type *int) as type *float64 
    
  • 不同类型的指针不能使用 == 或 != 比较
    var i int64 = 1
    var f float64 = 1.0
    pi, pf := &i, &f
    
    pi == pf // Invalid operation: pi == pf (mismatched types *int64 and *float64) 
    pi != pf // Invalid operation: pi != pf (mismatched types *int64 and *float64) 
    

什么是unsafe.Pointer

下面的代码来自于: $GOROOT/src/unsafe/unsafe.go

package unsafe

type ArbitraryType int
type Pointer *ArbitraryType

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

原来unsafe.Pointer就是*ArbitraryType, 任意类型的指针, 其最终是*int类型.

上面还出现了uintptr, 其定义是:

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

也就是说uintptr是个整数类型, 足够表示任意指针.

unsafe能做什么

unsafe 包提供了 2 点重要的能力:

  • 任何类型的指针和 unsafe.Pointer 可以相互转换。
  • uintptr 类型和 unsafe.Pointer 可以相互转换。

go中的pointer不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。

还有一点要注意的是,uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。

unsafe 包中的几个函数都是在编译期间执行完毕,毕竟,编译器对内存分配这些操作“了然于胸”。在 $GOROOT/src/cmd/compile/internal/gc/unsafe.go 路径下,可以看到编译期间 Go 对 unsafe 包中函数的处理。

如何利用unsafe包修改私有成员

在go中我们常说私有成员不能直接修改, 甚至我们都调用不了私有成员.
其实利用unsafe包可以做到这一点(确实是unsafe, 不安全.)

对于一个结构体,通过 offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。
这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址, 这在c语言中应该算一个常识。

事实上, 在之前关于linux内核之offset_of和container_of理解中, 也体现了这一点.

我们来看一个例子:

package main

import (
	"fmt"
	"unsafe"
)

type Programmer struct {
	name string
	age uint8
}

func main() {
	p := Programmer{"gerrylon", 18}
	fmt.Println(p) // {gerrylon 18}

	// name是结构体的第一个成员, 和结构体地址相同, 所以下面两句都行
	// name := (*string)(unsafe.Pointer(&p))
	name := (*string)(unsafe.Pointer(&p.name))
	*name = "gl"

	// 结构体的起始地址, 加上age字段的偏移, 也就是age的地址
	age := (*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.age)))
	*age = 19
	fmt.Println(p) // {gl 19}

	// 直接取得age字段的地址
	age = (*uint8)(unsafe.Pointer(&p.age))
	*age = 20
	fmt.Println(p) // {gl 20}
}

上面的例子比较简单, 但是很基础, 仔细看肯定能看懂.
当然这没什么意思, 都在一个包了, 直接调字段都能修改了, 还用什么unsafe…

那如果我们要修改别的包的私有字段呢?

// unsafe_model包
package unsafe_model

type Programmer struct {
	name string
	age uint8
}

// main包
p := unsafe_model.Programmer{}
fmt.Println(p) // { 0}

// p.name = "gl" // Unresolved reference 'name'

name := (*string)(unsafe.Pointer(&p))
*name = "gl"
fmt.Println(p) // {gl 0}

// unsafe.Sizeof("") 即name字段的占的内存大小
age := (*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof("")))
*age = 19
fmt.Println(p) // {gl 19}

可以看到, 我们通过unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(""))取得age字段的地址, 再进行后续运算.

这也说明, 在结构体中, 字段的顺序是很重要的, 不要随意更改.

如何实现字符串和byte切片的零拷贝转换

$GOPATH/reflect/value.go中, 描述了string和slice的header:

type StringHeader struct {
	Data uintptr
	Len  int
}

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

那么我们就可以这样做:

func string2bytes(s string) []byte {
   str := (*reflect.StringHeader)(unsafe.Pointer(&s))
   by := reflect.SliceHeader{
   	Data: str.Data,
   	Len:  str.Len,
   	Cap:  str.Len,
   }
   ret := *(*[]byte)(unsafe.Pointer(&by))
   // ret := *(*[]byte)(unsafe.Pointer(&s)) // 不行, 会导致返回的slice的cap有问题
   return ret
}


func bytes2string(bs []byte) string {
   // 现在两种方法都行
   // by := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
   // str := reflect.StringHeader{
   // 	Data: by.Data,
   // 	Len:  by.Len,
   // }
   // return *(*string)(unsafe.Pointer(&str))
   return *(*string)(unsafe.Pointer(&bs))
}

参考

https://qcrao91.gitbook.io/go/biao-zhun-ku/unsafe

欢迎补充指正

(完)

你可能感兴趣的:(golang学习笔记)