“我报名参加金石计划1期应战——分割10万奖池,这是我的第1篇文章,点击查看活动详情”
前语
学习Go半年之后,我决定重新开始阅读《The Go Programing Language》,对书中涉及要点进行全面解说,这是Go言语知识查漏补缺系列的文章第三篇。
我也开源了一个Go言语的学习库房,有需要的同学能够关注,其中将收拾往期精彩文章、以及Go相关电子书等资料。
库房地址:github.com/BaiZe1998/g…
而本文的内容就是针对《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的行为