Go中的字符串

在看这篇文章之前,可以先看:

《Go中的数组》

《Go中的rune》

字符串(string)是由0个或多个字符组成的有限序列。字符串可以看作一个由字符组成的数组 ,和数组一样,字符串也是一种不可变的数据类型。

字符串是Go语言的基础数据类型中的一种,其他基础数据类型还有布尔类型和数字类型。

字符串是utf8编码实现的,直接使用len()函数拿到的是字符串的字节数,使用utf8.RuneCountInString()才能拿到字符串的字符数量。

字符串的使用:

go 复制代码
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的变长变长编码方式,字符串中的数据是这样存储的:

其中每个长方形代表1字节。

go 复制代码
	b := "ab" + "汉字"

	for i := 0; i < len(b); i++ {
		fmt.Printf("%05v %c \n", b[i], b[i])
	}

这种循环方式打印内容如下:

shell 复制代码
00097 a 
00098 b 
00230 æ 
00177 ± 
00137 
00229 å 
00173 
00151 

因为例子中的每个汉字使用3个字节表示,但是使用上面代码的for循环遍历的时候,是按照每个字节对应的编码来找到对应字符的,能正常打印出ASCII字符,但是无法正常打印汉字。

(不知为何,打印汉字的时候打印出的字符和ASCII表对不上,比如230对应的ASCII字符应该是 <math xmlns="http://www.w3.org/1998/Math/MathML"> μ \mu </math>μ,但是打印出了æ)

go 复制代码
	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)
	}

这两种方式打印的内容如下:

shell 复制代码
97 a
98 b
27721 汉
23383 字

字符串的底层结构

字符串的底层数据类型是StringHeader

(我现在用的Go的版本是1.20.5,根据源代码注释内容,字符串的部分有修改,要用unsafe.String而不是reflect.StringHeader,和参考的书籍中提到的内容已经不一样,但是整体思路应该是差不多的,所以还是就按照书上的思路先了解一下字符串)

go 复制代码
// 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"进行赋值,进行字符串拼接时,是会创建新的字符串而不是修改原有的字符串。

比如以下这个例子:

go 复制代码
func f4() {
	a := "a"
	a += "b" // 创建新的字符串 "ab"
	a += "c" // 创建新的字符串 "abc"
	fmt.Println(a)
}

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

下面的代码通过修改a字符串的底层数据结构中的Data指针,就地修改了字符串的内容。虽然可以节省内存,但是一般情况下不这样使用。

go 复制代码
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 历冰、朱荣鑫、黄迪璇

相关推荐
程序员爱钓鱼6 小时前
Go语言实战案例 — 工具开发篇:实现一个图片批量压缩工具
后端·google·go
ChinaRainbowSea8 小时前
7. LangChain4j + 记忆缓存详细说明
java·数据库·redis·后端·缓存·langchain·ai编程
舒一笑8 小时前
同步框架与底层消费机制解决方案梳理
后端·程序员
minh_coo8 小时前
Spring框架事件驱动架构核心注解之@EventListener
java·后端·spring·架构·intellij-idea
白初&9 小时前
SpringBoot后端基础案例
java·spring boot·后端
计算机学姐11 小时前
基于Python的旅游数据分析可视化系统【2026最新】
vue.js·后端·python·数据分析·django·flask·旅游
该用户已不存在12 小时前
你没有听说过的7个Windows开发必备工具
前端·windows·后端
David爱编程12 小时前
深入 Java synchronized 底层:字节码解析与 MonitorEnter 原理全揭秘
java·后端
KimLiu12 小时前
LCODER之Python:使用Django搭建服务端
后端·python·django
再学一点就睡13 小时前
双 Token 认证机制:从原理到实践的完整实现
前端·javascript·后端