Yakstは、海外の役立つブログ記事などを人力で翻訳して公開するプロジェクトです。
5年弱前投稿 修正あり

私のプログラミングの始め方 : Go

プログラミング言語の最初の1歩を解説するサイト「How I Start」から、Peter Bourgon氏によるGo言語(golang)の始め方について。Goのシンプルさと標準ライブラリの使いやすさに焦点を当てた、分かりやすい解説。

原文
How I Start. Go With Peter Bourgon (English)
原文ライセンス
CC BY-NC-ND
翻訳依頼者
D98ee74ffe0fafbdc83b23907dda3665
翻訳者
D98ee74ffe0fafbdc83b23907dda3665 doublemarket
原著者への翻訳報告
未報告


Goは、信頼に値するスマートな人達によってデザインされ、大規模かつ成長を続けるオープンソースコミュニティによって継続的に改善されている、愛すべき小さなプログラミング言語である。

Goはシンプルであることを標榜しているが、時にはそのしきたりが少々分かりにくくなる時もある。ここでは、私がGoのプロジェクトをどのように始めたか、そしてどのようにGoの慣習に従うようになったかをお見せしようと思う。Webサービスのバックエンドを構築してみよう。

環境設定

もちろん、最初のステップはGoのインストールだ。オフィシャルサイトで、使用しているOSのバイナリディストリビューションを入手できる。Mac上でHomebrewを使っているなら、brew install goでもいいだろう。成功したら、以下のように動くはずだ。

$ go version
go version go 1.3.1 darwin/amd64

インストールが終わったら、残り唯一のやることはGOPATHを設定することだ。これは、全てのGoのコードとビルドの生成ファイルを置くためのルートディレクトリだ。GoのツールはGOPATH以下に、binとpkgとsrcという3つのサブディレクトリを作る。これに$HOME/goといった値を設定する人もいるが、単純に$HOMEにしてしまうのが私の好みだ。使用している環境で、これが使われるようにしなければならない。bashを使っているなら、以下のようにすればうまくいくだろう。

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

Goで使えるエディタやそのプラグインはたくさんある。個人的には、Sublime Textと素晴らしいプラグインであるGoSublimeの大ファンだ。しかし、言語として非常に分かりやすく、特に小さいプロジェクトにおいてそれが際立つので、普通のテキストエディタでも十分だろう。シンタックスハイライトすらしていない標準的なvimを使っているプロとしてフルタイムのGo開発者をしている人と働いていたりする。とにかく始めてみること以外にない。いつだって、シンプルさが天下を取るのだ。

新しいプロジェクト

ちゃんと動く環境ができたら、プロジェクトのために新しいディレクトリを作ろう。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 git@github.com:your-username/hello
$ cd hello

ふむ、では、最小のGoプログラムとなるmain.goを作ってみよう。

package main

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

go buildコマンドを実行して、カレントディレクトリ内の全てをコンパイルしよう。ディレクトリ名と同じバイナリファイルが生成される。

$ go build
$ ./hello
hello!

簡単だ!Goを書き始めて数年になるが、新しいプロジェクトを始める時はいつもこうしている。空っぽのgitリポジトリ、main.goファイル、そしてちょっとのタイピングだ。

一般的な慣習に則って作ったので、このアプリケーションは自動的にgo getできるものになる。この1つのファイルをcommitしてGitHubにpushすれば、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関数内で、Webサーバのルートパスにハンドラ関数を設定している。http.HandleFuncは、オフィシャルにはServeMuxと呼ばれており、デフォルトのHTTPルータとして動作する。

http.HandleFunc("/", hello)

関数helloは、決まった型のシグネチャを持ったhttp.HandlerFuncで、引数からHandleFuncに渡すことができる。ルートのパスに一致する新しいリクエストがHTTPサーバに来るたびに、サーバはhello関数を実行するゴルーチンを生成する。そうすると、hello関数はクライアントへレスポンスを返すためにhttp.ResponseWriterを使う。http.ResponseWriter.Writeは、引数として[]byteあるいはバイトスライスを取るので、文字列を単純に型変換すればよい。

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

これで、http.ListenAndServe経由で、デフォルトのServeMuxのHTTPサーバがポート8080で起動する。非同期で、ブロッキングあり、コールベースで動作し、割り込みがあるまで起動し続ける。コンパイルして、実行してみよう。

$ go build
./hello

他のターミナルかブラウザから、HTTPリクエストを投げてみよう。

$ curl http://localhost:8080
hello!

簡単だ!インストールしなければならないフレームワークはないし、ダウンロードすべき依存パッケージもなく、事前に作らなくてはならないプロジェクトのスケルトンもない。バイナリ自体がネイティブコードで、静的リンクされ、ランタイムの依存性もない。さらに、標準ライブラリのHTTPサーバは本番環境に耐えうる品質で、一般的な攻撃への耐性もある。インターネットから直接リクエストを受けて処理できて、中間処理は何もいらない。

ルートを追加する

helloと出力するだけではなく、もっと面白いことだってできる。都市を入力として与えると、天気予報APIを叩き、気温を付けて応答を返してみよう。OpenWeatherMapは、現在の予報を返すシンプルな無料のAPIを提供しており、これを使って都市名で検索することができる。この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は静的型付け言語なので、このレスポンスフォーマットに対応した構造を作る必要がある。欲しい情報だけが取れれば、全ての情報を取得する必要はない。ここでは、都市名と、(面白いことに)ケルビンで表された気温だけを取ることにしよう。天気予報APIから返される必要なデータを表現した構造体を定義しよう。

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

typeは、新しい型を定義するキーワードで、ここではweatherDataを定義し、構造体を宣言している。構造体の各フィールドには、名前(NameMain)と型(stringともう1つのstruct)、タグと呼ばれているものがある。タグはメタデータのようなもので、上で作った構造体に、APIのレスポンスを直接アンマーシャルするためにencoding/jsonパッケージを使えるようにするものだ。PythonやRubyのような動的言語だともう少しコードが長くなってしまうが、このような書き方ができるのも型安全の非常に望ましい点だと言える。GoにおけるJSONについては、こちらのブログ記事このコード例を参照して欲しい。

さて、構造体を定義したので、次はそこにデータを投入する方法を定義する必要がある。そのための関数を書こう。

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構造体とエラーを返す。これが、Goにおける基本的なエラーハンドリングの慣習だ。関数は何らかの動作をエンコードしており、その動作は失敗することがある。今回の例で言えば、OpenWeatherMapに対するGETリクエストは何らかの理由で失敗することがあるわけで、期待していないデータが返ってくることもある。クライアントは、呼び出されたコンテキストに従い、その時に応じた方法で処理するように期待されているので、どんな場合でもnilでないエラーをクライアントに返してやるべきということになる。

http.Getが成功すると、レスポンスボディのクローズをdefer(遅延)する。つまり、クローズの処理は関数のスコープを抜け出す時(query関数から戻る時)に実行される。これはリソース管理の洗練された形だ。その一方、weatherData構造体をアロケートし、json.Decoderでレスポンスボディを構造体に直接アンマーシャルする。

余談だが、json.NewDecoderは、インターフェイスというGoの洗練された機能を利用している。Decoderは、明確にHTTPレスポンスボディを受け取るわけではなく、http.Response.Bodyの形式になっているio.Readerインタフェースを受け取る。Decoderは、型に対して他の動作(Read)を行うメソッドを呼び出すという動作(Decode)を提供する。Goでは、インタフェースに対する処理を行う関数として、動作を実装する。これにより、データと制御構造を分離でき、モックによるテスタビリティを保ち、非常に簡単に理解できるコードになる。

最後に、デコードが成功したら、成功を示すためにエラーとしてnilを付け加えて、weatherDataを呼び出し元に返す必要がある。そこで、関数をリクエストハンドラにつなげてみよう。

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)
})

ここでは、ハンドラを別関数としてではなくインラインで定義している。/weather/の後に続くものを受け取り、それを都市名として扱うために、strings.SplitNを使っている。クエリを実行してエラーがあった場合、http.Errorヘルパ関数を使ってそれをクライアントに伝える。HTTPリクエストが終了できるように、ここではreturnが必要になる。エラーが起きなかった場合、クライアントにJSONを送ることを通知し、それからjson.NewEncoderを使ってJSON形式のweatherDataを直接送信する。

ここまでのコードはなかなか良く、手続き的で、理解しやすい。誤解の余地はないし、分かりやすい間違いを見逃すこともないだろう。"hello, world"ハンドラを/helloに移動して、必要なものをインポートすれば、プログラムの完成だ。

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}}

コミットしてプッシュすべし!

複数のAPIへ問い合わせる

複数の天気予報APIに問い合わせて平均をとれば、都市に対してより正確な気温が出せるはず。残念ながら、多くの天気予報APIでは認証が必要だ。そんなわけで、Weather UndergroundからAPIキーを取得しよう。

全ての天気予報APIを同じ動作で扱いたいので、それは、その動作をインタフェースとしてコードにする理由になる。

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

これで、前のOpenWeatherMapのクエリ関数を、weatherProviderインタフェースを満たす型に変換できた。HTTP GETするための状態を保存しておく必要はないので、空の構造体で構わない。何が起きたのか見られるようにロギングのための行だけ追加しよう。

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
}

レスポンスからケルビンの気温を取り出したいだけなので、レスポンスの構造体をインラインで定義することができる。その他は、openWeatherMap構造体に作ったメソッドと同じようなクエリ関数だ。これで、openWeatherMapのインスタンスをweatherProviderとして使えるようになった。

Weather Undergroundでも同じようにしてみよう。唯一違うのは、APIキーを渡す必要があることだ。キーを構造体内に保存して、メソッドからそれを使う。同じような関数になるだろう。

(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のtemperatureメソッドとよく似ていることに注意しよう。独立したweatherProviderを型に集約し、その型にtemperatureメソッドを定義すると、他のweatherProviderからなるメタweatherProviderを実装できる。

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
}

完璧だ。weatherProviderを受け取るmultiWeatherProviderをどこでも渡せるようになった。

それでは、前と同じようにこれを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レスポンスに加えて、サーバのログにも出力があるだろう。

$ ./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"}

並列処理を行う

現在のところ、次から次へ順番に、同期的なAPIへ問い合わせを行っている。しかし、同時に問い合わせを行えない理由はない。それによって、レスポンスタイムを減らせるだろう。

そのためには、Goの並列処理の慣習である、ゴルーチンとチャネルを利用する。それぞれのAPIへの問い合わせ処理を、並列に動作するゴルーチンの中で呼び出す。それらのレスポンスを、1つのチャネルに集め、全ての処理が終わってから、平均値の計算を実行する。

func (w multiWeatherProvider) temperature(city string) (float64, error) {
    // 気温とエラーに対するチャネルを作る。
    // それぞれのプロバイダは1つだけ値をプッシュする。
    temps := make(chan float64, len(w))
    errs := make(chan error, len(w))

    // 各プロバイダは匿名関数でゴルーチンを呼び出す。
    // 関数はtemperatureメソッドを呼び出してレスポンスを返す。
    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

    // 各プロバイダから気温あるいはエラーを収集する。
    for i := 0; i < len(w); i++ {
        select {
        case temp := <-temps:
            sum += temp
        case err := <-errs:
            return 0, err
        }
    }

    // 前と同じく、平均を返す。
    return sum / float64(len(w)), nil
}

これで、リクエスト全体の処理時間は、最も遅いweatherProviderにかかる時間と同じになった。multiWeatherProviderの動作を変更する必要はあったが、変わらずシンプルで同期的なweatherProviderインタフェースの要求を満たしたままであることに注意してほしい。

さあ、コミットしてプッシュしよう!

シンプルさ

手頃なステップ数で、Goの標準ライブラリだけを使って、「hello world」から始まり、並列処理可能でREST風なバックエンドサーバを作るところまでを試してみた。このコードは、ほとんどのサーバアーキテクチャ上でフェッチしてデプロイできる。生成されるバイナリは、自己完結していて高速だ。そして、最も重要なのは、コードが単純で読みやすく理解しやすいことだ。必要なら、簡単にメンテナンスや拡張ができる。これらすべての特徴が、Goにおけるシンプルさに対する強固で宗教的ともいえる情熱によるものだと信じている。Rob "Commander" Pikeの表現によれば、「より少ないということは、幾何級数的により多くなるということ(less is exponentially more)」だ。

さらなる課題

最終的なコードをGithubからフォークしよう。

他のweatherProviderを追加してみるとか? (ヒント : forecast.ioはいいだろう)

multiWeatherProviderにタイムアウトを実装してみるとか? (ヒント : time.Afterを見てみよう)

次の記事
大規模環境でMySQLのGTIDを適用して得られた教訓
前の記事
MariaDBの監査プラグインの導入

同じタグの付いた翻訳済み記事

Feed small 記事フィード

新着記事Twitterアカウント

新着翻訳リクエストTwitterアカウント