Go 博客

从零到 Go:24 小时内在 Google 首页上线

雷纳尔多·阿吉亚尔
2011 年 12 月 13 日

介绍

本文由 Google 搜索团队的软件工程师雷纳尔多·阿吉亚尔撰写。他分享了他开发第一个 Go 程序并将其发布给数百万用户(这一切都在一天内完成!)的经验。

我最近有机会参与一个规模小但影响力很大的“20% 项目”:2011 年感恩节 Google 涂鸦。涂鸦的特色是一个火鸡,它是通过随机组合不同风格的头、翅膀、羽毛和腿部制作的。用户可以通过点击火鸡的不同部位来自定义它。这种交互性是通过 JavaScript、CSS 和 HTML 的组合在浏览器中实现的,从而动态创建火鸡。

一旦用户创建了一个个性化的火鸡,就可以通过发布到 Google+ 与朋友和家人分享。点击“分享”按钮(此处未显示)会在用户的 Google+ 信息流中创建一个包含火鸡快照的帖子。快照是一个与用户创建的火鸡匹配的单个图像。

由于火鸡的 8 个部位(头部、腿部、羽毛等)各有 13 种选择,因此可以生成超过 8 亿个可能的快照图像。预先计算所有这些图像显然是不可行的。相反,我们必须动态生成快照。将此问题与对即时可扩展性和高可用性的需求相结合,平台的选择就变得显而易见了:Google App Engine!

接下来我们需要决定使用哪个 App Engine 运行时。图像处理任务受 CPU 限制,因此性能是这种情况下的决定因素。

为了做出明智的决定,我们进行了一次测试。我们快速为新的 Python 2.7 运行时(它提供了 PIL,一个基于 C 的图像库)和 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 响应写入器将图像返回给用户。

如果发生任何错误,我们将向用户提供 defaultImage,并将错误记录到 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 世界问候示例Go 包文档展示 Draw 包的博客文章。由于开发服务器和语言本身使快速迭代成为可能,我能够在不到 24 小时内学习该语言并构建一个超快速、生产就绪的涂鸦生成器。

Google Code 项目 下载完整的应用程序源代码(包括图像)。

特别感谢 Guillermo Real 和 Ryan Germick 设计了此涂鸦。

下一篇文章:使用 Go 构建 StatHat
上一篇文章:Go 编程语言两周年
博客索引