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

相关推荐
TeDi TIVE8 小时前
springboot和springframework版本依赖关系
java·spring boot·后端
雨辰AI8 小时前
SpringBoot3 + 人大金仓 V9 微服务监控实战|Prometheus+Grafana+SkyWalking 全链路监控
数据库·后端·微服务·grafana·prometheus·skywalking
Nicander8 小时前
理解 mybatis 源码:vibe-coding一个mini-mybatis
后端·mybatis
小呆呆6669 小时前
Codex 穷鬼大救星
前端·人工智能·后端
FelixBitSoul10 小时前
缓存淘汰策略全解:从原理到手写实现(Java / Go / Python)
后端·面试
AI人工智能+电脑小能手10 小时前
【大白话说Java面试题】【Java基础篇】第29题:静态代理和动态代理的区别是什么
java·开发语言·后端·面试·代理模式
dovens11 小时前
SpringBoot集成MQTT客户端
java·spring boot·后端
❀͜͡傀儡师11 小时前
Spring Boot 集成 RocksDB 实战:打造高性能 KV 存储加速层
java·spring boot·后端·rocksdb
TeamDev11 小时前
如何在 DotNetBrowser 中使用本地 AI 模型
前端·后端·.net