实践过程中的一些小问题。
包管理
Golang的包管理极其玄幻,在1.11以前,依赖库均下载到GOPATH/src,安装到GOPATH/pkg。首先,难以维护多个版本,或者说对依赖库就没有版本的限定,其次,正在开发的项目也要放到GOPATH里,整个依赖树才能正常运行。
Go 1.11之后,Go modules略微改善了这个状况,在我的视角来看,现在的包管理变成:
1
2go mod init
go get xxx也不用再配置GOPATH,默认为用户目录,下载的包位置即
~/go/pkg/mod
。在项目里多了go.mod和go.sum两个文件,以控制依赖库的版本。有趣的是,不一定要先go get,在源码里直接import之后,go build会自动将依赖下载并添加到go.mod中。
go mod tidy
则用来去除多余的依赖。
切片类型
- Golang的切片类型十分有趣,从原理上讲,它是一个特殊的结构,包含指向相关数组的指针,切片长度以及切片容量。
- 切片只是底层数组的一个片段引用,比裸指针使用起来多了更多检查,不至于Segmentation fault。
- append函数,可以将一个或多个新元素追加到某个切片后,当底层数组的长度足够,那就只需要修改切片长度,并且复制元素。当底层数组cap用尽,append会分配一块新的内存,并将原有数组拷贝。很显然,这时指针会发生变化,因此,append总是以
a = append(a, x1, x2)
形式出现。
非侵入式接口
- 真正的鸭子类型,并且在编译期就做足了检查,非常美妙。
- 只要一个struct实现了某个interface要求的所有接口,那它就可以被当作interface类型,不需要声明implement。
- 感觉对代码重构提供不少便利,也不必先设计接口再实现,只管先实现一些模块,当发现方法和参数相同或接近时,再稍作调整,外部声明一个interface,就可以实现多态的用法。
if err的哲学
Golang的代码里,if err满天飞。
典型的C风格错误处理,try catch主要的好处是可以跨层抛出,将错误处理留给外层合适的函数来处理。使用return的方式抛出异常,则需要调用链上的每一层都写错误处理代码,冗余很多。
此外,Golang的函数并不声明返回的err具体包括哪些类型,往往需要付出更多代价,查阅源码或文档,才能写好错误处理。
error本身只是一个接口,里面只有一个error() string 方法,返回错误信息。官方建议是,每个错误都声明新的类型,实现error方法,上层用type-switch判断错误类型。
1
2
3
4
5
6
7
8
9
10switch t := areaIntf.(type) {
case *Square:
fmt.Printf("Type Square %T with value %v\n", t, t)
case *Circle:
fmt.Printf("Type Circle %T with value %v\n", t, t)
case nil:
fmt.Printf("nil value: nothing to check?\n")
default:
fmt.Printf("Unexpected type %T\n", t)
}哲学在于一句话:让代码更容易看出控制流走向。虽然if-err结构很多,但它也算是让代码更可读了。
epoll
- 编写网络服务的过程中,很自然地想要使用epoll去管理网络连接。随后我发现,Golang中的网络库已经结合了epoll。
- 简而言之,只需要创建多个Go routine,直接以同步阻塞的方式去编写代码。
匿名字段
- 算是对结构体的一种超级加强。
1 | type innerS struct { |
- 在结构体里直接写某一种类型名,同种类型只能写一个,这时使用
o.in2
直接访问内嵌的数据。 - 显然,会出现命名冲突的情况,首先外层地会覆盖内层,如果同级,则程序员显式地指出
o.innerS.in2
。 - 当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型继承了这些方法:将父类型放在子类型中来实现亚型。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果,也类似 Ruby 中的混入(mixin)。
一等公民:函数
- Golang中的函数也是可以当作函数参数的类型。
- 具备闭包特性。
Reference
- 强烈推荐,值得反复查阅: The way to Go
- 起步时参考的项目:Drone