在 Go 中,字符串是要特别注意的,因为与其他语言相比,它们在实现上是不同的。

什么是字符串

Go 中的字符串是字节的切片。可以通过将一组字符括在双引号 **" "** 中来创建字符串。

让我们看一个简单的例子,创建一个 string 并打印出来。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. name := "Hello World"
  7. fmt.Println(name)
  8. }


Run in playground

上面程序将输出 Hello World

Go 中的字符串是符合 Unicode,并且采用 UTF-8 编码。

访问字符串的单个字节

由于字符串是字节切片,所以可以访问字符串的每个字节。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func printBytes(s string) {
  6. fmt.Printf("Bytes: ")
  7. for i := 0; i < len(s); i++ {
  8. fmt.Printf("%x ", s[i])
  9. }
  10. }
  11. func main() {
  12. name := "Hello World"
  13. fmt.Printf("String: %s\n", name)
  14. printBytes(name)
  15. }

Run in playground

%s 是打印字符串的格式指定符。在第 16 行,输入的字符串被打印出来。在上面程序的第 9 行,len(s) 返回字符串中的字节数,我们使用 for 循环将这些字节以十六进制的形式打印出来。%x 是十六进制的格式指定符。上面程序的输出结果是

  1. String: Hello World
  2. Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64

这些是 Hello World 的 Unicode UT8 编码值。为了更好地理解字符串,需要对 Unicode 和 UTF-8 有基本的了解。我建议阅读https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/,了解更多关于 Unicode 和 UTF-8 的知识。

访问一个字符串的单个字符

让我们稍微修改一下上面的程序来输出字符串的字符。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func printBytes(s string) {
  6. fmt.Printf("Bytes: ")
  7. for i := 0; i < len(s); i++ {
  8. fmt.Printf("%x ", s[i])
  9. }
  10. }
  11. func printChars(s string) {
  12. fmt.Printf("Characters: ")
  13. for i := 0; i < len(s); i++ {
  14. fmt.Printf("%c ", s[i])
  15. }
  16. }
  17. func main() {
  18. name := "Hello World"
  19. fmt.Printf("String: %s\n", name)
  20. printChars(name)
  21. fmt.Printf("\n")
  22. printBytes(name)
  23. }

Run in playground

上面程序第 17 行,printChars 函数的 %c 格式说明符用于输出字符串的字符。程序将输出

  1. 48 65 6c 6c 6f 20 57 6f 72 6c 64
  2. H e l l o W o r l d

虽然上面的程序看起来是一个合法的访问字符串单个字符的方法,但这有一个严重的 bug。让我们来看看这个 bug 是什么。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func printBytes(s string) {
  6. fmt.Printf("Bytes: ")
  7. for i := 0; i < len(s); i++ {
  8. fmt.Printf("%x ", s[i])
  9. }
  10. }
  11. func printChars(s string) {
  12. fmt.Printf("Characters: ")
  13. for i := 0; i < len(s); i++ {
  14. fmt.Printf("%c ", s[i])
  15. }
  16. }
  17. func main() {
  18. name := "Hello World"
  19. fmt.Printf("String: %s\n", name)
  20. printChars(name)
  21. fmt.Printf("\n")
  22. printBytes(name)
  23. fmt.Printf("\n\n")
  24. name = "Señor"
  25. fmt.Printf("String: %s\n", name)
  26. printChars(name)
  27. fmt.Printf("\n")
  28. printBytes(name)
  29. }

Run in playground

上面程序的输出是

  1. String: Hello World
  2. Characters: H e l l o W o r l d
  3. Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64
  4. String: Señor
  5. Characters: S e à ± o r
  6. Bytes: 53 65 c3 b1 6f 72

上面程序的第 30 行,我们输出 Señor 字符,它输出的却是错误的 S e à ± o r。为什么上面的 Hello World 没问题,而下面的 Señor 有问题呢?原因是 ñ 的 Unicode 编码为 U+00F1,其 UTF-8 encoding 占用 2 字节,分别是 c3 和 b1。我们输出字符时,是假设每个代码点将是一个字节长,这是错误的。在 UTF-8 编码中,一个码点可以占用超过一个字节。我们该怎么解决这个问题呢?这就是 rune 派上了用场。

rune

rune 是 go 的内置类型,它是 int32 的别名。rune 表示 Go 中的 Unicode 码点。码点占用多少字节并不重要,它可以用 rune 表示。让我们修改上面的程序来使用 rune来输出字符。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func printBytes(s string) {
  6. fmt.Printf("Bytes: ")
  7. for i := 0; i < len(s); i++ {
  8. fmt.Printf("%x ", s[i])
  9. }
  10. }
  11. func printChars(s string) {
  12. fmt.Printf("Characters: ")
  13. runes := []rune(s)
  14. for i := 0; i < len(runes); i++ {
  15. fmt.Printf("%c ", runes[i])
  16. }
  17. }
  18. func main() {
  19. name := "Hello World"
  20. fmt.Printf("String: %s\n", name)
  21. printChars(name)
  22. fmt.Printf("\n")
  23. printBytes(name)
  24. fmt.Printf("\n\n")
  25. name = "Señor"
  26. fmt.Printf("String: %s\n", name)
  27. printChars(name)
  28. fmt.Printf("\n")
  29. printBytes(name)
  30. }

Run in playground

在上面的程序中第 16 行,字符串被转换成 rune 切片。然后我们对它进行循环并打印字符。这个程序输出

  1. 48 65 6c 6c 6f 20 57 6f 72 6c 64
  2. H e l l o W o r l d
  3. 53 65 c3 b1 6f 72
  4. S e ñ o r

这下就完美了😀。

用 range 来循环字符串

上面程序是遍历字符串的单个 rune 的完美方法。但是 Go 为我们提供了一个更简单的方法,使用 for range 循环来实现这一点。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func printCharsAndBytes(s string) {
  6. for index, rune := range s {
  7. fmt.Printf("%c starts at byte %d\n", rune, index)
  8. }
  9. }
  10. func main() {
  11. name := "Señor"
  12. printCharsAndBytes(name)
  13. }

Run in playground

上面的程序第 8 行中,使用 for range 循环迭代字符串。循环返回 rune 和 rune 字节开始的位置。这个程序输出

  1. S starts at byte 0
  2. e starts at byte 1
  3. ñ starts at byte 2
  4. o starts at byte 4
  5. r starts at byte 5

从上面的输出中可以看出,由于下一个字符 o 从第 4 字节开始,而不是从第 3 字节开始😀,所以 ñ 占用了 2 个字节。

用字节切片来构造字符串

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. byteSlice := []byte{0x43, 0x61, 0x66, 0xC3, 0xA9}
  7. str := string(byteSlice)
  8. fmt.Println(str)
  9. }

Run in playground

上面程序中的 byteSlice 包含 UTF-8 Encoded 的十六进制字节字符串“Café”。程序输出 Café.。

如果我们有十进制数的十六进制值。上面程序还会奏效吗

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. byteSlice := []byte{67, 97, 102, 195, 169}//decimal equivalent of {'\x43', '\x61', '\x66', '\xC3', '\xA9'}
  7. str := string(byteSlice)
  8. fmt.Println(str)
  9. }

Run in playground

上面的程序也会输出 Café

用 runes 切片来构造字符串

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. runeSlice := []rune{0x0053, 0x0065, 0x00f1, 0x006f, 0x0072}
  7. str := string(runeSlice)
  8. fmt.Println(str)
  9. }

Run in playground

在上面的程序中,runeSlice 包含字符串 Señor 十六进制的 Unicode 码点。程序输出 Señor

字符串的长度

utf8 package 中的 RuneCountInString(s string) (n int) 函数可以用来查找字符串的长度。这个方法接收一个字符串作为参数,并返回其中的 runes 数量。

正如我们前面所讨论的,len(s) 是用来查找字符串中的字节数的,它并不返回字符串的长度。正如我们已经讨论过的,一些 Unicode 字符的码点占据了超过 1 个字节。使用 len 来查找这些字符串的长度,会返回错误的字符串长度。

  1. package main
  2. import (
  3. "fmt"
  4. "unicode/utf8"
  5. )
  6. func main() {
  7. word1 := "Señor"
  8. fmt.Printf("String: %s\n", word1)
  9. fmt.Printf("Length: %d\n", utf8.RuneCountInString(word1))
  10. fmt.Printf("Number of bytes: %d\n", len(word1))
  11. fmt.Printf("\n")
  12. word2 := "Pets"
  13. fmt.Printf("String: %s\n", word2)
  14. fmt.Printf("Length: %d\n", utf8.RuneCountInString(word2))
  15. fmt.Printf("Number of bytes: %d\n", len(word2))
  16. }

Run in playground

程序输出:

  1. String: Señor
  2. Length: 5
  3. Number of bytes: 6
  4. String: Pets
  5. Length: 4
  6. Number of bytes: 4

以上输出确认 len(s) 和 RuneCountInString(s) 返回的值不同😀。

字符串的比较

== 运算符用于比较两个字符串是否相等。如果两个字符串都相等,那么结果就是 true,否则就是 false。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func compareStrings(str1 string, str2 string) {
  6. if str1 == str2 {
  7. fmt.Printf("%s and %s are equal\n", str1, str2)
  8. return
  9. }
  10. fmt.Printf("%s and %s are not equal\n", str1, str2)
  11. }
  12. func main() {
  13. string1 := "Go"
  14. string2 := "Go"
  15. compareStrings(string1, string2)
  16. string3 := "hello"
  17. string4 := "world"
  18. compareStrings(string3, string4)
  19. }

Run in playground

在上面的 compareStrings 函数中, 第 8 行是用 == 运算符比较两个字符串 str1 和 str2 是否相等。如果它们相等,则打印相应的信息,函数返回。

上面程序打印,

  1. Go and Go are equal
  2. hello and world are not equal

字符串的拼接

在 Go 中有多种方法来执行字符串连接。让我们来看看其中的几种方法。

最简单的字符串连接方法是使用 + 操作符。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. string1 := "Go"
  7. string2 := "is awesome"
  8. result := string1 + " " + string2
  9. fmt.Println(result)
  10. }

Run in playground

在上面的程序中,在第 10 行中,string1 与 string2 拼接,中间有一个空格。这个程序打印,

  1. Go is awesome

第二种连接字符串的方法是使用 fmt package 的 Sprintf 函数。

Sprintf 函数根据输入的格式指定器格式化一个字符串,并返回结果的字符串。让我们使用 Sprintf 函数重写上面的程序。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. string1 := "Go"
  7. string2 := "is awesome"
  8. result := fmt.Sprintf("%s %s", string1, string2)
  9. fmt.Println(result)
  10. }

Run in playground

在上面程序的第 10 行,%s %s 是 Sprintf 的格式指定器输入。这个格式指定器接收两个字符串作为输入,中间有一个空格。这将把两个字符串连接起来,产生的字符串存储在 result 中。这个程序打印,

  1. Go is awesome

字符串是不可变的

字符串在 Go 中是不可变的。创建字符串后,就无法更改它。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func mutate(s string)string {
  6. s[0] = 'a'//any valid unicode character within single quote is a rune
  7. return s
  8. }
  9. func main() {
  10. h := "hello"
  11. fmt.Println(mutate(h))
  12. }

Run in playground

在第 8 行我们尝试将字符串的第一个字符更改为 'a' 。这是不允许的,因为字符串是不可变的,因此程序抛出错误 main.go:8: cannot assign to s[0]。

要解决字符串不能改变的问题,请将字符串转换为 rune 切片。然后,就可以愉快的更改了,然后返回新的字符串。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func mutate(s []rune) string {
  6. s[0] = 'a'
  7. return string(s)
  8. }
  9. func main() {
  10. h := "hello"
  11. fmt.Println(mutate([]rune(h)))
  12. }

Run in playground

在程序的第 7 行中,mutate 函数接受 rune 切片作为参数。然后它将切片的第一个元素更改为’a’,将 rune 转换成字符串并返回它。第 14 行中函数被调用。h 被转换为 rune 切片并传递给 mutate。该程序输出 aello

我在github中创建了一个程序,其中包含我们讨论的所有内容。你可以在这里下载。

原文链接

https://golangbot.com/strings/