2010年9月,我们介绍了Go Playground,这是一个完全由Go代码组成和返回程序运行结果的web服务器。
如果你是一位Go程序员,那你很可能已经通过阅读Go教程或执行Go文档中的示例程序的途径使用过Go Playground了。
你也可以通过点击 talks.golang.org上幻灯片中的“Run” 按钮或某个博客上的程序(比如最近一篇关于字符串的blog)而使用之.
本文我们将学习Go playground是如何实现并与其它服务整合的。其实现涉及到不同的操作系统和运行时间,这里我们假设大家用来编写Go的系统都基本相同。
playground服务有三部分:
后端程序本身很简单,所以这里我们不讨论它的实现。有趣的部分是我们如何在一个安全环境下安全地执行任意用户代码,于此同时还提供如时间、网络及文件系统等的核心功能。
为从Google的基础设施隔离用户程序,后端将它们运行在原生客户端(或“NaCl”)中,原生客户端(NaCl)—一个Google开发的技术,允许x86程序在Web浏览器中安全执行。后端使用一个能生成NaCl可执行文件的特殊版gc工具。
(这个特殊的工具将合并到Go 1.3中。想了解更多,阅读设计文档。如果你想提前体验NaCl,你可以检出一个包含所有变更的分支。)
本地客户端会限制程序占用CPU和RAM的使用量,此外还会阻止程序访问网络和文件系统。然而这会导致一个问题,Go程序的许多关键优势,比如并发和网络访问。此外访问文件系统,对于许多程序也是至关重要的。我们需要时间功能,才展现高效的并发性能。显然我们需要网络和文件系统,才能显示出来访问网络和文件系统方面的优势。
尽管现在这些功能都被支持了,但是2010年发布的第一版playground时,没有一项被支持的。当前时间功能是在2009年11月10的被支持的,可是time.Sleep却不能使用,而且多数与系统和网络有关的包都不被支持的
一年后,我们在playground上面实现了一个伪时间,这才使得程序可以有个正确的休眠行为。较新的playground更新引入了伪网络和伪文件系统,这使得playground的工具链与正常的Go工具链相同。这些新引入的功能会在下面具体阐述。
playground里面的程序可用CPU时间和内存都是有限的。除此以外程序实际使用时间也是有限制的。这是因为每个运行在playground的程序都消耗着后台资源,以及占据客户端和后台间的基础设施。限制每个程序的运行时间让我们的维护更加可遇见,而且可以保护我们免受拒绝服务攻击。
但是当程序使用时间功能函数的时候,这些限制将变得非常不合适。在Go Concurrency Patterns 讲话中通过一个例子来演示这个糟糕的问题。这是一个使用时间功能函数比如time.Sleep和time.After的例子程序,当运行在早期的playground中时,这些程序的休眠会失效而且行为很奇怪(有时甚至出现错误)
通过使用一个高明的小把戏,我们可以使得Go程序认为它是在休眠,而实际上这个休眠没有花费任何时间。在介绍这个小把戏之前,我们需要了解调度程序是管理goroutine的休眠的原理。
当一个goroutine调用time.Sleep(或者其他相似函数),调度器会在挂起的计时器堆中添加中增加一个计时器,并让goroutine休眠。在这期间,一个特殊的goroutine计算器管理着这个堆。当这个特殊的goroutine计算器开始工作时,首先,它告诉调度器,当堆中的下一个挂起的计时器准备计时的时候唤醒自己,然后它自己就开始休眠了。当这个特殊计时器被唤醒后首先是检测是否有计时器超时了,如果有那么就唤醒相应的goroutine,然后又回到休眠状态。
明白了这个原理后,那个小把戏只是改变唤醒goroutine的计时器的条件。调度器并不是经过一段时间后进行唤醒,而且仅仅等待一个所有goroutines 都阻塞的死锁产生后就进行唤醒。
其它翻译版本 (1) 加载中playground运行时版本中维护着一个内部时钟。当修改后的调度器检测到一个死锁,那么它将检查是否有一些挂起的计时器。如果有的话,它会将内部时钟的时间调整到最早计时器的促发时间,然后唤醒goroutine计时器。这样一直循环往复,程序都认为时间过去了,而实际上休眠几乎没有耗时。
这些调度器的改变细节详见 proc.c 和 time.goc。
伪时间解决了后台资源耗尽的问题,但是程序的输出该怎么办呢?看见一个在休眠的程序,却几乎不耗时地正确完成工作了,这是得多么的奇怪啊!
下面的程序每秒输出当前时间,然后三秒后退出.试着运行一下。
func main() { stop := time.After(3 * time.Second) tick := time.NewTicker(1 * time.Second) defer tick.Stop() for { select { case <-tick.C: fmt.Println(time.Now()) case <-stop: return } } }
这是如何做到的? 这其实是后台、前端和客户端合作的结果。
我们捕获到每次向标准输出和标准错误输出的时间,并把这个时间提供给客户端。那么客户端就可以以正确的时间间隔输出,以至于这个输出就像是本地程序输出的一样。
playground的运行环境包提供了一个在每个写入数据之前引入一个小“回放头”的特殊写函数,它。回放头中包含一个逻辑字符,当前时间,要写入数据长度。一个写操作的回放头结构如下:
0 0 P B <8-byte time> <4-byte data length> <data>
这个程序的原始输出类似这样:
x00x00PBx11x74xefxedxe6xb3x2ax00x00x00x00x1e2009-11-10 23:00:01 +0000 UTC x00x00PBx11x74xefxeex22x4dxf4x00x00x00x00x1e2009-11-10 23:00:02 +0000 UTC x00x00PBx11x74xefxeex5dxe8xbex00x00x00x00x1e2009-11-10 23:00:03 +0000 UTC
前端将这些输出解析为一系列事件并返回给客户端一个事件列表的JSON对象:
{ "Errors": "", "Events": [ { "Delay": 1000000000, "Message": "2009-11-10 23:00:01 +0000 UTC " }, { "Delay": 1000000000, "Message": "2009-11-10 23:00:02 +0000 UTC " }, { "Delay": 1000000000, "Message": "2009-11-10 23:00:03 +0000 UTC " } ] }
JavaScript客户端(在用户的Web浏览器中运行的)然后使用提供的延迟间隔回放这个事件。对用户来说看起来程序是在实时运行。
在Go本地客户端(NaCl)的工具链上构建的程序,是不能访问本地机器的文件系统的。为了解决这个问题syscall包中有个文件访问的函数(Open, Read, Write等等)都是操作在一个内存文件系统上的。这个内存文件系统是由syscall包自身实现的。既然syscall包是一个Go代码与操作系统内存间的一个接口,那么用户程序会将这个伪文件系统会和一个真实的文件系统一个样看待。
下面的示例程序将数据写入一个文件,让后复制内容到标准输出。试着运行一下(你也可以进行编辑)
func main() { const filename = "/tmp/file.txt" err := ioutil.WriteFile(filename, []byte("Hello, file system "), 0644) if err != nil { log.Fatal(err) } b, err := ioutil.ReadFile(filename) if err != nil { log.Fatal(err) } fmt.Printf("%s", b) }当一个进程开始,这个伪文件系统加入/dev目录下的设备和一个/tmp空目录。那么程序可以对这个文件系统和平常一样进行操作,但是进程退出后,所有对文件系统的改变将会丢失 本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务