GoLang 学习笔记(六)--Effective Go(高效编程风格)(二)
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. 初始化
- 枚举常量可以用 iota
- init 函数:
- 每个 .go 文件都可以包含多个 init 函数,这些函数会在所有全局变量初始化结束后调用。这么设置的目的是为了表示 init 函数运行=初始化完毕。
- init 的作用有两个:
- 由于常量的值必须是常量,不能是调用函数生成的。init 函数里可以放置这些变量。
- 校验程序。
3. 接口
effective go 里又再次给接口加了一条很有用的说明,或者受定义。
如果一个类型能够实现这些方法,那么就能够用在这里。
“能够用在这里”:在某些时候表现为,就能够使用我们提供的函数。
3.1 常用的接口
比较常用的接口有:
- 世人皆知 stringer
- 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.newieee
和 adler32.new
两种构造函数,虽然看起来它们应该各自返回 crc32 和 adler32 相关的某个类型,但其实它们返回的是同一个 hash.hash32
的接口类型的值(这也是为什么要设计成接口可以储存所有实现这个接口方法的类型)。这样想让你的代码从使用 crc32 转成使用 adler32 就非常简单了,无需改动其他的,只需要换个构造函数即可。
- 换个理解方式,无论是海尔洗衣机还是格力洗衣机,都是洗衣机,无论调用海尔还是格力函数,返回的都是洗衣机这个接口类型的值(这个值里可能储存海尔,也可能储存格力,但这都无所谓)。这样后续的代码就不用管到底是海尔还是格力了
(不过说实话,这也是因为强类型语言才需要这种东西来松耦合,弱类型语言根本就不用接口就能实现这种程度的事情)
4. 空白标识符
除了广为人知的用法以外。还可以:
var a = 1 // 不想这么早用,但是编译器老是报错,烦死了 _ = a // 好了
5. 内嵌
- 接口内嵌:就相当于多个接口的方法
- 类型内嵌:注意和子类型的区别
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 信道的各种用法
- 最基础的,通信数据
- 使用一个无缓冲信道,来控制同步,信道可以传一些没有意义的数据,但是通过无缓冲的特性,可以控制流程。
- 使用缓冲信道来控制吞吐量,依旧是传输无用的数据达到控制流程的目的。下面的代码控制了同时最多只能有
maxoutstanding
个process
同时运行。 - 可以根据 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。