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字符应该是 μ \mu μ,但是打印出了æ)

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 历冰、朱荣鑫、黄迪璇

相关推荐
AI绘画哇哒哒6 小时前
【建议收藏!】35岁后端血泪忠告,这3类人别硬转Agent(过来人亲述)
java·人工智能·后端·ai·程序员·大模型·agent
唐青枫7 小时前
一个值多种形态:Zig union、Tagged Union 与内存布局实战
后端
卷无止境7 小时前
Windows 上丝滑开发 Python,并稳定构建 Docker 镜像
后端·python·docker
Nturmoils7 小时前
备份完不算完,先还原到临时库验一遍
后端
Csvn8 小时前
📊 SQL 入门 Day 22:视图与物化视图
后端·sql
Csvn8 小时前
🐍 Day 4: Python 控制流 — 条件、循环与推导式的艺术
后端·python
2601_953720828 小时前
【计算机毕业设计】基于Vue与Spring Boot的高校兼职信息服务平台设计与实现
spring boot·后端·课程设计
再吃一根胡萝卜10 小时前
用 Rust 写一个桌面悬浮图标:为什么它比 Python 更适合 AI 桌面工具?
后端
IT_陈寒10 小时前
Vue的computed属性把我坑惨了,原来我一直用错姿势
前端·人工智能·后端
evans在进步11 小时前
Spring Boot 核心机制详解:可执行 JAR、CORS、静态资源与配置绑定
spring boot·后端·jar