工作中遇到的问题 -- Go避免内存拷贝的强制类型转换

当你使用要对一个变量从一个类型强制转换成另一个类型,其实都会发生内存的拷贝,而这种拷贝会对性能有所影响的,因此如果可以在转换的时候避免内存的拷贝就好了。

庆幸的是,在一些特定的类型下,这种想法确实是可以实现的。

比如将字符串转成 []byte 类型。

// string to []byte
s1 := "hello"
b := []byte(s1)

// []byte to string
s2 := string(b)

不发生内存拷贝的方法如下:

func main() {
	msg1 := "hello"
	sh := *(*reflect.StringHeader) (unsafe.Pointer(&msg1))
	bh := reflect.SliceHeader{
		Data: sh.Data,
		Len:  sh.Len,
		Cap:  sh.Len,
	}
	msg2 := *(*[]byte)(unsafe.Pointer(&bh))
	fmt.Printf("%v", msg2)
}

要理解这一段代码,我们需要3个知识点:

  • 一种定义变量怪异方法
  • 字符串的底层数据结构
  • 切片的底层数据结构

一种定义变量的怪异方法

name := (string) ("我真的很喜欢你")

那么我们再反过来看这一段代码:

tmp := *(*reflect.StringHeader)(unsafe.Pointer(&msg))

由于第一个括号里面是一个指针类型,那么第二个括号里面肯定是指针的值。

而通过 unsafe.Pointer 就可以将 &msg 指针的内存地址取出来。

两个括号合起来就是,声明并定义了一个 *reflect.StringHeader 类型的指针变量,对应的指针值还是原来 msg1 的内存地址。

那最前面的的那个那个 * ,大家应该都知道,是从*reflect.StringHeader 类型的指针变量中取出值。

然后我们来看一下StringHeader这个结构体:

type StringHeader struct {
 Data uintptr
 Len  int
}

同样的,SliceHeader则是切片的底层数据结构

type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}

对咯,只要把 StringHeader 里的 Data 塞给 SliceHeader 里的 Data,再把 SliceHeader 里的 Len 塞给 SliceHeader 里的 Len 和 Cap ,就多费任何的空间创造出一个新的变量。

bh := reflect.SliceHeader{
  Data: sh.Data,
  Len:  sh.Len,
  Cap:  sh.Len,
}

最后再把 SliceHeader 通过上面的强制转换方法,再转成 []byte 就可以了,中间就不会有任何的内存拷贝的过程。