go中string与[]byte的高效转换(unsafe包)

我们知道在go的设计确保了一些安全的属性来限制很多种可能出现错误的情况,因为go是一个强类型的静态类型语言。所以会在编译器对阻止一些不正确的类型转换。

在string和byte[]这两个类型中允许byte[]向string的直接转换,但是不允许byte[]向string的直接转换,写成代码大概是这样:

// yte[]直接转换为string,反过来就不可以了
var str = []byte("hello world")
var data = string(a)

当然我们也可以把string和byte[]用作另一种类型的初始化,这样可以做到两个类型的通用转换:

// string转bytes
var str string = "hello world"
var data []byte = []byte(str)
var data [10]byte
data[0] = 'H'
data[1] = 'W'
var str string = string(data[:])

以上的转换方法可行,但效率欠佳,因为每次的转换其实都伴随着所有数据的拷贝。

我们先来看看两种类型的底层数据结构的实现:

// string 的底层数组结构如下
struct string {
	unit8 *str
	int len
}

// 而 []byte 的底层结构如下
struct uint8 {
	unit8 *array
	int len
	int cap
}

我们可以看到其实内部差别并不大,只是slice中多了一个容量而已,这也很好理解,毕竟是动态扩展的。

所以我们可以使用unsafe包执行高效的转换。但是注意unsafe包中的内容无法保证和go的未来版本兼容,所以还是需要谨慎使用,我们来看看实现,这也是我们的最终可应用在代码中的版本

// string转ytes
func Str2sbyte(s string) (b []byte) {
	*(*string)(unsafe.Pointer(&b)) = s	// 把s的地址付给b
	*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 2*unsafe.Sizeof(&b))) = len(s)	// 修改容量为长度
	return
}

// []byte转string
func Sbyte2str(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}

我们来简单说一说其中的运行原理:

  1. unsafe.Pointer可以获取参数中值的地址。
  2. unsafe.Pointer可以转换为uintptr类型,后者保存着指针指向地址的数值,值得一提的是,uintptr的大小并不明确,但是可存放完整指针
  3. 对于Str2sbyte的第二条语句,也就是修改容量这里,我们使用加上2*unsafe.Sizeof(&b)来使得指针的偏移指向cap,因为前两个值的大小其实和一个指针的大小是一样的。

有一点值得思考,就是我们是否需要把uintptr类型的值当做一个临时变量,也就是类似于这样的写法:

func Str2sbyte(s string) (b []byte) {
	*(*string)(unsafe.Pointer(&b)) = s	
	temp := uintptr(unsafe.Pointer(&b))
	*(*int)(unsafe.Pointer(temp + 2*unsafe.Sizeof(&b))) = len(s)
	return
}

答案是不可以。

原因是temp中实际存储的是指针的地址值,而在go中一个变量的地址随时可能发生改变,因为go中原生支持协程,也就是goroutine,而在一个程序中goroutine的数量成千上万也并不奇怪,这也就意味着goroutine的栈不能是一个固定大小,否则对于并发的限制就太大了,所以goroutine的栈是可增长的,所以其初始值并不大,一般是2KB,所以在运行过程中经常可能出现栈的改变,这意味着所有值的地址都会发生改变。回到上面修改后的Str2sbyte函数,如果在第二句和第三句之间出现栈切换,那么我们记录的uintptr就无效了,其指向了一个未知的地址,这可能导致一些及其隐晦的错误。还有一些GC算法也可能导致此类问题,比如移动的垃圾回收器,但是所幸当前go并没有使用此类垃圾回收器。

posted @ 2022-07-02 13:17  李兆龙的博客  阅读(1843)  评论(0编辑  收藏  举报