Go中的字符串
字符串(string
)是由0个或多个字符组成的有限序列。字符串可以看作一个由字符组成的数组,和数组一样,字符串也是一种不可变的数据类型。
字符串是Go语言的基础数据类型中的一种,其他基础数据类型还有布尔类型和数字类型。
字符串是utf8
编码实现的,直接使用len()
函数拿到的是字符串的字节数,使用utf8.RuneCountInString()
才能拿到字符串的字符数量。
字符串的使用:
func useString() {
a := "a"
b := "ab" + "汉字" // 字符串拼接会产生一个新的字符串
fmt.Println(len(a), len(b)) // 字节数量 1, 8 (2 + 3 * 2)
fmt.Println(utf8.RuneCountInString(b)) // 字符数量 4
for i := 0; i < len(b); i++ {
fmt.Printf("%05v %c \n", b[i], b[i])
}
for _, val := range b {
fmt.Printf("%v %c\n", val, val)
}
for i, size := 0, 0; i < len(b); i += size {
r, byteSize := utf8.DecodeRuneInString(b[i:])
size = byteSize
fmt.Printf("%v %c\n", r, r)
}
}
byte
是uint8
的别名,代表一个ASCII字符。rune
是int32
的别名,代表一个Unicode字符。UTF-8是一种Unicode的变长变长编码方式,字符串中的数据是这样存储的:
其中每个长方形代表1字节。
b := "ab" + "汉字"
for i := 0; i < len(b); i++ {
fmt.Printf("%05v %c \n", b[i], b[i])
}
这种循环方式打印内容如下:
00097 a
00098 b
00230 æ
00177 ±
00137
00229 å
00173
00151
因为例子中的每个汉字使用3个字节表示,但是使用上面代码的for
循环遍历的时候,是按照每个字节对应的编码来找到对应字符的,能正常打印出ASCII字符,但是无法正常打印汉字。
(不知为何,打印汉字的时候打印出的字符和ASCII表对不上,比如230对应的ASCII字符应该是μ\muμ,但是打印出了æ)
for _, val := range b {
fmt.Printf("%v %c\n", val, val)
}
for i, size := 0, 0; i < len(b); i += size {
r, byteSize := utf8.DecodeRuneInString(b[i:])
size = byteSize
fmt.Printf("%v %c\n", r, r)
}
这两种方式打印的内容如下:
97 a
98 b
27721 汉
23383 字
字符串的底层结构
字符串的底层数据类型是StringHeader
:
(我现在用的Go的版本是1.20.5,根据源代码注释内容,字符串的部分有修改,要用unsafe.String
而不是reflect.StringHeader
,和参考的书籍中提到的内容已经不一样,但是整体思路应该是差不多的,所以还是就按照书上的思路先了解一下字符串)
// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
//
// In new code, use unsafe.String or unsafe.StringData instead.
type StringHeader struct {
Data uintptr
Len int
}
Data
指向字符串对应的字符数组,Len
是字节的数量。
字符串是不可变的,不能像数组那样用索引下标a[0] = "a1"
进行赋值,进行字符串拼接时,是会创建新的字符串而不是修改原有的字符串。
比如以下这个例子:
func f4() {
a := "a"
a += "b" // 创建新的字符串 "ab"
a += "c" // 创建新的字符串 "abc"
fmt.Println(a)
}
每一个字符串拼接,都会生成新的字符串。比较耗内存。图中不同的颜色表示不同的内存:
下面的代码通过修改a
字符串的底层数据结构中的Data
指针,就地修改了字符串的内容。虽然可以节省内存,但是一般情况下不这样使用。
func f5() {
a := "a"
p := (*reflect.StringHeader)(unsafe.Pointer(&a))
b := make([]byte, p.Len+2)
for i := 0; i < p.Len; i++ {
tmp := uintptr(unsafe.Pointer(p.Data))
b[i] = *(*byte)(unsafe.Pointer(tmp + uintptr(i)))
}
b[p.Len] = 'b'
b[p.Len+1] = 'c'
q := (*reflect.SliceHeader)(unsafe.Pointer(&b))
p.Data = q.Data
p.Len = p.Len + 2
fmt.Println(a) // a被就地修改了,变为了abc
}
图中不同的颜色表示不同的内存,比起通过字符串拼接产生新字符串,这样写要节约内存一些。
参考文章
《深入Go语言——原理、关键技术与实战》by 历冰、朱荣鑫、黄迪璇
转载自:https://juejin.cn/post/7340240633999523849