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. 使用一个无缓冲信道,来控制同步,信道可以传一些没有意义的数据,但是通过无缓冲的特性,可以控制流程。
  3. 使用缓冲信道来控制吞吐量,依旧是传输无用的数据达到控制流程的目的。下面的代码控制了同时最多只能有 maxoutstandingprocess 同时运行。
  4. 可以根据 cpu 的数量,进行优化并发(可以通过 runtime.numcpu() 获取 cpu 的数量)
    不过,有些时候用户会自己分配 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,代码如下:

    当上面的 do(work) 中调用了 panic(),那么只会停止这个 safelydo() 调用,不会影响其他的 goroutine。