Go语言圣经-Notes
1 入门
获取命令行参数
存储在 os.Args
变量中, 其为一个字符串切片.
同样, os.Args[0]
保存当前命令本身的名字 (不是文件名).
循环
Go 语言只有 for
循环这一种循环语句, 而有多种形式.
range 函数
如:
1 |
|
其中,collection
可以是数组、切片、字符串、映射或通道,index
是当前元素的索引(或键),value
是当前元素的值。
strings.Join
用于将一个切片进行字符串拼接.
如:
1 |
|
1.3 查找重复的行
bufio
库
主要有三种类型:
Reader
Writer
Scanner
创建的语法都如:
1 |
|
类型的 Text()
方法可以获取读取的字符串:
1 |
|
Scan
函数在读到一行时返回 true
, 不再有输入时返回 false
, 且可以设置分隔符.
os.Stdin
和os.Open
返回的描述符的类型
都是 *os.File
类型指针.
向函数传入
map
类型数据
其传递的是 map
的引用 (可能本身就是指针).
io/ioutil
库
其中的 ReadFile
函数用于读取指定文件的全部内容. 返回值是字符串切片.
strings.Split
函数
将字符串分割成字串的切片
1.5 获取 URL
使用 net
之下的包.
net/http
包
http.Get
用于发出 get
请求. 其会返回一个结构体:
1 |
|
读取响应流内容:
1 |
|
关闭响应流:
1 |
|
获取状态码:
1 |
|
io.Copy
如:
1 |
|
其会从 src
中读取内容, 并将结果写入到 dst
中.
注意 dst
是 io.Writer
对象.
strings.HasPrefix
其函数声明为:
1 |
|
检查一个字符串是否有前缀 prefix
.
1.6 并发获取多个 URL
time
标准库
time.Now()
返回本地时间.
获取相隔的时间:
1 |
|
ioutil.Discard
不要的数据可以输出到这里, 如:
1 |
|
1.7 Web 服务
同样用 net/http
标准库.
示例如:
1 |
|
1.8 本章重点
switch
的 else if
用法:
1 |
|
这种形式被称为无 tag
switch.
2 程序结构
2.3 变量
常见零值:
- 数值类型:
0
- 布尔类型:
false
- 字符串类型:
''
(空字符串) - 接口或引用类型(包括
slice
, 指针,map
,chan
和函数):nil - 数组或结构体类型: 对应元素的零值
2.3.1 简短变量声明
当简短变量声明左边有变量在相同的词法域内声明过了, 则对其只有赋值行为.
需要确保简短变量声明语句必须至少声明一个新的变量.
2.3.2 指针
在 Go 语言中, 返回函数中局部变量的地址也是安全的.
flag
标准库
flag.Bool
, 创建布尔型标志参数的变量flag.String
, 创建字符串类型标志参数的变量flag.Args()
, 访问普通命令行参数flag.Parse
, 更新标志参数对应的值
示例:
1 |
|
2.3.3 new 函数
new(T)
创建一个 T 类型的匿名变量, 初始化为 T 类型的零值, 然后返回变量地址, 返回的指针类型为 *T
1 |
|
2.3.4 变量的生命周期
一个变量的有效周期只取决于是否可达.
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间.
2.5 类型
对于每一个类型 T
, 都有一个对应的类型转换操作 T(x)
, 用于将 x
转为 T
类型.
2.6 包和文件
包级别的名字, 在同一个包的其他源文件也是可以直接访问的.
每个源文件的包声明前紧跟着的注释是 包注释 , 通常, 包注释的第一句应该是包的功能概要说明, 一个包通常只有一个源文件有包注释.
2.6.1 包的导入
golang.org/x/tools/cmd/goimports
导入工具, 可以根据需要自动添加或删除导入的包.
2.6.2 包的初始化
包级变量的初始化会按照依赖顺序:
1 |
|
对于一个包中的多个 .go
源文件, Go 语言的构建工具首先会将 .go
文件根据文件名排序, 然后依次调用编译器编译.
每个包在解决依赖的前提下, 以导入声明的顺序初始化, 每个包只会被初始化一次.
main
包是最后初始化的.
2.7 作用域
不能混淆作用域和生命周期.
一个隐式的词法域 – for
的初始化部分:
1 |
|
这里声明了三个不同的 x
.
这种隐式词法域也出现在 if
, switch
等语句.
在包级别, 声明的顺序并不会影响作用域范围.
3 基础数据类型
Go 语言将数据类型分为四类:
- 基础类型, 数字, 字符串和布尔
- 复合类型, 数组和结构体
- 引用类型, 切片, 指针, 字典, 函数, 通道
- 接口类型
3.4 布尔型
用于转换的简单函数:
1 |
|
3.5 字符串
内置的 len
函数可以返回一个字符串中的字节数目, 索引操作 s[i]
返回第 i 个字节 (注意是字节而不是字符) 的字节值:
1 |
|
3.5.1 字符串面值
原生的字符串面值用反引号包裹 (不会有转义操作).
3.5.4 字符串和 Byte 切片
标准库中有四个包对字符串处理比较重要:
bytes
strings
strconv
unicode
strings.LastIndex
返回一个字符在一个字符串中最后一次出现的索引:
1 |
|
这里 index
的值为 5
3.5.5 字符串和数字的转换
由 strconv
提供可用函数.
3.6 常量
常量表达式的值在编译期计算, 而不是在运行期.
每种常量的潜在类型都是基础类型: boolean
, string
或数字.
一个常量的声明也可以包含一个类型和一个值, 如果没有显示指明类型, 则从右边的表达式推断.
3.6.1 iota 常量生成器
其用于生成一组以相似规则初始化的常量, 但是不用每行都写一遍初始化表达式. 在一个 const
声明语句中, 在第一个声明的常量所在的行, iota
将会被置为 0
, 然后在每一个有常量声明的行加一。
如:
1 |
|
3.6.2 无类型常量
声明常量时不指定类型可以获得更高的精度, 且可以直接用于更多的表达式而不需要显式的类型转换.
可以分为 6 种类型:
- 无类型布尔型
- 无类型整数
- 无类型字符
- 无类型浮点数
- 无类型复数
- 无类型字符串
对于一个没有显示类型的变量声明, 常量的形式将隐式决定变量的默认类型, 如:
1 |
|
若要给变量一个不同的类型, 需显示地将无类型的常量转化为所需的类型, 如:
1 |
|
4 复合数据类型
数组和结构体都是有固定内存大小的数据结构, 而 slice
和 map
则是动态的数据结构.
- 每个数组元素都是完全相同的类型
- 结构体是由异构的元素组成
4.1 数组
注意 [3]int
和 4[int]
是两种不同的数组类型.
数组的长度必须是常量表达式, 其在编译阶段确定.
注意, 在 Go 语言中, 数组不会被隐式当作指针. 因此传递给函数时需要显示传入一个数组指针. 如:
1 |
|
4.2 切片
一个 slice 由三个部分构成:
- 指针
- 长度
- 容量
其底层是引用一个数组对象.
指针指向第一个 slice 元素对应的底层数组元素的地址. 如:
1 |
|
打印的值相同.
长度不能超过容量. 容量一般从 slice 的开始位置到底层数据的结尾位置.
从一个数组获取的多个切片可以共享底层的数据.
如:
1 |
|
输出 Test suc
.
直接声明一个切片数组:
1 |
|
其会隐式创建一个合适大小的数组, 然后 slice 的指针指向底层的数组.
注意 , 和数组不同, slice 不能直接用 ==
比较, 因为 slice 的元素是间接引用的 (一个固定的 slice 值在不同时刻可能包含不同的元素)
可以用标准库的 bytes.Equal
函数来判断.
7 接口
7.5 接口值
接口值 (interface value) 是指接口类型的变量在运行时持有的实际值. 接口值由两部分组成: 动态类型 (dynamic type) 和动态值 (dynamic value).
对于一个接口的零值就是它的类型和值的部分都是 nil
:
10 包和工具
查看标准包:
1 |
|
为了避免冲突, 所有非标准库包的导入路径建议以所在组织的互联网域名为前缀.
如使用 bubbletea 这个包, 引入方式则为:
1 |
|
这个仓库的位置就是 https://github.com/charmbracelet/bubbletea
. (这里 tea
是别名)
注意, 用 import
引入的都是 package name, 而不是文件名. 似乎每一个目录都只会含有一个包.
以 _test.go
为后缀的源文件, 且以 _test
为后缀的包名, 都是由 go test
独立编译的外部测试文件.
导入两个具有相同包名的包的方法:
1 |
|
(给一个取新的包名)
包的匿名导入
导入一个包而不使用
1 |
|
(也叫匿名导入)
工具
GOPATH
环境变量, 用于指定当前工作目录. 这个目录下会被创建三个子目录:
src
, 存储源代码pkg
, 保存编译后的包的目标文件bin
, 保存编译后的可执行程序
GOROOT
环境变量, 指定 Go 的安装目录, 带标准库的位置. 目录结构与 GOPATH
下一致.
可以用 go env
查看所有相关环境变量.
包文档
如果一个注释之后紧跟着包声明语句, 那注释则为整个包的文档.
打开包/成员/方法的文档:
1 |
|
内部包
若一个包的导入路径包含 internal
名称, 其会被做一些特殊处理: 只能被和 internal
目录有同一个父目录的包所导入.
比如 net/http/internal/chunked
只能被:
net/http
net/http/httputil
导入.
11 测试
Go 以来 go test
测试命令以及一组按照约定方式编写的测试函数进行测试.
go test
在包目录内, 所有以 _test.go
为后缀名的源文件在执行 go build
时不会被构建成包的一部分. (其属于 go test
测试的一部分)
*_test.go
文件中有三种函数:
- 测试函数, 以
Test
为函数名前缀的函数 - 基准测试函数, 以
Benchmark
为函数名前缀的函数 - 示例函数, 以
Example
为函数名前缀的函数
go test
命令会遍历所有的 *_test.go
文件中符合上述命名规则的函数, 生成一个临时的 main 包用于调用相应的测试函数, 接着构建并运行, 报告测试结果, 清除测试中生成的临时文件.
测试函数
如:
1 |
|
注意必须导入 testing 包, t
用于报告日志信息:
1 |
|
还有类似 Printf
的 t.Errorf
. t.Fatal
和 t.Fatalf
可用于停止当前测试函数.
go test -v
用于打印每个测试函数的名字和运行时间.
参数 -run
对应一个正则表达式, 只有测试函数名正确匹配的函数才会被 go test
测试命令运行:
1 |
|
剖析
开启标志以生成分析文件:
1 |
|
示例函数
如:
1 |
|
其没有函数参数和返回值.
可用于:
- 演示函数用法, 作为文档
- 让
go test
运行
12 反射
用于在运行时更新变量和检查他们的值, 调用它们的方法和他们支持的内在操作.
12.2 reflect.Type 和 reflect.Value 两个类型
reflect.TypeOf
方法 和reflect.Type
类型
函数 reflect.TypeOf
接受任意的 interface {}
类型, 并以 reflect.Type
形式返回其动态类型, 其总是返回具体的类型:
1 |
|
fmt.Printf
用 %T
参数在内部使用 reflect.TypeOf
来输出.
reflect.ValueOf
方法 和reflect.Value
类型
函数 reflect.ValueOf
接受任意的 interface {}
类型, 并以 reflect.Value
形式返回其动态值, 也是具体类型.
1 |
|
fmt.Printf
用 %v
参数在内部使用 reflect.ValueOf
来输出.
reflect.Value.Interface
方法, 是 reflect.ValueOf
的逆操作, 返回一个 interface {}
类型, 装载与 reflect.Value
相同的具体值.
reflect.Value
类型常用的还有:
Type()
, 获取对应的reflect.Type
类型值CanAddr()
, 判断是否能取址IsValid()
, 判断其值是否有效 (空指针或未初始化为无效)Pointer()
, 返回其存储的指针Len()
, 获取其长度, 适用于数组, 切片, 映射, 字符串等类型, 其余返回 0Index(i int)
, 用于获取切片, 数组或字符串等类型索引为 i 的元素的reflect.Value
Kind()
, 返回reflect.Value
的底层类型的枚举值
方法.
12.5 通过 reflect.Value
修改值
一个变量就是一个可寻址的内存空间, 里面存储了一个值, 并且存储的值可以通过内存地址来更新.
有一些 reflect.Values
是可取地址的, 而有一些不行.
1 |
|
所有通过 reflect.ValueOf(x)
返回的 reflect.Value
都是不可取地址的 (都只是值的拷贝).
可以用 reflect.ValueOf(&x).Elem()
获取任意变量 x 对应的可取地址的 Value.
可以用 reflect.Value
的 CanAddr
方法判断其是否可以被取地址:
1 |
|
每当通过指针间接地获取的 reflect.Value
都是可取地址的, 即使开始的是一个不可取地址的 Value.
13 底层编程
使用 unsafe
包, 其由编译器实现, 提供一些访问语言内部特性的方法. 可以通过 import
导入.
13.1 unsafe.Sizeof, Alignof 和 Offsetof
这几个函数对理解原生的内存布局有帮助.
unsafe.Sizeof 函数
unsafe.Sizeof
函数返回操作数在内存中的字节大小, 参数可以是任意类型的表达式, 但其不会对表达式进行求值.
如:
1 |
|
注意, 其只会计算类型中固定部分的大小, 不会包含其中指针指向的内存大小, 示例:
1 |
|
如果使用 unsafe.Sizeof(Person{})
来计算 Person
类型的大小, 其只会计算 Name
, Age
字段以及 Ptr
指针本身的大小.
unsafe.Alignof 函数
其返回对应参数的类型需要对齐的倍数.
unsafe.Offsetof
函数
其参数必须是一个字段 x.f
, 然后返回 f
字段相对于 x
起始地址的偏移量, 包括可能的空洞.
13.2 unsafe.Pointer
unsafe.Pointer
是特别定义的一种指针类型, 类似 C 语言中的 void*
类型的指针, 可以包含任意类型变量的地址.
大多数指针类型会写成 *T
, 表示是 “一个指向 T 类型变量的指针”.
一个普通的 *T
类型指针可以被转化为 unsafe.Pointer
类型指针, 并且一个 unsafe.Pointer
类型指针也可以被转回普通的指针 (并不需要与原类型相同).
示例:
1 |
|
13.3 深度相等判断
reflect.DeepEqual
函数可以对两个值进行深度相等判断. 其支持任意的数据类型, 会递归对变量做比较操作.
reflect.DeepEqual
函数的缺陷在于, 其会将一个 nil
值的 map 和非 nil 值但是空的 map 视作不相等, 同样 nil
值的 slice 和非 nil
但是空的 slice 也视作不相等.
(可查看书中的示例函数)