Hacks on Computer Vision

[翻译]Go With Peter Bourgon -- 我是如何开始使用Go的

2014.09.07

好久没翻译了,假期无事,且翻译一篇Go的博文,原链接猛戳这里


Go是一门可爱的编程语言,由一群你可以信任的聪明人设计的,并由庞大并不断增长的开源社区持续提升。

Go意味着简洁,但有时规范有些难以理解。我想向你展示我是如何开始我的所有Go项目的,以及如何使用Go的风格。让我们为一个web应用建立一个后端服务。

设置你的环境

当然,第一步是安装Go。你可以使用官方网站上你对应操作系统的二进制发布版。如果你在Mac上用Homebrew,brew install go就可以了。当你完成时,下面的代码应该可以有效果:

$ go version
go version go 1.3.1 darwin/amd64

一旦安装好了之后,剩下要做的就是设置你的GOPATH。这个是将保留你所有的Go代码和构建的产品的跟目录。Go的工具将在你的GOPATH里创建3个子目录:bin,pkg和src。有些人设置它如$HOME/go,但我更倾向于直接使用$HOME。确保它已经输出到你的环境中。如果你使用bash,下面的代码可以完成这个功能:

$ echo 'export GOPATH=$HOME' >> $HOME/.profile
$ source $HOME/.profile
$ go env | grep GOPATH
GOPATH="/Users/peter"

有很多编辑器和插件可以用于Go的编写。我个人是Sublime Text和超级棒的GoSublime插件的超级粉丝。但这门语言是非常明确的,尤其是对于小型项目,所以普通的文字编辑器就足够了。我的一些专业、全职的Go开发者同事仍然使用vanilla vim,甚至没有用语法高亮。当然你不需要那样开始。一如既往,简洁为王。

一个新的项目

有了一个可用的环境,我们将为项目新建一个文件夹。Go的工具链希望所有的代码都存在于$GOPATH/src,所以我们总是在那里工作。工具链也可以直接引入托管在Github或者Bitbucket上的项目并与之交互,假设它们在正确的位置。

对于本例,我们在Github上建立一个新的空仓库。我将假定它叫“hello”。然后,在你的$GOPATH里给它建个家。

$ mkdir -p $GOPATH/src/github.com/your-username
$ cd $GOPATH/src/github.com/your-username
$ git clone [email protected]:your-username/hello
$ cd hello

非常棒。创建main.go,这将绝对是我们最小的Go程序。

package main

func main() {
    println("hello!")
}

调用go build来编译当前文件夹下的所有文件。它将生成和当前文件夹名相同的二进制文件。

$ go build
$ ./hello
hello!

蛋定!即使写了几年的Go,我依然像这样开始我的新项目。一个空的git仓库,一个main.go,一点打字输入。

既然我们仔细遵循常用的约定,你的应用就自动可以用go获得。如果你注释并将这单个文件推到Github,任何拥有一个可用的Go安装的人都可以这样做:

$ go get github.com/your-username/hello
$ $GOPATH/bin/hello
hello!

制作一个web服务器

让我们把hello,world改变成一个web服务器。下面是全部代码。

package main

import "net/http"

func main() {
    http.HandleFunc("/", hello)
    http.ListenAndServe(":8080", nil)
}

func hello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello!"))
}

需要展开说明的就一点。首先,我们需要从标准库中引入net/http包。

import "net/http"

然后,在main函数中,我们在服务器的根路径下安装一个handler函数。http.HandleFunc 操作默认的HTTP路由,官方叫法是ServeMux

http.HandleFunc("/", hello)

hello函数是一个http.HandlerFunc,这意味着它有特定类型的签名,可以作为一个参数传到HandleFunc。每次一个新的匹配根路径的请求到达HTTP服务器,服务器将生成一个新的goroutine来执行hello函数。hello函数简单使用http.ResponseWriter向客户端写一个响应。因为http.ResponseWriter.Write接收更为一般的[]byte,或者byte-slice,作为参数,我们对字符串做一个简单的类型转换。

func hello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello!"))
}

最后,我们在端口8080通过http.ListenAndServe的默认ServeMux来启动HTTP服务器。这是同步的,或者阻塞的调用,除非被打断,将会一直保持程序alive。和前面一样编译并运行。

$ go build
./hello

在另一个终端,或者你的浏览器,来一个HTTP请求。

$ curl http://localhost:8080
hello!

简单吧!没有安装任何框架,没有下载任何依赖,没有创建任何项目结构。而且二进制本身就是个原生的代码,静态链接,没有运行时的依赖。再加上,标准库中的HTTP服务器是生产级别的,可以抵御常见的攻击。它可以直接为互联网的请求服务——而没有中间件的需求。

添加更多路由

我们可以做更多有意思的事,而不仅仅是hello。让我们将一个城市作为输入,调用一个天气API,并返回一个温度的响应。OpenWeatherMap当前天气信息提供了一个简单免费的API,我们可以通过城市来查询。它返回的响应像这样(部分做了编辑):

{
    "name": "Tokyo",
    "coord": {
        "lon": 139.69,
        "lat": 35.69
    },
    "weather": [
        {
            "id": 803,
            "main": "Clouds",
            "description": "broken clouds",
            "icon": "04n"
        }
    ],
    "main": {
        "temp": 296.69,
        "pressure": 1014,
        "humidity": 83,
        "temp_min": 295.37,
        "temp_max": 298.15
    }
}

Go是一门静态类型的语言,所以我们需要创造一个结构来表示这个响应的格式。我们不需要获取信息的所有部分,仅仅我们需要的就够了。就目前而言,我们仅需要得到城市的名字和温度,(有意思的)是返回的开式温度。我们将定义一个struct来表示从天气API返回的我们需要的数据。

type weatherData struct {
    Name string `json:"name"`
    Main struct {
        Kelvin float64 `json:"temp"`
    } `json:"main"`
}

type关键字定义了一个新的类型,我们叫做weatherData,并声明为一个struct。struct中的每个域都有一个名字(如,NameMain),一个类型(string,另一个匿名struct),已知为一个tag。tags就像metadata,允许我们直接使用encoding/json包来直接将API的响应解入到我们的struct中。这比像Python或Ruby这样的动态语言需要多一些的敲键盘打字,但它给我们带来更高的类型安全特性。对于更多关于JSON和Go的信息,猛击这篇博客,或这个样例代码

我们已经定义了结构体,现在我们需要定义一个方法来调用它。让我们写个函数这么做吧。

func query(city string) (weatherData, error) {
    resp, err := http.Get("http://api.openweathermap.org/data/2.5/weather?q=" + city)
    if err != nil {
        return weatherData{}, err
    }

    defer resp.Body.Close()

    var d weatherData

    if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
        return weatherData{}, err
    }

    return d, nil
}

这个函数输入一个代表城市的字符串,返回一个weatherData struct和一个error。这是Go中的一个基础的错误管理习惯。函数编码行为,行为可能失败。对我们而言,对OpenWeatherMap的GET请求可能会因为任何原因失败,返回的数据可能不是我们期望的。在任一种情况下,我们返回客户端一个非nil的错误,客户端期望可以在调用的上下文中合理的处理它。

如果http.Get成功,我们defer调用来关闭响应主体,这将在我们离开函数范围后才执行(当我们返回查询函数时),这是个优雅的资源管理模式。同时,我们分配一个weatherData struct,使用json.Decoder直接从响应主体中解码数据到我们的struct中。

说点题外话,json.NewDecoder利用了Go的一个优雅的特性,即interfaces。Decoder不接收具体的HTTP响应;而是接收一个io.Reader interface,而http.Response.Body正好满足。Decoder提供了一个行为(Decode),正好仅通过调用一个满足另一个行为(Read)类型的方法就可以满足。在Go中,就操作interface的函数而言,我们倾向于实现行为。它可以清楚的分离数据和控制层,通过复制进行测试非常容易,代码也更加的合理。

最后,如果解码成功,我们返回调用者weatherData一个nil error来表明成功。现在让我们把函数和请求处理连接起来吧。

http.HandleFunc("/weather/", func(w http.ResponseWriter, r *http.Request) {
    city := strings.SplitN(r.URL.Path, "/", 3)[2]

    data, err := query(city)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    json.NewEncoder(w).Encode(data)
})

这里,我们内联的定义了handler,而不是写成一个分开的函数。我们使用strings.SplitN来获取在路径/weather/后面的所有数据,把它当作一个城市。我们生成请求,如果有错误,我们通过http.Error helper函数报告给客户端。那时我们需要返回,这样HTTP请求就完成了。否则,我们告诉客户端我们将传送JSON数据,直接使用json.NewEncoder来JSON-encode编码weatherData。

目前代码很漂亮,程序上也很容易理解。没有误解的可能,也不会错过常见的错误。如果我们把“hello, world”的handler移到/hello下,并生成必要的imports,就得到完整的程序了:

package main

import (
    "encoding/json"
    "net/http"
    "strings"
)

func main() {
    http.HandleFunc("/hello", hello)

    http.HandleFunc("/weather/", func(w http.ResponseWriter, r *http.Request) {
        city := strings.SplitN(r.URL.Path, "/", 3)[2]

        data, err := query(city)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        json.NewEncoder(w).Encode(data)
    })

    http.ListenAndServe(":8080", nil)
}

func hello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello!"))
}

func query(city string) (weatherData, error) {
    resp, err := http.Get("http://api.openweathermap.org/data/2.5/weather?q=" + city)
    if err != nil {
        return weatherData{}, err
    }

    defer resp.Body.Close()

    var d weatherData

    if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
        return weatherData{}, err
    }

    return d, nil
}

type weatherData struct {
    Name string `json:"name"`
    Main struct {
        Kelvin float64 `json:"temp"`
    } `json:"main"`
}

和前面一样,编译并运行。

$ go build
$ ./hello
$ curl http://localhost:8080/weather/tokyo
{"name":"Tokyo","main":{"temp":295.9}}

注释并push吧!

查询多个API

也许我们可以通过查询并平均多个温度API的方式来构建一个城市更加准确的温度。不幸的是,大多数的温度API需要验证。那么,先得到Weather Underground的API key吧.

因为我们想使用同一方式使用温度API,那将这个行为编码为一个interface是合理的。

type weatherProvider interface {
    temperature(city string) (float64, error) // in Kelvin, naturally
}

现在,我们可以将旧的OpenWeatherMap查询函数转换为一个满足weatherProvider interface的类型了。因为我们不需要存储任何状态来生成HTTP GET,我们将使用一个空的struct。我们将添加一行简单的logging,这样我们就可以看到发生了什么。

type openWeatherMap struct{}

func (w openWeatherMap) temperature(city string) (float64, error) {
    resp, err := http.Get("http://api.openweathermap.org/data/2.5/weather?q=" + city)
    if err != nil {
        return 0, err
    }

    defer resp.Body.Close()

    var d struct {
        Main struct {
            Kelvin float64 `json:"temp"`
        } `json:"main"`
    }

    if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
        return 0, err
    }

    log.Printf("openWeatherMap: %s: %.2f", city, d.Main.Kelvin)
    return d.Main.Kelvin, nil
}

因为我们仅想从响应中提取开式温度,我们可以内联的定义响应的struct。否则,它就基本上和查询函数很像了,仅在openWeatherMap struct定义一个方法。那样的话,我们可以使用openWeatherMap的一个实例作为weatherProvider。

我们也这样处理Weather Underground。唯一的区别在于我们需要提供API key。我们将在struct中保存key,在方法中使用它。它将是一个非常相似的函数。

(注意:Weather Underground不像OpenWeatherMap那样可以漂亮的处理歧义的城市。因此我们在例子中将跳过一些重要的逻辑来处理有歧义的城市名字。)

type weatherUnderground struct {
    apiKey string
}

func (w weatherUnderground) temperature(city string) (float64, error) {
    resp, err := http.Get("http://api.wunderground.com/api/" + w.apiKey + "/conditions/q/" + city + ".json")
    if err != nil {
        return 0, err
    }

    defer resp.Body.Close()

    var d struct {
        Observation struct {
            Celsius float64 `json:"temp_c"`
        } `json:"current_observation"`
    }

    if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
        return 0, err
    }

    kelvin := d.Observation.Celsius + 273.15
    log.Printf("weatherUnderground: %s: %.2f", city, kelvin)
    return kelvin, nil
}

既然现在我们有了一些温度的提供者,让我们写个函数来查询它们,并返回平均温度。为了简化起见,如果我们遇到任何错误,我们将放弃。

func temperature(city string, providers ...weatherProvider) (float64, error) {
    sum := 0.0

    for _, provider := range providers {
        k, err := provider.temperature(city)
        if err != nil {
            return 0, err
        }

        sum += k
    }

    return sum / float64(len(providers)), nil
}

注意:函数定义和weatherProvider温度方法非常相似。如果我们将单独的weatherProviders收集为一个type,在这个type上定义温度方法,我们可以实现一个meta-weatherProvider,由其他的weatherProviders组成。

type multiWeatherProvider []weatherProvider

func (w multiWeatherProvider) temperature(city string) (float64, error) {
    sum := 0.0

    for _, provider := range w {
        k, err := provider.temperature(city)
        if err != nil {
            return 0, err
        }

        sum += k
    }

    return sum / float64(len(w)), nil
}

完美。我们可以传递一个multiWeatherProvider到任何接收一个weatherProvider的地方。

现在,我们可以把它和HTTP服务器连接起来,和前面的非常相似。

func main() {
    mw := multiWeatherProvider{
        openWeatherMap{},
        weatherUnderground{apiKey: "your-key-here"},
    }

    http.HandleFunc("/weather/", func(w http.ResponseWriter, r *http.Request) {
        begin := time.Now()
        city := strings.SplitN(r.URL.Path, "/", 3)[2]

        temp, err := mw.temperature(city)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        json.NewEncoder(w).Encode(map[string]interface{}{
            "city": city,
            "temp": temp,
            "took": time.Since(begin).String(),
        })
    })

    http.ListenAndServe(":8080", nil)
}

编译,运行,GET,和前面一样。除了JSON响应,你可以在服务器的logs里看到一些输出。

$ ./hello
2015/01/01 13:14:15 openWeatherMap: tokyo: 295.46
2015/01/01 13:14:16 weatherUnderground: tokyo: 273.15
$ curl http://localhost/weather/tokyo
{"city":"tokyo","temp":284.30499999999995,"took":"821.665230ms"}

注释并push吧!

使它并行化

现在我们仅仅同步一个接着一个地查询API。但显然我们并不是不可以同时查询它们。这样就可以减少我们的响应时间。

为了这个目的,我们利用Go的并行基元:goroutines和channels。我们将在对应的可同时运行的goroutine里生成其API查询。我们将在单独的channel里收集响应,当都完成时计算平均值。

func (w multiWeatherProvider) temperature(city string) (float64, error) {
    // Make a channel for temperatures, and a channel for errors.
    // Each provider will push a value into only one.
    temps := make(chan float64, len(w))
    errs := make(chan error, len(w))

    // For each provider, spawn a goroutine with an anonymous function.
    // That function will invoke the temperature method, and forward the response.
    for _, provider := range w {
        go func(p weatherProvider) {
            k, err := p.temperature(city)
            if err != nil {
                errs <- err
                return
            }
            temps <- k
        }(provider)
    }

    sum := 0.0

    // Collect a temperature or an error from each provider.
    for i := 0; i < len(w); i++ {
        select {
        case temp := <-temps:
            sum += temp
        case err := <-errs:
            return 0, err
        }
    }

    // Return the average, same as before.
    return sum / float64(len(w)), nil
}

现在,我们的请求花费和最慢的那个weatherProvider的时间相同。我们仅需要改变multiWeatherProvider的行为,而这显然仍旧满足简单的、同步weatherProvider interface。

注释并push吧!

简单化

我们从’hello world'开始,在有限的步骤和仅使用Go标准库的情况下完成了一个并发的、REST化的后端服务器。我们的代码可以获得并部署在几乎任何架构上。得到的二进制文件是自包含的,运行非常快。最重要的是,代码是易读可懂的。必要的话,它可以轻松的维护并扩展。我认为所有这些特性都是Go坚定信奉简单化的结果。正如Rob “Commander” Pike所言,less is exponentially more.

进一步的练习

在github上Fork最终的代码。

你能添加另一个weatherProvider吗?(提示:forecast.io就挺不错。)

你能在multiWeatherProvider实现一个超时吗?(提示:看下time.After。)

__EOF__

本文作者HackCV
版权声明本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
本文链接https://hackcv.com/posts/%E7%BF%BB%E8%AF%91go-with-peter-bourgon-%E6%88%91%E6%98%AF%E5%A6%82%E4%BD%95%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8go%E7%9A%84/

发表评论