Go 博客
从零开始使用 Go:24 小时内在 Google 首页上线
简介
本文由 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 编程语言两岁了
博客目录