在〈认识预定义类型〉中略略谈过字符串,表面看来,用双引号("
)或反引号(`)括起来的文本就是字符串,默认类型为string
,实际在 Go 中,字符串是由只读的 UTF-8 编码字节所组成。
字符串入门
先从简单的开始,在 Go 源码中,如果你编写"Go语言"
这么一段文本,那么会产生一个字符串,默认类型为string
,字符串是只读的,一旦创建,就无法改变字符串内容。
使用string
定义变量若无指定初值,默认是空字符串""
,可以使用+
对两个字符串进行连接,由于字符串是只读的,因此实际上连接的动作,会产生新的字符串,如果想比较两个字符串的相等性,可以使用==
、!=
、<
、<=
、>
、>=
依字典顺序比较。
package main
import "fmt"
func main() {
text1 := "Go语言"
text2 := "Cool"
var text3 string
fmt.Println(text1 + text2) // Go语言Cool
fmt.Printf("%q\n", text3) // ""
fmt.Println(text1 > text2) // true
}
上面的例子中,由于使用fmt.Println
显示空字符串时看不到什么,因此改用fmt.Printf
,并使用%q
来脱离无法显示的字符。
使用""
时不可换行,如果你的字符串想要换行,方法之一是分两个字符串,并用+
连接。例如:
text := "Go语言" +
"Cool"
另一个方式是以重音符 ` 定义字符串,例如:
package main
import "fmt"
func main() {
text := `Go语言
Cool`
fmt.Printf("%q\n", text) // "Go语言\n Cool"
}
使用 ` 定义的字符串,会完全保留换行与空白,因此,在上头你可以看到被保留的换行与空白字符,如果使用fmt.Println(text)
,显示时也会看到对应的换行与空白。使用 ` 定义的字符串,也不会转译字符,例如:
package main
import "fmt"
func main() {
text := `Go语言\nCool`
fmt.Println(text) // Go语言\nCool
}
在这个例子中可以看到,使用 ` 时,不会对\n
做转译的动作,因此,你会直接看到显示了「Go语言\nCool」。
在 Go 中可以使用的转译有:
- \a:U+0007,警告或响铃
- \b:U+0008,倒退(backspace)
- \f:U+000C,馈页(form feed)
- \n:U+000A,换行(newline)
- \r:U+000D,归位(carriage return)
- \t:U+0009,水平 tab
- \v:U+000b,垂直 tab
- \\:U+005c,反斜线(backslash)
- \":U+0022,双引号
- \ooo:字节表示,o 为八进制数字
- \xhh:字节表示,h 为十六进制数字
- \uhhhh:Unicode 点点表示,使用四个 16 进制数字
- \Uhhhhhhhh:Unicode 点点表示,使用八个 16 进制数字
只读字节片段
那么,想知道一个字符串的长度该怎么做呢?Go 中有个len
函数,当它作用于字符串时,结果可能会令一些从其他程序语言,像是 Java 过来的人感到讶异:
package main
import "fmt"
func main() {
fmt.Println(len("Go语言")) // 8
}
显示的结果是 8 而不是 3,给个提示,Go 的字符串实现使用 UTF-8,是的!len
返回的是字节长度,因为 Go 的字符串,本质上是 UTF-8 编码后的字节组成,如果你使用fmt.Printf("%x", "Go语言")
,会显示 476fe8aa9ee8a880,47 是「G」的字节以 16 进制数字表示的结果,6f 是 o,e8aa9e 是「语」的三个字节分别以 16 进制数字表示的结果,e8a880 是「言」。
不单是如此,Go 中可以使用[]
与索引来获取字符串的字节数据,是的,字节!返回的类型是byte
(uint8
),"Go语言"[0]
获取的是 G 的字节数据,"Go语言"[1]
获取的是 o 的字节数据,"Go语言"[2]
呢?获取的是「语」的 UTF-8 实现中,第一个字节数据,也就是 e8。可以用以下这个程序片段来印证:
package main
import "fmt"
func main() {
text := "Go语言"
for i := 0; i < len(text); i++ {
fmt.Printf("%x ", text[i])
}
}
虽然还没正确介绍for
循环,不过程序应该很清楚,用循环递增的i
值来获取指定索引处的数据,结果是显示「47 6f e8 aa 9e e8 a8 80 」。
这个字节序列是怎么决定的?当你写下"Go语言"
,你的 .go 源码文件是什么编码呢?是的!UTF-8,Go 就是从这当中获取"Go语言"
字节序列,每个字节就是 UTF-8 的一个码元(code unit)。
虽说字符串是只读的字节片段,不过,实际的字节是隐藏在字符串底层,如果你想获取,必须转为[]byte
,例如:
package main
import "fmt"
func main() {
text1 := "Go语言"
bs := []byte(text1)
bs[0] = 103
text2 := string(bs)
fmt.Println(text1) // Go语言
fmt.Println(text2) // go语言
}
注意,你不是真的获取字符串底层的字节数据,只是获取复本,因此,在范例中可以看到,虽然对text2
的字节做了修改,text1
是不受影响的,记得,字符串是只读的,一旦创建,没有方式可以改变其内容。
string 与索引
实际上,Go 的字符串支持片段操作,slice 操作时的索引是针对字节,然而,返回的类型还是string
,例如,"Go语言"[0:2]
,返回"Go"
,因为指定要切割出索引 0 开始,索引 2 结束(但不包括 2)的部份,也就是 47 与 6f 这两个字节,但是以string
返回。
那么,如果是"Go语言"[2:3]
呢?嗯,返回的字符串是"\xe8"
!这是什么?事实上,Go 中的字符串可以是任意字节片段,因此,你可以如下定义字符串:
package main
import "fmt"
func main() {
text := "\x47\x6f\xe8\xaa\x9e\xe8\xa8\x80"
fmt.Println(text) // Go语言
}
片段操作时,如果省略冒号之后的数字,则默认获取至字符串尾端的子字符串,例如"Go语言"[3:]
会返回"\xaa\x9e\xe8\xa8\x80"
的字符串,如果省略冒号之前的数字,默认从索引 0 开始,例如"Go语言"[:2]
会获取"Go"
的字符串,也就是"\x47\x6f"
的字符串,如果是"Go语言"[:]
,那么就是获取全部字符串内容了。
strings
包中有不少字符串可用的方法,想做字符串操作时,可以多加利用,不过要看清楚是针对什么在操作。例如strings.Index
:
package main
import "fmt"
import "strings"
func main() {
text := "Go语言"
fmt.Printf("%d\n", strings.Index(text, "言")) // 5
}
返回的索引值是 5 而不是 3,这是因为"言"
的第一个字节,是在"Go语言"
UTF-8 编码后的字节组成中第 5 个索引位置。
问题来了,如果对于"Go语言"
,想逐一获取'G'
、'o'
、'语'
、'言'
该怎么办?当然不能用text[n]
,这只会获取第 n 个字节,可以将字符串类型转换为[]rune
:
package main
import "fmt"
func main() {
text := "Go语言"
cs := []rune(text)
fmt.Printf("%c\n", cs[2]) // 语
fmt.Println(len(cs)) // 4
}
字符串类型转换为[]rune
时,会将 UTF-8 编码的字节,转换为 Unicode 码,在这个例子中可以看到,cs[2]
确实地获取了第三个文本「语」,而len
也确实获取数量 4。
如〈认识预定义类型〉中谈过的,在 Go 中并没有字符对应的类型,只有码的概念,rune
为int32
的别名,可用来存储 Unicode 码(code point),如果使用fmt.Printf("%d\n", cs[2])
,会显示 35486,这就是「语」的 Unicode 码,35486 的 16 进制表示是 8a9e,因此,如果你写'\u8a9e'
,也会得到一个rune
代表着「语」,fmt.Printf("%c", '\u8a9e')
也会显示「语」,当然,直接写'语'
也是可以得到一个rune
。
想从rune
得到一个string
,可以直接写string('语')
就可以了。如果想以rune
为单位来遍历字符串,而不是以字节遍历,可以使用for range
,例如:
package main
import "fmt"
func main() {
text := "Go语言"
for index, runeValue := range text {
fmt.Printf("%#U 位起始位置 %d\n", runeValue, index)
}
}
可以看到,for range
可以同时获取每个rune
在字符串中的位起始位置,以及rune
值,%U
可以用 16 进制显示rune
,如果是%#U
,还会一并显示码的打印形式。
这个程序的执行结果会显示:
U+0047 'G' 位起始位置 0
U+006F 'o' 位起始位置 1
U+8A9E '语' 位起始位置 2
U+8A00 '言' 位起始位置 5
总而言之,Go 的字符串是由 UTF-8 编码的字节构成,在〈Strings, bytes, runes and characters in Go〉谈到了这么设计的理由是,「字符」的定义太模棱两可了,Go 为了避免模棱两可,就将字符串定义为 UTF-8 编码的字节构成,而rune
用于存储码。
PS. 这大概也是为何,我会整理出〈乱码 1/2〉的原因 … XD