這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文:Inside the Go Playground
簡介 2010年9月,我們介紹了Go Playground,這是一個完全由Go程式碼群組成和返回程式運行結果的web伺服器。 如果你是一位Go程式員,那你很可能已經通過閱讀Go教程或執行Go文檔中的樣本程式的途徑使用過Go Playground了。 你也可以通過點擊 talks.golang.org上投影片中的“Run” 按鈕或某個部落格上的程式(比如最近一篇關於字串的blog)而使用之. 本文我們將學習Go playground是如何?並與其它服務整合的。其實現涉及到不同的作業系統和已耗用時間,這裡我們假設大家用來編寫Go的系統都基本相同。 |
cmy00cmy 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
概覽 playground服務有三部分:
- 一個運行於Google服務之上的後端。它接收RPC請求,使用gc工具編譯使用者程式,執行,並將程式的輸出(或編譯錯誤)作為RPC響應返回。
- 一個運行在GAE上的前端。它接收來自用戶端的HTTP請求並產生相應的RPC請求到後端。它也做一些緩衝。
- 一個JavaScript用戶端實現的使用者介面,並產生到前端的HTTP請求。
|
Garfielt 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
後端 後端程式本身很簡單,所以這裡我們不討論它的實現。有趣的部分是我們如何在一個安全環境下安全地執行任意使用者代碼,於此同時還提供如時間、網路及檔案系統等的核心功能。 為從Google的基礎設施隔離使用者程式,後端將它們運行在原生用戶端(或“NaCl”)中,原生用戶端(NaCl)—一個Google開發的技術,允許x86程式在Web瀏覽器中安全執行。後端使用一個能產生NaCl可執行檔的特殊版gc工具。 (這個特殊的工具將合并到Go 1.3中。想瞭解更多,閱讀設計文檔。如果你想提前體驗NaCl,你可以檢出一個包含所有變更的分支。) |
Garfielt 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
本地用戶端會限制程式佔用CPU和RAM的使用量,此外還會阻止程式訪問網路和檔案系統。然而這會導致一個問題,Go程式的許多關鍵優勢,比如並發和網路訪問。此外訪問檔案系統,對於許多程式也是至關重要的。我們需要時間功能,才展現高效的並發效能。顯然我們需要網路和檔案系統,才能顯示出來訪問網路和檔案系統方面的優勢。 儘管現在這些功能都被支援了,但是2010年發布的第一版playground時,沒有一項被支援的。目前時間功能是在2009年11月10的被支援的,可是time.Sleep卻不能使用,而且多數與系統和網路有關的包都不被支援的 一年後,我們在playground上面實現了一個偽時間,這才使得程式可以有個正確的休眠行為。較新的playground更新引入了偽網路和偽檔案系統,這使得playground的工具鏈與正常的Go工具鏈相同。這些新引入的功能會在下面具體闡述。 |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
偽時間 playground裡面的程式可用CPU時間和記憶體都是有限的。除此以外程式實際使用時間也是有限制的。這是因為每個運行在playground的程式都消耗著後台資源,以及佔據用戶端和後台間的基礎設施。限制每個程式的已耗用時間讓我們的維護更加可遇見,而且可以保護我們免受拒絕服務的攻擊。 但是當程式使用時間功能函數的時候,這些限制將變得非常不合適。在Go Concurrency Patterns 講話中通過一個例子來示範這個糟糕的問題。這是一個使用時間功能函數比如time.Sleep和time.After的例子程式,當運行在早期的playground中時,這些程式的休眠會失效而且行為很奇怪(有時甚至出現錯誤) |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
通過使用一個高明的小把戲,我們可以使得Go程式認為它是在休眠,而實際上這個休眠沒有花費任何時間。在介紹這個小把戲之前,我們需要瞭解發送器是管理goroutine的休眠的原理。 當一個goroutine調用time.Sleep(或者其他相似函數),調度器會在掛起的計時器堆中添加中增加一個計時器,並讓goroutine休眠。在這期間,一個特殊的goroutine計算機管理著這個堆。當這個特殊的goroutine計算機開始工作時,首先,它告訴調度器,當堆中的下一個掛起的計時器準備計時的時候喚醒自己,然後它自己就開始休眠了。當這個特殊計時器被喚醒後首先是檢測是否有計時器逾時了,如果有那麼就喚醒相應的goroutine,然後又回到休眠狀態。 明白了這個原理後,那個小把戲只是改變喚醒goroutine的計時器的條件。調度器並不是經過一段時間後進行喚醒,而且僅僅等待一個所有goroutines 都阻塞的死結產生後就進行喚醒。 |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
其它翻譯版本(1) |
正在載入...
playground運行時版本中維護著一個內部時鐘。當修改後的調度器檢測到一個死結,那麼它將檢查是否有一些掛起的計時器。如果有的話,它會將內部時鐘的時間調整到最早計時器的促發時間,然後喚醒goroutine計時器。這樣一直迴圈往複,程式都認為時間過去了,而實際上休眠幾乎沒有耗時。 這些調度器的改變細節詳見 proc.c 和 time.goc。 偽時間解決了後台資源耗盡的問題,但是程式的輸出該怎麼辦呢?看見一個在休眠的程式,卻幾乎不耗時地正確完成工作了,這是得多麼的奇怪啊! |
Mitisky 翻譯於 10 個月 前 0人頂 頂 翻譯的不錯哦! |
下面的程式每秒輸出目前時間,然後三秒後退出.試著運行一下。 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 } }} 這是如何做到的? 這其實是後台、前端和用戶端合作的結果。 我們捕獲到每次向標準輸出和標準錯誤輸出的時間,並把這個時間提供給用戶端。那麼用戶端就可以以正確的時間間隔輸出,以至於這個輸出就像是本地程式輸出的一樣。 |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
playground的運行環境包提供了一個在每個寫入資料之前引入一個小“回放頭”的特殊寫函數,它。回放頭中包含一個邏輯字元,目前時間,要寫入資料長度。一個寫操作的回放頭結構如下: 0 0 P B <8-byte time> <4-byte data length> 這個程式的原始輸出類似這樣: \x00\x00PB\x11\x74\xef\xed\xe6\xb3\x2a\x00\x00\x00\x00\x1e2009-11-10 23:00:01 +0000 UTC\x00\x00PB\x11\x74\xef\xee\x22\x4d\xf4\x00\x00\x00\x00\x1e2009-11-10 23:00:02 +0000 UTC\x00\x00PB\x11\x74\xef\xee\x5d\xe8\xbe\x00\x00\x00\x00\x1e2009-11-10 23:00:03 +0000 UTC 前端將這些輸出解析為一系列事件並返回給用戶端一個事件列表的JSON對象: { "Errors": "", "Events": [ { "Delay": 1000000000, "Message": "2009-11-10 23:00:01 +0000 UTC\n" }, { "Delay": 1000000000, "Message": "2009-11-10 23:00:02 +0000 UTC\n" }, { "Delay": 1000000000, "Message": "2009-11-10 23:00:03 +0000 UTC\n" } ]} JavaScript用戶端(在使用者的Web瀏覽器中啟動並執行)然後使用提供的延遲間隔回放這個事件。對使用者來說看起來程式是在即時運行。 |
Garfielt 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
偽檔案系統 在Go本地用戶端(NaCl)的工具鏈上構建的程式,是不能訪問本地機器的檔案系統的。為瞭解決這個問題syscall包中有個檔案訪問的函數(Open, Read, Write等等)都是操作在一個記憶體檔案系統上的。這個記憶體檔案系統是由syscall包自身實現的。既然syscall包是一個Go代碼與作業系統記憶體間的一個介面,那麼使用者程式會將這個偽檔案系統會和一個真實的檔案系統一個樣看待。 下面的樣本程式將資料寫入一個檔案,讓後複製內容到標準輸出。試著運行一下(你也可以進行編輯) func main() { const filename = "/tmp/file.txt" err := ioutil.WriteFile(filename, []byte("Hello, file system\n"), 0644) if err != nil { log.Fatal(err) } b, err := ioutil.ReadFile(filename) if err != nil { log.Fatal(err) } fmt.Printf("%s", b)} 當一個進程開始,這個偽檔案系統加入/dev目錄下的裝置和一個/tmp空目錄。那麼程式可以對這個檔案系統和平常一樣進行操作,但是進程退出後,所有對檔案系統的改變將會丟失 |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
在初始化的時候,可以上傳zip壓縮檔(詳見unzip_nacl.go)迄今為止只會在進行標準庫測試的時候,我們會使用解壓縮工具來提供測試資料檔案。可是我們打算playground程式可以運行文檔樣本、部落格文章和Golang的教程裡面的資料。 具體實現詳見 fs_nacl.go 和 fd_nacl.go檔案(由於是_nacl的尾碼,所以只有當GOOS被設定為nacl時候,這些檔案才會被加入到syscall包中)。 這個偽檔案系統由fsys struct代表。其中一個全域執行個體(稱為fs)在初始化的時候被建立。各種和檔案有關的函數都操作在fs上,而不是進行真實的系統調用。例如,這裡有個syscall.Open函數: func Open(path string, openmode int, perm uint32) (fd int, err error) { fs.mu.Lock() defer fs.mu.Unlock() f, err := fs.open(path, openmode, perm&0777|S_IFREG) if err != nil { return -1, err } return newFD(f), nil} |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
檔案描述符被一個稱為files的全域片段記錄著。每個檔案描述符對應著一個file,而且每個file都會提供一 fileImpl介面的實現。這裡有幾個介面的實現:
- fsysFile代表常規檔案和裝置 (such as/dev/random) ,
- 標準輸入輸出和標準錯誤都是naclFile的執行個體,這可以使用系統調用來操作真實檔案(這是playground中的程式唯一訪問外部環境的途徑,
- 網路通訊端有著自己的實現,下面章節中會討論.
偽網路訪問 和檔案系統一樣,playground的網路堆棧是由syscall包在進程內部類比出來的,這可以讓playground項目使用傳回位址(127.0.0.1)。但不能請求其他主機。 |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
運行下面可執行檔執行個體代碼。這個程式首先會監聽TCP的連接埠,接著等待串連的到來,然後將串連傳來的資料複製到標準輸出,最後程式退出。在另外一個goroutine中,他會串連那個監聽中的連接埠,然後向串連裡面寫入資料,最後關閉。 func main() { l, err := net.Listen("tcp", "127.0.0.1:4000") if err != nil { log.Fatal(err) } defer l.Close() go dial() c, err := l.Accept() if err != nil { log.Fatal(err) } defer c.Close() io.Copy(os.Stdout, c)}func dial() { c, err := net.Dial("tcp", "127.0.0.1:4000") if err != nil { log.Fatal(err) } defer c.Close() c.Write([]byte("Hello, network\n"))} 網路的介面比檔案要複雜的多,所以偽網路的介面的實現會比偽檔案系統的要龐大和複雜的多。偽網路必須類比讀和寫的逾時,以及處理不同地址類型和協議等等。 具體實現詳見net_nacl.go。推薦從netFile開始閱讀,因為這是網路通訊端對於fileImpl介面的實現。 |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
前端 playground的前端是另外一個簡單的程式 (不到100行). 它的主要功能是接受用戶端的HTTP請求,然後向後台發出對應的RPC請求,同時還會完成一些緩衝工作。 前端提供一個HTTP處理常式,詳見http://golang.org/compile。這個處理常式接受帶有body標籤(其中包含要啟動並執行Go程式碼)和一個可選version標籤(多數用戶端應該是‘2’)的POST請求。 當前端收到一個HTTP編譯請求的時候,它首先查看緩衝,檢查之前是否有過同樣的編譯請求。如果發現存在同,那麼就會將緩衝的響應直接返回。緩衝可以防止像Go首頁上那樣的福士化程式讓後台過載。如果發現該請求之前沒有被緩衝過,那麼前端會向後台發出相應的RPC請求,然後緩衝背景響應,接著分析對應的事件回放(詳見偽時間),最後通過HTTP響應將JSON格式的對象返回到用戶端(像上面描述那樣)。 |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
用戶端 各種使用playground的網站,共用著一些同樣的Javascript代碼來搭建使用者提供者(代碼視窗和輸出視窗,運行按鈕等等),通過這些介面來後playground前端互動。 具體實現在go.tool資產庫的playground.js檔案中,可以通過go.tools/godoc/static包來匯入。 其中一些代碼較為簡潔,也有一些比較繁雜, 因為這是由幾個不同的用戶端代碼合并出來的。 playground函數使用一些HTML元素,然後構成一個互動playground視窗小組件。如果你想將playground添加到你的網站的話,你就可以使用這些函數。 Transport介面 (非正式的定義, 是JavaScript指令碼)的設計是依據網站前端互動方式提。 HTTPTransport 是一個Transport的實現,可以發送如前描述的以HTTP為基礎的協議。 SocketTransport 是另外一個實現,發送WebSocket (詳見下面的'Playing offline')。 為了遵守同源策略,各種網站伺服器(例如godoc)通過playground在http://golang.org/compile下的服務來完成代理請求。這個代理是通過共有的go.tools/playground包來完成的。 |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
離線運行 不管是Go Tour還是Present Tool都可以離線運行。 這樣的離線功能對於訪問網路有限制的人們來說,實在太棒了。 為了離線運行,這些工具在本地運行一個特殊版本的playground後端。這個特殊的後端使用的是常規GO 工具,這些工具沒有上面提到的那些修改,而且使用WebSocker來與用戶端進行通訊。 WebSocket的後端實現詳見go.tools/playground/socket包。在Inside Present講話中討論了代碼細節。 |
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
其他用戶端 playground服務不單單只有為了給Go項目官方使用 (Go by Example 是另外一個例子) 。我們很高興你能在你的網站使用該服務。我們唯一的要求就是您事先和我們聯絡,在您的請求中使用唯一使用者代理程式(這樣我們可以確認您的身份),此外您提供的服務是有益於Go社區的。 結束語 不論是godoc,是tour,還是這樣的blog,playground已經成為Go文檔系列中不可或缺的一部分了。隨著最近的偽檔案系統和偽網路堆棧的引入,我們將激動地完善我們的學習資料來覆蓋這些新內容。 但是,最後,playground只是冰山一角,隨著本地用戶端(Native Client)將要支援Go1.3,我們期盼著社區做出更棒的功能。 這篇文章是12月12號的Go Advent Calendar中的一篇,Go AdventCalendar是一系列的部落格文章集合。 作者 Andrew Gerrand 相關文章
- Learn Go from your browser
- Introducing the Go Playground
|
Mitisky 翻譯於 10 個月 前 1人頂 頂 翻譯的不錯哦! |
本文中的所有譯文僅用於學習和交流目的,轉載請務必註明文章譯者、出處、和本文連結
我們的翻譯工作遵照 CC 協議,如果我們的工作有侵犯到您的權益,請及時聯絡我們