1. Go 原生字符串的好处
1.1 string类型的数据是不可变的,提高了字符串的并发安全性和存储利用率。
Go 语言规定,字符串类型的值在它的生命周期内是不可改变的。如果我们声明了一个字符串类型的变量,那我们是无法通过这个变量改变它对应的字符串值的,但这并不是说我们不能为一个字符串类型变量进行二次赋值。
什么意思呢?我们看看下面的代码就好理解了:
var s string = "hello"
s[0] = 'k' // 错误:字符串的内容是不可改变的
s = "gopher" // ok
最大好处就是不用再担心字符串的并发安全问题。Go 字符串可以被多个 Goroutine 共享,开发者不用因为担心并发安全问题,使用会带来一定开销的同步机制。
1.2 没有结尾’\0’,而且获取长度的时间复杂度是常数时间,消除了获取字符串长度的开销。
C 语言获取字符串的长度可以调用标准库的 strlen 函数,函数的实现原理是遍历字符串中的每个字符并计数,直到遇到结尾’\0’停止。显然这是一个线性时间复杂度的算法。并且它存在一个约束, 那就是传入的字符串必须有结尾’\0’,结尾’\0’是字符串的结束标志。如果你使用过 C 语言,想必也吃过字符串结尾’\0’的亏。
Go 语言修正了这个缺陷,Go 字符串中没有结尾’\0’。而且 Go 获取字符串长度是一个常数级时间复杂度
1.3 支持原始字符串
如果要在 C 中构造多行字符串,一般就是两个方法:要么使用多个字符串的自然拼接,要么需要结合续行符""。但因为有转义字符的存在,我们很难控制好格式。Go 语言就简单多了,通过一对反引号原生支持构造原始字符串。而且,Go 语言原始字符串中的任意转义字符都不会起到转义的作用
var s string = ` \n\p[]""dddd`
打印结果: \n\p[]""dddd
1.4 对非 ASCII 字符提供原生支持,消除了源码在不同环境下显示乱码的可能
Go 语言源文件默认采用的是 Unicode 字符集,它囊括了几乎所有主流非 ASCII 字符。Go 字符串中的每个字符都 是一个 Unicode 字符,并且这些 Unicode 字符是以 UTF-8 编码格式存储在内存当中的。
2. Go 运行时中的 string
// GOROOT/src/reflect/value.go
type StringHeader struct {
Data uintptr
Len int
}
string 类型其实是一个“描述符”,它本身并不真正存储字符串数据,而是由一个指向底层存储的指针和字符串的长度字段组成的。
Go 编译器把源码中的 string 类型映射为运行时的一个二元组(Data, Len),真实的字符串值数据就存储在一个被 Data 指向的底层数组中。通过 Data 字段,我们可以得到这个数组的内容:
func dumpBytesArray(arr []byte) {
fmt.Printf("[")
for _, b := range arr{
fmt.Printf("%c ", b)
}
fmt.Printf("]\n")
}
func main() {
s := "hello"
// 将string类型转换为运行时中的StringHeader
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Printf("0x%x\n", hdr.Data) // 0x52a21c
p := (*[5]byte)(unsafe.Pointer(hdr.Data))//获取data字段所指向数组的指针
dumpBytesArray((*p)[:])//输出底层数组内容 [h e l l o]
}
知道了string类型的实现原理,再回头看获取长度时间复杂度为常量就很好理解了,len()函数直接获取字符串的Len字段并返回即可。
我们还可以得到一个结论,将 string 类型通过函数 / 方法参数传入不会带来太多的开销。因为传入的仅仅是一个“描述符”,而不是真正的字符串数据。