とよぶ

歌いながらコード書いてます

Golang周辺のツールをいろいろ使いながらGin Web FrameworkでAPIを作る

Golangで開発する上でライブラリの依存関係解決どうするとか、Object Relational Mapping(ORM)どうするとか、hot deployしながら開発するのどうするとかあると思うんですがその辺りを軽く書きながらAPI作っていきたいと思います。

Docker使って開発するあたりの解説は以前のブログを参照されたい。

takasing104.hateblo.jp

できること

  • Golangで依存ライブラリの管理(と現場言っていいものかは微妙)
  • ORMを使ったDBアクセス
  • Mac上でソースコードを変更した際、Dockerコンテナに修正が反映されてフレームワークがリロードされる
  • Webフレームワークを利用したAPI開発

ツールと環境

Frameworkの選定

昨今ではGolangのWebフレームワークいくつかありますね。

  • Revel
  • Martini
  • Beego
  • Goji

フルスタックフレームワークは学習コスト高いのでやめました。
Martiniはパフォーマンスがあまりよくないみたいなので、Martiniライクでパフォーマンス改善されたgojiかginが選択肢になりました。
gojiの売りはgraceful restartだったりするのですが、Docker環境だとBlue-Green deploymentすると思うのであまり重要ではないと思います。
かつ、gojiよりginのほうがEndpointに差し込む関数インターフェースが簡潔なので、今回はGinを使用することにします。

依存ライブラリの管理について

ここがまだGolangが未開拓というかまだまだだなというところなんですが、Goの思想として

最初からあるGoの目標は、MakefileやMakefileに変わる様々な現代的な構成ファイルを書く必要をなくし、 ソースコード自体にある情報のみを使ってGoのコードをビルドできるようにすることでした。

とか

新しいGoコマンドの目標は、Goプログラムは必要なインポート文を書くということ以上に開発者側で構成ファイルや追加の作業をせずにコンパイルできるようにする必要がある。という、私たちの理想へ回帰しています。

Goコマンドについて - The Go Programming Language

という考えがあります。 つまり設定ファイル(゚⊿゚)イラネってことですね。 これがどういうことかというと、go getするだけでライブラリ落としてこれるのですが、一方で.goファイルには

import "github.com/gin-gonic/gin"

としか書かれないので、ライブラリのバージョン指定ができないわけですね。。。
外部ライブラリ使ったことある人はわかると思うんですけど、ライブラリは普通にバグ仕込まれてくるので、バージョン指定しないと、毎回go getして開発メンバー間で異なるバージョンで開発をするという危ういスタイルを余儀なくされるだけでなく、作成したアプリケーション自体がライブラリのバグによって動作しなくなるという可能性があります(テストをちゃんと書きましょう。あっはいスミマセンデシタ)。

一応Golangが公式で推奨しているスタイルがあるものの、そこまで確立していないのが現状です。
PackageManagementTools · golang/go Wiki · GitHub

生暖かい目で見守りましょう。
今回はいろんなライブラリとかツールで使われているGodepを使用します。

Godepのスタイルとしては、ライブラリのソースコードを自分のアプリケーションのリポジトリにコミットして使用するというなかなかワイルドな感じになっていてあまり気に入らなかったりするのですが、誰かイケてるやつ作ってくださいませんかねw
冒頭で「依存ライブラリの管理(と現場言っていいものかは微妙) 」と書いたのはそういうことです。

アプリケーションの解説

github.com

エンドポイントの作成

GinではSinatraなどと同様に、一つのエンドポイントに関数を渡して処理を行います。
この際のメソッドのインターフェースはgithub.com/gin-gonic/gin/gin.goでシンプルに定義されています。

type (
  HandlerFunc func(*Context)
...

なので、エンドポイントに対する関数を定義して、その関数に対する参照をルーターに渡します。
routes/article.go

func GetArticle(c *gin.Context) {
    id := c.Params.ByName("id")
    v, err := c.Get("dbMap")
    if err != nil {
        log.Printf("error: %s", err.Error())
        c.JSON(http.StatusBadRequest, gin.H{"status": "bad request"})
        return
    }
    dbMap, ok := v.(*gorp.DbMap)
    if !ok {
        c.JSON(http.StatusBadRequest, gin.H{"status": "bad request"})
        return
    }
    art, err := dbMap.Get(model.Article{}, id)
    if err != nil {
        log.Printf("error: %s", err.Error())
        c.JSON(http.StatusInternalServerError, gin.H{"status": "db error"})
        return
    }
    if art == nil {
        c.JSON(http.StatusNotFound, gin.H{"status": "not found"})
        return
    }
    c.JSON(http.StatusOK, gin.H{"status": "ok", "article": art})
}

routes/router.go

func Init(app *app.App) *gin.Engine {

    router := gin.Default()

    router.Use(app.AppendEnv)

    v1 := router.Group("/v1")
    {
        v1.Use(filter.Auth)
        v1.GET("/article/:id", GetArticle)
        v1.POST("/article", PostArticle)
    }

    return router
}

これで/v1/article/1などとリクエストが来た場合はGetArticleメソッドが実行されるわけです。

filter処理

上記のようにエンドポイントを設定するより前の部分でUseメソッドを使ってメソッドを渡しておくことで、filterのような処理をすることができます。

ここではDbMap(後述)をContextに追加する処理と、Cookieによるユーザー認証を行っています。
この場合もGinでは同じ関数のインターフェースなのでとてもシンプルであることがわかります。

DBアクセスとORM

今回はPostgreSQLのDBにアクセスするようにしています。

db/postgres.go

package db

import (
    _ "github.com/lib/pq" // for sql.Open
    "github.com/go-gorp/gorp"
    "database/sql"
    "golang-gin-api/model"
)

func InitDb() *gorp.DbMap {
    db, err := sql.Open("postgres", "host=db user=takasing dbname=toyo password=takapass sslmode=disable")
    if err != nil {
        // handle error
    }
    dbmap := &gorp.DbMap{Db: db, Dialect: gorp.PostgresDialect{}}
    dbmap.AddTableWithName(model.Article{}, "articles").SetKeys(true, "Id")
    dbmap.AddTableWithName(model.User{}, "users").SetKeys(true, "Id")
    return dbmap
}

database/sqlパッケージを使用してDbへの接続情報を生成しています。
ここで注意したいのがimport文のところ。

_ "github.com/lib/pq"

Golangでは使用していないimport文がソースコードに残っていたらコンパイルが通らないのですが、今回のDBのドライバようにソースコード上で使用されていないけど必要なものに関してはアンダースコアをつけてimport文を書くことでコンパイルエラーが回避され、無事DBへの接続が生成されます。
(これに気づかず結構ハマった(^q^))

また、Golangではパッケージ内の型のフィールドやメソッドを外部に開く時は、先頭の文字を大文字にすることでそれを実現しているんですが、この場合DBのカラム名と異なることがあります。
そのままではうまくマッピングしてくれないので、Golangのタグの機能を使って関連付けてやります。
model/article.go

type Article struct {
    Id int `json:"id"`
    Title string `json:"title" validate:"min=4,max=64,nonzero"`
    Content string `json:"content"`
    CreatedAt time.Time `json:"created_at" db:"created_at"`
}

タグはバリデーションとかjsonのマッピングとかにも使われます。

実行

$ vagrant up
$ vagrant ssh
core@core-01 ~ $ ./share/container.sh -pgx
core@core-01 ~ $ docker exec -it postgres bash
root@6b18b5a5b43c:/# cd /project
root@6b18b5a5b43c:/# sh database.sh
root@6b18b5a5b43c:/# exit
core@core-01 ~ $ docker logs -f golang
...
app log here
...

ブラウザに172.17.8.102のドメインにuidでvalueを1にしてCookie焼きます。
そしてAdvanced REST API ClientとかでAPIを叩いてみてください。

詳しくはソースコードを御覧くださいw

所感

  • 僕は会社入ってからプログラム書き始めたクチなので、JavaとRubyしかまともに書いてないゆとりプログラマなんですが、Golangはまだまだデバッグとかむずいっすね。
    ポインタ(^q^)
  • Ginはまだ新しいフレームワークなので、ドキュメントがあまりないのですが、Martini、ひいてはSinatraインスパイアというところに着目してドキュメント探せばある程度参考になるのではないでしょうか。
    フレームワーク自体はシンプルなので、Golangの言語仕様さえある程度頭に入れば簡単にAPIは書けそうですね。
  • Golang的にしかたのないことなんですけど、複数の返り値をつかってerrorを制御するので、うまいこと書かないとコードは長くなりますね。。。たるい。

TODO

  • Dockerまわりの設定はDocker ComposeとかDocker Machineとかあるので書き換えたい。