string类型和[]byte类型是咱们编程时最常使用到的数据结构。本文将探讨二者之间的转换方式,经过分析它们之间的内在联系来拨开迷雾。golang
两种转换方式web
标准转换编程
go中string与[]byte的互换,相信每一位gopher都能马上想到如下的转换方式,咱们将之称为标准转换。设计模式
1// string to []byte
2s1 := "hello"
3b := []byte(s1)
4
5// []byte to string
6s2 := string(b)
强转换数组
经过unsafe和reflect包,能够实现另一种转换方式,咱们将之称为强转换(也经常被人称做黑魔法)。安全
1func String2Bytes(s string) []byte {
2 sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
3 bh := reflect.SliceHeader{
4 Data: sh.Data,
5 Len: sh.Len,
6 Cap: sh.Len,
7 }
8 return *(*[]byte)(unsafe.Pointer(&bh))
9}
10
11func Bytes2String(b []byte) string {
12 return *(*string)(unsafe.Pointer(&b))
13}
性能对比
既然有两种转换方式,那么咱们有必要对它们作性能对比。微信
1// 测试强转换功能
2func TestBytes2String(t *testing.T) {
3 x := []byte("Hello Gopher!")
4 y := Bytes2String(x)
5 z := string(x)
6
7 if y != z {
8 t.Fail()
9 }
10}
11
12// 测试强转换功能
13func TestString2Bytes(t *testing.T) {
14 x := "Hello Gopher!"
15 y := String2Bytes(x)
16 z := []byte(x)
17
18 if !bytes.Equal(y, z) {
19 t.Fail()
20 }
21}
22
23// 测试标准转换string()性能
24func Benchmark_NormalBytes2String(b *testing.B) {
25 x := []byte("Hello Gopher! Hello Gopher! Hello Gopher!")
26 for i := 0; i < b.N; i++ {
27 _ = string(x)
28 }
29}
30
31// 测试强转换[]byte到string性能
32func Benchmark_Byte2String(b *testing.B) {
33 x := []byte("Hello Gopher! Hello Gopher! Hello Gopher!")
34 for i := 0; i < b.N; i++ {
35 _ = Bytes2String(x)
36 }
37}
38
39// 测试标准转换[]byte性能
40func Benchmark_NormalString2Bytes(b *testing.B) {
41 x := "Hello Gopher! Hello Gopher! Hello Gopher!"
42 for i := 0; i < b.N; i++ {
43 _ = []byte(x)
44 }
45}
46
47// 测试强转换string到[]byte性能
48func Benchmark_String2Bytes(b *testing.B) {
49 x := "Hello Gopher! Hello Gopher! Hello Gopher!"
50 for i := 0; i < b.N; i++ {
51 _ = String2Bytes(x)
52 }
53}
测试结果以下数据结构
1$ go test -bench="." -benchmem
2goos: darwin
3goarch: amd64
4pkg: workspace/example/stringBytes
5Benchmark_NormalBytes2String-8 38363413 27.9 ns/op 48 B/op 1 allocs/op
6Benchmark_Byte2String-8 1000000000 0.265 ns/op 0 B/op 0 allocs/op
7Benchmark_NormalString2Bytes-8 32577080 34.8 ns/op 48 B/op 1 allocs/op
8Benchmark_String2Bytes-8 1000000000 0.532 ns/op 0 B/op 0 allocs/op
9PASS
10ok workspace/example/stringBytes 3.170s
注意,-benchmem
能够提供每次操做分配内存的次数,以及每次操做分配的字节数。并发
当x的数据均为"Hello Gopher!"时,测试结果以下app
1$ go test -bench="." -benchmem
2goos: darwin
3goarch: amd64
4pkg: workspace/example/stringBytes
5Benchmark_NormalBytes2String-8 245907674 4.86 ns/op 0 B/op 0 allocs/op
6Benchmark_Byte2String-8 1000000000 0.266 ns/op 0 B/op 0 allocs/op
7Benchmark_NormalString2Bytes-8 202329386 5.92 ns/op 0 B/op 0 allocs/op
8Benchmark_String2Bytes-8 1000000000 0.532 ns/op 0 B/op 0 allocs/op
9PASS
10ok workspace/example/stringBytes 4.383s
强转换方式的性能会明显优于标准转换。
读者能够思考如下问题
1.为何强转换性能会比标准转换好?
2.为何在上述测试中,当x的数据较大时,标准转换方式会有一次分配内存的操做,从而致使其性能更差,而强转换方式却不受影响?
3.既然强转换方式性能这么好,为何go语言提供给咱们使用的是标准转换方式?
原理分析
要回答以上三个问题,首先要明白是string和[]byte在go中究竟是什么。
[]byte
在go中,byte是uint8的别名,在go标准库builtin中有以下说明:
1// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
2// used, by convention, to distinguish byte values from 8-bit unsigned
3// integer values.
4type byte = uint8
在go的源码中src/runtime/slice.go
,slice的定义以下:
1type slice struct {
2 array unsafe.Pointer
3 len int
4 cap int
5}
array是底层数组的指针,len表示长度,cap表示容量。对于[]byte来讲,array指向的就是byte数组。
string
关于string类型,在go标准库builtin中有以下说明:
1// string is the set of all strings of 8-bit bytes, conventionally but not
2// necessarily representing UTF-8-encoded text. A string may be empty, but
3// not nil. Values of string type are immutable.
4type string string
翻译过来就是:string是8位字节的集合,一般但不必定表明UTF-8编码的文本。string能够为空,可是不能为nil。string的值是不能改变的。
在go的源码中src/runtime/string.go
,string的定义以下:
1type stringStruct struct {
2 str unsafe.Pointer
3 len int
4}
stringStruct表明的就是一个string对象,str指针指向的是某个数组的首地址,len表明的数组长度。那么这个数组是什么呢?咱们能够在实例化stringStruct对象时找到答案。
1//go:nosplit
2func gostringnocopy(str *byte) string {
3 ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
4 s := *(*string)(unsafe.Pointer(&ss))
5 return s
6}
能够看到,入参str指针就是指向byte的指针,那么咱们能够肯定string的底层数据结构就是byte数组。
综上,string与[]byte在底层结构上是很是的相近(后者的底层表达仅多了一个cap属性,所以它们在内存布局上是可对齐的),这也就是为什么builtin中内置函数copy会有一种特殊状况copy(dst []byte, src string) int
的缘由了。
1// The copy built-in function copies elements from a source slice into a
2// destination slice. (As a special case, it also will copy bytes from a
3// string to a slice of bytes.) The source and destination may overlap. Copy
4// returns the number of elements copied, which will be the minimum of
5// len(src) and len(dst).
6func copy(dst, src []Type) int
7
区别
对于[]byte与string而言,二者之间最大的区别就是string的值不能改变。这该如何理解呢?下面经过两个例子来讲明。
对于[]byte来讲,如下操做是可行的:
1b := []byte("Hello Gopher!")
2b [1] = 'T'
string,修改操做是被禁止的:
1s := "Hello Gopher!"
2s[1] = 'T'
而string能支持这样的操做:
1s := "Hello Gopher!"
2s = "Tello Gopher!"
字符串的值不能被更改,但能够被替换。string在底层都是结构体stringStruct{str: str_point, len: str_len}
,string结构体的str指针指向的是一个字符常量的地址, 这个地址里面的内容是不能够被改变的,由于它是只读的,可是这个指针能够指向不一样的地址。
那么,如下操做的含义是不一样的:
1s := "S1" // 分配存储"S1"的内存空间,s结构体里的str指针指向这块内存
2s = "S2" // 分配存储"S2"的内存空间,s结构体里的str指针转为指向这块内存
3
4b := []byte{1} // 分配存储'1'数组的内存空间,b结构体的array指针指向这个数组。
5b = []byte{2} // 将array的内容改成'2'
图解以下
由于string的指针指向的内容是不能够更改的,因此每更改一次字符串,就得从新分配一次内存,以前分配的空间还须要gc回收,这是致使string相较于[]byte操做低效的根本缘由。
标准转换的实现细节
[]byte(string)的实现(源码在src/runtime/string.go
中)
1// The constant is known to the compiler.
2// There is no fundamental theory behind this number.
3const tmpStringBufSize = 32
4
5type tmpBuf [tmpStringBufSize]byte
6
7func stringtoslicebyte(buf *tmpBuf, s string) []byte {
8 var b []byte
9 if buf != nil && len(s) <= len(buf) {
10 *buf = tmpBuf{}
11 b = buf[:len(s)]
12 } else {
13 b = rawbyteslice(len(s))
14 }
15 copy(b, s)
16 return b
17}
18
19// rawbyteslice allocates a new byte slice. The byte slice is not zeroed.
20func rawbyteslice(size int) (b []byte) {
21 cap := roundupsize(uintptr(size))
22 p := mallocgc(cap, nil, false)
23 if cap != uintptr(size) {
24 memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
25 }
26
27 *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
28 return
29}
这里有两种状况:s的长度是否大于32。当大于32时,go须要调用mallocgc分配一块新的内存(大小由s决定),这也就回答了上文中的问题2:当x的数据较大时,标准转换方式会有一次分配内存的操做。
最后经过copy函数实现string到[]byte的拷贝,具体实如今src/runtime/slice.go
中的slicestringcopy
方法。
1func slicestringcopy(to []byte, fm string) int {
2 if len(fm) == 0 || len(to) == 0 {
3 return 0
4 }
5
6 // copy的长度取决与string和[]byte的长度最小值
7 n := len(fm)
8 if len(to) < n {
9 n = len(to)
10 }
11
12 // 若是开启了竞态检测 -race
13 if raceenabled {
14 callerpc := getcallerpc()
15 pc := funcPC(slicestringcopy)
16 racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
17 }
18 // 若是开启了memory sanitizer -msan
19 if msanenabled {
20 msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
21 }
22
23 // 该方法将string的底层数组从头部复制n个到[]byte对应的底层数组中去(这里就是copy实现的核心方法,在汇编层面实现 源文件为memmove_*.s)
24 memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
25 return n
26}
copy实现过程图解以下
string([]byte)的实现(源码也在src/runtime/string.go
中)
1// Buf is a fixed-size buffer for the result,
2// it is not nil if the result does not escape.
3func slicebytetostring(buf *tmpBuf, b []byte) (str string) {
4 l := len(b)
5 if l == 0 {
6 // Turns out to be a relatively common case.
7 // Consider that you want to parse out data between parens in "foo()bar",
8 // you find the indices and convert the subslice to string.
9 return ""
10 }
11 // 若是开启了竞态检测 -race
12 if raceenabled {
13 racereadrangepc(unsafe.Pointer(&b[0]),
14 uintptr(l),
15 getcallerpc(),
16 funcPC(slicebytetostring))
17 }
18 // 若是开启了memory sanitizer -msan
19 if msanenabled {
20 msanread(unsafe.Pointer(&b[0]), uintptr(l))
21 }
22 if l == 1 {
23 stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]])
24 stringStructOf(&str).len = 1
25 return
26 }
27
28 var p unsafe.Pointer
29 if buf != nil && len(b) <= len(buf) {
30 p = unsafe.Pointer(buf)
31 } else {
32 p = mallocgc(uintptr(len(b)), nil, false)
33 }
34 stringStructOf(&str).str = p
35 stringStructOf(&str).len = len(b)
36 // 拷贝字节数组至字符串
37 memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b)))
38 return
39}
40
41// 实例stringStruct对象
42func stringStructOf(sp *string) *stringStruct {
43 return (*stringStruct)(unsafe.Pointer(sp))
44}
可见,当数组长度超过32时,一样须要调用mallocgc分配一块新内存。最后经过memmove完成拷贝。
强转换的实现细节
1. 万能的unsafe.Pointer指针
在go中,任何类型的指针*T均可以转换为unsafe.Pointer类型的指针,它能够存储任何变量的地址。同时,unsafe.Pointer类型的指针也能够转换回普通指针,并且能够没必要和以前的类型*T相同。另外,unsafe.Pointer类型还能够转换为uintptr类型,该类型保存了指针所指向地址的数值,从而可使咱们对地址进行数值计算。以上就是强转换方式的实现依据。
而string和slice在reflect包中,对应的结构体是reflect.StringHeader和reflect.SliceHeader,它们是string和slice的运行时表达。
1type StringHeader struct {
2 Data uintptr
3 Len int
4}
5
6type SliceHeader struct {
7 Data uintptr
8 Len int
9 Cap int
10}
2. 内存布局
从string和slice的运行时表达能够看出,除了SilceHeader多了一个int类型的Cap字段,Date和Len字段是一致的。因此,它们的内存布局是可对齐的,这说明咱们就能够直接经过unsafe.Pointer进行转换。
[]byte转string图解
string转[]byte图解
Q&A
Q1.为何强转换性能会比标准转换好?
对于标准转换,不管是从[]byte转string仍是string转[]byte都会涉及底层数组的拷贝。而强转换是直接替换指针的指向,从而使得string和[]byte指向同一个底层数组。这样,固然后者的性能会更好。
Q2.为何在上述测试中,当x的数据较大时,标准转换方式会有一次分配内存的操做,从而致使其性能更差,而强转换方式却不受影响?
标准转换时,当数据长度大于32个字节时,须要经过mallocgc申请新的内存,以后再进行数据拷贝工做。而强转换只是更改指针指向。因此,当转换数据较大时,二者性能差距会越发明显。
Q3.既然强转换方式性能这么好,为何go语言提供给咱们使用的是标准转换方式?
首先,咱们须要知道Go是一门类型安全的语言,而安全的代价就是性能的妥协。可是,性能的对比是相对的,这点性能的妥协对于如今的机器而言微乎其微。另外强转换的方式,会给咱们的程序带来极大的安全隐患。
以下示例
1a := "hello"
2b := String2Bytes(a)
3b[0] = 'H'
a是string类型,前面咱们讲到它的值是不可修改的。经过强转换将a的底层数组赋给b,而b是一个[]byte类型,它的值是能够修改的,因此这时对底层数组的值进行修改,将会形成严重的错误(经过defer+recover也不能捕获)。
1unexpected fault address 0x10b6139
2fatal error: fault
3[signal SIGBUS: bus error code=0x2 addr=0x10b6139 pc=0x1088f2c]
Q4. 为何string要设计为不可修改?
我认为有必要思考一下该问题。string不可修改,意味它是只读属性,这样的好处就是:在并发场景下,咱们能够在不加锁的控制下,屡次使用同一字符串,在保证高效共享的状况下而不用担忧安全问题。
取舍场景
在你不肯定安全隐患的条件下,尽可能采用标准方式进行数据转换。
当程序对运行性能有高要求,同时知足对数据仅仅只有读操做的条件,且存在频繁转换(例如消息转发场景),可使用强转换。
题外话
因为我的在18年以后申请的微信公众号均没有了留言功能,因此一直不能和读者进行交流反馈。苦尽甘来,公众号新增了读者讨论功能,能够经过它代替留言功能啦。读者朋友们有什么想说的,尽情留言吧!
往期精彩推荐
Golang技术分享
长按识别二维码关注咱们
更多golang学习资料
回复关键词1024

本文分享自微信公众号 - Golang技术分享(gh_1ac13c0742b7)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。