Go 部落格

從零到 Go:24 小時內上線 Google 主頁

Reinaldo Aguiar
2011 年 12 月 13 日

引言

本文由 Google 搜尋團隊的軟體工程師 Reinaldo Aguiar 撰寫。他分享了開發第一個 Go 程式並將其釋出給數百萬觀眾的經驗——所有這些都在一天之內完成!

我最近有機會參與了一個小型但備受矚目的“20% 專案”:2011 年感恩節 Google Doodle。這個塗鴉透過隨機組合不同風格的頭部、翅膀、羽毛和腿部來製作一隻火雞。使用者可以透過點選火雞的不同部位來自定義它。這種互動性是透過 JavaScript、CSS 和當然還有 HTML 的組合在瀏覽器中實現的,可以即時生成火雞。

使用者建立了個性化火雞後,可以透過釋出到 Google+ 與朋友和家人分享。點選“分享”按鈕(此處未顯示)會在使用者的 Google+ 資訊流中建立一個包含火雞快照的帖子。快照是匹配使用者所建立火雞的單個影像。

火雞有 13 種頭、8 種腿部組合(一對腿算一種)、多種羽毛等,共有 13 種選擇,每種部件有 13 種替代方案,因此可以生成超過 8 億種可能的快照影像。顯然,預先計算所有這些影像是不切實際的。相反,我們必須即時生成快照。結合這個需求以及對即時可伸縮性和高可用性的需求,平臺選擇顯而易見:Google App Engine!

接下來我們需要決定使用哪個 App Engine 執行時。影像處理任務是 CPU 密集型的,因此效能是此情況下的決定性因素。

為了做出明智的決定,我們進行了一項測試。我們為新的Python 2.7 執行時(提供基於 C 的影像庫PIL)和 Go 執行時快速準備了幾個等效的演示應用程式。每個應用程式都生成一個由多個小影像組成的影像,將其編碼為 JPEG,並將 JPEG 資料作為 HTTP 響應傳送。Python 2.7 應用程式處理請求的中位數延遲為 65 毫秒,而 Go 應用程式的中位數延遲僅為 32 毫秒。

因此,這個問題似乎是嘗試實驗性 Go 執行時的絕佳機會。

我之前沒有 Go 經驗,時間也很緊迫:兩天就要準備好上線。這令人望而生畏,但我將其視為一個從不同、常被忽視的角度測試 Go 的機會:開發速度。一個沒有 Go 經驗的人能多快上手並構建出效能和可伸縮性兼備的產品?

設計

方法是將火雞的狀態編碼到 URL 中,並即時繪製和編碼快照。

每個塗鴉的基礎都是背景

一個有效的請求 URL 可能如下所示:http://google-turkey.appspot.com/thumb/20332620][http://google-turkey.appspot.com/thumb/20332620

“/thumb/”後面的字母數字字串(以十六進位制表示)指示為每個佈局元素選擇的選項,如下圖所示

程式的請求處理程式會解析 URL,以確定路徑中每個元件選擇的元素,在背景影像上繪製相應的影像,並將結果作為 JPEG 提供。

如果發生錯誤,將提供一個預設影像。提供錯誤頁面沒有意義,因為使用者永遠不會看到它——瀏覽器幾乎肯定會將其載入到影像標籤中。

實施

在包作用域中,我們聲明瞭一些資料結構來描述火雞的元素、相應影像的位置以及它們應在背景影像上的繪製位置。

var (
    // dirs maps each layout element to its location on disk.
    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 maps each URL character position to
    // its corresponding layout element.
    urlMap = [...]string{"b", "h", "i", "m", "r", "p", "f", "w"}

    // layoutMap maps each layout element to its position
    // on the background image.
    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 個預設影像)載入到全域性變數中。

var (
    // elements maps each layout element to its images.
    elements = make(map[string][]*image.RGBA)

    // backgroundImage contains the background image data.
    backgroundImage *image.RGBA

    // defaultImage is the image that is served if an error occurs.
    defaultImage *image.RGBA

    // loadOnce is used to call the load function only on the first request.
    loadOnce sync.Once
)

// load reads the various PNG images from disk and stores them in their
// corresponding global variables.
func load() {
    defaultImage = loadPNG(defaultImageFile)
    backgroundImage = loadPNG(backgroundImageFile)
    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))
        }
    }
}

請求按直接的順序處理

  • 解析請求 URL,解碼路徑中每個字元的十進位制值。

  • 複製背景影像作為最終影像的基礎。

  • 使用 layoutMap 在背景影像上繪製每個影像元素,以確定它們的繪製位置。

  • 將影像編碼為 JPEG

  • 透過將 JPEG 直接寫入 HTTP 響應寫入器將影像返回給使用者。

如果發生任何錯誤,我們將向用戶提供預設影像,並將錯誤記錄到 App Engine 控制檯以供後續分析。

這是帶解釋性註釋的請求處理程式程式碼

func handler(w http.ResponseWriter, r *http.Request) {
    // Defer a function to recover from any panics.
    // When recovering from a panic, log the error condition to
    // the App Engine dashboard and send the default image to the user.
    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)
            }
        }
    }()

    // Load images from disk on the first request.
    loadOnce.Do(load)

    // Make a copy of the background to draw into.
    bgRect := backgroundImage.Bounds()
    m := image.NewRGBA(bgRect.Dx(), bgRect.Dy())
    draw.Draw(m, m.Bounds(), backgroundImage, image.ZP, draw.Over)

    // Process each character of the request string.
    code := strings.ToLower(r.URL.Path[len(prefix):])
    for i, p := range code {
        // Decode hex character p in place.
        if p < 'a' {
            // it's a digit
            p = p - '0'
        } else {
            // it's a letter
            p = p - 'a' + 10
        }

        t := urlMap[i]    // element type by index
        em := elements[t] // element images by type
        if p >= len(em) {
            panic(fmt.Sprintf("element index out of range %s: "+
                "%d >= %d", t, p, len(em)))
        }

        // Draw the element to m,
        // using the layoutMap to specify its position.
        draw.Draw(m, layoutMap[t], em[p], image.ZP, draw.Over)
    }

    // Encode JPEG image and write it as the response.
    w.Header().Set("Content-type", "image/jpeg")
    w.Header().Set("Cache-control", "public, max-age=259200")
    jpeg.Encode(w, m, &imageQuality)
}

為簡潔起見,我在這些程式碼列表中省略了幾個輔助函式。完整的程式碼請參見原始碼

效能

此圖表——直接來自 App Engine 控制檯——顯示了上線期間的平均請求延遲。如您所見,即使在負載下,它也從未超過 60 毫秒,中位數延遲為 32 毫秒。考慮到我們的請求處理程式正在即時進行影像處理和編碼,這速度快得驚人。

結論

我發現 Go 的語法直觀、簡單且簡潔。我過去在解釋型語言方面有很多工作經驗,儘管 Go 是一種靜態型別和編譯型語言,但編寫這個應用程式的感覺更像是使用動態解釋型語言。

隨附的SDK提供的開發伺服器在任何更改後都會快速重新編譯程式,因此我可以像使用解釋型語言一樣快速迭代。它也非常簡單——設定我的開發環境不到一分鐘。

Go 出色的文件也幫助我快速完成了這項工作。文件是從原始碼生成的,因此每個函式的文件都直接連結到相關的原始碼。這不僅使開發者能夠非常快速地理解特定函式的功能,還鼓勵開發者深入研究包的實現,從而更容易學習良好的風格和約定。

在編寫此應用程式時,我只使用了三個資源:App Engine 的Hello World Go 示例Go 包文件以及一個展示 Draw 包的部落格文章。由於開發伺服器和語言本身實現的快速迭代,我能夠在不到 24 小內掌握這門語言並構建一個超快速、生產就緒的塗鴉生成器。

Google Code 專案下載完整的應用程式原始碼(包括影像)。

特別感謝 Guillermo Real 和 Ryan Germick 設計了這個塗鴉。

下一篇文章:使用 Go 構建 StatHat
上一篇文章:Go 程式語言兩週年
部落格索引