GO语言中string和[]byte的区别及转换
区别
在我们日常的开发中经常需要处理字符串,而在GO语言中,字符串和[]byte是两种不同的类型。
- 首先来看string的底层定义(src/runtime/string.go):
type stringStruct struct {
str unsafe.Pointer
len int
}
- []byte的底层定义(src/runtime/slice.go):
type slice struct {
array unsafe.Pointer
len int
cap int
}
二者都包含一个指向底层数组的指针,和底层数组的长度。不同点在于:
- string是不可变的,一旦创建就不能修改,因此适合用于只读场景;
- []byte是可变的,可以修改,且包含一个容量信息(cap);
(注:这里就不展开slice的扩容机制了,可以参考网上其他信息)
什么叫string是不可变的呢?举个例子:
s := "hello world"
s[0] = 'H' // 编译错误:cannot assign to s[0]
(注:这里提一嘴go语言中单引号用来表示byte类型,双引号用来表示string类型)
string不可变的含义是不能修改string底层数组的某个元素,但我们可以修改string引用的底层数组:
s := "hello world"
s = "another string"
这时候s的底层数组已经发生了变化,我们创建了一个新的底层数组(another string)并将s的指针指向它。原先的底层数组(hello world)将等待gc回收。
[]byte是可变的,我们可以修改它的元素:
b := []byte{'h', 'e', 'l', 'l', 'o'}
b[0] = 'H'
这时候b的底层数组中第一个元素已经变成了'H'。
转换
在实际使用时,我们可能需要将string与[]byte互相转换,有以下两种常见的方式:
普通转换
string转[]byte:
s := "hello world"
b := []byte(s)
[]byte转string:
b := []byte{'h', 'e', 'l', 'l', 'o'}
s := string(b)
强转换 (有风险 谨慎使用)
- 在go版本<1.20中
通过unsafe包和reflect包实现,其主要原理是拿到底层数组的指针,然后转换成[]byte或string。
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))
}
注:refelct包中的SliceHeader和StringHeader是切片、string的运行时表现
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
- 在go版本>=1.20中 由于安全性问题reflect包中的StringHeader和SliceHeader已被标注为deprecated,建议使用unsafe包来实现转换。
// Deprecated: Use unsafe.String or unsafe.StringData instead.
// Deprecated: Use unsafe.Slice or unsafe.SliceData instead.
func String2Bytes(s string) []byte {
// StringData获取string的底层数组指针,unsafe.Slice通过指针和长度构建切片
return unsafe.Slice(unsafe.StringData(s), len(s))
}
func Bytes2String(b []byte) string {
// SliceData获取切片的底层数组指针,unsafe.String通过指针和长度构建string
return unsafe.String(unsafe.SliceData(b), len(b))
}
注:强转换可能出现重大问题!!!如下:
str := "hello world"
bs := String2Bytes(str)
bs[0] = 'H'
// str作为string不可修改,bs作为[]byte可修改,通过强转换二者指向同一个底层数组
// 修改bs时会出现严重错误,通过 defer + recover 也不能捕获
/*
unexpected fault address 0x1dc1f8
fatal error: fault
[signal 0xc0000005 code=0x1 addr=0x1dc1f8 pc=0x1a567b]
*/
两种转换的性能对比
转换函数:
func String2Bytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
func Bytes2String(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
func String2Bytes_basic(s string) []byte {
return []byte(s)
}
func Bytes2String_basic(b []byte) string {
return string(b)
}
测试代码:
func BenchmarkS2B(b *testing.B) {
str := "hello world hello world hello world hello world "
for i := 0; i < b.N; i++ {
_ = String2Bytes(str)
}
}
func BenchmarkB2S(b *testing.B) {
bs := []byte("hello world hello world hello world hello world ")
for i := 0; i < b.N; i++ {
_ = Bytes2String(bs)
}
}
func BenchmarkS2Bbasic(b *testing.B) {
str := "hello world hello world hello world hello world "
for i := 0; i < b.N; i++ {
_ = String2Bytes_basic(str)
}
}
func BenchmarkB2Sbasic(b *testing.B) {
bs := []byte("hello world hello world hello world hello world ")
for i := 0; i < b.N; i++ {
_ = Bytes2String_basic(bs)
}
}
测试结果:
goos: windows
goarch: amd64
pkg: hello/convert
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkS2B-16 1000000000 0.3371 ns/op 0 B/op 0 allocs/op
BenchmarkB2S-16 1000000000 0.5940 ns/op 0 B/op 0 allocs/op
BenchmarkS2Bbasic-16 48838329 24.82 ns/op 48 B/op 1 allocs/op
BenchmarkB2Sbasic-16 50250835 22.35 ns/op 48 B/op 1 allocs/op
显然使用强转换的性能更高,原因在于对于标准转换,无论是从 []byte 转 string 还是 string 转 []byte 都会涉及底层数组的拷贝。而强转换是直接替换指针的指向,从而使得 string 和 []byte 指向同一个底层数组。当数据长度大于 32 个字节时,标准转换需要通过 mallocgc 申请新的内存,之后再进行数据拷贝工作。所以,当转换数据较大时,两者性能差距会愈加明显。
两种转换方式的选择:
- 在你不确定安全隐患的条件下,尽量采用标准方式进行数据转换。
- 当程序对运行性能有高要求,同时满足对数据仅仅只有读操作的条件,且存在频繁转换(例如消息转发场景),可以使用强转换。