go支持原生字符串类型, go string 类型的数据是不可变的,提高了字符串的并发安全性和存储利用率,我们不能修改字符串的某一部分值,可以进行二次赋值操作
var s string = "hello"
s[0] = 'k' // 错误:字符串的内容是不可改变的
s = "gopher" // ok
类型的数据是不可变的这种特性,也去除了字符串的并发安全问题,
针对同一个字符串值,无论它在程序的几个位置被使用,Go 编译器只需要为它分配一块存储就好了,大大提高了存储利用率。
在 C 语言中,获取一个字符串的长度可以调用标准库的 strlen 函数,这个函数的实现原理是遍历字符串中的每个字符并做计数,直到遇到字符串的结尾’\0’停止。显然这是一个线性时间复杂度的算法,执行时间与字符串中字符个数成正比,计算字符串长度是一个(o)n级别复杂度了
go语言去c掉了语言特性中结尾\0 的机制,计算字符串长度是一个简单的常数级别
C 语言没有内置对非 ASCII 字符(如中文字符)的支持。
Go 语言源文件默认采用的是 Unicode 字符集,Unicode 字符集是目前市面上最流行的字符集,它囊括了几乎所有主流非 ASCII 字符(包括中文字符)。Go 字符串中的每个字符都是一个 Unicode 字符,并且这些 Unicode 字符是以 UTF-8 编码格式存储在内存当中的
Go 语言中的字符串值也是一个可空的字节序列,字节序列中的字节个数称为该字符串的长度。一个个的字节只是孤立数据,不表意。
var a = "中国人"
fmt.Printf("the length of s = %d\n", len(a)) //字节长度 the length of s = 9
for i := 0; i < len(a); i++ {
fmt.Printf("0x%x ", a[i]) //这个字符串有9个字节 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba
}
字符串是由一个可空的字符序列构成
var a = "中国人"
fmt.Printf("the length of s = %d\n", utf8.RuneCountInString(a)) //字符长度 the length of s = 3
for _, c := range a {
fmt.Printf("0x%x ", c) //返回字符集表中的码点,每一个字符对应的 unicode 码点 0x4e2d 0x56fd 0x4eba
}
Unicode 码点,就是指将 Unicode 字符集中的所有字符“排成一队”,字符在这个“队伍”中的位次,就是它在 Unicode 字符集中的码点。也就说,一个码点唯一对应一个字符,“码点”的概念和我们马上要讲的 rune 类型有很大关系。
Go 使用 rune 这个类型来表示一个 Unicode 码点。rune 本质上是 int32 类型的别名类型,它与 int32 类型是完全等价的
由于一个 Unicode 码点唯一对应一个 Unicode 字符。所以我们可以说,一个 rune 实例就是一个 Unicode 字符,一个 Go 字符串也可以被视为 rune 实例的集合。我们可以通过字符字面值来初始化一个 rune 变量。
s := 'a'
s := "a"
UTF-32编码方案,固定长度字节 将所有 Unicode字符的码点都按照固定 4 字节编码。
UTF-8编码方案:变长度字节,根据 Unicode字符的码点序号不同,所编码的字节数不同。1~4 个字节
UTF-8 编码使用的字节数量从 1 个到 4 个不等,UTF-32 只能使用4个字节,utf-8 能 更好的利用内存的使用,以及 utf-8 支持所有 ASCII 码以及语言字节,UTF-32 和1字节的ASCII 有兼容问题,UTF-8 编码方案 成为了主流的编码方案
Go 语言在运行时层面通过一个二元组结构(Data, Len)来表示一个 string 类型变量,其中 Data 是一个指向存储字符串数据内容区域的指针值,Len 是字符串的长度。因此,本质上,一个 string 变量仅仅是一个“描述符”,并不真正包含字符串数据。因此,我们即便直接将 string 类型变量作为函数参数,其传递的开销也是恒定的,不会随着字符串大小的变化而变化。
在字符串的实现中,真正存储数据的是底层的数组。字符串的下标操作本质上等价于底层数组的下标操作
var s = "中国人"
fmt.Printf("0x%x\n", s[0]) // 0xe4:字符“中” utf-8编码的第一个字节
Go 有两种迭代形式:常规 for 迭代与 for range 迭代。你要注意,通过这两种形式的迭代对字符串进行操作得到的结果是不同的。
var a = "中国人"
fmt.Printf("the length of s = %d\n", len(a)) //字节长度 the length of s = 9
for i := 0; i < len(a); i++ {
fmt.Printf("0x%x ", a[i]) //这个字符串有9个字节 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba
}
var a = "中国人"
fmt.Printf("the length of s = %d\n", utf8.RuneCountInString(a)) //字符长度 the length of s = 3
for _, c := range a {
fmt.Printf("0x%x ", c) //返回字符集表中的码点,每一个字符对应的 unicode 码点 0x4e2d 0x56fd 0x4eba
}
var s = "hell"
s = s + "word"
var a = "hell"
a += "word"
println(s, a) //hellword hellword
+/+= 方法实现字符串拼接,Go 还提供了 strings.Builder、strings.Join、fmt.Sprintf 等函数来进行字符串连接操作
var b strings.Builder
for i := 3; i >= 1; i-- {
fmt.Fprintf(&b, "%d...", i)
}
b.WriteString("ignition")
fmt.Println(b.String())//3...2...1...ignition
s := []string{"foo", "bar", "baz"}
fmt.Println(strings.Join(s, ", "))
println(s)//foo, bar, baz
var a = "中国人"
fmt.Printf("the length of s = %d\n", utf8.RuneCountInString(a)) //字符长度 the length of s = 3
Go 字符串类型支持各种比较关系操作符,包括 = =、!= 、>=、<=、> 和 <。在字符串的比较上,Go 采用字典序的比较策略,分别从每个字符串的起始处,开始逐个字节地对两个字符串类型变量进行比较。
func main() {
// ==
s1 := "世界和平"
s2 := "世界" + "和平"
fmt.Println(s1 == s2) // true
// !=
s1 = "Go"
s2 = "C"
fmt.Println(s1 != s2) // true
// < and <=
s1 = "12345"
s2 = "23456"
fmt.Println(s1 < s2) // true
fmt.Println(s1 <= s2) // true
// > and >=
s1 = "12345"
s2 = "123"
fmt.Println(s1 > s2) // true
fmt.Println(s1 >= s2) // true
}
Go 支持字符串与字节切片、字符串与 rune 切片的双向转换,并且这种转换无需调用任何函数,只需使用显式类型转换就可以了
var s string = "中国人"
//string => []rune
rs := []rune(s)
fmt.Printf("%x\n", rs) //[4e2d 56fd 4eba]
// string -> []byte
bs := []byte(s)
fmt.Printf("%x\n", bs) //e4b8ade59bbde4baba
// []rune -> string
s1 := string(rs)
fmt.Printf(s1) // 中国人
// []byte -> string
s2 := string(bs)
fmt.Println(s2) // 中国人
这样的转型看似简单,但无论是 string 转切片,还是切片转 string,这类转型背后也是有着一定开销的。这些开销的根源就在于 string 是不可变的,运行时要为转换后的类型分配新内存。
好记性不如烂笔头 本文学自 极客时间 Tony Bai · Go 语言第一课