先说结论,unit8 == byte,int32 == rune,string底层是以 []byte存储的。
byte 是 uint8 的别名,rune 是 int32 的别名,这句话怎么理解,看一下 Go 的源代码就理解了。在 Go 的源代码中有如下定义:
可以很清晰的看到,byte 实际上就是 uint8,rune 实际上就是 int32;
那既然二者完全一致,为什么还需要划分呢?
byte 一般用来存储 ASCII 字符或者原始二进制数或者任何UTF-8字节存储,由于此用途非常适合使用8位的无符号整型,所以就直接将 byte = uint8。但是我们说,uint8 本身也会有一些其他的用途呀,那么为了区分用途,为了让程序员看到这个类型之后知道这个类型是用来干嘛的,我们才让 byte = uint8。这样描述可能还不够具体,我们结合一个例子来理解:
package main
import "fmt"
func printByte(b byte) { // func printByte(b uint8)
fmt.Printf("Type of b is %T", b)
}
func main() {
var b byte = 'a'
printByte(b)
}
我们看到这里,有一个 printByte 函数,接受一个 byte 类型的传参。那么当程序员看到这里的 byte 的时候,就知道这个变量接下来可能要进行一些和原始二进制或者 ASCII 有关的操作,就能够大概理解他的意思。而如果这里的 byte 变成了 uint8,那么程序员就知道这是一个 8 位的无符号整型,就知道接下来要对这个数进行一些整型相关的操作。
实际上在 func printByte(b byte) {这一行无论是 byte 还是 uint8,程序本身没有任何区别,因为在 Go 的底层默认这二者就是完全相等的,但是起了一个别名之后,我们就知道他们之间的用途不同,从而可以更好的理解后续对该变量相关的操作和逻辑。
Rune 和 int32 同理,他们本质也是完全一致,只是为了能够让程序员在看到某个变量的类型是 rune 的时候,会立即明白“哦,这是一个 rune 类型的变量,接下来他应该是要用来表示一些 Unicode字符,这个函数接收这样一个类型的变量应该是和字符相关的一些操作”。
接下来讲一下 string,在 Go 中,string 底层是使用 []byte 存储的。
注意:
string和[]byte的区别在于string是不可变的。
看一下下面的例子:
func main() {
s := "HelloGo"
fmt.Printf("type of s is %T\n", s)
fmt.Printf("type of s[0] is %T", s[0])
}
// output:
// type of s is string
// type of s[0] is uint8
那么可以看到,对于 s 中具体的字符,都是 byte 类型的。
另外,string 类型底层既然是 []byte 存储的,那对于中文类型,比如说 s := "你好",这种该怎么存储呢?说底层是 byte,那岂不是存储不了了?我们来试试看,是什么情况:
func main() {
s := "你好Go"
fmt.Printf("type of s is %T\n", s)
fmt.Printf("type of s[0] is %T", s[0])
}
// output:
// type of s is string
// type of s[0] is uint8
那么可以看到,我们的字符串是可以正常输出的,没有问题,这是为什么呢?另外我们还注意到, s[0] 的结果居然是一个 uint8 ?一个 uint8 怎么可以存储汉字 “你” 的编码呢?我们来通过下面这段代码,揭开 string 的奥秘。
func main() {
s := "你好Go"
fmt.Printf("type of s is %T\n", s)
fmt.Printf("type of s[0] is %T\n", s[0])
fmt.Println("length of s is ", len(s))
fmt.Println(s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7])
for i, ch := range s {
fmt.Printf("idx = %v, val = %v\n", i, ch)
fmt.Printf("idx = %v, val = %v\n", i, string(ch))
}
}
// output:
//type of s is string
//type of s[0] is uint8
//length of s is 8
//228 189 160 229 165 189 71 111
//idx = 0, val = 20320
//idx = 0, val = 你
//idx = 3, val = 22909
//idx = 3, val = 好
//idx = 6, val = 71
//idx = 6, val = G
//idx = 7, val = 111
//idx = 7, val = o
我们仔细查看这个代码,string 的奥秘就在其中。可以看到,对于 s := "你好Go"来说,其长度是 8。那么,为什么是 8 呢?因为,汉字 “你” 和 “好” 都是 Unicode 编码,采用 UTF-8 来编码,而 “你” 和 “好” 都需要 3 个字节来编码,那么 3 个字节就需要 3 个 byte 类型来实现,而对于后面的字母 Go 就只需要 1 个 byte 来实现(UTF-8是动态编码的),所以 s 总长度为 8。
我们在上面代码中看到的 s[0] ~ s[7] 的输出实际上就是他们的 UTF-8 编码,看到 s[0] s[1] s[2],他们的输出是 228 189 160,实际上这三者拼接起来就是汉字 “你” 的UTF-8 编码。
到这里,我们就只剩代码中的 for 循环还没有理解了。对于 string 类型,range 会自动按照 “Unicode码点(rune)”来循环的,所以我们看到,for 循环一共输出了 4 次。
这里注意,对于
for i := 0; i < len(s); i++的循环依旧是按照字节来循环的,只有使用for i, v := range s才会根据 Unicode码点(rune) 来循环
到这里,我们就可以对 uint8、byte、int32、rune 以及 string 有一个比较清晰的认知了。