代码版本:1.12.7

Gostring并不像C一样只是个单纯的指针指向一片空间,而是在底层封装了一层结构体来存储。字符串构建过程是先跟据字符串构建stringStruct,再转换成string。

string在runtime包中就是stringStruct,对外呈现叫做string。

主要代码在src/runtime/string.go

常量定义

1
2
3
4
5
6
7
8
9
// The constant is known to the compiler.
// There is no fundamental theory behind this number.
// 这句话的意思是,就是想设32,没有原因:)
const tmpStringBufSize = 32

const (
maxUint = ^uint(0)
maxInt = int(maxUint >> 1)
)

为啥maxUintmaxInt要这么写而不是写成常量是因为不同平台计算出来的不一样。

类型定义

1
2
3
4
5
6
7
8
9
10
11
12
type tmpBuf [tmpStringBufSize]byte

type stringStruct struct {
str unsafe.Pointer
len int
}

// Variant with *byte pointer type for DWARF debugging.
type stringStructDWARF struct {
str *byte
len int
}

文件的一开头就定义了一个tmpBuf这个类型,看名字也知道是一个临时存储用的东西。

下面的stringStruct才是真正的string定义,在stringStruct中存储了具体数据的指针和数据的长度。而stringStructDWARF这个则是debug的时候用的。

这么一个结构体,可以发现里面存储的是一个具体数据的指针和数据的长度.string数据结构跟slice有些类似,只不过切片还有一个表示容量的成员,string本质上可以看做一个只读的字节切片.事实上stringslice,准确的说是[]byte经常发生转换。

底层存储

1
2
3
4
5
6
7
8
9
10
11
12
s1 := "hello world"
s2 := "hello world"
s3 := "hello world"
fmt.Println(&s1,&s2,&s3)
fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&s1)),
(*reflect.StringHeader)(unsafe.Pointer(&s2)),
(*reflect.StringHeader)(unsafe.Pointer(&s3)))

/*
0xc000010200 0xc000010210 0xc000010220
&{17633267 11} &{17633267 11} &{17633267 11}
*/

可以看出来,string在底层的data段都是一个地址,不同的字符串变量指向相同的底层数组,这是因为字符串是只读的,为了节省内存,相同字面量的字符串通常对应于同一字符串常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

func main() {
s1 := "aaa"
s2 := "bbb" + "ccc"
s3 := "aaa" + "bbb"
s4 := "ddd"
_, _, _,_ = s1, s2, s3,s4
}
//go tool compile -S -B -N -l main.go
go.string."aaa" SRODATA dupok size=3
0x0000 61 61 61 aaa
go.string."bbbccc" SRODATA dupok size=6
0x0000 62 62 62 63 63 63 bbbccc
go.string."aaabbb" SRODATA dupok size=6
0x0000 61 61 61 62 62 62 aaabbb
go.string."ddd" SRODATA dupok size=3
0x0000 64 64 64 ddd

可以看到,代码中的”aaa”+”bbb”会被编译器合并为一个”aaabbb”常量字符串。

string与[]byte的无拷贝转换

这里其实得了解到这两者的底层实现

1
2
3
4
5
6
7
8
9
reflect.StringHeader{//string的本质
Data uintptr
Len int
}
reflect.SliceHeader{//slice的本质
Data uintptr
Len int
Cap int
}

可以发现其实两者的结构是类似的,所以可以直接利用unsafe来进行转换。

string to []byte

简单的转换方案:

1
2
3
4
5
//string to []byte
func string2bytes(s string)[]byte{
bs := *(*[]byte)(unsafe.Pointer(&s))
return bs
}

但是这种转换会有一个问题,因为stringslice少了cap字段,所以其实赋值过来的时候cap是丢失的,所以一般更推荐以下这种

1
2
3
4
5
6
7
8
9
func string2bytes(s string)[]byte{
str := (*reflect.StringHeader)(unsafe.Pointer(&s))
bs := reflect.SliceHeader{
Data: str.Data,
Len: str.Len,
Cap: str.Len,
}
return *(*[]byte)(unsafe.Pointer(&bs))
}

需要注意的是,常量string(本身存在的数据区域决定了可不可以修改)转换出来的[]byte是不可修改的,如果修改,会发生致命错误,recover也是捕捉不到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
go func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var s0 = string([]byte{'q','w','e','r','t','y','u','i','o','p'})
bs0 := string2bytes(s0)
bs0[0]='z' //这里是没有问题的
fmt.Println(s0)
var s = "qwerqwerqwerwer"
bs := string2bytes(s)
bs[0]='r' //这里会发生错误
fmt.Println(bs)
}
/*
unexpected fault address 0x10ce0f7
fatal error: fault
[signal SIGBUS: bus error code=0x2 addr=0x10ce0f7 pc=0x109cf7f]
*/

[]byte to string

string转为[]byte不同,sliceheader中多的cap字段可以直接忽略,[]byte可以直接转为string

1
2
3
4
// []byte to string
func bytes2string(bs []byte) string {
return *(*string)(unsafe.Pointer(&bs))
}

当然,也可以写的规范点

1
2
3
4
5
6
7
8
func bytes2string(bs []byte) string {
bsh := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
s := reflect.StringHeader{
Data: bsh.Data,
Len: bsh.Len,
}
return *(*string)(unsafe.Pointer(&s))
}

而对于[]byte转出来的string,在[]byte本身发生变化是,string也是会同步发生变化的

1
2
3
4
5
6
7
func main() {
bs := []byte{'a','b','c','d','e'}
s := bytes2string(bs)
fmt.Println(s)//abcde
bs[0]='z' // 这里如果[]byte修改了,string也会同步修改
fmt.Println(s)//zbcde
}

性能对比

goos: darwin goarch: amd64

方法名 操作耗时
BenchmarkString2Bytes_normal-4 8.80 ns/op
BenchmarkString2Bytes-4 0.400 ns/op
BenchmarkBytes2String_normal-4 5.57 ns/op
BenchmarkBytes2String-4 0.354 ns/op

函数定义

类型转换相关的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185

//string转成stringStruct,就是指针强转
func stringStructOf(sp *string) *stringStruct {
return (*stringStruct)(unsafe.Pointer(sp))
}

// slicebytetostringtmp returns a "string" referring to the actual []byte bytes.
//
// Callers need to ensure that the returned string will not be used after
// the calling goroutine modifies the original slice or synchronizes with
// another goroutine.
// 调用者得确保不会在另外一个goroutine里修改源切片
// The function is only called when instrumenting
// and otherwise intrinsified by the compiler.
// 这个函数仅在检测时调用,或者是编译器优化的时候用。
// Some internal compiler optimizations use this function.
// - Used for m[T1{... Tn{..., string(k), ...} ...}] and m[string(k)]
// where k is []byte, T1 to Tn is a nesting of struct and array literals.
// - Used for "<"+string(b)+">" concatenation where b is []byte.
// - Used for string(b)=="foo" comparison where b is []byte.
//byte切片转string,也是指针强转,指向的还是原来的地址,所以会相互影响
func slicebytetostringtmp(b []byte) string {
if raceenabled && len(b) > 0 {
//竞争检测,忽略
racereadrangepc(unsafe.Pointer(&b[0]),
uintptr(len(b)),
getcallerpc(),
funcPC(slicebytetostringtmp))
}
if msanenabled && len(b) > 0 {
//Memory Sanitizer 用于检测危险指针等内存问题,忽略
msanread(unsafe.Pointer(&b[0]), uintptr(len(b)))
}
return *(*string)(unsafe.Pointer(&b))
}

//string转byte切片,这个就不是指针强转了,是内存拷贝
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
}

//string转rune切片,一般情况是多字节编码的时候用(我猜的)
func stringtoslicerune(buf *[tmpStringBufSize]rune, s string) []rune {
// two passes.
// 双扫描算法。。也就是先for一次计数,然后判断空间,然后再for一次做正事
// unlike slicerunetostring, no race because strings are immutable.
n := 0
for range s {
//注意这里是用range来遍历s来计算长度而不是用len,因为len算的是字节数
//对于多字节编码,比如中文来说,字数!=字节数
n++
}

var a []rune
if buf != nil && n <= len(buf) {
*buf = [tmpStringBufSize]rune{}
a = buf[:n]
} else {
a = rawruneslice(n)
}

n = 0
for _, r := range s {
a[n] = r
n++
}
return a
}

//rune切片转string,一般是多字节编码用(还是我猜的)
func slicerunetostring(buf *tmpBuf, a []rune) string {
if raceenabled && len(a) > 0 {
//竞争检测,忽略
racereadrangepc(unsafe.Pointer(&a[0]),
uintptr(len(a))*unsafe.Sizeof(a[0]),
getcallerpc(),
funcPC(slicerunetostring))
}
if msanenabled && len(a) > 0 {
//Memory Sanitizer 用于检测危险指针等内存问题,忽略
msanread(unsafe.Pointer(&a[0]), uintptr(len(a))*unsafe.Sizeof(a[0]))
}
//这个变量没用,单纯为了调用encoderune这个函数,因为这个函数会把r转到第一个参数里面,下面再详细看这个函数
var dum [4]byte
size1 := 0
for _, r := range a {
// 算大小
size1 += encoderune(dum[:], r)
}
//申请空间
s, b := rawstringtmp(buf, size1+3)
size2 := 0
for _, r := range a {
// check for race
if size2 >= size1 {
break
}
//干正事
size2 += encoderune(b[size2:], r)
}
return s[:size2]
}

// int转string,注意是 int ,不是int64
// 虽然我也不知道为啥int转string非要传一个int64,只能理解是编译器会干点什么
func intstring(buf *[4]byte, v int64) (s string) {
//runeSelf是一个常量 0x80,也就是127
//staticbytes是一个常量数组,里面是0-127的的字节码
if v >= 0 && v < runeSelf {
// 所以这个其实是直接扫表加速处理
stringStructOf(&s).str = unsafe.Pointer(&staticbytes[v])
stringStructOf(&s).len = 1
return
}

var b []byte
if buf != nil {
//有空间,直接用
b = buf[:]
s = slicebytetostringtmp(b)
} else {
//没空间,申请
s, b = rawstring(4)
}
//rune其实就是int32,所以把int64转int32再转int64看是不是自己来判断是否是一个int大小的
if int64(rune(v)) != v {
v = runeError
}
//再算一遍大小,并且填到b里面去
n := encoderune(b, rune(v))
return s[:n]
}

// 这个函数不是string.go里的,是在src/runtime/utf8.go:104
// encoderune writes into p (which must be large enough) the UTF-8 encoding of the rune.
// It returns the number of bytes written.
// 注释的意思是,会把rune进行utf8编码写到p(必须保证空间足够大)里,
// 返回值是写入的字节数
func encoderune(p []byte, r rune) int {
// Negative values are erroneous. Making it unsigned addresses the problem.
switch i := uint32(r); {
case i <= rune1Max: //1<<7 -1 127 ,一个字节的情况
p[0] = byte(r)
return 1
case i <= rune2Max: //1<<11 -1 2047,两个字节
_ = p[1] // eliminate bounds checks
p[0] = t2 | byte(r>>6)
p[1] = tx | byte(r)&maskx
return 2
case i > maxRune, surrogateMin <= i && i <= surrogateMax: //看是不是不再unicode处理范围
// maxRune '\U0010FFFF' // Maximum valid Unicode code point.
// Code points in the surrogate range are not valid for UTF-8.
//const (
// surrogateMin = 0xD800
// surrogateMax = 0xDFFF
//)
r = runeError
fallthrough
case i <= rune3Max:// 1<<16 -1 65535,三个字节
_ = p[2] // eliminate bounds checks
p[0] = t3 | byte(r>>12)
p[1] = tx | byte(r>>6)&maskx
p[2] = tx | byte(r)&maskx
return 3
default:// 自然就是四个字节啦
_ = p[3] // eliminate bounds checks
p[0] = t4 | byte(r>>18)
p[1] = tx | byte(r>>12)&maskx
p[2] = tx | byte(r>>6)&maskx
p[3] = tx | byte(r)&maskx
return 4
}
}

内存申请相关的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// rawstring allocates storage for a new string. The returned
// string and byte slice both refer to the same storage.
// The storage is not zeroed. Callers should use
// b to set the string contents and then drop b.
// 申请size大小的一片内存(不为空,就是纯内存空间)
// 返回的s和b其实指向的一个地方,不过类型不一样
// 调用者应该用b来进行赋值,完成后就释放b
func rawstring(size int) (s string, b []byte) {
// 这个函数是申请空间,第一个参数是大小,第二个参数是类型,第三个参数是是否初始化为0
// 小对象直接在goroutine的栈上申请
// 大对象(>32kb)会直接在堆上申请
p := mallocgc(uintptr(size), nil, false)

stringStructOf(&s).str = p
stringStructOf(&s).len = size

*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}

return
}

// rawbyteslice allocates a new byte slice. The byte slice is not zeroed.
// 申请size大的byte切片
func rawbyteslice(size int) (b []byte) {
//内存大小对齐处理,对go来说内存片是分成不同大小来处理的
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
}

// rawruneslice allocates a new rune slice. The rune slice is not zeroed.
// 申请size大小的rune切片,rune是4字节的,所以函数里出现了很多4
func rawruneslice(size int) (b []rune) {
if uintptr(size) > maxAlloc/4 {
//看看内存够不够
throw("out of memory")
}
//下面和上面一样的处理方式
mem := roundupsize(uintptr(size) * 4)
p := mallocgc(mem, nil, false)
if mem != uintptr(size)*4 {
memclrNoHeapPointers(add(p, uintptr(size)*4), mem-uintptr(size)*4)
}

*(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(mem / 4)}
return
}

工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
func concatstrings(buf *tmpBuf, a []string) string {
//首先声明一些用的上的变量
idx := 0
l := 0
count := 0

for i, x := range a {
n := len(x)
if n == 0 {
//如果当前的这个string长度为0直接跳过
continue
}
if l+n < l {
//判断是不是超过大小,为什么这么写呢,举个例子
//比如 l是一个int64,那么它的最大值就是2^63-1
//如果我的l现在是2^63,然后n现在是2
//那么我的l就会越界变成最小值
//所以判断是不是加完小于自己就行了
throw("string concatenation too long")
}
l += n
count++
idx = i
}
if count == 0 {
return ""
}

// 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.
// 想明白这个位置的话,首先得理解go里面对变量的内存处理
// 我们都知道go里面的函数是可以返回一个地址的,这点跟c不一样,
// c里面如果返回一个局部变量的地址,这个地址在函数执行完之后会被释放掉
// 但是go里面返回的地址则不会,是别的地方可用的,go是怎么实现的呢?
// 主要是go会对代码进行逃逸分析,如果说这个变量的适用范围不仅仅在自己的调用栈
// 那么会直接把它分配到堆上而不是自己的栈上。
if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
//stringDataOnStack见下方工具函数
return a[idx]
}
//判断内存空间够不够,不够就申请
//这里返回的s是实际的string,b其实是一个游标指针,方便后面拷贝
//同见下方工具函数
s, b := rawstringtmp(buf, l)
for _, x := range a {
//开始实际的拷贝,每次都是把x拷贝到b指向到空间,然后把b往后移动len(x)长度继续考呗
copy(b, x)
b = b[len(x):]
}
return s
}

//下面的那2,3,4,5 就不说了,是编译器用来做加速优化的,因为其实大部分字符串相加不会超过5个。

func concatstring2(buf *tmpBuf, a [2]string) string {
return concatstrings(buf, a[:])
}

func concatstring3(buf *tmpBuf, a [3]string) string {
return concatstrings(buf, a[:])
}

func concatstring4(buf *tmpBuf, a [4]string) string {
return concatstrings(buf, a[:])
}

func concatstring5(buf *tmpBuf, a [5]string) string {
return concatstrings(buf, a[:])
}

// s中找t开始位置,正常朴素算法
func index(s, t string) int {
if len(t) == 0 {
return 0
}
for i := 0; i < len(s); i++ {
if s[i] == t[0] && hasPrefix(s[i:], t) {
return i
}
}
return -1
}
// 判断t在不在s中
func contains(s, t string) bool {
return index(s, t) >= 0
}
// 判断s是否以prefix开头
func hasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}

// atoi parses an int from a string s.
// The bool result reports whether s is a number
// representable by a value of type int.
// atoi,也是朴素算法
func atoi(s string) (int, bool) {
if s == "" {
return 0, false
}

neg := false
if s[0] == '-' {
neg = true
s = s[1:]
}

un := uint(0)
for i := 0; i < len(s); i++ {
c := s[i]
if c < '0' || c > '9' {
return 0, false
}
if un > maxUint/10 {
// overflow
return 0, false
}
un *= 10
un1 := un + uint(c) - '0'
if un1 < un {
// overflow
return 0, false
}
un = un1
}

if !neg && un > uint(maxInt) {
return 0, false
}
if neg && un > uint(maxInt)+1 {
return 0, false
}

n := int(un)
if neg {
n = -n
}

return n, true
}

// atoi32 is like atoi but for integers
// that fit into an int32.
// 就是调一下上方的函数
func atoi32(s string) (int32, bool) {
if n, ok := atoi(s); n == int(int32(n)) {
return int32(n), ok
}
return 0, false
}

辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// stringDataOnStack reports whether the string's data is
// stored on the current goroutine's stack.
// 判断string是否在当前栈空间内
func stringDataOnStack(s string) bool {
ptr := uintptr(stringStructOf(&s).str)
stk := getg().stack
//对于go来说,每个goroutine的栈空间记录都是记录栈顶和栈底的地址
//所以只用判断这个指针是不是在这两之间就可以判断在不在当前栈空间上
return stk.lo <= ptr && ptr < stk.hi
}

// 封装了一下,如果buf空间够就直接用buf不够才申请
func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {
if buf != nil && l <= len(buf) {
b = buf[:l]
//把byte的切片转成string
s = slicebytetostringtmp(b)
} else {
//申请空间
s, b = rawstring(l)
}
return
}

//go:nosplit
//无拷贝的byte转string,就是指针类型转换
func gostringnocopy(str *byte) string {
ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
s := *(*string)(unsafe.Pointer(&ss))
return s
}

结语

所以从string.go里来看,里面其实就两种东西

  1. two passes 先计数,然后判断内存,然后做操作
  2. 指针类型转换