Go 中的字符串值得特别关注,与其他语言相比,Go 中的字符串实现方式有所不同。

字符串

在 Go 中,使用双引号 "" 声明字符串:

s := "Hello world"
fmt.Println("len(s):",len(s))
fmt.Println(s);       

输出:

len(s): 11
Hello world

上面的代码声明了字符串 slen() 函数返回字符串 s 的字节数(包括空格)。在 Go 中,字符串其实是只读的字节切片

s := "Hello world"
for i:=0;i<len(s);i++ {
	fmt.Print(s[i]," ")
}

你觉得上面的代码会输出什么,是一个个字母吗?其实不是:

72 101 108 108 111 32 119 111 114 108 100

输出的是每个字母在 ASCII 码表 上对应的十进制数字。
正如大家熟知的,Go 语言采用 UTF-8 编码,这种编码方式与 ASCII 编码兼容,只不过 ASCII 编码只需 1 个字节,而 UTF-8 需要 1-4 个字节表示一个符号。

s := "Hello world"
for i:=0;i<len(s) ;i++  {
	fmt.Printf("%c ",s[i])
}
fmt.Println("")
for i:=0;i<len(s) ;i++  {
	fmt.Printf("%v ",s[i])      
}
fmt.Println("")
for i:=0;i<len(s) ;i++  {
	fmt.Printf("%x ",s[i])    
}
fmt.Println("")
for i:=0;i<len(s) ;i++  {
	fmt.Printf("%T ",s[i])
}

输出

H e l l o   w o r l d 
72 101 108 108 111 32 119 111 114 108 100 
48 65 6c 6c 6f 20 77 6f 72 6c 64 
uint8 uint8 uint8 uint8 uint8 uint8 uint8 uint8 uint8 uint8 uint8 

上面的代码,%v 格式化输出字节对应的十进制值;%x 以十六进制输出;%T 格式化输出值的类型。从结果可以看出,值的类型都是 uint8byte 类型,typeuint8 的别名,byte 类型在我的 文章 中有所介绍。
我们来看下:一个字符串中包含非 ASCII 码的字符 会是怎样的情况。

s := "Hellõ World"
fmt.Println("len(s):", len(s))
for i := 0; i < len(s); i++ {
	fmt.Printf("%c ", s[i])
}
fmt.Println("")
for i := 0; i < len(s); i++ {
	fmt.Printf("%v ", s[i])
}
fmt.Println("")
for i := 0; i < len(s); i++ {
	fmt.Printf("%x ", s[i])
}

输出

len(s): 12
H e l l à µ   W o r l d 
72 101 108 108 195 181 32 87 111 114 108 100 
48 65 6c 6c c3 b5 20 57 6f 72 6c 64

上面的例子中,将 o 替换成 õ 。从结果可以看出,字符串的字节长度是 12 ,说明 õ 占用两个字节。然而 õ 的输出变成了 Ã µõUnicode 码点是 U+00F5 ,其 UTF-8 编码 占两个字节 c3b5for 循环按字节读取,c3 (十进制 195 )对应字符 Ãb5 (十进制 181 )对应字符 µ详见这里
熟悉 ASCIIUTF-8Unicode 更有利于理解这些知识,关于这些知识,不会在本文展开细讲,有兴趣的可以参考 这里
UTF-8 编码中,一个码点占用至少一字节,如果我们还是以一个码点占用一个字节去打印字符肯定会出问题,就像上面的例子一样。那有没有办法解决这个问题,好在 Go 为我们提供了 rune

Rune

rune 是 Go 的内置数据类型,是 int32 的别名,表示 Go 中的 Unicode 代码点。用 rune 数据类型,开发人员就不必关心代码点占用几个字节了。

s := "Hellõ World"
r := []rune(s)

fmt.Println("len(r):", len(r))
for i := 0; i < len(r); i++ {
	fmt.Printf("%c ", r[i])
}
fmt.Println("")
for i := 0; i < len(r); i++ {
	fmt.Printf("%v ", r[i])
}
fmt.Println("")
for i := 0; i < len(r); i++ {
	fmt.Printf("%x ", r[i])
}
fmt.Println("")
for i := 0; i < len(r); i++ {
	fmt.Printf("%T ", r[i])
}

输出

len(r): 11
H e l l õ   W o r l d 
72 101 108 108 245 32 87 111 114 108 100 
48 65 6c 6c f5 20 57 6f 72 6c 64 
int32 int32 int32 int32 int32 int32 int32 int32 int32 int32 int32 

上面的代码,字符串 s 通过类型转化成了 rune 切片。 õUnicode 码点就是 U+00F5 ,对应十进制的 245 ,参考这。 切片 r 的长度就是:11 ;输出的 int32 ,印证了 runeint32 的别名。

for range 字符串

上面的例子已经很好解决了之前遇到的问题,有种更好的方式 – range string 。使用 range 循环一个字符串,将返回 rune 类型的字符和字节索引。

s := "HellõWorld"
for index, char := range s {
	fmt.Printf("%c starts at byte index %d \n", char,index)
}

输出

H starts at byte index 0 
e starts at byte index 1 
l starts at byte index 2 
l starts at byte index 3 
õ starts at byte index 4 
W starts at byte index 6 
o starts at byte index 7 
r starts at byte index 8 
l starts at byte index 9 
d starts at byte index 10

从输出结果可以看出,õ 占用了两个字节:索引 4 和 5。

文章读到这,可能会有个疑问,怎么获取字符串的长度呢?

Length of the string

可以使用 RuneCountInString() 函数,原型是这样的:

func RuneCountInString(s string) (n int)

返回字符串中 rune 字符的个数。

s := "Hellõ 中国"
length := utf8.RuneCountInString(s)
fmt.Println(length)

输出:8

字符串是不可变的

前面我们已经说过,字符串是只读的字节切片,一旦创建,是不可更改的。如果强制修改,就会报错:

s := "Hello World"
s[0] = "h"

报错:cannot assign to s[0]

这篇文章有几个比较重要的点:

  1. 字符串是只读的字节切片;
  2. rune 表示 Go 中的 Unicode 代码点;
  3. Go 采用 UTF-8 编码,这种编码方式是 Unicode 的实现方式之一;
  4. 熟悉 ASCIIUTF-8Unicode ,可以参考 ; 更有利于理解这篇文章;

希望这篇文章能够解决你对 Go string 的一些疑问,有不懂的可以留言讨论!

(全文完)

扫码关注领取学习资料!