0%

《Golang》String原理

每个类型都需要一个边界,这样才能直到这个类型的数据是什么,比如int32,边界就是4个字节,在读取int32数据时,读取4个字节就行了

那么string的边界是什么呢?

在C语言中,string的边界就是”\0”,读取的时候只要碰到”\0”,就代表这个string的结束。

但是这样有个问题,假设我的一个string里面确实有”\0”这个字符,那么这个string就不会被很好的识别,而会被切断

所以现在的很多string设计都没有沿用C语言对string的设计。这篇讲讲Golang怎么设计string的

String结构

1
2
3
4
type stringStruct struct {
str unsafe.Pointer // 指向底层数组的指针
len int // 底层数组大小
}

String concat

a+=”123”内部是怎么运作的?

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
func concatstrings(buf *tmpBuf, a []string) string {
idx := 0
l := 0 // 记录总共concat了多长的字符
count := 0 // 记录concat了几个有效的string
for i, x := range a {
n := len(x)
if n == 0 { // 长度为0,无效字符串
continue
}
if l+n < l {
throw("string concatenation too long")
}
l += n
count++
idx = i
}
if count == 0 { // 如果都是长度为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.
if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
return a[idx]
}
s, b := rawstringtmp(buf, l) // 新初始化一个string以及对应的slice,自动选择分配栈上还是堆上
for _, x := range a { // 数据复制到底层数组,注意:这里是虽然是复制到slice的底层数组,但是string跟slice指向了同一个底层数组
copy(b, x)
b = b[len(x):]
}
return s
}

func stringDataOnStack(s string) bool { // 检查string是否分配在栈中
ptr := uintptr(stringStructOf(&s).str) // 拿到指向数据的指针值
stk := getg().stack
return stk.lo <= ptr && ptr < stk.hi // 判断指针值是否在g的栈内
}

func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {
if buf != nil && l <= len(buf) { // 如果string长度不大于32个字节,则分配在栈上,否则分配到堆中
b = buf[:l]
s = slicebytetostringtmp(b)
} else {
s, b = rawstring(l)
}
return
}

[]byte与string之间的转换

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
func slicebytetostring(buf *tmpBuf, b []byte) (str string) { // string([]byte{}) 会翻译成这个函数
l := len(b)
if l == 0 {
// Turns out to be a relatively common case.
// Consider that you want to parse out data between parens in "foo()bar",
// you find the indices and convert the subslice to string.
return ""
}
if raceenabled {
racereadrangepc(unsafe.Pointer(&b[0]),
uintptr(l),
getcallerpc(),
funcPC(slicebytetostring))
}
if msanenabled {
msanread(unsafe.Pointer(&b[0]), uintptr(l))
}
if l == 1 {
stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]])
stringStructOf(&str).len = 1
return
}

var p unsafe.Pointer
if buf != nil && len(b) <= len(buf) {
p = unsafe.Pointer(buf) // 32字节以内栈中分配
} else {
p = mallocgc(uintptr(len(b)), nil, false) // 堆中分配
}
stringStructOf(&str).str = p
stringStructOf(&str).len = len(b)
memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b))) // b slice的底层数组拷贝到p指向的位置
return
}

func stringtoslicebyte(buf *tmpBuf, s string) []byte { // string强转成[]byte会翻译成这个方法。如果是[]byte("123")这种,则编译器会直接转化"123",不会翻译成这个方法了
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{} // 32字节以内栈中分配
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s) // 底层数组拷贝
return b
}

[]rune和string之间的转换

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
func stringtoslicerune(buf *[tmpStringBufSize]rune, s string) []rune { // string强转成[]rune会翻译成这个方法
// two passes.
// unlike slicerunetostring, no race because strings are immutable.
n := 0
for range s { // 计算出string中有几个rune字符
n++
}

var a []rune
if buf != nil && n <= len(buf) { // 32个rune字符以内,分配栈中
*buf = [tmpStringBufSize]rune{}
a = buf[:n]
} else {
a = rawruneslice(n)
}

n = 0
for _, r := range s { // range出来的都是rune
a[n] = r
n++
}
return a
}

func slicerunetostring(buf *tmpBuf, a []rune) string { // []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 {
msanread(unsafe.Pointer(&a[0]), uintptr(len(a))*unsafe.Sizeof(a[0]))
}
var dum [4]byte
size1 := 0
for _, r := range a { // 计算出rune数组中所有rune转换成bytes总共占用多少字节
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) // rune转换成bytes写入b中,也就是写入了s中
}
return s[:size2]
}

整型转换成string

实际上就是:整型先转换成rune,然后转换成string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func intstring(buf *[4]byte, v int64) (s string) { // int强转成string会翻译成这个方法
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) // 分配4个字节大小的string,因为一个rune字符最多占用4个字节
}
if int64(rune(v)) != v {
v = runeError
}
n := encoderune(b, rune(v)) // 整型先转换成rune,然后转换成string
return s[:n]
}

总结

  1. 在强转时,如果源数据是一个已知常量(比如a:=[]byte(“123”)),则编译器会自行转化,不会翻译成对应的强转方法
  2. string结构体就是由slice的前两个结构体组成,具有一样的含义。虽然他两结构差不多,但是string的行为是值类型的行为,比如string的赋值是拷贝赋值,底层数组都会拷贝
  3. 如果string长度不大于32个字节,则底层数组分配在栈上,否则分配到堆中。在string concat时就会进行判断
  4. string底层数组就是字节数组[]byte,string的长度就是字节数组的长度
  5. string在使用切片操作a[2:5]时,取的直接是[2:5)的字节,而range迭代时,取出的都是最多占用4字节的rune字符(自动解码出来)



微信关注我,及时接收最新技术文章