“我报名参加金石计划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"}

基于月份数组创立切片,且不同切片底层或许同享一片数组空间

我开源了一个Go学习仓库|笔记分享(三)

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也愈加丰富):

我开源了一个Go学习仓库|笔记分享(三)

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,则调用方有职责正确处理它,下面介绍五种常见处理办法:

  1. 传递:
// 某func部分节选
resp, err := http,Get(url)
if err != nil {
 // 将对Get回来的err处理交给当时func的调用方
 return nil, err
}

fmt.Errorf()格式化,增加更多描绘信息,并创立一个了新的error(参阅fmt.Sprintf的格式化)

我开源了一个Go学习仓库|笔记分享(三)

当error终究被处理的时分,需要反映出其过错的调用链式关系

而且error的内容安排在一个项目中需要一致,以便于后期借助工具一致剖析

  1. 过错重试

我开源了一个Go学习仓库|笔记分享(三)

  1. 高雅关闭

假如无法处理,能够挑选高雅关闭程序,可是推荐将这步作业交给main包的程序,而库函数则挑选将error传递给其调用方。

我开源了一个Go学习仓库|笔记分享(三)

运用log.Fatalf愈加方便

我开源了一个Go学习仓库|笔记分享(三)

会默认输出error的打印时间

我开源了一个Go学习仓库|笔记分享(三)

  1. 挑选将过错打印

我开源了一个Go学习仓库|笔记分享(三)

或许输出到规范过错流

我开源了一个Go学习仓库|笔记分享(三)

  1. 少量情况下,能够挑选疏忽过错,而且假如过错挑选回来,则正确情况下省掉else,保持代码整齐

我开源了一个Go学习仓库|笔记分享(三)

EOF(End of File)

输入的时分没有更多内容则触发io.EOF,而且这个error是提早界说好的

我开源了一个Go学习仓库|笔记分享(三)

我开源了一个Go学习仓库|笔记分享(三)

5.3 作为值的函数

函数是一种类型类型,能够作为参数,而且对应变量是“引证类型”,其零值为nil,相同类型能够赋值

我开源了一个Go学习仓库|笔记分享(三)

我开源了一个Go学习仓库|笔记分享(三)

函数作为参数的比如,将一个引证类型的参数传递给多个func,能够为这个参数多次赋值(Hertz结构中运用了这种扩展性的思想)

我开源了一个Go学习仓库|笔记分享(三)

5.4 匿名函数

函数的显式声明需要在package层面,可是在函数的内部也能够创立匿名函数

我开源了一个Go学习仓库|笔记分享(三)

从上能够看出f寄存着匿名函数的引证,而且它是有状态的,保护了一个递加的x

捕获迭代变量引发的问题

正确版别

我开源了一个Go学习仓库|笔记分享(三)

过错版别

我开源了一个Go学习仓库|笔记分享(三)

一切循环内创立的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]发生越界)

我开源了一个Go学习仓库|笔记分享(三)

// 同样是越界的测验函数
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 变参函数

我开源了一个Go学习仓库|笔记分享(三)

vals此刻是一个int类型的切片,下面是不同的调用办法

我开源了一个Go学习仓库|笔记分享(三)

我开源了一个Go学习仓库|笔记分享(三)

虽然…int参数的效果与[]int很类似,可是其类型仍是不同的,变参函数经常用于字符串的格式化printf

我开源了一个Go学习仓库|笔记分享(三)

测验

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声明,则后进先出次序触发

我开源了一个Go学习仓库|笔记分享(三)

我开源了一个Go学习仓库|笔记分享(三)

defer也能够用于调试杂乱的函数(经过return一个func的方式)

我开源了一个Go学习仓库|笔记分享(三)

我开源了一个Go学习仓库|笔记分享(三)

测验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回来值:

我开源了一个Go学习仓库|笔记分享(三)

我开源了一个Go学习仓库|笔记分享(三)

此刻double(x)的成果先计算出来,后经过了defer内result += x的赋值,最终得到12

此外由于defer一般涉及到资源收回,那么假如有循环方式的资源请求,需要在循环内defer,不然或许呈现遗漏

5.7 panic(溃散)

Go的编译器已经在编译时检测了许多过错,假如Go在运转时触发如越界、空指针引证等问题,会触发panic(溃散)

panic也能够手动声明触发条件

我开源了一个Go学习仓库|笔记分享(三)

发生panic时,defer所界说的函数会触发(逆序),程序会在操控台打印panic的日志,而且打印出panic发生时的函数调用栈,用于定位过错呈现的方位

func test() {
  fmt.Println("start")
}
​
func main() {
  defer test()
  panic("panic")
}
// 成果
start
panic: panic

panic不要随意运用,虽然预查看是一个好的习惯,可是大多数情况下你无法预估runtime时过错触发的原因

我开源了一个Go学习仓库|笔记分享(三)

手动触发panic发生在一些重大的error呈现时,当然假如发生程序的溃散,应该高雅释放资源如文件io

关于panic发生时defer的逆序触发如下:

我开源了一个Go学习仓库|笔记分享(三)

我开源了一个Go学习仓库|笔记分享(三)

我开源了一个Go学习仓库|笔记分享(三)

我开源了一个Go学习仓库|笔记分享(三)

5.8 recover(康复)

panic发生时,能够经过recover关键字进行接纳(有点像反常2捕获),能够做一些资源释放,或许过错报告作业,因而能够高雅关闭系统,而不是直接溃散

我开源了一个Go学习仓库|笔记分享(三)

假如recover()在defer中被调用,则当时函数运转发生panic,会触发defer中的recover(),而且回来的是panic的相关信息,不然在其他时间调用recover()将回来nil(没有发挥recover()效果)

上图中的事例recover()接受到panic后,挑选打印panic内容,将其看作是一个过错,而不挑选中止程序运转,因而也就有了“康复”的意义

可是recover()不能无端运用,由于panic的发生,只报告过错,放任程序持续履行,往往会使得程序后续的运转呈现不可估计的问题,即使是运用recover,也只关注当时办法内的panic,而不要去考虑处理其他包的办法调用或许发生的panic,由于这更难把握程序运转的安全

因而只要少量情况下运用recover,而且的确是有这个需求,不然仍是建议触发panic的行为

我开源了一个Go学习仓库|笔记分享(三)