在 Go 中,字符串是要特别注意的,因为与其他语言相比,它们在实现上是不同的。
什么是字符串
Go 中的字符串是字节的切片。可以通过将一组字符括在双引号 **" "**
中来创建字符串。
让我们看一个简单的例子,创建一个 string 并打印出来。
package main
import (
"fmt"
)
func main() {
name := "Hello World"
fmt.Println(name)
}
上面程序将输出 Hello World
。
Go 中的字符串是符合 Unicode,并且采用 UTF-8 编码。
访问字符串的单个字节
由于字符串是字节切片,所以可以访问字符串的每个字节。
package main
import (
"fmt"
)
func printBytes(s string) {
fmt.Printf("Bytes: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func main() {
name := "Hello World"
fmt.Printf("String: %s\n", name)
printBytes(name)
}
%s 是打印字符串的格式指定符。在第 16 行,输入的字符串被打印出来。在上面程序的第 9 行,len(s) 返回字符串中的字节数,我们使用 for 循环将这些字节以十六进制的形式打印出来。%x 是十六进制的格式指定符。上面程序的输出结果是
String: Hello World
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 的知识。
访问一个字符串的单个字符
让我们稍微修改一下上面的程序来输出字符串的字符。
package main
import (
"fmt"
)
func printBytes(s string) {
fmt.Printf("Bytes: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
fmt.Printf("Characters: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i])
}
}
func main() {
name := "Hello World"
fmt.Printf("String: %s\n", name)
printChars(name)
fmt.Printf("\n")
printBytes(name)
}
上面程序第 17 行,printChars
函数的 %c 格式说明符用于输出字符串的字符。程序将输出
48 65 6c 6c 6f 20 57 6f 72 6c 64
H e l l o W o r l d
虽然上面的程序看起来是一个合法的访问字符串单个字符的方法,但这有一个严重的 bug。让我们来看看这个 bug 是什么。
package main
import (
"fmt"
)
func printBytes(s string) {
fmt.Printf("Bytes: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
fmt.Printf("Characters: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i])
}
}
func main() {
name := "Hello World"
fmt.Printf("String: %s\n", name)
printChars(name)
fmt.Printf("\n")
printBytes(name)
fmt.Printf("\n\n")
name = "Señor"
fmt.Printf("String: %s\n", name)
printChars(name)
fmt.Printf("\n")
printBytes(name)
}
上面程序的输出是
String: Hello World
Characters: H e l l o W o r l d
Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64
String: Señor
Characters: S e à ± o r
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来输出字符。
package main
import (
"fmt"
)
func printBytes(s string) {
fmt.Printf("Bytes: ")
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
fmt.Printf("Characters: ")
runes := []rune(s)
for i := 0; i < len(runes); i++ {
fmt.Printf("%c ", runes[i])
}
}
func main() {
name := "Hello World"
fmt.Printf("String: %s\n", name)
printChars(name)
fmt.Printf("\n")
printBytes(name)
fmt.Printf("\n\n")
name = "Señor"
fmt.Printf("String: %s\n", name)
printChars(name)
fmt.Printf("\n")
printBytes(name)
}
在上面的程序中第 16 行,字符串被转换成 rune 切片。然后我们对它进行循环并打印字符。这个程序输出
48 65 6c 6c 6f 20 57 6f 72 6c 64
H e l l o W o r l d
53 65 c3 b1 6f 72
S e ñ o r
这下就完美了😀。
用 range 来循环字符串
上面程序是遍历字符串的单个 rune 的完美方法。但是 Go 为我们提供了一个更简单的方法,使用 for range 循环来实现这一点。
package main
import (
"fmt"
)
func printCharsAndBytes(s string) {
for index, rune := range s {
fmt.Printf("%c starts at byte %d\n", rune, index)
}
}
func main() {
name := "Señor"
printCharsAndBytes(name)
}
上面的程序第 8 行中,使用 for range
循环迭代字符串。循环返回 rune 和 rune 字节开始的位置。这个程序输出
S starts at byte 0
e starts at byte 1
ñ starts at byte 2
o starts at byte 4
r starts at byte 5
从上面的输出中可以看出,由于下一个字符 o 从第 4 字节开始,而不是从第 3 字节开始😀,所以 ñ 占用了 2 个字节。
用字节切片来构造字符串
package main
import (
"fmt"
)
func main() {
byteSlice := []byte{0x43, 0x61, 0x66, 0xC3, 0xA9}
str := string(byteSlice)
fmt.Println(str)
}
上面程序中的 byteSlice 包含 UTF-8 Encoded 的十六进制字节字符串“Café”。程序输出 Café
.。
如果我们有十进制数的十六进制值。上面程序还会奏效吗
package main
import (
"fmt"
)
func main() {
byteSlice := []byte{67, 97, 102, 195, 169}//decimal equivalent of {'\x43', '\x61', '\x66', '\xC3', '\xA9'}
str := string(byteSlice)
fmt.Println(str)
}
上面的程序也会输出 Café
用 runes 切片来构造字符串
package main
import (
"fmt"
)
func main() {
runeSlice := []rune{0x0053, 0x0065, 0x00f1, 0x006f, 0x0072}
str := string(runeSlice)
fmt.Println(str)
}
在上面的程序中,runeSlice
包含字符串 Señor
十六进制的 Unicode 码点。程序输出 Señor
。
字符串的长度
utf8 package 中的 RuneCountInString(s string) (n int) 函数可以用来查找字符串的长度。这个方法接收一个字符串作为参数,并返回其中的 runes 数量。
正如我们前面所讨论的,len(s) 是用来查找字符串中的字节数的,它并不返回字符串的长度。正如我们已经讨论过的,一些 Unicode 字符的码点占据了超过 1 个字节。使用 len 来查找这些字符串的长度,会返回错误的字符串长度。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
word1 := "Señor"
fmt.Printf("String: %s\n", word1)
fmt.Printf("Length: %d\n", utf8.RuneCountInString(word1))
fmt.Printf("Number of bytes: %d\n", len(word1))
fmt.Printf("\n")
word2 := "Pets"
fmt.Printf("String: %s\n", word2)
fmt.Printf("Length: %d\n", utf8.RuneCountInString(word2))
fmt.Printf("Number of bytes: %d\n", len(word2))
}
程序输出:
String: Señor
Length: 5
Number of bytes: 6
String: Pets
Length: 4
Number of bytes: 4
以上输出确认 len(s) 和 RuneCountInString(s) 返回的值不同😀。
字符串的比较
== 运算符用于比较两个字符串是否相等。如果两个字符串都相等,那么结果就是 true,否则就是 false。
package main
import (
"fmt"
)
func compareStrings(str1 string, str2 string) {
if str1 == str2 {
fmt.Printf("%s and %s are equal\n", str1, str2)
return
}
fmt.Printf("%s and %s are not equal\n", str1, str2)
}
func main() {
string1 := "Go"
string2 := "Go"
compareStrings(string1, string2)
string3 := "hello"
string4 := "world"
compareStrings(string3, string4)
}
在上面的 compareStrings 函数中, 第 8 行是用 == 运算符比较两个字符串 str1 和 str2 是否相等。如果它们相等,则打印相应的信息,函数返回。
上面程序打印,
Go and Go are equal
hello and world are not equal
字符串的拼接
在 Go 中有多种方法来执行字符串连接。让我们来看看其中的几种方法。
最简单的字符串连接方法是使用 + 操作符。
package main
import (
"fmt"
)
func main() {
string1 := "Go"
string2 := "is awesome"
result := string1 + " " + string2
fmt.Println(result)
}
在上面的程序中,在第 10 行中,string1 与 string2 拼接,中间有一个空格。这个程序打印,
Go is awesome
第二种连接字符串的方法是使用 fmt package 的 Sprintf 函数。
Sprintf 函数根据输入的格式指定器格式化一个字符串,并返回结果的字符串。让我们使用 Sprintf 函数重写上面的程序。
package main
import (
"fmt"
)
func main() {
string1 := "Go"
string2 := "is awesome"
result := fmt.Sprintf("%s %s", string1, string2)
fmt.Println(result)
}
在上面程序的第 10 行,%s %s 是 Sprintf 的格式指定器输入。这个格式指定器接收两个字符串作为输入,中间有一个空格。这将把两个字符串连接起来,产生的字符串存储在 result 中。这个程序打印,
Go is awesome
字符串是不可变的
字符串在 Go 中是不可变的。创建字符串后,就无法更改它。
package main
import (
"fmt"
)
func mutate(s string)string {
s[0] = 'a'//any valid unicode character within single quote is a rune
return s
}
func main() {
h := "hello"
fmt.Println(mutate(h))
}
在第 8 行我们尝试将字符串的第一个字符更改为 'a'
。这是不允许的,因为字符串是不可变的,因此程序抛出错误 main.go:8: cannot assign to s[0]。
要解决字符串不能改变的问题,请将字符串转换为 rune 切片。然后,就可以愉快的更改了,然后返回新的字符串。
package main
import (
"fmt"
)
func mutate(s []rune) string {
s[0] = 'a'
return string(s)
}
func main() {
h := "hello"
fmt.Println(mutate([]rune(h)))
}
在程序的第 7 行中,mutate
函数接受 rune 切片作为参数。然后它将切片的第一个元素更改为’a’,将 rune 转换成字符串并返回它。第 14 行中函数被调用。h
被转换为 rune 切片并传递给 mutate
。该程序输出 aello
我在github中创建了一个程序,其中包含我们讨论的所有内容。你可以在这里下载。