Go 语言初步
这几天认真玩起了 Go。所谓认真玩,就是拿 Go 写点程序,前后大约两千行吧。
据说 Go 的最佳开发平台是 Mac OS ,我没有。其次应该是 Linux 。Windows 版还没全部搞定,但是也可以用了。如果你用 google 搜索,很容易去到一个叫 go-windows 的开源项目上。千万别上当,这是个废弃的项目。如果你用这个,很多库都没有,而且语法也是老的。我在 Windows 下甚至不能正确链接自己写的多个 package 。活跃的 Windows 版是 gomingw ,对于 Windows 用户,装一个 mingw32 以后就可以开始玩了。
就三天来实战经历,我喜欢上这门新语言有如下原因:
mix-in 的接口风格。非常接近于我在用 C 时惯用的面向对象风格。有语法上的支持要舒服多了。以平坦的方式编写函数,没有层次。而后用 interface 把需要的功能聚合在一起。没有继承层次,只有组合功能。
强类型系统。使得犯错误的机会大大降低。正确通过编译,几乎就没有什么 bug 了。而编写程序又有点使用 lua 这种动态语言的感觉,总之,写起来很舒服。
内置的 string / slice 类型,以及 gc 。这是我觉得现代编程必须的东西。手工管理未必有更高的效率,但一定有更多的出错机会。至少,我一直主张有一个方便的 string 不变量的基本类型的(参见这一篇)。
defer 是个有趣使用的东西,用它来实现 RAII 比 C++ 利用栈上对象的析构函数的 trick 方案让人塌实多了。go 在语言设计上是很吝啬新的关键字的。但多出一个关键字 defer ,并用内建函数 panic / recover 来解决许多看似应该用 exception 解决的问题要漂亮的多。
zero 初始化。我一直觉得 C++ 的构造函数特别多余。按我用 C 的惯例,一切数据结构都应该用 0 初始化。所以 C 里有 calloc 这个函数。go 把这点贯彻了。不会再有未定义的数据。
包系统特别的好。而且严格定义了包的初始化过程,即 init 函数。在我自己的 C 语言构建的项目中,实现了几乎一样的机制,甚至也叫 init 。但是有语言层面的支持就是好。对,只有 init 没有 exit 。正合我意。
goroutine 是个相当有用的设计。8 年前,我给 C 实现了 coroutine 库,并用在项目里,并坚信,程序就应该这么写。但是没有语言级的支持,用起来还是很麻烦。goroutine 不仅简化了许多业务逻辑的编写,而且天生就是为并发编程而生的。select/chan 可能是唯一正确的并发编程的模型。Erlang 还是太小众了,而 Go 可以延用 Erlang 的模型,却有着纯正的 C 语言血统,我想会被更多人接受的。虽然 Go 依然可以用共享状态加锁的方案,但不推荐使用。chan 用习惯了,还是相当方便的。
{ 要不要独立占一行的信仰之争终于结束了。还记得前段时间有位同学来 email 指责我开源的代码没有章法。程序写的太乱。他的理由就是,我的 { 都没有独占一行。好了,争论可以结束了。在 Go 里,如果你把 { 从 if/for 语言的行末去掉,放在下一行。编译器是不会让你通过的。(除非你再加一个 ; )我很欣慰 ;)
我发现我花了四年时间锤炼自己用 C 语言构建系统的能力,试图找到一个规范,可以更好的编写软件。结果发现只是对 Go 的模仿。缺乏语言层面的支持,只能是一个拙劣的模仿。
对于有 C 基础的同学,比如我,学习 Go 毫不费力。按这篇文章的指引即可。Rob Pike 的三日教程 PPT ,我心急,用了一个下午就看完了,并且做完了练习。
不过实战编写程序还是需要反复查阅文档的。学习一门新语言,就是在学习它的各种惯用法和库。而不是去模拟熟悉的语言。我在编写代码的时候,时刻问自己,在 Go 里,通常用什么手法来处理这个问题。接下来就是不断的查询文档了。从这个意义上讲,学习新东西还是很累的。好在 Go 的各种设计都非常切合我的本意,所以自然是越写越舒服了。
至于把变量类型申明都放在后面,按 Sean 同学的话说,有种真气逆行的感觉。对我来说倒是小问题,几个小时就习惯了。反而 C 语言那种亦前亦后的方式才是奇怪呢。
说一下我的练手项目。我用 Go 重新实现了处理多连接的服务器。当然,现在的设计方案和几年前写 blog 时的方案有略微的不同。
需求是这样的:
这个服务会监停一个端口,允许外部多个连接的接入,并可以把这个连接上的数据包汇总发到后端的一个连接上。简单的说,就是一个 N 对 1 的数据处理器。把 N 个 TCP 数据流合成一个数据流。
一个服务的处理上限是 64K 的连接,使用 2 字节的 id 号区分不同的外部连接。我定义了简单的协议,每个数据片段有 3 字节的数据头。分别是数据长度一字节和 2 字节的连接 id 号。
这个服务仅仅做数据流的合并,而不规定数据逻辑上的分包。对内的数据管道上看起来的数据流就是这样的:
len idlo idhi content ... len idlo idhi content ... len idlo idhi content ...
处理合并起来的数据流非常简单,只需要通过一个 IO 管道 (可以是 socket 也可以的 stdio/stdout ,对于 Go 来说,甚至可以是一个 in-memory Pipe )这方便后端的程序不再考虑多连接的问题。
后端服务需要可以控制连接服务器。最基本的功能就是可以强制断开某个外部连接。并且可以获得新的外部连接接入或离开的信号。
更进一步,应该由后端服务器来控制连接服务器对外监听端口的开启与关闭,以及外部连接的上限等。
为了简化设计,我选择在一个特殊的内部连接(0号连接)上收发内部的控制指令。并且使用 \r\n 分割的文本协议。
用 Go 来实现这个服务非常简洁。全部我只使用了 240 行左右的 Go 代码。所有的网络连接都使用独立的 goroutine 来控制。每个都以阻塞方式处理 socket 。主循环仅仅使用一个 select ,这类似 Erlang 的事件驱动模型。
对于控制指令流,创建一个 in-memory Pipe 即可。在对内的数据流上,过滤到 id 为 0 的数据包,转发到这个 Pipe 上。使用 bufio 把 Pipe 转换成一个 bufio.Reader 接口,就可以方便的使用 ReadString 方法去读以回车分割的文本行,进而排发到解析指令的 goroutine 中,把结构化指令利用 chan 发进消息 select 循环。整个只需要不到 10 行代码。
大多数 goroutine 内部都是一个 for 循环,结束条件是和它通讯的 chan 被外部关闭。
需要稍微考虑性能的地方是给外部连接的数据包加上三字节的头转发到内部通道上。这里如果每个包都用 make 创建一个新的 array slice 会有一些内部管理上的开销。我的做法是每次申请 16k 的 array ,再创建一个 slice 去组包,如果这个 16k 的 array 没用完,就会顺着用下去。如 Go 的教程中所言,slice 的创建是很廉价的,想来也是如此。它只是对 array 的部分引用。
Go 的引用和值分得很清楚,这使它更像 C 而不是 Java ,却又提供了 C++ 提供不了的安全性。
用 Go 写网络程序,真是非常舒心。socket 和 file 在 interface 上的统一,暗合 Unix 之道。程序嘛,就是处理输入,产生输出。Reader 和 Writer 接口让人愉快。