Appearance
The Go Programming Language - 内置数据类型
1. 基础数据类型
boolstringint、int8、int16、int32、int64uint、uint8、uint16、uint32、uint64uintptrbyte//uint8的别名rune//int32的别名,标识一个Unicode码float32、float64complex64、complex128- array
- slice
mapchan- ……
需要注意的是:
- Go 语言中没有引用类型,包括 slice、
map、chan等实际上也都是值类型; - 在 32 位系统下:
int、uint占用 4 个字节(32 位); - 在 64 位系统下:
int、uint占用 8 个字节(64 位); uintptr是无符号的整数类型,没有指定具体的bit大小但是足以容纳指针;
2. NaN、Inf
math 包中除了提供大量常用的数学函数外,还提供了 IEEE754 浮点数标准中定义的特殊值的创建和测试:正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;还有 NaN 非数。
Go
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"1
2
2
函数 math.IsNaN 用于测试一个数是否是非数 NaN(math.NaN 则返回非数对应的值)。虽然可以用 math.NaN 来表示一个非法的结果,但是测试一个结果是否是非数 NaN 则是充满风险的,因为 NaN 和任何数都是不相等的(在浮点数中,NaN、正无穷大和负无穷大都不是唯一的,每个都有非常多种的 bit 模式表示):
Go
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"1
2
2
3. 复数
Go 语言提供了两种精度的复数类型:complex64 和 complex128,分别对应 float32 和 float64 两种浮点数精度。内置的 complex 函数用于构建复数,内建的 real 和 imag 函数分别返回复数的实部和虚部:
Go
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"1
2
3
4
5
2
3
4
5
如果一个浮点数面值或一个十进制整数面值后面跟着一个 i,例如 3.141592i 或 2i,它将构成一个复数的虚部,复数的实部是 0:
Go
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -14. 字符串
4.1. 原生字符串
一个原生的字符串面值形式是 `...`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行。
- 在原生字符串面值内部是无法直接写
`字符的,可以用八进制或十六进制转义或 + "`" 链接字符串常量完成; - 唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的(Windows 系统会把回车和换行一起放入文本文件中);
4.2. 基于 UTF-8
字符串是值类型,是一个不可改变的 UTF-8 字符序列。
UTF-8 编码规则解析
UTF-8是一种变长字节编码方式。- 对于单字节字符,字节的第一位为 0,后面 7 位为这个字符的
Unicode码,因此单字节的UTF-8编码与ASCII码相同。- 对于 n 字节字符(理论上 n 可以达到 6,但实际上只用到 4),第一个字节的前 n 位都设为 1,第 n+1 位设为 0,后面字节的前两位一律设置为 10,剩下的二进制位全部为这个字符的
Unicode码。n = 1(00000000 ~ 0000007F):0xxxxxxx n = 2(00000080 ~ 000007FF):110xxxxx 10xxxxxx n = 3(00000800 ~ 0000FFFF):1110xxxx 10xxxxxx 10xxxxxx n = 4(00010000 ~ 001FFFFF):11110xxx 10xxxxxx 10xxxxxx 10xxxxxx n = 5(00200000 ~ 03FFFFFF):111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx n = 6(04000000 ~ 7FFFFFFF):1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
以汉字 “你” 为例,“你” 的
Unicode是u4F60(01001111 01100000),落在 n=3 的区间,二进制从低位往高位取,每次取 6 位按照上面的格式进行填充,剩余的高位用 0 填补得到:11100100 10111101 10100000(0xE4BDA0)
4.3. 字符串强制转换
如果是将一个 []rune 类型的 Unicode 字符 slice 或数组转为 string,则对它们进行 UTF8 编码:
Go
// "program" in Japanese katakana
s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]"
fmt.Println(string(r)) // "プログラム"1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
将一个整数转型为字符串意思是生成以只包含对应 Unicode 码点字符的 UTF8 字符串:
Go
fmt.Println(string(65)) // "A", not "65"
fmt.Println(string(0x4eac)) // "京"1
2
2
如果对应码点的字符是无效的,则用 \uFFFD 无效字符作为替换:
Go
fmt.Println(string(1234567)) // "�"4.4. 字符串长度
- 字符串所占的字节长度可以通过函数
len()来获取,例如:len(str)。 - 字符串包含的字符个数可以通过函数
utf8.RuneCountInString来获取,例如:utf8.RuneCountInString(str)。
Go
import "unicode/utf8"
s := "Hello, 世界"
fmt.Println(len(s)) // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"1
2
3
4
5
2
3
4
5
4.5. 比较字符串
一般的比较运算符(==、!=、<、<=、>=、>)是通过在内存中按字节比较来实现字符串比较的,因此比较的结果是字符串自然编码的顺序。
4.6. 字符串默认值
主要注意的是字符串的默认值是空字符串(""),而不是 nil,实际上字符串也不能赋值 nil。
4.7. 遍历字符串
4.7.1. 逐一解码
Go
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%d\t%c\n", i, r)
i += size
}1
2
3
4
5
2
3
4
5
每一次调用 DecodeRuneInString 函数都返回一个 r 和长度,r 对应字符本身,长度对应 r 采用 UTF8 编码后的编码字节数目。长度可以用于更新第 i 个字符在字符串中的字节索引位置。
4.7.2. for range
通过以上编码方式是笨拙的,我们需要更简洁的语法。幸运的是,Go 语言的 range 循环在处理字符串的时候,会自动隐式解码 UTF8 字符串。
Go
for i, r := range "Hello, 世界" {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}1
2
3
2
3
4.7.3. 编码错误处理
每一个 UTF8 字符解码,不管是显式地调用 utf8.DecodeRuneInString 解码或是在 range 循环中隐式地解码,如果遇到一个错误的 UTF8 编码输入,将生成一个特别的 Unicode 字符 \uFFFD,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号 "�"。当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的 UTF8 字符串。
4.8. 截取字符串
字符串截取,strings.Index 正向搜索子字符串,strings.LastIndex 反向搜索子字符串,搜索的起始位置可以通过切片偏移操作:
Go
str, subStr, subStrLen := "我要可口可乐", "可", len("可")
offset := strings.Index(str, subStr) + subStrLen
secondIndex := strings.Index(str[offset:], subStr)
fmt.Print(str[offset+secondIndex:])1
2
3
4
2
3
4
控制台输出:
Text
可乐需要注意的是:字符串切片,切片的索引是基于字符串的字节索引的,如果一个字符串包含中文,则可以先将字符串转为 rune 切片再截取。
例如:
Go
a := "啊123"[1:]
fmt.Printf("%s", a)1
2
2
会输出乱码:
Text
��123修改索引后:
Go
a := "啊123"[3:]
fmt.Printf("%s", a)1
2
2
才输出正常:
Text
123将字符串转为 rune 切片再截取:
Go
a := []rune("啊123")
b := a[1:]
fmt.Printf("%s", string(b))1
2
3
2
3
Text
1234.9. 修改字符串
Go 语言的字符串无法直接修改每一个字符元素,只能通过重新构造新的字符串并赋值给原来的字符串变量。
Go
str := "hello everybody"
strBytes := []byte(str)
for i := 0; i < 5; i++ {
strBytes[i] = byte('1' + i)
}
newStr := string(strBytes)
fmt.Println(str)
fmt.Println(newStr)1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
控制台输出:
Text
hello everybody
12345 everybody1
2
2
4.10. 拼接字符串
Go 语言中也有类似于 StringBuilder 的机制来进行高效的字符串连接,例如:
Go
sb := bytes.Buffer{}
sb.WriteString("hello")
sb.WriteString(" ")
sb.WriteString("world")
fmt.Println(sb.String())1
2
3
4
5
2
3
4
5
控制台输出:
Text
hello worldNote
如果是为了拼接字符串,则推荐使用
strings.Builder。
bytes.Buffer和strings.Builder底层实现逻辑基本类似,差别就在于转字符串时,前者会发生内存拷贝,后者是对byte数组做强制转换。
4.11. 格式化打印
4.11.1. 通用
| 占位符 | 输出格式 | 举例 | 输出 |
|---|---|---|---|
%v | 默认格式 | Printf("%v", people) | {zhangsan} |
%+v | 在 %v 的基础上添加字段名 | Printf("%+v", people) | {Name:zhangsan} |
%#v | 值的 Go 语法表示 | Printf("%#v", people) | main.Human{Name:"zhangsan"} |
%T | 类型 | Printf("%T", people) | main.Human |
%% | "%" 字符本身 | Printf("%%") | % |
4.11.2. 布尔值
| 占位符 | 输出格式 | 举例 | 输出 |
|---|---|---|---|
%t | true 或 false | Printf("%t", true) | true |
4.11.3. 整数
| 占位符 | 输出格式 | 举例 | 输出 |
|---|---|---|---|
%b | 2 进制 | Printf("%b", 5) | 101 |
%o | 8 进制 | Printf("%o", 10) | 12 |
%d | 10 进制 | Printf("%d", 0x12) | 18 |
%x | 16 进制(小写) | Printf("%x", 13) | d |
%X | 16 进制(大写) | Printf("%X", 13) | D |
%c | 相应 Unicode 码点所表示的字符 | Printf("%c", 0x4E2D) | 中 |
%q | 单引号围绕的字符字面值,由 Go 语法安全地转义 | Printf("%q", 0x4E2D) | ‘中’ |
%U | Unicode 格式,U+1234 等同于 U+%04X | Printf("%U", 0x4E2D) | U+4E2D |
4.11.4. 浮点数和复数
| 占位符 | 输出格式 | 举例 | 输出 |
|---|---|---|---|
%f | 浮点,默认精度为 6 | Printf("%f", 10.2) | 10.200000 |
%b | 无小数部分,指数幂为 2 的科学计数法,与 strconv.FormatFloat 的 ‘b’ 转换格式一致 | ||
%e | 科学计数法(小写 e),默认精度为 6 | Printf("%e", 10.2) | 1.020000e+01 |
%E | 科学计数法(大写 E),默认精度为 6 | Printf("%E", 10.2) | 1.020000E+01 |
%g | 更紧凑的(末尾无 0)输出,根据情况自动选择 %e 或 %f | Printf("%g", 10.20) | 10.2 |
%G | 更紧凑的(末尾无 0)输出,根据情况自动选择 %E 或 %f | Printf("%G", 10.20+2i) | (10.2+2i) |
4.11.5. 字符串和字节切片
| 占位符 | 输出格式 | 举例 | 输出 |
|---|---|---|---|
%s | 字符串(string 或 []byte) | Printf("%s", []byte("Go 语言")) | Go 语言 |
%q | 双引号围绕的字符串,由 Go 语法安全地转义 | Printf("%q", "Go 语言") | "Go 语言" |
%x | 十六进制,小写字母,每字节两个字符 | Printf("%x", "golang") | 676f6c616e67 |
%X | 十六进制,大写字母,每字节两个字符 | Printf("%X", "golang") | 676F6C616E67 |
4.11.6. 指针
| 占位符 | 输出格式 | 举例 | 输出 |
|---|---|---|---|
%p | 十六进制表示,前缀 0x | Printf("%p", &people) | 0x4f57f0 |
4.11.7. 宽度、精度
宽度和精度格式化控制的是
Unicode码点的数量(不同于C的printf这两个因数指的是字节的数量)。宽度和精度两者任一个或两个都可以使用 “*” 号取代,此时它们的值将被对应的参数(按 “*” 号和 verb 出现的顺序,即控制其值的参数会出现在要表示的值前面)控制,这个控制参数必须是
int类型。对于大多数类型的值,宽度是输出的最小字符数,如有必要会用空格填充。
对于字符串,宽度指最终输出的文本最小宽度,精度指字符串值被输出的最大字符数,必要时会发生截断,例如:
Gostr := "hello world" fmt.Printf("%%s :%s;\n", str) fmt.Printf("%%4s :%4s;\n", str) fmt.Printf("%%15s :%15s;\n", str) fmt.Printf("%%4.s :%4.s;\n", str) fmt.Printf("%%4.3s :%4.3s;\n", str) fmt.Printf("%%.3s :%.3s;\n", str)1
2
3
4
5
6
7控制台输出:
Text%s :hello world; %4s :hello world; %15s : hello world; %4.s : ; %4.3s : hel; %.3s :hel;1
2
3
4
5
6对于整数,宽度和精度都设置输出总长度。采用精度时表示右对齐并用 0 填充,而宽度默认表示用空格填充,例如:
Goval := 123 fmt.Printf("%%d :%d;\n", val) fmt.Printf("%%4d :%4d;\n", val) fmt.Printf("%%10d :%10d;\n", val) fmt.Printf("%%4.d :%4.d;\n", val) fmt.Printf("%%7.5d :%7.5d;\n", val) fmt.Printf("%%4.5d :%4.5d;\n", val) fmt.Printf("%%.5d :%.5d;\n", val)1
2
3
4
5
6
7
8控制台输出:
Text%d :123; %4d : 123; %10d : 123; %4.d : 123; %7.5d : 00123; %4.5d :00123; %.5d :00123;1
2
3
4
5
6
7对于浮点数,宽度设置输出总长度,精度设置小数部分长度(如果有的话)。对于
%g、/%G精度设置的是总数字个数。例如,对数字 123.45,格式 %6.2f 输出 123.45,格式 %.4g 输出 123.5。%e和%f的默认精度是 6,%g、%G的默认精度是可以将该值区分出来需要的最小数字个数。对复数,宽度和精度会分别用于实部和虚部,结果用小括号包裹。因此 %f 用于 1.2+3.4i 输出 (1.200000+3.400000i)。
| 举例 | 说明 |
|---|---|
%f | 默认宽度,默认精度 |
%9f | 宽度 9,默认精度 |
%.2f | 默认宽度,精度 2 |
%9.2f | 宽度 9,精度 2 |
%9.f | 宽度 9,精度 0 |
4.11.8. 其它占位符
| 占位符 | 输出格式 | 举例 | 输出 |
|---|---|---|---|
| + | 总打印数值的正负号。对于 %q(也就是 %+q)会转义输出 ASCII 编码的字符 | Printf("%+q", "中文") | "\u4e2d\u6587” |
| - | 输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐) | ||
| # | 切换格式%#o:八进制数前加 0%#x、%#X:十六进制数前加 0x$#p:指针去掉前面的 0x%#q:如果 strconv.CanBackquote 返回真会输出反引号括起来的未转义字符串%#U:%U + 空格 + %q | Printf("%#U\n", 0x4E2D) | U+4E2D '中’ |
| (空格) | 为数值中省略的正负号留出空白(% d)以十六进制( % x、% X)打印字符串或切片时,在字节之间用空格隔开 | ||
| 0 | 填充前导的 0 而非空格,对于数字,这会将填充移到正负号之后 |
4.12. 字符串相关工具包
标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv 和 unicode 包:
strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能;bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示;strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换;unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值;path和path/filepath包提供了关于文件路径名更一般的函数操作。
5. 指针
5.1. new 函数
表达式 new(T) 将创建一个 T 类型的匿名变量,初始化为 T 类型的零值,然后返回变量地址,返回的指针类型为 *T。
以下两个 newInt 函数是等同的:
Go
func newInt() *int {
return new(int)
}1
2
3
2
3
Go
func newInt() *int {
var dummy int
return &dummy
}1
2
3
4
2
3
4
Note
用
new创建变量和普通变量声明语句方式创建变量没有什么区别。new 函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活。
5.2. 局部变量逃逸
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间(与 var 还是 new 声明变量的方式没有关系)。
Go
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
f 函数里的 x 变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的;用 Go 语言的术语说,这个 x 局部变量从函数 f 中逃逸了。
而当 g 函数返回时,变量 *y 将是不可达的,也就是说可以马上被回收的。因此,*y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间(也可以选择在堆上分配,然后由 Go 语言的 GC 回收这个变量的内存空间)。
如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收,从而可能影响程序的性能。
5.3. unsafe.Pointer
在 Go 语言中没有指针运算,也就是说你不能对一个指针变量进行逻辑运算(如 p++),但是通过 unsafe 包可以实现同样的功能。
假如普通指针用 *T 表示,unsafe.Pointer 用 Pointer 表示,那么 *T、Pointer、uintptr 之间的转换关系可以这样表示:
*T←→Pointer←→uintptr
需要注意的是:
uintptr可以进行逻辑运算,也就是说咱们可以通过uintptr来计算指针地址偏移;- 但是
uintptr没有指针的语义,换句话说就是uintptr表示的只是一个数值,该数值地址所在的对象有可能被GC无情回收; - 而
Pointer有指针语义,可以保护其所指对象在有用的时候不会被GC回收; - 我们不能直接通过
*来获取Pointer指针指向的真实变量值,因为我们不知道变量的具体类型;
不要试图引入一个 uintptr 类型的临时变量,因为它可能会破坏代码的安全性。
Go
// 错误示范,不要将 uintptr 值保存到临时变量
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 421
2
3
4
2
3
4
在使用 uintptr 的过程中需要注意以下问题:
原内存地址可能发生改变
有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收器被称为移动
GC(虽然 Go 语言还没有使用移动GC,但未来可能考虑实现)。当一个变量被移动,所有的保存该变量旧地址的指针必须同时被更新为变量移动后的新地址。从垃圾收集器的视角来看,一个Pointer是一个指向变量的指针,因此当变量被移动时对应的指针也必须被更新;但是uintptr类型的临时变量只是一个普通的数字,所以其值不应该被改变。上面错误的代码因为引入一个非指针的临时变量tmp,导致垃圾收集器无法正确识别这个是一个指向变量x的指针。当第二个语句执行时,变量x可能已经被转移,这时候临时变量tmp也就不再是现在的&x.b地址。第三行代码向之前无效地址空间的赋值语句将彻底摧毁整个程序!另外当一个协程的栈的大小改变(grow)时,一个新的内存段将申请给此栈使用。原先已经开辟在老的内存段上的内存块将很有可能被转移到新的内存段上,或者说这些内存块的地址将改变。相应地,引用着这些开辟在此栈上的内存块的指针(它们同样开辟在此栈上)中存储的地址也将得到刷新。所以这也是
uintptr变量不要轻易使用的原因。不再被使用的内存块随时可能被
GC回收Go// 假设此函数不会被内联(inline) func createInt() *int { return new(int) } func foo() { x, y, z := createInt(), createInt(), createInt() py, pz := unsafe.Pointer(y), uintptr(unsafe.Pointer(z)) *x = 1 // okay *(*int)(py) = 2 // okay *(*int)(unsafe.Pointer(pz)) = 3 // dangerous }1
2
3
4
5
6
7
8
9
10
11
12
13我们可以使用
runtime.KeepAlive函数来确保该值在被调用之前仍处于被使用中,而避免被GC回收。Go// 假设此函数不会被内联(inline) func createInt() *int { return new(int) } func foo() { x, y, z := createInt(), createInt(), createInt() py, pz := unsafe.Pointer(y), uintptr(unsafe.Pointer(z)) *x = 1 // okay *(*int)(py) = 2 // okay *(*int)(unsafe.Pointer(pz)) = 3 // okay runtime.KeepAlive(z) // keep z alive }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在 unsafe 标准库包的文档中列出了六种 非类型安全指针的使用模式:
将
*T1转为*T2。确保 T2 类型占用的内存空间大小不大于 T1。
Gofunc Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }1
2
3将
Pointer转为uintptr,但不转回Pointer。这种模式中 uintptr 通常用于打印输出(但是也有其它安全的途径可以实现此目的)。
将
Pointer转为uintptr,通过逻辑运算后转回Pointer。需要注意的是,这两次转换必须出现在同一个表达式中,下面是一个错误示例:
Go// INVALID: uintptr cannot be stored in variable before conversion back to Pointer. u := uintptr(p) p = unsafe.Pointer(u + offset)1
2
3将
Pointer转为uintptr类型参数并传递给syscall.Syscall函数调用。在这种情况下,将
Pointer转为uintptr的转换操作也必须是出现在函数调用的表达式中。Gosyscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))实际上编译器会针对
syscall.Syscall做特殊判断,确保该地址所指向的内存块对象不会被GC回收。将
reflect.Value.Pointer或reflect.Value.UnsafeAddr方法的uintptr返回值转换为Pointer。reflect标准库包中的Value类型的Pointer和UnsafeAddr方法都返回一个uintptr值,而不是一个unsafe.Pointer值。这样设计的目的是避免用户不引用unsafe标准库包就可以将这两个方法的返回值转换为任何类型安全指针类型。因为这样的设计,所以我们需要在两个方法返回
uintptr结果的时候立即转换为Pointer类型。否则可能将出现一个短暂的时间窗供GC将该地址所在的对象回收掉。Gop := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))以上的写法是安全的,而以下的写法是错误的:
Go// INVALID: uintptr cannot be stored in variable before conversion back to Pointer. u := reflect.ValueOf(new(int)).Pointer() p := (*int)(unsafe.Pointer(u))1
2
3将一个
reflect.SliceHeader或者reflect.StringHeader值的Data字段转换为非类型安全指针,及其逆转换。一般说来,我们只应该从一个已经存在的字符串值得到一个
reflect.StringHeader指针,或者从一个已经存在的切片值得到一个reflect.SliceHeader指针,而不应该从一个StringHeader值生成一个字符串,或者从一个SliceHeader值生成一个切片。比如,下面的代码是不安全的:Go// INVALID: a directly-declared header will not hold Data as a reference. var hdr reflect.StringHeader hdr.Data = uintptr(unsafe.Pointer(p)) hdr.Len = n s := *(*string)(unsafe.Pointer(&hdr)) // p possibly already lost1
2
3
4
5直接将字符串强制转
[]byte实际上会发生一次底层的字节序列复制,而通过非类型安全途径可以避免这个问题:Gofunc String2ByteSlice(str string) (bs []byte) { strHdr := (*reflect.StringHeader)(unsafe.Pointer(&str)) sliceHdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) sliceHdr.Data = strHdr.Data sliceHdr.Len = strHdr.Len sliceHdr.Cap = strHdr.Len // 下面的 KeepAlive 是必要的。 runtime.KeepAlive(&str) return }1
2
3
4
5
6
7
8
9
10实际上,为了避免因为忘记调用
runtime.KeepAlive函数而造成的风险,在日常编程中更推荐使用我们自定义Data字段类型为Pointer的SliceHeader和StringHeader结构体Gotype SliceHeader struct { Data unsafe.Pointer Len int Cap int } type StringHeader struct { Data unsafe.Pointer Len int } func String2ByteSlice(str string) (bs []byte) { strHdr := (*StringHeader)(unsafe.Pointer(&str)) sliceHdr := (*SliceHeader)(unsafe.Pointer(&bs)) sliceHdr.Data = strHdr.Data sliceHdr.Len = strHdr.Len sliceHdr.Cap = strHdr.Len // 此 KeepAlive 调用变得不再必需。 //runtime.KeepAlive(&str) return }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
6. 数组
- 数组的长度是数组类型的一个组成部分(所以
[3]int和[4]int是两种不同的数据类型) - 如果将数组作为函数的参数类型,那么在函数调用时,整个数组将发生数据复制(Go 语言当中没有引用类型,将切片作为参数传递之所以不会产生 “数据复制”,是因为切片实际上是一个结构体,其内部维护了一个指向数组对象的指针,作为函数参数传递时产生 “数据复制” 的只是这个结构体本身)
- 如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过
==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。
6.1. 数组初始化
我们可以使用数组字面值语法用一组值来初始化数组:
Go
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"1
2
3
2
3
上面 q 数组的定义还可以简化为:
Go
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"1
2
2
也可以指定一个索引和对应值列表的方式初始化,就像下面这样:
Go
type Currency int
const (
USD Currency = iota // 美元
EUR // 欧元
GBP // 英镑
RMB // 人民币
)
symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
fmt.Println(RMB, symbol[RMB]) // "3 ¥"1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,未指定初始值的元素将用零值初始化。例如:
Go
r := [...]int{99: -1}定义了一个含有 100 个元素的数组 r,最后一个元素被初始化为 -1,其它元素都是用 0 初始化。
7. 切片
切片可以抽象为一个包含以下 3 个变量的结构体:
- 指向原生数组的指针
- 切片元素的个数(length)
- 切片已分配的存储空间(capacity)
切片可动态增减元素,当需要存储的元素超过存储能力(capacity)时,将发生一次重新分配内存和搬送内存块的操作。因此合理地设置存储能力的值,可以大幅降低数组切片内部重新分配内存和搬送内存块的频率,从而大幅提升程序性能。
7.1. 通过 make 创建切片
并非一定要事先准备一个数组才能创建数组切片,通过 make 函数也可以灵活地创建数组切片(通过这种方式事实上还会有一个对应的匿名数组被创建出来):
Go
// 创建一个初始元素个数为 5 的数组切片,元素初始值为 0
mySlice1 := make([]int, 5)
// 创建一个初始元素个数为 5 的数组切片,元素初始值为 0,并预留 10 个元素的存储空间
mySlice2 := make([]int, 5, 10)
// 直接创建并初始化包含 5 个元素的数组切片
mySlice3 := []int{1, 2, 3, 4, 5}1
2
3
4
5
6
2
3
4
5
6
7.2. 切片比较
和数组不同的是,slice 之间不能比较,因此我们不能使用 == 操作符来判断两个 slice 是否含有全部相等元素。不过标准库提供了高度优化的 bytes.Equal 函数来判断两个字节型 slice 是否相等([]byte),但是对于其他类型的 slice,我们必须自己展开每个元素进行比较:
Go
func equal(x, y []string) bool {
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
slice 唯一合法的比较操作是和 nil 比较,例如:
Go
if summer == nil { /* ... */ }7.3. 切片零值
slice 的零值同样也是 nil。
一个 nil 值的 slice 并没有底层数组,长度和容量都是 0。但是也有非 nil 值的 slice 的长度和容量也是 0 的,例如 []int{} 或 make([]int, 3)[3:];所以如果需要测试一个 slice 是否是空的,应该使用 len(s) == 0 来判断,而不是 s == nil 来判断。
Go
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil1
2
3
4
2
3
4
7.4. append 函数
append 函数对于理解 slice 底层是如何工作的非常重要,所以让我们仔细查看究竟是发生了什么。下面是第一个版本的 appendInt 函数,专门用于处理 []int 类型的 slice:
Go
func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen <= cap(x) {
// There is room to grow. Extend the slice.
z = x[:zlen]
} else {
// There is insufficient space. Allocate a new array.
// Grow by doubling, for amortized linear complexity.
zcap := zlen
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x) // a built-in function; see text
}
z[len(x)] = y
return z
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
每次调用 appendInt 函数,必须先检测 slice 底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展 slice(依然在原有的底层数组之上),将新添加的 y 元素复制到新扩展的空间,并返回 slice。因此,输入的 x 和输出的 z 共享相同的底层数组。
如果没有足够的增长空间的话,appendInt 函数则会先分配一个足够大的 slice 用于保存新的结果,先将输入的 x 复制到新的空间,然后添加 y 元素。结果 z 和输入的 x 引用的将是不同的底层数组。
虽然通过循环复制元素更直接,不过内置的 copy 函数可以方便地将一个 slice 复制另一个相同类型的 slice。copy 函数的第一个参数是要复制的目标 slice,第二个参数是源 slice,目标和源的位置顺序和 dst = src 赋值语句是一致的。两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。copy 函数将返回成功复制的元素的个数(我们这里没有用到),等于两个 slice 中较小的长度,所以我们不用担心覆盖会超出目标 slice 的范围。
内置的 append 函数可能使用比 appendInt 更复杂的内存扩展策略。因此,通常我们并不知道 append 调用是否导致了内存的重新分配,因此我们也不能确认新的 slice 和原始的 slice 是否引用的是相同的底层数组空间。同样,我们不能确认在原先的 slice 上的操作是否会影响到新的 slice。因此,通常是将 append 返回的结果直接赋值给输入的 slice 变量。
7.5. Slice 使用技巧
让我们看看更多的例子,比如旋转 slice、反转 slice 或在 slice 原有内存空间修改元素。给定一个字符串列表,下面的 nonempty 函数将在原有 slice 内存空间之上返回不包含空字符串的列表:
Go
// Nonempty is an example of an in-place slice algorithm.
package main
import "fmt"
// nonempty returns a slice holding only the non-empty strings.
// The underlying array is modified during the call.
func nonempty(strings []string) []string {
i := 0
for _, s := range strings {
if s != "" {
strings[i] = s
i++
}
}
return strings[:i]
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
比较微妙的地方是,输入的 slice 和输出的 slice 共享一个底层数组。这可以避免分配另一个数组,不过原来的数据将可能会被覆盖,正如下面两个打印语句看到的那样:
Go
data := []string{"one", "", "three"}
fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
fmt.Printf("%q\n", data) // `["one" "three" "three"]`1
2
3
2
3
因此我们通常会这样使用 nonempty 函数:data = nonempty(data)。
nonempty 函数也可以使用 append 函数实现:
Go
func nonempty2(strings []string) []string {
out := strings[:0] // zero-length slice of original
for _, s := range strings {
if s != "" {
out = append(out, s)
}
}
return out
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
一个 slice 可以用来模拟一个 stack。最初给定的空 slice 对应一个空的 stack,然后可以使用 append 函数将新的值压入 stack:
Go
stack = append(stack, v) // push vstack 的顶部位置对应 slice 的最后一个元素:
Go
top := stack[len(stack)-1] // top of stack通过收缩 stack 可以弹出栈顶的元素
Go
stack = stack[:len(stack)-1] // pop要删除 slice 中间的某个元素并保存原有的元素顺序,可以通过内置的 copy 函数将后面的子 slice 向前依次移动一位完成:
Go
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
func main() {
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 8 9]"
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:
Go
func remove(slice []int, i int) []int {
slice[i] = slice[len(slice)-1]
return slice[:len(slice)-1]
}
func main() {
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 9 8]
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
8. map
内置的 make 函数可以创建一个 map:
Go
ages := make(map[string]int) // mapping from strings to ints我们也可以用 map 字面值的语法创建 map,同时还可以指定一些最初的 key/value:
Go
ages := map[string]int{
"alice": 31,
"charlie": 34,
}1
2
3
4
2
3
4
这相当于:
Go
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 341
2
3
2
3
使用内置的 delete 函数可以删除元素:
Go
delete(ages, "alice") // remove element ages["alice"]即使这些元素不在 map 中也没有关系,所有这些操作是安全的。如果一个查找失败将返回 value 类型对应的零值,例如,即使 map 中不存在 “bob” 下面的代码也可以正常工作,因为 ages["bob"] 失败时将返回 0:
Go
ages["bob"] = ages["bob"] + 1 // happy birthday!这样也可以:
Go
ages["bob"] += 1这样也还是可以:
Go
ages["bob"]++如果 key 不存在,那么将得到 value 对应类型的零值,这个规则很实用,但是有时候可能需要知道对应的元素是否真的是在 map 之中,例如:
Go
age, ok := ages["bob"]
if !ok { /* "bob" is not a key in this map; age == 0. */ }1
2
2
Map 的迭代顺序是不确定的,如果要按顺序遍历 key/value 对,我们必须显式地对 key 进行排序,可以使用 sort 包的 Strings 函数对字符串 slice 进行排序。下面是常见的处理方式:
Go
import "sort"
names := make([]string, 0, len(ages))
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Go 语言中并没有提供一个 set 类型,但是 map 中的 key 也是不相同的,可以用 map 实现类似 set 的功能。为了说明这一点,下面的 dedup 程序读取多行输入,但是只打印第一次出现的行。dedup 程序通过 map 来表示所有的输入行所对应的 set 集合,以确保已经在集合存在的行不会被重复打印。
Go
func main() {
seen := make(map[string]bool) // a set of strings
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
line := input.Text()
if !seen[line] {
seen[line] = true
fmt.Println(line)
}
}
if err := input.Err(); err != nil {
fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
os.Exit(1)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
有时候我们需要一个 map 或 set 的 key 是 slice 类型,但是 map 的 key 必须是可比较的类型,但是 slice 并不满足这个条件。不过,我们可以通过两个步骤绕过这个限制。第一步,定义一个辅助函数 k,将 slice 转为 map 对应的 string 类型的 key,确保只有 x 和 y 相等时 k(x) == k(y) 才成立。然后创建一个 key 为 string 类型的 map,在每次对 map 操作时先用 k 辅助函数将 slice 转化为 string 类型。
Go
var m = make(map[string]int)
func k(list []string) string { return fmt.Sprintf("%q", list) }
func Add(list []string) { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }1
2
3
4
5
6
2
3
4
5
6
9. 自定义类型
Go
type 类型名字 底层类型Go
type Celsius float64
type Fahrenheit float64
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
对于每一个类型 T,都有一个对应的类型转换操作 T(x),用于将 x 转为 T 类型(如果 T 是指针类型,可能会需要用小括弧包装 T,比如 (*int)(0))。类型转换不会改变值本身,但是会使它们的语义发生变化。例如上方的 Celsius(t) 和 Fahrenheit(t)。
底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持。
Go
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
// fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch1
2
3
4
2
3
4
命名类型还可以为该类型的值定义新的行为,这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。下面的声明语句,Celsius 类型的参数 c 出现在了函数名的前面,表示声明的是 Celsius 类型的一个名叫 String 的方法:
Go
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }许多类型都会定义一个 String 方法,因为当使用 fmt 包的打印方法时,将会优先使用该类型对应的 String 方法返回的结果打印:
Go
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String1
2
3
4
5
6
7
2
3
4
5
6
7
10. 结构体
10.1. 内存对齐
关于为什么要内存对齐,就是为了加快内存的存取速度,是一种用空间换时间的做法。
不同平台类型 T 内存大小和对齐值都是不一样的。具体使用的时候要按照自己的本机实际情况为准。不过大部分面试题或者我们讨论的时候都是按照 64 位进行的。
Go
fmt.Printf("bool: %d\n", unsafe.Alignof(bool(true)))
fmt.Printf("int32: %d\n", unsafe.Alignof(int32(0)))
fmt.Printf("int8: %d\n", unsafe.Alignof(int8(0)))
fmt.Printf("int64: %d\n", unsafe.Alignof(int64(0)))
fmt.Printf("byte: %d\n", unsafe.Alignof(byte(0)))
fmt.Printf("string: %d\n", unsafe.Alignof("xxx"))
fmt.Printf("map: %d\n", unsafe.Alignof(map[string]string{}))1
2
3
4
5
6
7
2
3
4
5
6
7
Text
bool: 1
int32: 4
int8: 1
int64: 8
byte: 1
string: 8
map: 81
2
3
4
5
6
7
2
3
4
5
6
7
10.1.1. 对齐规则
- 结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度
#pragma pack(n)或当前成员变量类型的长度unsafe.Sizeof,取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍。 - 结构体本身,对齐值必须为编译器默认对齐长度
#pragma pack(n)或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值。 - 结合以上两点,可得知若编译器默认对齐长度
#pragma pack(n)超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的。
10.1.2. 举例
以 Part1 结构体为例:
Go
type Part1 struct {
A bool
B int32
C int8
D int64
E byte
}1
2
3
4
5
6
7
2
3
4
5
6
7
分析:
| 成员 | 类型 | 默认对齐值 | 偏移量 | 解析 | 内存布局 |
|---|---|---|---|---|---|
| A | bool | 1 | 0 | 第一个成员的偏移量是 0,占用 1 位 | A |
| B | int32 | 4 | 1 | 根据规则 1,对齐值是当前成员类型的对齐值,所以偏移量是 4 的整数倍,所以需要先补 3 位 (2-4),再第 5 位开始填充 | Axxx BBBB |
| C | int8 | 1 | 8 | 根据规则 1,偏移量是对齐值的整数倍,8/1 = 8,故不需要偏移,从第 9 位开始填充 1 位 | Axxx BBBB C |
| D | int64 | 8 | 9 | 根据规则 1,偏移量是对齐值的整数倍,故需要先补 7 位 (9-16),从 17 位开始填充 8 位 | Axxx BBBB Cxxx xxxx DDDD DDDD |
| E | byte | 1 | 24 | 根据规则 1,24/1 = 24,不需要偏移,从第 25 位开始填充 | Axxx BBBB Cxxx xxxx DDDD DDDD E |
| 最终调整 | struct | 8 | 25 | 根据规则 2,结构体的的对齐值是 8,由于目前的偏移量是 25,不是 8 的整数倍,故需要填充至 8 的最小整数倍,同时比 25 大,故是 32 | Axxx BBBB Cxxx xxxx DDDD DDDD Exxx xxxx |
如果我们对 Part1 结构体字段的顺序进行调整:
Go
type Part2 struct {
E byte
C int8
A bool
B int32
D int64
}1
2
3
4
5
6
7
2
3
4
5
6
7
| 成员 | 类型 | 默认对齐值 | 偏移量 | 解析 | 内存布局 |
|---|---|---|---|---|---|
| E | byte | 1 | 0 | 第一个成员的偏移量是 0,占用 1 位 | E |
| C | int8 | 1 | 1 | 根据规则 1,1/1 = 1,不需要偏移,从第 2 位开始填充 1 位 | EC |
| A | bool | 1 | 2 | 根据规则 1,2/1 = 2,不需要偏移,从第 3 位开始填充 1 位 | ECA |
| B | int32 | 4 | 4 | 根据规则 1,对齐值是当前成员类型的对齐值,所以偏移量是 4 的整数倍,所以需要先补 1 位 (4),再第 5 位开始填充 4 位 | ECAx BBBB |
| D | int64 | 8 | 9 | 根据规则 1,8/1 = 8,不需要偏移,从第 9 位开始填充 8 位 | ECAx BBBB DDDD DDDD |
| 最终调整 | struct | 8 | 16 | 根据规则 2,结构体的的对齐值是 8,由于目前的偏移量是 16,是 8 的整数倍,故是 16 |
所以有的时候我们调整结构体字段的顺序是可以节省内存的。
10.2. 结构体传参
如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在 Go 语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。
Go
func AwardAnnualRaise(e *Employee) {
e.Salary = e.Salary * 105 / 100
}1
2
3
2
3
因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:
Go
pp := &Point{1, 2}它与下面的语句是等价的:
Go
pp := new(Point)
*pp = Point{1, 2}1
2
2
10.3. 结构体嵌入和匿名成员
Go 语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。
下面的代码中,Circle 和 Wheel 各自都有一个匿名成员。我们可以说 Point 类型被嵌入到了 Circle 结构体,同时 Circle 类型被嵌入到了 Wheel 结构体:
Go
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
得益于匿名嵌入的特性,我们可以直接访问子属性而不需要给出完整的路径:
Go
var w Wheel
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 201
2
3
4
5
2
3
4
5
在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员 Circle 和 Point 都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。
不幸的是,结构体字面值并没有简短表示匿名成员的语法,因此下面的语句不能编译通过:
Go
w = Wheel{8, 8, 5, 20} // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields1
2
2
结构体字面值必须遵循类型声明时的结构,所以我们只能用下面的两种语法(它们彼此是等价的):
Go
w = Wheel{Circle{Point{8, 8}, 5}, 20}
w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}
fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
w.X = 42
fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
其实匿名成员并不要求是结构体类型,任何命名的类型都可以作为结构体的匿名成员。另外,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一个有简单行为的对象组合成有复杂行为的对象。组合是 Go 语言中面向对象编程的核心。
11. JSON
11.1. 编码
Go
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}
var movies = []Movie{
{Title: "Casablanca", Year: 1942, Color: false,
Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
{Title: "Cool Hand Luke", Year: 1967, Color: true,
Actors: []string{"Paul Newman"}},
{Title: "Bullitt", Year: 1968, Color: true,
Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
通过 json.Marshal 进行编码:
Go
data, err := json.Marshal(movies)
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)1
2
3
4
5
2
3
4
5
JSON
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr
id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac
tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"
Actors":["Steve McQueen","Jacqueline Bisset"]}]1
2
3
4
2
3
4
通过 json.MarshalIndent 进行格式化编码:
Go
data, err := json.MarshalIndent(movies, "", " ")
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)1
2
3
4
5
2
3
4
5
JSON
[
{
"Title": "Casablanca",
"released": 1942,
"Actors": [
"Humphrey Bogart",
"Ingrid Bergman"
]
},
{
"Title": "Cool Hand Luke",
"released": 1967,
"color": true,
"Actors": [
"Paul Newman"
]
},
{
"Title": "Bullitt",
"released": 1968,
"color": true,
"Actors": [
"Steve McQueen",
"Jacqueline Bisset"
]
}
]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
细心的读者可能已经注意到,其中 Year 名字的成员在编码后变成了 released,还有 Color 成员编码后变成了小写字母开头的 color。这是因为构体成员 Tag 所导致的。一个构体成员 Tag 是和在编译阶段关联到该成员的元信息字符串:
Text
Year int `json:"released"`
Color bool `json:"color,omitempty"`1
2
2
结构体的成员 Tag 可以是任意的字符串面值,但是通常是一系列用空格分隔的 key:"value" 键值对序列;因为值中含义双引号字符,因此成员 Tag 一般用原生字符串面值的形式书写。json 开头键名对应的值用于控制 encoding/json 包的编码和解码的行为,并且 encoding/... 下面其它的包也遵循这个约定。
11.2. 解码
解码通过 json.Unmarshal 函数完成。下面的代码将 JSON 格式的电影数据解码为一个结构体 slice,结构体中只有 Title 成员。通过定义合适的 Go 语言数据结构,我们可以选择性地解码 JSON 中感兴趣的成员。当 Unmarshal 函数调用返回,slice 将被只含有 Title 信息值填充,其它 JSON 成员将被忽略:
Go
var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"1
2
3
4
5
2
3
4
5
11.3. 基于流的解码/编码器
我们除了可以使用 json.Marshal、json.Unmarshal 函数处理 JSON 编码、解码。我们还可以使用基于流式的编码、解码器 json.Encoder、json.Decoder。
learnjson/github_issue.go:
Go
package learnjson
import (
"time"
)
const GITHUB_ISSUE_URL = "https://api.github.com/search/issues"
type IssuesSearchResult struct {
Total int `json:"total_count"`
Items []*Issue `json:"items"`
}
type Issue struct {
Number int `json:"number"`
HTMLURL string `json:"html_url"`
Title string `json:"title"`
State string `json:"state"`
User *User `json:"user"`
CreatedAt time.Time `json:"created_at"`
Body string `json:"body"` // in markdown format
}
type User struct {
Login string `json:"login"`
HTMLURL string `json:"html_url"`
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
learnjson/get_github_issues.go
Go
package learnjson
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"strings"
)
func SearchIssues(terms []string) (*IssuesSearchResult, error) {
url := GITHUB_ISSUE_URL + "?q=" + url.QueryEscape(strings.Join(terms, " "))
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("search query failed: %s", resp.Status)
}
var result IssuesSearchResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
resp.Body.Close()
return nil, err
}
resp.Body.Close()
return &result, nil
}
func TestGetGithubIssues(q []string) {
result, err := SearchIssues(q)
if err != nil {
log.Fatal(err)
return
}
fmt.Printf("%d issue:\n", result.Total)
for _, item := range result.Items {
fmt.Printf("#%-5d %9.9s %.55s\n", item.Number, item.User.Login, item.Title)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Go
package main
import (
"hello/learnjson"
"os"
)
func main() {
learnjson.TestGetGithubIssues(os.Args[1:])
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
12. 文本模板和 HTML 模板
12.1. 文本模板
文本模板在 text/template 包中实现,一个模板通常包含了一个或多个 {{action}} 对象,action 是一个表达式,action 虽然简短但是可以输出复杂的打印值,模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流 if-else 语句和 range 循环语句,还有其它实例化模板等诸多特性。
Go
const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User: {{.User.Login}}
Title: {{.Title | printf "%.64s"}}
Age: {{.CreatedAt | daysAgo}} days
{{end}}`1
2
3
4
5
6
7
2
3
4
5
6
7
在上例模板中:
{{.TotalCount}}将展开结构体的TotalCount成员并以默认的方式打印值;{{range .Items}}和{{end}}对应一个循环 action,因此它们之间的内容可能会被展开多次,循环每次迭代的当前值对应当前的Items元素的值;在
Title这一行的 action 中,第二个操作是一个printf函数,是一个基于fmt.Sprintf实现的内置函数,所有模板都可以直接使用;在一个 action 中,
|操作符表示将前一个表达式的结果作为后一个函数的输入,类似于 UNIX 中管道的概念。对于
Age部分,第二个动作是一daysAgo函数:Gofunc daysAgo(t time.Time) int { return int(time.Since(t).Hours() / 24) }1
2
3
生成模板的输出需要两个处理步骤:
- 分析模板并转为内部表示(分析模板部分一般只需要执行一次);
- 基于指定的输入执行模板;
下面的代码创建并分析上面定义的模板 templ:
Go
report, err := template.New("report").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ)
if err != nil {
log.Fatal(err)
}1
2
3
4
5
6
2
3
4
5
6
我们还可以使用 template.Must 简化模板分析的错误处理,它接受一个模板和一个 error 类型的参数,检测 error 是否为 nil(如果不是 nil 则发出 panic 异常),然后返回传入的模板。
Go
var report = template.Must(template.New("issuelist").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ))
func main() {
result, err := github.SearchIssues(os.Args[1:])
if err != nil {
log.Fatal(err)
}
if err := report.Execute(os.Stdout, result); err != nil {
log.Fatal(err)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
12.2. HTML 模板
HTML 模板在 html/template 包中实现,它的 API 和模板语言 text/template 包相同,但是增加了一个将字符串自动转义特性,这可以避免输入字符串和 HTML、JavaScript、CSS 或 URL 语法产生冲突、安全等问题。
Go
import "html/template"
var report = template.Must(template.New("issuelist").Parse(`
<h1>{{.TotalCount}} issues</h1>
<table>
<tr style='text-align: left'>
<th>#</th>
<th>State</th>
<th>User</th>
<th>Title</th>
</tr>
{{range .Items}}
<tr>
<td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
<td>{{.State}}</td>
<td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
<td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
</tr>
{{end}}
</table>
`))1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
我们也可以通过对信任的 HTML 字符串使用 template.HTML 类型来抑制这种自动转义的行为。还有很多采用类型命名的字符串类型分别对应信任的 JavaScript、CSS 和 URL。
Go
func main() {
const templ = `<span>A: {{.A}}</span></br><span>B: {{.B}}</span>`
t := template.Must(template.New("escape").Parse(templ))
var data struct {
A string // untrusted plain text
B template.HTML // trusted HTML
}
data.A = "<b>Hello!</b>"
data.B = "<b>Hello!</b>"
if err := t.Execute(os.Stdout, data); err != nil {
log.Fatal(err)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
将以上程序的输出内容保存到 HTML 文件,并在浏览器中打开,可以看到 A 的粗体标签被作为文本直接打印了出来:
