Go 部落格

從零開始使用 Go:24 小時內在 Google 首頁上線

Reinaldo Aguiar
2011 年 12 月 13 日

簡介

本文由 Reinaldo Aguiar 撰寫,他是 Google 搜尋團隊的軟體工程師。他分享了自己開發第一個 Go 程式並在一天之內面向數百萬使用者上線的經歷!

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

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

火雞的 8 個部分(頭部、腿部、不同的羽毛等)每個部分有 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 提供。

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

實現

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

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 的 Go Hello World 示例Go 包文件以及 一篇展示 Draw 包的部落格文章。得益於開發伺服器和語言本身帶來的快速迭代,我能夠在不到 24 小時內掌握這門語言並構建出一個超快速、可用於生產環境的塗鴉生成器。

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

特別感謝設計此塗鴉的 Guillermo Real 和 Ryan Germick。

下一篇文章:使用 Go 構建 StatHat
上一篇文章:Go 程式語言兩歲了
部落格目錄