從零到 Go:24 小時內登上 Google 主頁的 Go 語言應用“火雞”doodle 開發紀實
本文是 Google 搜索團隊軟件工程師 Reinaldo Aguiar 發表在 Go 語言博客的客座文章,他分享了在一天之內完成首款 Go 程序的開發并發布給數百萬受眾的經歷。
我最近有幸參與了一項雖小卻曝光率極高的“20% 項目”——2011 年感恩節的 Google Doodle。這幅 doodle 中的火雞由不同樣式的頭、翅膀、羽毛與爪子隨機組合而成。用戶可以通過點擊火雞的不同部位自定義組合。這種互動通過 JavaScript、CSS 實現,由瀏覽器實時渲染出各種火雞。
用戶制作出的個性化火雞可以分享到 Google+ 上。點擊“分享”按鈕(圖中未給出)即可在用戶的 Google+ 流中生成一篇含有火雞圖片的帖子。要滿足這種需求,圖片必須是單獨一張,且與用戶所制作的火雞完全相同。
由于火雞的八個部位(頭、雙爪、幾片羽毛等)各有 13 種樣式,用戶可能設計出八億多種火雞。預先制作好八億多張圖片顯然行不通。因此,必須在服務端實時生成圖片。出于即時擴展性與高度可用性的共同需求,合適的平臺非常明顯:Google App Engine!
接下來要決定的就是選用哪款 App Engine runtime 了。圖像處理任務極度依賴 CPU,所以這種情況下性能是決定性因素。
為確保可靠,我們首先進行了測試。我們為新版 Python 2.7 runtime(該版本提供基于 C 的圖像處理庫 PIL) 與 Go runtime 準備了一些等效的演示應用。各應用分別合成幾張小圖片生成圖像文件,編碼為 JPEG,并將 JPEG 數據作為 HTTP 響應發回客戶端。Python 2.7 應用處理請求的中位響應時間為 65 毫秒,而 Go 應用的中位延時僅為 32 毫秒。
因此這成為了試用 Go runtime 的大好機會。
此前我對 Go 語言毫無經驗,而時間又很緊:兩天內達到生產需求。雖然緊張,我還是將它視作從另一常被忽略的方面——開發速度——測試 Go 的機會。完全沒有 Go 語言開發經驗的人能在多快的時間內掌握并開發出高性能高擴展性的應用?
設計
基本步驟是在 URL 中編碼火雞各態、實時繪制并編碼圖像。
各 doodle 的基礎是背景圖畫:
有效的請求 URL 形如:
http://google-turkey.appspot.com/thumb/20332620
/thumb/
后面跟著的數字字串(十六進制)代表各外觀元素要繪制的形狀,如下圖所示:
程序的請求接管器解析 URL 決定各組件所選定的元素,在背景上繪制對應圖像,并返回 JPEG 成品。
如果出錯則返回默認圖像。不必返回錯誤頁面,因為用戶不可能看到——瀏覽器肯定是在加載 image
標記中的 URL。
實現
在軟件包層面,我們聲明了一些數據結構,描述火雞的各個元素、對應圖像所在文件夾,以及各圖像應繪制在背景圖上的位置。
var ( // 各外觀元素存儲位置的文件夾映射。 dirs = map[string]string{ "h": "img/heads", "b": "img/eyes_beak", "i": "img/index_feathers", "m": "img/middle_feathers", "r": "img/ring_feathers", "p": "img/pinky_feathers", "f": "img/feet", "w": "img/wing", }<span style="color:#008000;">//</span><span style="color:#008000;"> urlMap 映射各 URL 字符與所對應的外觀元素。</span><span style="color:#008000;"> </span> urlMap = [...]string{"b", "h", "i", "m", "r", "p", "f", "w"} <span style="color:#008000;">//</span><span style="color:#008000;"> layoutMap 映射各外觀元素與在背景圖像上的位置。</span><span style="color:#008000;"> </span> layoutMap = map[string]image.Rectangle{ "h": {image.Pt (109, 50), image.Pt (166, 152)}, "i": {image.Pt (136, 21), image.Pt (180, 131)}, "m": {image.Pt (159, 7), image.Pt (201, 126)}, "r": {image.Pt (188, 20), image.Pt (230, 125)}, "p": {image.Pt (216, 48), image.Pt (258, 134)}, "f": {image.Pt (155, 176), image.Pt (243, 213)}, "w": {image.Pt (169, 118), image.Pt (250, 197)}, "b": {image.Pt (105, 104), image.Pt (145, 148)}, }
)</pre> </div>
上述各點的幾何位置是通過圖像中各元素的實際位置而得到的。
每次請求都從磁盤加載圖像是很浪費的重復行為,因此我們在收到首個請求時就將全部 106 幅圖像(13×8 個元素 + 1 幅背景 + 1 幅默認圖)加載到全局變量中。
var ( // elements 映射各外觀元素及其圖像。 elements = make (map[string][]*image.RGBA)<span style="color:#008000;">//</span><span style="color:#008000;"> backgroundImage 含背景圖像數據。</span><span style="color:#008000;"> </span> backgroundImage *image.RGBA <span style="color:#008000;">//</span><span style="color:#008000;"> defaultImage 是出錯時返回的圖像。</span><span style="color:#008000;"> </span> defaultImage *image.RGBA <span style="color:#008000;">//</span><span style="color:#008000;"> loadOnce 用于僅在首次請求時調用 load 函數。</span><span style="color:#008000;"> </span> loadOnce sync.Once
)// load 函數從磁盤讀取各 PNG 圖像,并存儲到對應的全局變量中。 func load () { defaultImage = loadPNG (默認圖像文件) backgroundImage = loadPNG (背景圖像文件) for dirKey, dir := range dirs { paths, err := filepath.Glob (dir + "/.png") if err != nil { panic (err) } for _, p := range paths { elements[dirKey] = append (elements[dirKey], loadPNG (p)) } } }</pre> </div>
請求按下述順序處理:
- 解析請求 URL,按順序解碼出各字符的十進制值。
- 為背景圖像創建副本,作為最終圖像的基礎。
- 在背景圖像上繪制各圖像元素(使用
layoutMap
判斷應繪制的位置。)- 將圖像編碼為 JPEG
- 將 JPEG 直接寫入 HTTP 響應寫入器中,將圖像返回給用戶。
如果出錯,則將
defaultImage
返回給用戶,并在 App Engine 控制臺記下日志,供日后分析之用。下面是含說明注釋的請求接管器代碼:
func handler (w http.ResponseWriter, r http.Request) { // Defer 函數可以從錯亂中恢復。 // 恢復時將錯誤情況記錄到 App Engine 控制臺并給用戶發送默認圖像。 defer func () { if err := recover (); err != nil { c := appengine.NewContext (r) c.Errorf ("%s", err) c.Errorf ("%s", "Traceback: %s", r.RawURL) if defaultImage != nil { w.Header () .Set ("Content-type", "image/jpeg") jpeg.Encode (w, defaultImage, &imageQuality) } } }()<span style="color:#008000;">//</span><span style="color:#008000;"> 在首次請求時從磁盤加載圖像。</span><span style="color:#008000;"> </span> loadOnce.Do (load) <span style="color:#008000;">//</span><span style="color:#008000;"> 創建背景副本,作為繪制基礎。</span><span style="color:#008000;"> </span> bgRect := backgroundImage.Bounds () m := image.NewRGBA (bgRect.Dx (), bgRect.Dy ()) draw.Draw (m, m.Bounds (), backgroundImage, image.ZP, draw.Over) <span style="color:#008000;">//</span><span style="color:#008000;"> 處理請求字串中的各個字符。</span><span style="color:#008000;"> </span> code := strings.ToLower (r.URL.Path[len (prefix):]) <span style="color:#0000ff;">for</span> i, p := range code { <span style="color:#008000;">//</span><span style="color:#008000;"> 解碼遇到的十六進制字符 p。</span><span style="color:#008000;"> </span> <span style="color:#0000ff;">if</span> p < 'a' { <span style="color:#008000;">//</span><span style="color:#008000;"> 是數字</span><span style="color:#008000;"> </span> p = p - '0' } <span style="color:#0000ff;">else</span> { <span style="color:#008000;">//</span><span style="color:#008000;"> 是字母</span><span style="color:#008000;"> </span> p = p - 'a' + 10 } t := urlMap[i] <span style="color:#008000;">//</span><span style="color:#008000;"> 按索引查找元素類型</span><span style="color:#008000;"> </span> em := elements[t] <span style="color:#008000;">//</span><span style="color:#008000;"> 按類型查找元素圖像</span><span style="color:#008000;"> </span> <span style="color:#0000ff;">if</span> p >= len (em) { panic (fmt.Sprintf ("元素索引越界 %s: "+ "%d >= %d", t, p, len (em))) } <span style="color:#008000;">//</span><span style="color:#008000;"> 將元素繪制到 m 上</span><span style="color:#008000;"> </span> <span style="color:#008000;">//</span><span style="color:#008000;"> 使用 layoutMap 指定其位置。</span><span style="color:#008000;"> </span> draw.Draw (m, layoutMap[t], em[p], image.ZP, draw.Over) } <span style="color:#008000;">//</span><span style="color:#008000;"> 編碼為 JPEG 圖像并寫為響應。</span><span style="color:#008000;"> </span> w.Header () .Set ("Content-type", "image/jpeg") w.Header () .Set ("Cache-control", "public, max-age=259200") jpeg.Encode (w, m, &imageQuality)
}</pre> </div>
為簡潔起見,這些代碼段中我省略了一些輔助函數。完整代碼請參閱源碼。
性能
該圖表從 App Engine 控制臺截取,展示了發布后的平均請求時間。顯然,即使在高負載情況下也沒有超過 60 ms,中位延遲時間為 32 ms。考慮請求接管器在處理圖像并實時編碼,這已經相當快了。
結論
我覺得 Go 語言的語法直觀、簡單且潔凈。我過去常與解析型語言打交道,盡管 Go 是靜態錄入編譯型語言,編寫這款應用的感覺卻更像是在用動態解析型語言。
開發服務器提供了可以在程序有變動后迅速重新編譯的 SDK,所以開發部署與解析型語言一樣快。而且非常簡單——我只花了不到一分鐘就配置好了開發環境。
Go 語言優秀的文檔也幫助了我迅速完成開發。文檔是從源代碼生成的,各函數的文檔與相關聯的源碼直接鏈接。這不僅可以讓開發者迅速理解特定函數的作用,還鼓勵開發者深入挖掘軟件包的實現,簡化了對良好編程風格與規則的掌握。
編寫這款應用的過程中,我只參考了三份資源:App Engine 的 Hello World Go 示例、Go 軟件包文檔以及一篇演示 Draw 軟件包的博文。感謝開發服務器的迅速部署,以及該語言自身的優異特性,我得以在 24 小時內掌握該語言,并開發出超快、滿足生產需求的 doodle 生成器。
應用的完整源碼(包括圖像文件)可以在 Google Code 項目中下載到。
向設計該 doodle 的 Guillermo Real 與 Ryan Germick 致以特別的謝意。
原文:From zero to Go: launching on the Google homepage in 24 hours
來自: 谷奧本文由用戶 openkk 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!相關資訊
相關經驗