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

相关推荐
摇滚侠3 小时前
Spring Boot 3零基础教程,IOC容器中组件的注册,笔记08
spring boot·笔记·后端
程序员小凯5 小时前
Spring Boot测试框架详解
java·spring boot·后端
你的人类朋友6 小时前
什么是断言?
前端·后端·安全
程序员小凯7 小时前
Spring Boot缓存机制详解
spring boot·后端·缓存
i学长的猫7 小时前
Ruby on Rails 从0 开始入门到进阶到高级 - 10分钟速通版
后端·ruby on rails·ruby
用户21411832636028 小时前
别再为 Claude 付费!Codex + 免费模型 + cc-switch,多场景 AI 编程全搞定
后端
茯苓gao8 小时前
Django网站开发记录(一)配置Mniconda,Python虚拟环境,配置Django
后端·python·django
Cherry Zack8 小时前
Django视图进阶:快捷函数、装饰器与请求响应
后端·python·django
爱读源码的大都督9 小时前
为什么有了HTTP,还需要gPRC?
java·后端·架构
码事漫谈9 小时前
致软件新手的第一个项目指南:阶段、文档与破局之道
后端