Go 语言实现——[]byte 和 string¶
[]byte 和 string 互相类型转换复制底层数据吗?¶
[]byte("helloworld")
string([]byte{0x1, 0x2})
复制,或者说大部分情况下复制。
虽然 []byte 和 string 两个类型的底层结构差不多,string 的结构和 []byte 的前两个字段一致,但是大部分情况下类型转换的时候 go 还是会新建一个对象,然后将数据复制过去,而不是直接把 data 指针和 len 长度复制过去。因为 string 是 immutable 类型,[]byte 是 mutable 的。
// src/runtime/slice.go
type slice struct {
data uintptr
len int
cap int
}
// src/runtime/string.go
type stringStruct struct {
str unsafe.Pointer
len int
}
以下面这段代码为例:
// 保存到 t.go 然后 go tool compile -S t.go
b := []byte(s)
打印出这段代码编译出的汇编代码:
# 将临时变量 autotmp_2 的地址押入栈作为 runtime.stringtoslicebyte 的第一个参数
0x0021 00033 (t.go:6) LEAQ ""..autotmp_2+64(SP), AX
0x0026 00038 (t.go:6) MOVQ AX, (SP)
# 加载字符串的地址 AX 寄存器中
0x002a 00042 (t.go:6) LEAQ go.string."hello world"(SB), AX
# 在栈中直接构建 runtime.stringtoslicebyte 第二个参数 string 结构体
# 首先将前面字符串的地址 AX 赋值给 stringStruct.str
0x0031 00049 (t.go:6) MOVQ AX, 8(SP)
# 将长度信息赋值给 stringStruct.len
0x0036 00054 (t.go:6) MOVQ $11, 16(SP)
0x003f 00063 (t.go:6) PCDATA $1, $0
0x003f 00063 (t.go:6) NOP
# 调用 runtime.stringtoslicebyte 函数
0x0040 00064 (t.go:6) CALL runtime.stringtoslicebyte(SB)
下面是 runtime.stringtoslicebyte 的实现代码,该函数接受两个参数,第二个是要复制的字符串。如果转换后的变量没有逃逸的话,那么 go 会直接在栈上给对象分配空间并复制,第一个参数就是栈上的地址,如果逃逸了第一个参数就是 nil,runtime.stringtoslicebyte 会在堆上新建一个对象并复制。
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}
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
}
另外,也有不少情况编译器优化会不复制数据,直接指针指过去,[]byte 转 string 有两个方法,slicebytetostring 和 slicebytetostringtmp,其中 slicebytetostringtmp 就是不复制版本。
上层代码也可以使用不复制数据的类型转换:
import (
"unsafe"
)
func ByteSliceToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
参考&延伸:
https://github.com/golang/go/blob/master/src/runtime/slice.go
https://github.com/golang/go/blob/master/src/runtime/string.go
https://golang.design/under-the-hood/zh-cn/part1basic/ch01basic/asm/
https://syslog.ravelin.com/byte-vs-string-in-go-d645b67ca7ff
https://github.com/golang/go/wiki/CompilerOptimizations#string-and-byte
fasthttp 中 []byte、string 的一些优化小技巧¶
https://github.com/valyala/fasthttp#fasthttp-best-practices
复用 []byte¶
Do not allocate objects and []byte buffers - just reuse them as much as possible. Fasthttp API design encourages this.
fasthttp 中的很多接口有一个额外的 dst
参数,这个参数就是给复用 []byte 用的。
func Get(dst []byte, url string) (statusCode int, body []byte, err error)
fasthttp.Get
返回的 body 可以通过 dst 传给下一次调用重复使用,fasthttp.Get
中执行 dst = dst[:0]
重置 buffer,然后复用这个 buffer。
使用 sync.Pool 缓存频繁申请释放的对象¶
sync.Pool is your best friend
sync.Pool 的实现说明参见:golang-internals-sync-pool
使用样例可以参见:https://github.com/valyala/fasthttp/search?q=sync.pool
避免 []byte 和 string 的类型转换¶
Avoid conversion between []byte and string, since this may result in memory allocation+copy. Fasthttp API provides functions for both []byte and string - use these functions instead of converting manually between []byte and string. There are some exceptions - see this wiki page for more details
避免 []byte 和 string 之间的类型转换,因为大部分转换都需要重新分配内存再把数据拷贝过去。详见:[]byte 和 string 互相类型转换复制底层数据吗? 。
fasthttp 中底层一般都是使用 []byte 类型来存储 http 的数据,但有些接口也提供了 XxxString() 版本接受 string 参数,String 版本里是使用 append(b, s...)
这个方式来避免转换,直接将数据复制到底层 []byte 类型的 buffer 中的。
nil []byte 无需特殊对待¶
[]byte 的零值 nil 不用特殊对待,可以和空 slice []byte{}
一样的使用(nil 和 空 slice 只是指向底层数组的指针不一样,其它结构都是一样的,详见:golang-internals-nil)。
下面这些都是合法的:
var (
// both buffers are uninitialized
dst []byte
src []byte
)
// 下面这些都是合法的
dst = append(dst, src...)
copy(dst, src)
(string(src) == "")
(len(src) == 0)
src = src[:0]
for i, ch := range src {
doSomething(i, ch)
}
// 不需要像下面这样检查 []byte 是不是 nil,直接用就可以了
// srcLen := 0
// if src != nil {
// srcLen = len(src)
// }
srcLen := len(src)
string 可以被 append 给 []byte¶
var dst []byte
dst = append(dst, "foobar"...)
range string 的两种方式¶
for i, r := range s {
}
第一种,默认的方式,这种情况下,Go 假定字符串是 utf-8 编码的,range 的时候是一个字符一个字符的遍历的,这个字符可以是 Ascii 字符,也可以是中文、日文等任意合法的 unicode 字符。参见源码可以看到以上这段代码被翻译为了以下语句:
ha := s
for hv1 := 0; hv1 < len(ha); {
hv1t := hv1
hv2 := rune(ha[hv1])
// 检查当前字符是不是 ASCII 字符
if hv2 < utf8.RuneSelf {
hv1++
} else {
// 非 ASCII 字符需要解码
hv2, hv1 = decoderune(ha, hv1)
}
i, r = hv1t, hv2
// 原始 for 循环的 body 放在这
// ...
}
r
的类型为 rune
, rune
是 Go 给 unicode code point
起的一个别称。
// src/builtin/builtin.go
type rune = int32
// https://golang.org/ref/spec#Rune_literals
r := '⌘'
https://go.dev/blog/strings#code-points-characters-and-runes-h2
https://github.com/golang/go/blob/release-branch.go1.17/src/cmd/compile/internal/walk/range.go#L220
https://github.com/golang/go/blob/release-branch.go1.17/src/builtin/builtin.go#L92
第二种,一个字节一个字节的遍历,语法如下:
for i, b := range []byte(s) {
}
Go 编译器会对 range 后面的 []byte(s)
作优化,这种情况下不需要再申请内存复制数据,因为在这个写法下该 slice 就是只读的,没法再对其作修改了。
https://github.com/golang/go/wiki/CompilerOptimizations#string-and-byte