likes
comments
collection
share

Go中的字符串

作者站长头像
站长
· 阅读数 6

字符串(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)
	}
}

byteuint8的别名,代表一个ASCII字符。runeint32的别名,代表一个Unicode字符。UTF-8是一种Unicode的变长变长编码方式,字符串中的数据是这样存储的:

Go中的字符串

其中每个长方形代表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 

Go中的字符串

因为例子中的每个汉字使用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 字

Go中的字符串

字符串的底层结构

字符串的底层数据类型是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)
}

每一个字符串拼接,都会生成新的字符串。比较耗内存。图中不同的颜色表示不同的内存:

Go中的字符串

下面的代码通过修改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中的字符串

图中不同的颜色表示不同的内存,比起通过字符串拼接产生新字符串,这样写要节约内存一些。

参考文章

《深入Go语言——原理、关键技术与实战》by 历冰、朱荣鑫、黄迪璇