乘风原创程序

  • GoLang 学习笔记(六)--Effective Go(高效编程风格)(二)
  • 2021/7/2 11:36:52
  • 1. 数据(书接上回)

    1.1 map(映射)

    slice 不能用作 key,因为并没有定义两个 slice 是否相等的手段。

    1.2 string() 方法

    如果要在 string() 方法(stringer 接口)里使用 sprintf,请不要使用使用 %s 或者 字符串的 %v,因为一这又会再次调用 string() 方法。然后就会无限递归。

    1.3 append

    go 自己的 append 你是没办法自己写出来的(不过你可以写一个不那么强大的,只能 append 单个类型的函数),append 的实现需要编译器的帮助。因为 append 接收的 slice 的类型是不确定的。而在 go 中,你是无法在执行过程中改变函数的形参类型的。

    1.4 ...

    善用 ...

    2. 初始化

    1. 枚举常量可以用 iota
    2. init 函数:
      1. 每个 .go 文件都可以包含多个 init 函数,这些函数会在所有全局变量初始化结束后调用。这么设置的目的是为了表示 init 函数运行=初始化完毕。
      2. init 的作用有两个:
        1. 由于常量的值必须是常量,不能是调用函数生成的。init 函数里可以放置这些变量。
        2. 校验程序。

    3. 接口

    effective go 里又再次给接口加了一条很有用的说明,或者受定义。

    如果一个类型能够实现这些方法,那么就能够用在这里。
    

    “能够用在这里”:在某些时候表现为,就能够使用我们提供的函数。

    3.1 常用的接口

    比较常用的接口有:

    1. 世人皆知 stringer
    2. sort.interface:
      package sort
      type interface interface {
          len() int            // 获取元素数量
          less(i, j int) bool // i,j 是 index
          swap(i, j int)      // 交换
      }
      
      然后就可以用 sort.sort(<你的类型>) 排序了。
      不过实际上 sort 提供了很多类型转换函数,可以让很多类型的数据如 []int 不需要手动实现该接口就可以排序了。

    3.2 没用的小知识

    实际上是先有类型选择,再有类型断言。类型断言借鉴了类型选择的语法。

    3.3 通用性(这里解释了接口为什么要设计个可以储存值的设定)

    如果一个类型只实现了某一个接口,且这个类型并没有实现任何接口以外的方法,那就没有必要导出这个类型。
    如果是这种情况下,一个构造函数就应该返回这个接口类型的值,而不是那个未导出的类型。

    比如在 hash 这个库里,有 crc32.newieeeadler32.new两种构造函数,虽然看起来它们应该各自返回 crc32 和 adler32 相关的某个类型,但其实它们返回的是同一个 hash.hash32 的接口类型的值(这也是为什么要设计成接口可以储存所有实现这个接口方法的类型)。这样想让你的代码从使用 crc32 转成使用 adler32 就非常简单了,无需改动其他的,只需要换个构造函数即可。

    • 换个理解方式,无论是海尔洗衣机还是格力洗衣机,都是洗衣机,无论调用海尔还是格力函数,返回的都是洗衣机这个接口类型的值(这个值里可能储存海尔,也可能储存格力,但这都无所谓)。这样后续的代码就不用管到底是海尔还是格力了

    (不过说实话,这也是因为强类型语言才需要这种东西来松耦合,弱类型语言根本就不用接口就能实现这种程度的事情)

    4. 空白标识符

    除了广为人知的用法以外。还可以:

    var a = 1 // 不想这么早用,但是编译器老是报错,烦死了
    _ = a // 好了
    

    5. 内嵌

    1. 接口内嵌:就相当于多个接口的方法
    2. 类型内嵌:注意和子类型的区别
      type teacher struct {
          people *people // 正常子类型
      }
      
      type teacher struct {
          *people // 内嵌类型,不写字段名
      }
      
      var teacher teacher
      
      区别在于,使用内嵌类型后,可以直接通过 teacher.method() 类型来调用 *people 的方法。但是如果是正常子类型,就要 teacher.people.method() 来调用。好处不只是这样,想象一下接口。teacher 直接实现了 *people 所满足的接口。但是如果是正常子类型的话,就要在 teacher 上再次实现这个接口的方法(也叫接口转发),然后在这个方法里调用 *teacher.people.method(),才能实现接口转发,这样就比较麻烦。
      • 不过内部其实还是有一个隐含的字段,调用 teacher.method(),实际上还是调用内部 *people 类型的方法。
      • 而且仍旧可以通过和类型相同的字段来引用,比如上面的第二个结构体,其实还是可以通过 teacher.people 来引用的
        具体看。

    6. 并发

    并发的东西有些不好理解,请参阅。

    6.1 为什么要叫做 goroutine(go 程)

    因为要和现有术语(线程,协程,进程)区分开来。

    6.2 匿名函数

    go 中常用匿名函数来生成 goroutine。

    6.3 信道的各种用法

    1. 最基础的,通信数据
    2. 使用一个无缓冲信道,来控制同步,信道可以传一些没有意义的数据,但是通过无缓冲的特性,可以控制流程。image.png
    3. 使用缓冲信道来控制吞吐量,依旧是传输无用的数据达到控制流程的目的。下面的代码控制了同时最多只能有 maxoutstandingprocess 同时运行。image.png
    4. 可以根据 cpu 的数量,进行优化并发(可以通过 runtime.numcpu() 获取 cpu 的数量)image.png
      不过,有些时候用户会自己分配 cpu,可能会限制我们程序最大能使用多少个 cpu,为了尊重用户的选择。出现了另一个函数 numcpu = runtime.gomaxproc(0)(传入参数 0 是为了返回值),如果用户没有设置,就返回 runtime.numcpu

    6.4 并发 和 并行 的区别

    并发:用 可独立执行的组件 构建程序。
    并行:为了提高效率,同时使用多个 cpu。

    尽管运用 go 的并发特性能够在很多时候达到并行的效果。但是 go 本身只是为了并发。很多并行问题,go 并不适合。

    7. 错误

    原文。

    7.1 panic

    一般来说,出错的时候都是返回一个 err(各种 _, ok = xxx)。但是如果这是个不可恢复的错误呢?我们就是想要在出错的时候终止程序呢?
    panic 就是为此而生的,panic 会终止程序。

    panic 接收一个任意类型的参数,一般是字符串,并会在程序终止的时候打印出来。

    如果问题可以被解决,就尽量不要 panic,而是返回一个 error。除非真的之后完全进行不下去了。

    7.2 recover

    在调用 panic 的时候,程序会立刻终止,然后开始回溯 goroutine 栈,运行所有的 defer,直到到达栈顶端,最终完全终止。
    不过我们可以调用 recover 来让程序变成正常,因为此时只有 defer 能正常运行,recover 只能放在 defer 里。

    • 一个重要的作用就是,当某个 goroutine 产生 panic 的时候,不要影响其他的 goroutine,代码如下:
      image.png
      当上面的 do(work) 中调用了 panic(),那么只会停止这个 safelydo() 调用,不会影响其他的 goroutine。