來源:谷奧
本文是 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。
實現
在軟體包層面,我們宣告瞭一些資料結構,描述火雞的各個元素、對應影像所在資料夾,以及各影像應繪製在背景圖上的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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", } // urlMap 對映各 URL 字元與所對應的外觀元素。 urlMap = [...]string{"b", "h", "i", "m", "r", "p", "f", "w"} // layoutMap 對映各外觀元素與在背景影像上的位置。 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)}, } ) |
上述各點的幾何位置是通過影像中各元素的實際位置而得到的。
每次請求都從磁碟載入影像是很浪費的重複行為,因此我們在收到首個請求時就將全部 106 幅影像(13×8 個元素 + 1 幅背景 + 1 幅預設圖)載入到全域性變數中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
var ( // elements 對映各外觀元素及其影像。 elements = make(map[string][]*image.RGBA) // backgroundImage 含背景影像資料。 backgroundImage *image.RGBA // defaultImage 是出錯時返回的影像。 defaultImage *image.RGBA // loadOnce 用於僅在首次請求時呼叫 load 函式。 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)) } } } |
請求按下述順序處理:
1、解析請求 URL,按順序解碼出各字元的十進位制值。
2、為背景影像建立副本,作為最終影像的基礎。
3、在背景影像上繪製各影像元素(使用 layoutMap
判斷應繪製的位置。)
4、將影像編碼為 JPEG
5、將 JPEG 直接寫入 HTTP 響應寫入器中,將影像返回給使用者。
如果出錯,則將 defaultImage
返回給使用者,並在 App Engine 控制檯記下日誌,供日後分析之用。
下面是含說明註釋的請求接管器程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
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) } } }() // 在首次請求時從磁碟載入影像。 loadOnce.Do(load) // 建立背景副本,作為繪製基礎。 bgRect := backgroundImage.Bounds() m := image.NewRGBA(bgRect.Dx(), bgRect.Dy()) draw.Draw(m, m.Bounds(), backgroundImage, image.ZP, draw.Over) // 處理請求字串中的各個字元。 code := strings.ToLower(r.URL.Path[len(prefix):]) for i, p := range code { // 解碼遇到的十六進位制字元 p。 if p < 'a' { // 是數字 p = p - '0' } else { // 是字母 p = p - 'a' + 10 } t := urlMap[i] // 按索引查詢元素型別 em := elements[t] // 按型別查詢元素影像 if p >= len(em) { panic(fmt.Sprintf("元素索引越界 %s: "+ "%d >= %d", t, p, len(em))) } // 將元素繪製到 m 上 // 使用 layoutMap 指定其位置。 draw.Draw(m, layoutMap[t], em[p], image.ZP, draw.Over) } // 編碼為 JPEG 影像並寫為響應。 w.Header().Set("Content-type", "image/jpeg") w.Header().Set("Cache-control", "public, max-age=259200") jpeg.Encode(w, m, &imageQuality) } |
為簡潔起見,這些程式碼段中我省略了一些輔助函式。完整程式碼請參閱原始碼。
效能
該圖表從 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