Go unsafe 包

574 阅读6分钟

官方文档 : golang.org/pkg/unsafe/

简单介绍

unsafe很简单就是不安全,Java中也有(我原来写的一篇文章:anthony-dong.gitee.io/post/java-u…),而且使用Unsafe类可以做很多操作。

unsafe 其实就是拿到了数据真实的内存地址,你可以直接操作内存,脱离于语言本身的一些约束,但是由于直接操作内存太过于直接,暴力,所以对于有些时候操作和访问会很方便和快,也就是不走编译器的自己的内部逻辑,因为go本身是一个内存操作安全的语言

其中对于unsafe ,golang抽象的特别好,就几个方法核心的概念。

Pointer 和 uintptr

Pointer represents a pointer to an arbitrary type. There are four special operations available for type Pointer that are not available for other types:

- A pointer value of any type can be converted to a Pointer.
- A Pointer can be converted to a pointer value of any type.
- A uintptr can be converted to a Pointer.
- A Pointer can be converted to a uintptr.

官方给的定义,指针可以指向任意类型的指针,指针类型有四种特殊操作,而其他类型没有的,

-任何类型的指针值都可以转换为Pointer。
-Pointer可以转换为任何类型的指针值。
-uintptr可以转换为Pointer。
-Pointer可以转换为uintptr。
  • 含义其实就是,unsafe.Pointer是一个指针(指向一块内存区域),uintptr是一个整型,它的值是一个内存地址(就是一个数字),所以它不是引用,属于直接开辟的内存,所以时时刻刻可能被垃圾回收(go语言的内存地址会变化,指的是分配在栈上的内存,堆上是不会变化得,栈上初始是2k,当要扩容的时候是先开辟一块大的,然后再将2k的copy过去,使用这块大的,然后gc最后回收掉这个内存, 关于开辟在堆、栈上是程序开始的时候就确定的,会通过逃逸分析进行确定,也就是有些对象看似在栈上分配,实际上却去了堆上了,属于一种策略)。
type T struct {
	x int
	y *[1 << 10]byte // 1 << 10=1024 1k,
}

func main() {
	wg:=sync.WaitGroup{}
	wg.Add(1)
	go func() {
		t := T{y: new([1 << 10]byte)}
		addr := uintptr(unsafe.Pointer(&t.y[0]))
		set(addr)
		fmt.Println(t.y[0]) // 1
		wg.Done()
	}()
	wg.Wait()
}

func set(addr uintptr) {
	*(*byte)(unsafe.Pointer(addr)) = 1
}

这个没问题

func main() {
	wg:=sync.WaitGroup{}
	wg.Add(1)
	go func() {
		t := T{y: new([1 << 10]byte)}
		addr := uintptr(unsafe.Pointer(&t.y[0]))
		time.Sleep(time.Second) //加一个添加剂,让gc回收一下,
		set(addr)
		fmt.Println(t.y[0]) // 0 
		wg.Done()
	}()
	wg.Wait()
}
  • 前两句大概可以用如下实现,下面很简单是将一个 大的类型-> 小的类型 (a->b)必须要求a本身的内存大于b
func convert(f uint64) uint32 {
  // 任何指针类型可以转换为 Poniter
	pointer := unsafe.Pointer(&f)
  // Pointer可以转换为任何类型的指针值
	return *(*uint32)(pointer)
}
  • 其次就是后面两句,互相转换
type Foo struct {
	Name string
}
func setName(f *Foo) {
	_type := reflect.TypeOf(*f)
	field, _ := _type.FieldByName("Name")
  // uintptr类型
	u := field.Offset

  // Pointer类型
	pointer := unsafe.Pointer(f)

  // uintptr(pointer) Pointer类型可以转换为uintptr, 同时unsafe.Pointer(uintptr(pointer) + u)可以发现uintptr可以转换为Pointer
	name := (*string)(unsafe.Pointer(uintptr(pointer) + u))
	*name = "小李"
	fmt.Printf("%+v\n", *f)
}

func main() {
	setName(&Foo{})
}

以上就是两者的介绍,大致可以发现 Poniter的参数必须是指针(不能是空指针nil),同时Pointer可以转换为任意类型的其他指针。

  • 注意reflect包中的Pointer方法返回的是uintptr
u := reflect.ValueOf(new(int)).Pointer()
p := (*int)(unsafe.Pointer(u))
*p = 1000
fmt.Println(*p)
  • 其中还有一个特别好用的工具是,因为我们知道string底层本质上是 len+byte数组 , 而 切片本质上是 len+byte数组+容量,所以类型是 slice 到 string 没问题, 但是反过来会丢失 cap信息。
func convertStringToSlice() {
	str := "123456"
	var arr = *(*[]byte)(unsafe.Pointer(&str))
	fmt.Println(len(arr))// 6 
	fmt.Println(cap(arr)) // 824634171480 ,这个数字是随机的,根据挨着的内存决定
}
func convertSliceToString() {
	slice := make([]byte,100)
	str := *(*string)(unsafe.Pointer(&slice))
	fmt.Println(len(str)) // 100
}

如何解决上诉的问题:

type String struct {
	data string
	cap int
}
func convertStringToSlice() {
	str := String{"123456", 6}
	//str := "123456"
	var arr = *(*[]byte)(unsafe.Pointer(&str))
	fmt.Println(len(arr)) // 6
	fmt.Println(cap(arr)) // 6
}
  • 利用内存一致来转换一些对象
type UserModel struct { // 数据库对象
	Name     string
	Age      int
	Birthday time.Time
}
type UserDto struct { // 数据传输对象,还要求数据传出的格式是 标准格式
	Name     string `json:"name"`
	Age      int    `json:"age"`
	Birthday Time   `json:"birthday"`
}

type Time time.Time
func (this Time) MarshalText() (text []byte, err error) {
	i := time.Time(this)
	return []byte(i.Format("2006-01-02 15:04:05")), nil
}

如果手动的操作的话,需要一个个赋值, 很麻烦,还开辟新的内存,但是使用这个。

func BenchmarkName(b *testing.B) {
	for i := 0; i < b.N; i++ {
		model := UserModel{
			Name:     "tom",
			Age:      1,
			Birthday: time.Now(),
		}
		userDao := (*UserDto)(unsafe.Pointer(&model))
		_, _ = json.Marshal(userDao)
		//fmt.Println(string(bytes))
    //// {"name":"tom","age":1,"birthday":"2020-06-26 15:20:20"}
	}
}
  • 关于string的一些细节底层优化
// slicebytetostringtmp returns a "string" referring to the actual []byte bytes.
//
// Callers need to ensure that the returned string will not be used after
// the calling goroutine modifies the original slice or synchronizes with
// another goroutine.
//
// The function is only called when instrumenting
// and otherwise intrinsified by the compiler.
//
// Some internal compiler optimizations use this function.
// - Used for m[T1{... Tn{..., string(k), ...} ...}] and m[string(k)]
//   where k is []byte, T1 to Tn is a nesting of struct and array literals.//
// - Used for "<"+string(b)+">" concatenation where b is []byte. // 字符串比较
// - Used for string(b)=="foo" comparison where b is []byte. // 字符串比较
func slicebytetostringtmp(b []byte) string {
	if raceenabled && len(b) > 0 {
		racereadrangepc(unsafe.Pointer(&b[0]),
			uintptr(len(b)),
			getcallerpc(),
			funcPC(slicebytetostringtmp))
	}
	if msanenabled && len(b) > 0 {
		msanread(unsafe.Pointer(&b[0]), uintptr(len(b)))
	}
	return *(*string)(unsafe.Pointer(&b))
}

unsafe.Offsetof(structValue.field)

​ 这个主要是服务于结构体的,计算算结构体字段的偏移量的,和Java的操作类似

func TestDemo(t *testing.T) {
	model := UserModel{}

	modelAdd := uintptr(unsafe.Pointer(&model))
	nameAdd := unsafe.Offsetof(model.Name)
	*(*string)(unsafe.Pointer(modelAdd + nameAdd)) = "tom"
	ageAdd := unsafe.Offsetof(model.Age)
	*(*int)(unsafe.Pointer(modelAdd + ageAdd)) = 10
	birthAdd := unsafe.Offsetof(model.Birthday)
	*(*Time)(unsafe.Pointer(modelAdd + birthAdd)) = Time(time.Now())
	fmt.Println(model)
	fmt.Printf("name_offset=%d, age_offset=%d, birethAdd=%d\n", nameAdd, ageAdd, birthAdd)
}
// {tom 10 2020-06-26 15:42:02.909539 +0800 CST m=+0.000558247}
// name_offset=0, age_offset=16, birethAdd=24
// string 是16个字节
// int 是 8个字节
// 

unsafe.Sizeof()

​ 计算一个对象的大小,指针、结构体、基本数据类型 , 单位是字节 。参数随意了,指针、直接引用都可以

结构体 :

func main() {
   fmt.Println(unsafe.Sizeof(&time.Time{}))
}
// 指针是8个字节

考虑到可移植性,引用类型或包含引用类型的大小在32位平台上是4个字节,在64位平台上是8个字节。

类型 大小
bool 1个字节
intN, uintN, floatN, complexN N/8个字节(例如float64是8个字节)
int, uint, uintptr 1个机器字
*T 1个机器字
string 2个机器字(data,len)
[]T 3个机器字(data,len,cap)
map 1个机器字
func 1个机器字
chan 1个机器字
interface 2个机器字(type,value)

unsafe.Alignof()

返回对应参数的类型需要对齐的倍数. 和 Sizeof 类似, Alignof 也是返回一个常量表达式, 对应一个常量. 通常情况下布尔和数字类型需要对齐到它们本身的大小(最多8个字节), 其它的类型对齐到机器字大小.

可以理解为他是 sizeof/8 ,

参考

books.studygolang.com/gopl-zh/ch1…

golang.google.cn/pkg/unsafe/…