我们知道在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))
}
我们来简单说一说其中的运行原理:
-
unsafe.Pointer
可以获取参数中值的地址。 -
unsafe.Pointer
可以转换为uintptr
类型,后者保存着指针指向地址的数值,值得一提的是,uintptr的大小并不明确,但是可存放完整指针。 - 对于
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并没有使用此类垃圾回收器。