看过小许之前的文章《fasthttp是如何做到比net/http快十倍的》,相信你们还对极致的优化方式意犹未尽。
不过你发现没fasthttp关于string和[]byte的转换方式和大家平常普遍使用的方式不一样,fasthttp转换实现如下:
//[]byte转string func b2s(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } //string转[]byte func s2b(s string) (b []byte) { bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh.Data = sh.Data bh.Cap = sh.Len bh.Len = sh.Len return b }
为什么不用我们常见的string和[]byte的转换方式呢?这样做是怎么提高性能的呢?...
带着这些疑问,今天将分享下并总结string和[]byte的转换方式,不同的转换方式之间的实现和区别!
两种转换方式
如果此时此刻你刚好遇到面试官问你string和[]byte如何进行转换,有几种方式?你能答上来吗
反正在写这篇文章之前小许估计是答不出来的,哈哈!
毕竟知道的越多,不知道的也越多嘛
那今天我们就来聊聊,继续往下读之前,我们先了解下这两种数据类型:
string和[]byte
上图中可以看出 stringStruct和slice还是有一些相似之处,str和array指针指向底层数组的地址,len代表的就是数组长度。
关于string类型,在go标准库中官方说明如下:
// string is the set of all strings of 8-bit bytes, conventionally but not // necessarily representing UTF-8-encoded text. A string may be empty, but // not nil. Values of string type are immutable. type string string
string是8位字节的集合,string的定义在上图中左侧,通常但不一定代表UTF-8编码的文本。string可以为空,但是不能为nil,并且string的值是不能改变的。
为什么string类型没有cap字段
string的不可变性,也就不能直接向底层数组追加元素,所以不需要Cap。
而[]byte就是一个byte类型的切片,切片本质也是一个结构体。
这里我们先记住下这两种数据类型的特点,对后面的了解两者的转换有帮助!
标准方式
Golang中string与[]byte的互换,这是我们常用的,也是立马能想到的转换方式,这种方式称为标准方式。
// string 转 []byte s1 := "xiaoxu" b := []byte(s1) // []byte 转 string s2 := string(b)
那还有其他方式吗?当然有的,那就是强转换
强转换方式
强转换方式是通过unsafe和reflect包来实现的,代码如下:
//[]byte转string func b2s(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } //string转[]byte func s2b(s string) (b []byte) { bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh.Data = sh.Data bh.Cap = sh.Len bh.Len = sh.Len return b }
可以看出利用reflect.SliceHeader(代表一个运行时的切片) 和 unsafe.Pointer进行指针替换。
为什么可以这么做呢?
前面我们在讲string和[]byte类型的时候就提了,因为两者的底层结构的字段相似!
array和str的len是一致的,而唯一不同的就是cap字段,所以他们的内存布局上是对齐的。
分析
我们看下这两种转换方式底层是如何实现的,这些实现代码在标准库中都是有的,下面底层实现的代码来自Go 1.18.6版本。
标准方式底层实现
string转[]byte底层实现
先看string转[]byte的实现,(实现源码在 src/runtime/string.go 中)
const tmpStringBufSize = 32 //长度32的数组 type tmpBuf [tmpStringBufSize]byte //时间函数 func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte //判断字符串长度是否小于等于32 if buf != nil && len(s) <= len(buf) { *buf = tmpBuf{} b = buf[:len(s)] } else { //预定义数组长度不够,重新分配内存 b = rawbyteslice(len(s)) } copy(b, s) return b } // rawbyteslice allocates a new byte slice. The byte slice is not zeroed. //rawbyteslice函数 分配一个新的字节片。字节片未归零 func rawbyteslice(size int) (b []byte) { cap := roundupsize(uintptr(size)) p := mallocgc(cap, nil, false) if cap != uintptr(size) { memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size)) } *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)} return }
上面代码可以看出string转[]byte是,会根据字符串长度来决定是否需要重新分配一块内存。
• 预先定义了一个长度为32的数组
• 若字符串的长度不超过这个长度32的数组,copy函数实现string到[]byte的拷贝
• 若字符串的长度超过了这个长度32的数组,重新分配一块内存了,再进行copy
[]byte转string底层实现
再看[]byte转string的实现,(实现源码在 src/runtime/string.go 中)
const tmpStringBufSize = 32 //长度32的数组 type tmpBuf [tmpStringBufSize]byte //实现函数 func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) { ... if n == 1 { p := unsafe.Pointer(&staticuint64s[*ptr]) if goarch.BigEndian { p = add(p, 7) } stringStructOf(&str).str = p stringStructOf(&str).len = 1 return } var p unsafe.Pointer //判断字符串长度是否小于等于32 if buf != nil && n <= len(buf) { p = unsafe.Pointer(buf) } else { p = mallocgc(uintptr(n), nil, false) } stringStructOf(&str).str = p stringStructOf(&str).len = n //拷贝byte数组至字符串 memmove(p, unsafe.Pointer(ptr), uintptr(n)) return }
跟string转[]byte一样,当数组长度超过32时,同样需要调用mallocgc分配一块新内存
强转换底层实现
从标准的转换方式中,我们知道如果字符串长度超过32的话,会重新分配一块新内存,进行内存拷贝。
//string转[]byte func s2b(s string) (b []byte) { bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh.Data = sh.Data bh.Cap = sh.Len bh.Len = sh.Len return b }
强转换过程中,通过 神奇的unsafe.Pointer指针
• 任何类型的指针 *T 都可以转换为unsafe.Pointer类型的指针,可以存储任何变量的地址
• unsafe.Pointer 类型的指针也可以转换回普通指针,并且可以和类型*T不相同
refletc包的 reflect.SliceHeader 和 reflect.StringHeader分别代表什么意思?
reflect.SliceHeader:slice类型的运行时表示形式
reflect.StringHeader:string类型的运行时表示形式
//slice在运行时的描述符 type SliceHeader struct { Data uintptr Len int Cap int } //string在运行时的描述符 type StringHeader struct { Data uintptr Len int }
*(reflect.SliceHeader)(unsafe.Pointer(&b)) 的目的就是通过unsafe.Pointer 把它们转换为 *reflect.SliceHeader 指针。
而运行时表现形式 SliceHeader 和 StringHeader,而这两个结构体都有一个 Data 字段,用于存放指向真实内容的指针。
[]byte 和 string之间的转换,就可以理解为是通过 unsafe.Pointer 把 *SliceHeader 转为 *StringHeader,也就是 *[]byte 和 *string之间的转换。
那么我们就可以理解相对于标准转换方式,强转换方式的优点在哪了!
直接替换指针的指向,避免了申请新内存(零拷贝),因为两者指向的底层字段Data地址相同
总结
今天和大家一起了解了[]byte和string类型,以及[]byte和string的两种转换方式。
不过Go语言提供给我们使用的还是标准转换方式,主要是因为在你不确定安全隐患的情况下,使用强转化方式可能不必要的问题。
不过像fasthttp那样,对程序对运行性能有高要求,那就可以考虑使用强转换方式!