golang byte和string互转
两种转换方式
标准转换 go中string与[]byte的互换,相信每一位gopher都能立刻想到以下的转换方式,我们将之称为标准转换。
// string to []byte
s1 := "hello"
b := []byte(s1)
// []byte to string
s2 := string(b)
强转换 通过unsafe和reflect包,可以实现另外一种转换方式,我们将之称为强转换(也常常被人称作黑魔法)。
func String2Bytes(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
func Bytes2String(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
强转原理
在go的源码中src/runtime/slice.go,slice的定义如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
在go的源码中src/runtime/string.go,string的定义如下:
type stringStruct struct {
str unsafe.Pointer
len int
}
string与[]byte在底层结构上是非常的相近,对于[]byte与string而言,两者之间最大的区别就是string的值不能改变。
string在底层都是结构体stringStruct{str: str_point, len: str_len},string结构体的str指针指向的是一个字符常量的地址, 这个地址里面的内容是不可以被改变的,因为它是只读的,但是这个指针可以指向不同的地址。
string的指针指向的内容是不可以更改的,所以每更改一次字符串,就得重新分配一次内存,之前分配的空间还需要gc回收,这是导致string相较于[]byte操作低效的根本原因。
强转换的实现细节
万能的unsafe.Pointer指针
在go中,任何类型的指针\T都可以转换为unsafe.Pointer类型的指针,它可以存储任何变量的地址。同时,unsafe.Pointer类型的指针也可以转换回普通指针,而且可以不必和之前的类型T相同
。另外,unsafe.Pointer类型还可以转换为uintptr类型,该类型保存了指针所指向地址的数值,从而可以使我们对地址进行数值计算。以上就是强转换方式的实现依据。
Q&A
Q1. 为啥强转换性能会比标准转换好?
对于标准转换,无论是从[]byte转string还是string转[]byte都会涉及底层数组的拷贝。而强转换是直接替换指针的指向,从而使得string和[]byte指向同一个底层数组。这样,当然后者的性能会更好。
Q2. 既然强转换方式性能这么好,为啥go语言提供给我们使用的是标准转换方式?
首先,我们需要知道Go是一门类型安全的语言,而安全的代价就是性能的妥协。但是,性能的对比是相对的,这点性能的妥协对于现在的机器而言微乎其微。另外强转换的方式,会给我们的程序带来极大的安全隐患。
如下示例
a := “hello” b := String2Bytes(a) b[0] = ‘H’ a是string类型,前面我们讲到它的值是不可修改的。通过强转换将a的底层数组赋给b,而b是一个[]byte类型,它的值是可以修改的,所以这时对底层数组的值进行修改,将会造成严重的错误(通过defer+recover也不能捕获)。
Q3. 为啥string要设计为不可修改的?
我认为有必要思考一下该问题。string不可修改,意味它是只读属性,这样的好处就是:在并发场景下,我们可以在不加锁的控制下,多次使用同一字符串,在保证高效共享的情况下而不用担心安全问题。