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

4,751 阅读12分钟

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

基于月份数组创建切片,且不同切片底层可能共享一片数组空间

image-20220829102017677

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

image-20220829112405398

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的格式化)

image-20220901132429146

当error最终被处理的时候,需要反映出其错误的调用链式关系

并且error的内容组织在一个项目中需要统一,以便于后期借助工具统一分析

  1. 错误重试

image-20220902104510073

  1. 优雅关闭

如果无法处理,可以选择优雅关闭程序,但是推荐将这步工作交给main包的程序,而库函数则选择将error传递给其调用方。

image-20220902110047619

使用log.Fatalf更加方便

image-20220902110156627

会默认输出error的打印时间

image-20220902110305377

  1. 选择将错误打印

image-20220902111135436

或者输出到标准错误流

image-20220902111207116

  1. 少数情况下,可以选择忽略错误,并且如果错误选择返回,则正确情况下省略else,保持代码整洁

image-20220902111443372

EOF(End of File)

输入的时候没有更多内容则触发io.EOF,并且这个error是提前定义好的

image-20220902113648721

image-20220902113508658

5.3 作为值的函数

函数是一种类型类型,可以作为参数,并且对应变量是“引用类型”,其零值为nil,相同类型可以赋值

image-20220902114434886

image-20220902115114210

函数作为参数的例子,将一个引用类型的参数传递给多个func,可以为这个参数多次赋值(Hertz框架中使用了这种扩展性的思想)

image-20220902115541517

5.4 匿名函数

函数的显式声明需要在package层面,但是在函数的内部也可以创建匿名函数

image-20220902131528698

从上可以看出f存放着匿名函数的引用,并且它是有状态的,维护了一个递增的x

捕获迭代变量引发的问题

正确版本

image-20220902140757639

错误版本

image-20220902140812965

所有循环内创建的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]发生越界)

image-20220902143256855

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

image-20220903094436855

vals此时是一个int类型的切片,下面是不同的调用方式

image-20220903094623144

image-20220903094637593

虽然...int参数的作用与[]int很相似,但是其类型还是不同的,变参函数经常用于字符串的格式化printf

image-20220903094932334

测试

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

image-20220903101447485

image-20220903101507456

defer也可以用于调试复杂的函数(通过return一个func的形式)

image-20220903102940792

image-20220903102951317

测试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返回值:

image-20220903105051750

image-20220903105142699

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

此外因为defer一般涉及到资源回收,那么如果有循环形式的资源申请,需要在循环内defer,否则可能出现遗漏

5.7 panic(崩溃)

Go的编译器已经在编译时检测了许多错误,如果Go在运行时触发如越界、空指针引用等问题,会触发panic(崩溃)

panic也可以手动声明触发条件

image-20220903111918285

发生panic时,defer所定义的函数会触发(逆序),程序会在控制台打印panic的日志,并且打印出panic发生时的函数调用栈,用于定位错误出现的位置

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

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

image-20220903112652702

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

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

image-20220903113338468

image-20220903113346564

image-20220903113510294

image-20220903113535471

5.8 recover(恢复)

panic发生时,可以通过recover关键字进行接收(有点像异常2捕获),可以做一些资源释放,或者错误报告工作,因此可以优雅关闭系统,而不是直接崩溃

image-20220903131051578

如果recover()在defer中被调用,则当前函数运行发生panic,会触发defer中的recover(),并且返回的是panic的相关信息,否则在其他时刻调用recover()将返回nil(没有发挥recover()作用)

上图中的案例recover()接受到panic后,选择打印panic内容,将其看作是一个错误,而不选择停止程序运行,因此也就有了“恢复”的含义

但是recover()不能无端使用,因为panic的发生,只报告错误,放任程序继续执行,往往会使得程序后续的运行出现不可预计的问题,即使是使用recover,也只关注当前方法内的panic,而不要去考虑处理其他包的方法调用可能产生的panic,因为这更难把握程序运行的安全性

因此只有少数情况下使用recover,并且确实是有这个需求,否则还是建议触发panic的行为

image-20220903132912421