“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情”
前言
学习Go半年之后,我决定重新开始阅读《The Go Programing Language》,对书中涉及重点进行全面讲解,这是Go语言知识查漏补缺系列的文章第三篇。
我也开源了一个Go语言的学习仓库,有需要的同学可以关注,其中将整理往期精彩文章、以及Go相关电子书等资料。
而本文的内容就是针对《The Go Programing Language》第四、五章的整理,预计会用一个多月的时间完成这份笔记的更新。
区别于连篇累牍,我希望这份笔记是详略得当的,可能更适合一些对Go有着一些使用经验,但是由于是转语言或者速食主义者,对Go的许多知识点并未理解深刻(与我一般),笔记中虽然会带有一些个人的色彩,但是Go语言的重点我将悉数讲解。
再啰嗦一句:笔记中讲述一个知识点的时候有时并非完全讲透,或是浅尝辄止,或是抛出疑问却未曾解答。希望你可以接受这种风格,而有些知识点后续涉及到后续章节,当前未过分剖析,也会在后面进行更深入的讲解。
最后,如果遇到错误,或者你认为值得改进的地方,也很欢迎你评论或者联系我进行更正,又或者你也可以直接在仓库中提issue或者pr,或许这也是小小的一次“开源”。
四、复合类型
4.1 数组
长度不可变,如果两个数组类型是相同的则可以进行比较,且只有完全相等才会为true
a := [...]int{1, 2} // 数组的长度由内容长度确定
b := [2]int{1, 2}
c := [3]int{1, 2}
4.2 切片
切片由三部分组成:指针、长度(len)、容量(cap)
切片可以通过数组创建
// 创建月份数组
months := [...]string{1:"January", 省略部分内容, 12: "December"}
基于月份数组创建切片,且不同切片底层可能共享一片数组空间
fmt.Println(summer[:20]) // panic: out of range
endlessSummer := summer[:5] // 如果未超过summer的cap,则会扩展slice的len
fmt.Println(endlessSummer) // "[June July August September October]"
[]byte切片可以通过对字符串使用类似上述操作的方式获取
切片之间不可以使用==进行比较,只有当其判断是否为nil才可以使用
切片的zero value是nil,nil切片底层没有分配数组,nil切片的len和cap都为0,但是非nil切片的len和cap也可以为0(Go中len == 0的切片处理方式基本相同)
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 != nil
The append Function
使用append为slice追加内容,如果cap == len,则会触发slice扩容,下面是一个帮助理解的例子(使用了2倍扩容,并非是Go内置的append处理流程,那将会更加精细,api也更加丰富):
4.3 映射
map(hash table) — 无序集合,key必须是可以比较的(除了浮点数,这不是一个好的选择)
x := make(map[string]int)
y := map[string]int{
"alice": 12,
"tom": 34
}
z := map[string]int{}
// 内置函数
delete(y, "alice")
对map的元素进行取地址并不是一个好的注意,因为map的扩容过程中可能伴随着rehash,导致地址发生变化(那么map的扩容规则?)
ages["carol"] = 21 // panic if assignment to entry in nil map
// 判断key-value是否存在的方式
age, ok := ages["alice"]
if age, ok := ages["bob"]; !ok {
...
}
4.4 结构体
type Point struct {
x, y int
}
type Circle struct {
center Point
radius int
}
type Wheel struct {
circle Circle
spokes int
}
w := Wheel{Circle{Point{8, 8}, 5}, 20}
w := Wheel{
circle: Circle{
center: Point{x: 8, y: 8},
radius: 5,
},
spokes: 20,
}
4.5 JSON
// 将结构体转成存放json编码的byte切片
type Movie struct {
Title string
Year int `json:"released"` // 重定义json属性名称
Color bool `json:"color,omitempty"` // 如果是空值则转成json时忽略
}
data, err := json.Marshal(movie)
data2, err := json.MarshalIndent(movie, "", " ")
// 输出结果
{"Title":"s","released":1,"color":true}
{
"Title": "s",
"released": 1,
"color": true
}
// json解码
content := Movie{}
json.Unmarshal(data, &content)
fmt.Println(content)
4.6 文本和HTML模板
略
五、方法
5.1 方法声明
// 可以提前声明返回值z
func add(x, y int) (z int) {
z = x-y
return
}
如果两个方法的参数列表和返回值列表相同,则称之为拥有相同类型(same type)
参数是值拷贝,但是如果传入的参数是:slice、pointer、map、function,channel虽然是值拷贝,但是也是引用类型的值,会对其指向的值做出相应变更
你可能会遇到查看某些go的内置func源码的时候它没有声明func的body部分,例如append方法
// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type
事实上append在代码编译的时候,被替换成runtime.growslice以及相关汇编指令了(可以输出汇编代码查看细节),你可以在go的runtime包中找到相关实现,如下:
// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if asanenabled {
asanread(old.array, uintptr(old.len*int(et.size)))
}
// 省略...
}
声明函数时指定返回值的名称,可以在return时省略
func add(x, y int) (z int, err error) {
data, err := deal(x, y)
if err != nil {
return // 此时等价于return 0, nil
}
// 这里是赋值而不是声明,因为在返回值列表中声明过了
z = x+y
return // 此时等价于return z, nil
}
5.2 错误
error是一个接口,因此可以自定义实现error
type error interface {
Error() string
}
如果一个函数执行失败时需要返回的行为很单一可以通过bool来控制
func test(a int) (y int, ok bool) {
x, ok := test1(a)
if !ok {
return
}
y = x*x
return
}
更多情况下,函数处理时可能遇到多种类型的错误,则使用error,可以通过判断err是否为nil判断是否发生错误
func test(a int) (y int, err error) {
x, err := test1(a)
if err != nil {
return
}
y = x*x
return
}
// 打印错误的值
fmt.Println(err)
fmt.Printf("%v", err)
Go通过if和return的机制手动返回错误,使得错误的定位更加精确,并且促使你更早的去处理这些错误(而不是像其他语言一样选择抛出异常,可能使得异常由于调用栈的深入,导致最终处理不便)
错误处理策略
一个func的调用返回了err,则调用方有责任正确处理它,下面介绍五种常见处理方式:
- 传递:
// 某func部分节选
resp, err := http,Get(url)
if err != nil {
// 将对Get返回的err处理交给当前func的调用方
return nil, err
}
fmt.Errorf()格式化,添加更多描述信息,并创建一个了新的error(参考fmt.Sprintf的格式化)
当error最终被处理的时候,需要反映出其错误的调用链式关系
并且error的内容组织在一个项目中需要统一,以便于后期借助工具统一分析
- 错误重试
- 优雅关闭
如果无法处理,可以选择优雅关闭程序,但是推荐将这步工作交给main包的程序,而库函数则选择将error传递给其调用方。
使用log.Fatalf更加方便
会默认输出error的打印时间
- 选择将错误打印
或者输出到标准错误流
- 少数情况下,可以选择忽略错误,并且如果错误选择返回,则正确情况下省略else,保持代码整洁
EOF(End of File)
输入的时候没有更多内容则触发io.EOF,并且这个error是提前定义好的
5.3 作为值的函数
函数是一种类型类型,可以作为参数,并且对应变量是“引用类型”,其零值为nil,相同类型可以赋值
函数作为参数的例子,将一个引用类型的参数传递给多个func,可以为这个参数多次赋值(Hertz框架中使用了这种扩展性的思想)
5.4 匿名函数
函数的显式声明需要在package层面,但是在函数的内部也可以创建匿名函数
从上可以看出f存放着匿名函数的引用,并且它是有状态的,维护了一个递增的x
捕获迭代变量引发的问题
正确版本
错误版本
所有循环内创建的func捕获并共享了dir变量(相当于引用类型),所以创建后rmdirs切片内所有元素都有同一个dir,而不是每个元素获得dir遍历时的中间状态
因此正确版本中dir := d的操作为遍历的dir申请了新的内存存放
func main() {
arr := []int{1, 2, 3, 4, 5}
temp := make([]func(), 0)
for _, value := range arr {
temp = append(temp, func() {
fmt.Println(value)
})
}
for i := range temp {
temp[i]()
}
}
// 结果
5
5
5
5
5
另一种错误版本(i最终达到数组长度上界后结束循环,并且导致dirs[i]发生越界)
// 同样是越界的测试函数
func main() {
arr := []int{1, 2, 3, 4, 5}
temp := make([]func(), 0)
for i := 0; i < 5; i++ {
temp = append(temp, func() {
fmt.Println(arr[i])
})
}
for i := range temp {
temp[i]()
}
}
// 结果
panic: runtime error: index out of range [5] with length 5
以上捕获迭代变量引发的问题容易出现在延迟了func执行的情况下(先完成循环创建func、后执行func)
5.5 变参函数
vals此时是一个int类型的切片,下面是不同的调用方式
虽然...int参数的作用与[]int很相似,但是其类型还是不同的,变参函数经常用于字符串的格式化printf
测试
func test(arr ...int) int {
arr[0] = 5
sum := 0
for i := 0; i < len(arr); i++ {
sum += arr[i]
}
return sum
}
func main() {
arr := []int{1, 2, 3, 4, 5}
fmt.Println(test(arr...))
fmt.Println(arr)
}
// 切片确实被修改了
19
[5 2 3 4 5]
5.6 延后函数调用
defer通常用于资源的释放,对应于(open&close|connect&disconnect|lock&unlock)
defer最佳实践是在资源申请的位置紧跟使用,defer在当前函数return之前触发,如果有多个defer声明,则后进先出顺序触发
defer也可以用于调试复杂的函数(通过return一个func的形式)
测试1:
func test() func() {
fmt.Println("start")
defer func() {
fmt.Println("test-defer")
}()
return func() {
fmt.Println("end")
}
}
func main() {
defer test()()
fmt.Println("middle")
}
// 输出
start
test-defer
middle
end
可以观察到test()()分为两步执行,start在defer声明处打印,end在main函数return前打印,并且test内定义的defer也在test函数return前打印test-defer
此时start和end包围了main函数,因此可以用这种方式调试一些复杂函数,如统计执行时间
测试2:
func test() func() {
fmt.Println("start")
defer func() {
fmt.Println("test-defer")
}()
return func() {
fmt.Println("end")
}
}
func main() {
defer test()
fmt.Println("middle")
}
// 输出
middle
start
test-defer
此时将test()()改为test(),则未触发test打印end,并且先执行了打印middle
另一个特性:defer可以修改return返回值:
此时double(x)的结果先计算出来,后经过了defer内result += x的赋值,最后得到12
此外因为defer一般涉及到资源回收,那么如果有循环形式的资源申请,需要在循环内defer,否则可能出现遗漏
5.7 panic(崩溃)
Go的编译器已经在编译时检测了许多错误,如果Go在运行时触发如越界、空指针引用等问题,会触发panic(崩溃)
panic也可以手动声明触发条件
发生panic时,defer所定义的函数会触发(逆序),程序会在控制台打印panic的日志,并且打印出panic发生时的函数调用栈,用于定位错误出现的位置
func test() {
fmt.Println("start")
}
func main() {
defer test()
panic("panic")
}
// 结果
start
panic: panic
panic不要随意使用,虽然预检查是一个好的习惯,但是大多数情况下你无法预估runtime时错误触发的原因
手动触发panic发生在一些重大的error出现时,当然如果发生程序的崩溃,应该优雅释放资源如文件io
关于panic发生时defer的逆序触发如下:
5.8 recover(恢复)
panic发生时,可以通过recover关键字进行接收(有点像异常2捕获),可以做一些资源释放,或者错误报告工作,因此可以优雅关闭系统,而不是直接崩溃
如果recover()在defer中被调用,则当前函数运行发生panic,会触发defer中的recover(),并且返回的是panic的相关信息,否则在其他时刻调用recover()将返回nil(没有发挥recover()作用)
上图中的案例recover()接受到panic后,选择打印panic内容,将其看作是一个错误,而不选择停止程序运行,因此也就有了“恢复”的含义
但是recover()不能无端使用,因为panic的发生,只报告错误,放任程序继续执行,往往会使得程序后续的运行出现不可预计的问题,即使是使用recover,也只关注当前方法内的panic,而不要去考虑处理其他包的方法调用可能产生的panic,因为这更难把握程序运行的安全性
因此只有少数情况下使用recover,并且确实是有这个需求,否则还是建议触发panic的行为