Go语言的字符串与Java和python是一样的。具有不可变性。是一个只读的字节数组,如图所示。
因为Go的字符串具有不可变性,所以我们只能通过string
和[]byte
类型之间反复转换实现修改。
- 将这一段内存复制到栈上
- 将变量的类型转换成
[]byte
后并修改字节数据 - 将修改后的字节数组转换回
string
字符串的数据结构与切片的数据结构
首先来看一下字符串的数据结构:
type StringHeader struct {
Data uintptr
Len int
}
然后我们再来看一下切片的数据结构:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
可以看到和字符串相比,切片的数据结构仅仅多了一个Cap
而已。
所以我们说:字符串是只读的切片类型
字符串解析过程
在了解字符串解析过程之前,我们先来了解一下编译的大致流程
- 词法分析 – 解析源代码文件,把文件中的字符串序列转换成
Token
序列。这个字符串不包含空格,换行等字符。我们把执行词法分析的程序叫做词法分析器 - 语法分析 – 词法分析的输出的
Token
序列就是语法分析的输入。它可以把Token
序列转换成有意义的结构体,即语法树 - 类型检查 – 检查语法树中的定义和使用的类型,所有类型错误和不匹配都会在这个阶段暴露出来
- 中间代码生成 – 类型检查过后,编译器就不存在语法错误和类型不匹配的问题了,这个时候
gc.compileFunctions
会编译整个Go语言项目中的全部函数,在一个编译队列里面等待几个Goroutine
消费,并发执行Goroutine
会将所有函数对应的抽象语法树转换成中间代码。在这个过程中,代码已经做好了优化,例如SSA静态单赋值的优化 - 机器码生成
接下来回到字符串的解析过程。
词法分析器会在词法阶段解析字符串,该阶段会把无意义的字符串流转换成Token
字符串。
我们有两种方式来声明一个字符串
// 双引号
str1 := "this is a string"
// 单引号
str2 := `this is another
string`
当我们使用双引号的时候,如果字符串内部有""
的话,那么我们需要加上\
符号来避免编译器的解析错误。所以当我们要手写json
字符串的时候,我们最好使用单引号。
双引号字符串解析
先来看一下源码。
func (s *scanner) stdString() {
ok := true
s.nextch()
for {
// 逃逸双引号完成正确解析
if s.ch == '"' {
s.nextch()
break
}
if s.ch == '\\' {
s.nextch()
if !s.escape('"') {
ok = false
}
continue
}
if s.ch == '\n' {
s.errorf("newline in string")
ok = false
break
}
if s.ch < 0 {
s.errorAtf(0, "string not terminated")
ok = false
break
}
s.nextch()
}
s.setLit(StringLit, ok)
}
我们阅读这个代码,总结以下几点:
""
字符串使用\
来逃逸双引号
单引号字符串解析
func (s *scanner) rawString() {
ok := true
s.nextch()
for {
if s.ch == '`' {
s.nextch()
break
}
if s.ch < 0 {
s.errorAtf(0, "string not terminated")
ok = false
break
}
s.nextch()
}
// We leave CRs in the string since they are part of the
// literal (even though they are not part of the literal
// value).
s.setLit(StringLit, ok)
}
可以看到是把非反引号的所有字符都划分到当前字符串的范围中。
我们看到代码可以发现这两种转换方式都有一个共同特点:
s.setLit(StringLit, ok)
我们都会被标记为StringLit
并传递到语法分析阶段。然后执行basicLit
这个函数。
这个函数可以去掉字符串两边的引号等无关干扰,还原其本来面目
字符串拼接
Go语言的字符串使用+
来进行拼接。
Go会自动的判断要拼接的字符串的数量
- 如果数量少于或者等于5个,那么会调用
concatstring{2, 3, 4, 5}
等一系列函数 - 如果超过5个,那么会选择
runtime.Concatstrings
传入一个数组切片
但是不管使用concatstring{2, 3, 4, 5}
中的哪一个,最终都会调用runtime.concatstrings
这个函数会过滤空字符串并计算拼接后字符串的长度。
如果非空字符串的数量为1,并且当前字符串不在栈上,就直接返回这个字符串,不需要格外操作。
否则拼接的字符串是一块新的内存空间,与原来的字符串没有任何关联。
类型转换
图片来自于面向信仰编程
字符串和 []byte
中的内容虽然一样,但是字符串的内容是只读的,我们不能通过下标或者其他形式改变其中的数据,而 []byte
中的内容是可以读写的。不过无论从哪种类型转换到另一种都需要拷贝数据,而内存拷贝的性能损耗会随着字符串和 []byte
长度的增长而增长。
总结:由于这些都是拷贝的操作,所以我们要注意性能问题!!