go 字符串
go 字符串
2023/04/09
相关文档:
In [1]:
import (
ic "github.com/WAY29/icecream-go/icecream"
"os"
)
func init() {
ic.ConfigurePrefix("\u001B[37mic| \u001B[0m")
ic.ConfigureOutputFunction(os.Stdout)
ic.ConfigureArgNameFormatterFunc(func(name string) string {
return "\u001B[36m" + name + "\u001B[0m"
})
}
0. 关于 rune
标识 Unicode 码点(code point) 的整数值, 其类型声明为: type rune = int32
是 int32 的别名。
In [2]:
%%
var s = "世界"
rs := []rune(s)
bs := []byte(s)
ic.Ic(len(s), len(rs), len(bs))
Out[2]:
[37mic| [0m[36mlen(s)[0m: 6, [36mlen(rs)[0m: 2, [36mlen(bs)[0m: 6
1. base
- 不可变
- zeroed value:
""
- 底层数据结构
reflect.StringHeader
:可以发现其只比 SliceHeader 少了 Cap, 因为其自身不可变所以并不需要, 不可用 cap 函数.type StringHeader struct { // Data 是一个指针, 指向一个字节数组 Data uintptr Len int }
[]
取值, for…range 与 slice 完全相同. 依然需要注意[]
会引用原数据, 直接使用将使原数据将无法 gc - 传递的是 header, 指向同一字节数组, 但是 string 本身是不可修改的
string 同 slice 一样传递的是 header, 所以不会发生拷贝. 下面示例可以看到底层数组地址相同.
In [3]:
%%
s := "123"
s1 := s
s2 := s
// 指向同一字节数组
fmt.Printf("addr: s1=%v, s2=%v\n", unsafe.StringData(s1), unsafe.StringData(s2))
Out[3]:
addr: s1=0x4bb8f0, s2=0x4bb8f0
String interning
go 有字符串驻留优化, 字符串常量或内容相同的字符串的底层数组相同
In [4]:
%%
s1 := "123"
s2 := "12" + "3"
// 指向同一字节数组
fmt.Printf("addr: s1=%v, s2=%v\n", unsafe.StringData(s1), unsafe.StringData(s2))
Out[4]:
addr: s1=0x4bb8f0, s2=0x4bb8f0
2. 与 []byte/rune
转换
string 转 []byte/rune
:
[]rune(s)
会复制[]byte(s)
如果没有更改, 则会被编译器优化成零拷贝(zero-copy string->[]byte conversion), internal/escape
[]byte/rune
转 string: 始终复制
In [5]:
%%writefile /tmp/string2byte.go
// go build -gcflags='-l -m'
package main
import (
"fmt"
"unsafe"
)
func main() {
s := `hello`
sArrAddr := uintptr(unsafe.Pointer(unsafe.StringData(s)))
sb := []byte(s)
sbArrAddr := uintptr(unsafe.Pointer(unsafe.SliceData(sb)))
// sArrAddr 与 sbArrAddr 相同,因为 s 与 sb 共享底层数据
sbMod := []byte(s)
sbMod[0] = 'a'
sbModArrAddr := uintptr(unsafe.Pointer(unsafe.SliceData(sbMod)))
// sbModArrAddr 与 sArrAddr 不同,因为 sbMod 与 s 不共享底层数据
fmt.Printf("addr: s=%v, sbytes=%v, sbytesMod=%v\n", sArrAddr, sbArrAddr, sbModArrAddr)
}
Out[5]:
Cell contents written to "/tmp/string2byte.go".
In [6]:
!cd /tmp; go run -gcflags='-l -m' string2byte.go
Out[6]:
# command-line-arguments
./string2byte.go:13:15: ([]byte)(s) does not escape
./string2byte.go:13:15: zero-copy string->[]byte conversion
./string2byte.go:17:18: ([]byte)(s) does not escape
./string2byte.go:22:12: ... argument does not escape
./string2byte.go:22:54: sArrAddr escapes to heap
./string2byte.go:22:64: sbArrAddr escapes to heap
./string2byte.go:22:75: sbModArrAddr escapes to heap
Out[6]:
addr: s=4915400, sbytes=4915400, sbytesMod=824634298080
与 []byte
转换零拷贝
原理:
- string 的底层结构只比 slice 少了一个 cap, 元素容器都是都是指针指向的数组, 所以转换时丢弃或者补充 cap 即可
在标准库中就有类似写法, 如 strings.Builder#String()
, 源码:
func (b *Builder) String() string {
return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
}
在 gin 等框架也广泛使用。
func StringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
相关函数:
unsafe.SliceData
: 获取 slice 的底层数组指针unsafe.String
: 组装 string, 参数 ptr 是 []byte 的指针, len 是长度unsafe.StringData
: 获取 string 的底层数据指针unsafe.Slice
: 组装 slice, 参数 ptr 是 []byte 的指针, len 是长度, cap 会取 len
In [7]:
func BytesToString(bs []byte) string {
return unsafe.String(unsafe.SliceData(bs), len(bs))
}
func StringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
%%
// 可以看到三个地址相同
bs := []byte("hello")
bsArrAddr := uintptr(unsafe.Pointer(unsafe.SliceData(bs)))
sFromBs := BytesToString(bs)
sFromBsArrAddr := uintptr(unsafe.Pointer(unsafe.StringData(sFromBs)))
fmt.Printf("addr: bs=%v, \tsFromBs=%v\n", bsArrAddr, sFromBsArrAddr)
bs2 := StringToBytes(sFromBs)
bs2ArrAddr := uintptr(unsafe.Pointer(unsafe.SliceData(bs2)))
fmt.Printf("addr: bs=%v, \tbs2=\t%v\n", bsArrAddr, bs2ArrAddr)
Out[7]:
addr: bs=4962918, sFromBs=4962918
addr: bs=4962918, bs2= 4962918
修改底层字节数组
- slice 的底层数组构建 string, slice 可以修改,并且也会同时反映到 string, 再次拿出 string 的字节数组也是可修改的. 所以
rawstringtmp
不会报错 - string 原本的底层数组不可修改, 利用反射复用同一数组的 slice 也不可改(修改会导致 fatal error, 此种情况无法被
recover
! 类似例子还有 map 的并发读写 / channel 死锁 / 内存或线程耗尽 …)
In [8]:
%%
// 可以看到 bytes s sslice 三者都可以修改并且相互影响
bytes := []byte{'a', 'b'}
s := unsafe.String(unsafe.SliceData(bytes), len(bytes))
fmt.Println("org", bytes, s)
// slice 的底层数组构建 string, slice 可以修改,并且也会同时反映到 string
bytes[0] = 'x'
fmt.Println("mod bytes[0]", bytes, s)
// string 的底层数据来自于 slice, 可以再次取出并修改
sslice := unsafe.Slice(unsafe.StringData(s), len(s))
bytes[1] = 'y'
fmt.Println("mod bytes[1]", bytes, sslice)
Out[8]:
org [97 98] ab
mod bytes[0] [120 98] xb
mod bytes[1] [120 121] [120 121]
In [9]:
%%
s := "hello"
ss := unsafe.Slice(unsafe.StringData(s), len(s))
// string 原本的底层数组不可修改, 构建的 slice 不可改
// 所以下面会错误: unexpected fault address 0x... fatal error: fault
ss[0] = 'a'
_ = ss
Out[9]:
unexpected fault address 0x4bba66
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x2 addr=0x4bba66 pc=0x49997d]
goroutine 1 gp=0xc0000061c0 m=0 mp=0x569220 [running]:
runtime.throw({0x4bbb42?, 0x40b990?})
/usr/local/go/src/runtime/panic.go:1067 +0x48 fp=0xc000084ec0 sp=0xc000084e90 pc=0x465428
runtime.sigpanic()
/usr/local/go/src/runtime/signal_unix.go:914 +0x26c fp=0xc000084f20 sp=0xc000084ec0 pc=0x466b2c
main.main()
[7m[[ Cell [9] Line 7 ]][0m /tmp/gonb_015f6924/main.go:34 +0x5d fp=0xc000084f50 sp=0xc000084f20 pc=0x49997d
runtime.main()
/usr/local/go/src/runtime/proc.go:272 +0x28b fp=0xc000084fe0 sp=0xc000084f50 pc=0x43572b
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000084fe8 sp=0xc000084fe0 pc=0x46c3c1
goroutine 2 gp=0xc000006700 m=nil [force gc (idle)]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/proc.go:424 +0xce fp=0xc000042fa8 sp=0xc000042f88 pc=0x46554e
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:430
runtime.forcegchelper()
/usr/local/go/src/runtime/proc.go:337 +0xb3 fp=0xc000042fe0 sp=0xc000042fa8 pc=0x435a73
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000042fe8 sp=0xc000042fe0 pc=0x46c3c1
created by runtime.init.7 in goroutine 1
/usr/local/go/src/runtime/proc.go:325 +0x1a
goroutine 3 gp=0xc000006c40 m=nil [GC sweep wait]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/proc.go:424 +0xce fp=0xc000043780 sp=0xc000043760 pc=0x46554e
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:430
runtime.bgsweep(0xc000064000)
/usr/local/go/src/runtime/mgcsweep.go:277 +0x94 fp=0xc0000437c8 sp=0xc000043780 pc=0x4214f4
runtime.gcenable.gowrap1()
/usr/local/go/src/runtime/mgc.go:203 +0x25 fp=0xc0000437e0 sp=0xc0000437c8 pc=0x415e85
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc0000437e8 sp=0xc0000437e0 pc=0x46c3c1
created by runtime.gcenable in goroutine 1
/usr/local/go/src/runtime/mgc.go:203 +0x66
goroutine 4 gp=0xc000006e00 m=nil [GC scavenge wait]:
runtime.gopark(0xc000064000?, 0x4e0f08?, 0x1?, 0x0?, 0xc000006e00?)
/usr/local/go/src/runtime/proc.go:424 +0xce fp=0xc000043f78 sp=0xc000043f58 pc=0x46554e
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:430
runtime.(*scavengerState).park(0x568480)
/usr/local/go/src/runtime/mgcscavenge.go:425 +0x49 fp=0xc000043fa8 sp=0xc000043f78 pc=0x41ef29
runtime.bgscavenge(0xc000064000)
/usr/local/go/src/runtime/mgcscavenge.go:653 +0x3c fp=0xc000043fc8 sp=0xc000043fa8 pc=0x41f49c
runtime.gcenable.gowrap2()
/usr/local/go/src/runtime/mgc.go:204 +0x25 fp=0xc000043fe0 sp=0xc000043fc8 pc=0x415e25
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000043fe8 sp=0xc000043fe0 pc=0x46c3c1
created by runtime.gcenable in goroutine 1
/usr/local/go/src/runtime/mgc.go:204 +0xa5
goroutine 5 gp=0xc000007340 m=nil [finalizer wait]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/proc.go:424 +0xce fp=0xc000044620 sp=0xc000044600 pc=0x46554e
runtime.runfinq()
/usr/local/go/src/runtime/mfinal.go:193 +0x107 fp=0xc0000447e0 sp=0xc000044620 pc=0x414f07
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc0000447e8 sp=0xc0000447e0 pc=0x46c3c1
created by runtime.createfing in goroutine 1
/usr/local/go/src/runtime/mfinal.go:163 +0x3d
exit status 2
3. 拼接
go compiler 调试的设置: (测试发现自己程序不能使用外部包, 否则会 could not import xxx (file not found))
- debugger package:
cmd/compile
- program args:
E:\0-project\dev-go-web\main.go
+
拼接fmt
:Sprint
系列函数strings.Join
: strings#Joinstrings.Builder
: strings#Builder 实现了 write 相关方法, 相比bytes.Buffer
其 String() 方法不需要额外复制底层字节数组bytes.Buffer
: bytes#Buffer 实现了 read/write 相关方法
关于 +
拼接:
- 编译器会将
+
转为OADDSTR
而调用walkAddString
(walkExpr) walkAddString
会根据操作数(此处操作数已经是合并左侧常量后的, 比如"a" + "b" + varStr
会直接优化成"ab" + varStr
)的数量做优化:- 如果操作数小于等于 5, 则调用对应的
concatstring%d
, 然后调用concatstrings(buf, []string{操作数})
- 否则创建一个 slice 存储所有字符串, 调用
concatstrings
- 如果操作数小于等于 5, 则调用对应的
concatstring
相关函数可以 debug 通过上步的typecheck.LookupRuntime(fn)
的返回值的sym.Pkg
看到位于runtime
下, 具体位置是 runtime/string.go#L25 (runtime 下 debug 入口是自己程序而不是 compile). 其主要步骤是:- 遍历操作数计算拼接的长度 l, 以及非空操作数数量 count, 最后一个非空的位置 idx
- count == 0 则直接返回空串; count == 1: If there is just one string and either it is not on the stack or our result does not escape the calling frame (buf != nil), then we can return that string directly.
rawstringtmp
准备存储空间, s 是结果字符串, b 是 s 对应的底层字节数据. 关于此字符串的字节数组可修改见下面 修改底层字节数组- 遍历操作数 copy 到 b(源码
copy(b, x)
), copy 之后 b 需要移动到下个位置开始(源码b = b[len(x):]
)
Last updated on