golang 中 []byte 和string如何转换最快?
在 golang 中,我们经常需要对 []byte 和 string 进行转换,比如读写文件、处理网络数据、编码解码等场景。通常情况下,这不会成为系统的性能瓶颈,但是在某些极致情况下,也可能成为拖慢性能的关键因素。那么,如何选择合适的转换方式,以达到最快的速度呢?
一、标准转换
标准转换是最简单也最常用的转换方式,就是使用 []byte(s) 和 string(b) 这样的语法来进行转换,其中 s 是一个字符串,b 是一个字节切片。这种方式的优点是语法简洁,易于理解,而且可以保证类型安全和数据不变性。但是,这种方式的缺点是可能会涉及内存分配和复制操作,因为字符串在 golang 中是不可变的,所以每次转换都会创建一个新的数据。
package main
import (
"testing"
)
// 测试标准转换 string() 性能
func Benchmark_NormalBytes2String(b *testing.B) {
x := []byte("Hello Gopher! Hello Gopher! Hello Gopher!")
for i := 0; i < b.N; i++ {
_ = string(x)
}
}
// 测试标准转换 []byte 性能
func Benchmark_NormalString2Bytes(b *testing.B) {
x := "Hello Gopher! Hello Gopher! Hello Gopher!"
for i := 0; i < b.N; i++ {
_ = []byte(x)
}
}
运行 go test -bench="." -benchmem ,得到以下结果:
goos: darwin
goarch: amd64
pkg: example/standard
Benchmark_NormalBytes2String-8 83988709 13.60 ns/op 48 B/op 1 allocs/op
Benchmark_NormalString2Bytes-8 73114815 16.39 ns/op 48 B/op 1 allocs/op
PASS
ok example/standard 3.170s
可以看到,标准转换每次操作都会分配 48 字节的内存,并且耗时在 15 纳秒左右。
二、强制转换
强制转换是一种使用 unsafe 和 reflect 包来直接操作底层的数据结构的转换方式,这种方式可以避免内存分配和复制操作,从而提高性能。但是,这种方式的缺点是可能会破坏类型安全和数据不变性,因为它会直接修改原始数据的指针和长度信息,所以需要谨慎使用,并且遵守一些规则,比如不要修改原始数据、不要超出原始数据的范围、不要让原始数据被垃圾回收等。
我们可以通过以下代码来实现强制转换,并测试它的性能:
package main
import (
"bytes"
"reflect"
"testing"
"unsafe"
)
// 强制转换 string 到 []byte
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))
}
// 强制转换 []byte 到 string
func Bytes2String(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
// 测试强制转换 string() 性能
func Benchmark_Byte2String(b *testing.B) {
x := []byte("Hello Gopher! Hello Gopher! Hello Gopher!")
for i := 0; i < b.N; i++ {
_ = Bytes2String(x)
}
}
// 测试强制转换 []byte 性能
func Benchmark_String2Bytes(b *testing.B) {
x := "Hello Gopher! Hello Gopher! Hello Gopher!"
for i := 0; i < b.N; i++ {
_ = String2Bytes(x)
}
}
运行 go test -bench="." -benchmem ,得到以下结果:
goos: darwin
goarch: amd64
pkg: example/force
Benchmark_Byte2String-8 1000000000 0.3156 ns/op 0 B/op 0 allocs/op
Benchmark_String2Bytes-8 1000000000 0.3137 ns/op 0 B/op 0 allocs/op
PASS
ok example/force 1.586s
可以看到,强制转换每次操作都不会分配内存,并且耗时在 1 纳秒以下,性能明显优于标准转换。
三、拼接转换
拼接转换是一种使用 strings.Builder 或 bytes.Buffer 来拼接字符串或字节切片的转换方式。通常只有一个字符串或者字节切片,我们并不会选择这种方式来进行转换,因为这会增加操作步骤,性能不会太高。但是如果我们有很多字符串或者字节切片需要拼接并转换,这或许是一种方式。这种方式可以减少内存分配的次数,提高性能。
我们可以通过以下代码来实现拼接转换,并测试它的性能:
package main
import (
"bytes"
"strings"
"testing"
)
// 拼接转换 string 到 []byte
func StringJoinBytes(s string) []byte {
var b bytes.Buffer
b.WriteString(s)
return b.Bytes()
}
// 拼接转换 []byte 到 string
func BytesJoinString(b []byte) string {
var s strings.Builder
s.Write(b)
return s.String()
}
// 测试拼接转换 string() 性能
func Benchmark_BytesJoinString(b *testing.B) {
x := []byte("Hello Gopher! Hello Gopher! Hello Gopher!")
for i := 0; i < b.N; i++ {
_ = BytesJoinString(x)
}
}
// 测试拼接转换 []byte 性能
func Benchmark_StringJoinBytes(b *testing.B) {
x := "Hello Gopher! Hello Gopher! Hello Gopher!"
for i := 0; i < b.N; i++ {
_ = StringJoinBytes(x)
}
}
运行 go test -bench="." -benchmem ,得到以下结果:
goos: darwin
goarch: amd64
pkg: example/join
Benchmark_BytesJoinString-8 66973782 18.25 ns/op 48 B/op 1 allocs/op
Benchmark_StringJoinBytes-8 55316959 21.21 ns/op 64 B/op 1 allocs/op
PASS
ok example/join 2.112s
可以看到,拼接转换每次操作只会分配一次内存,并且耗时在 10 纳秒左右,性能比标准转换还要差。
四、总结
可以看到,我们在 golang 中可以使用标准转换、强制转换或拼接转换来进行 []byte 和 string 的转换。
- 标准转换是最常用的方式,语法简洁易懂,但可能涉及内存分配和复制操作。
- 强制转换可以避免内存分配和复制操作,性能更高,但需要注意类型安全和数据不变性。
- 拼接转换适用于需要拼接多个字符串或字节切片的场景,可以减少内存分配次数,提高性能。根据具体的使用场景和需求,选择合适的转换方式可以达到最快的速度。