どくぴーの備忘録

真面目なことを書こうとするクソメガネのブログ。いつ投げ捨てられるのかは不明

DB付きWeb APIが急に欲しくなったのでGAE/Goで雑に実装する

まずはじめに

この記事は任意のアドベントカレンダーに属するものではありません。

何の話なの

Golangの勉強をはじめましたという話です。具体的にはいつぞやの記事に書いていたもののWeb API側を作った時の話です。

e10dokup.hateblo.jp

本業はAndroidアプリエンジニアなので、正直に言うとサーバサイドがよくわかってない感じからのスタートなので備忘録的に書いていく感じでやっていきましょう。

GAE/Goを使ってみる

宙に浮いているさくらのVPSがあるのですが、なにぶん時間がないということで今回はGAE/Goを使ってGoogle Cloud SQLと組み合わせることにしました。対して派手なことしないし今回の用途(とりあえずDBを持ったWeb APIの実装がしたい)に限っては必要十分って言う感じです。適当にWAFにはginを、ORマッパーにはgormを選択しています。

Golangのインストール

多分ググったほうが早い。僕はこの辺を見たりしながらインストールしました。

インストール - The Go Programming Language

macユーザならhomebrew一発でやれば良さそうな気がします。

qiita.com

GAE/Goのインストール

多分こちらもググったほうが早い。Google Cloud SDKをインストールしたり、Golang用のAppEngine SDKをダウンロードしてきてPATHを張ったりしました。

とりあえず動くようにしたい!ってモチベーションだったので、このあたりの記事を参考にやっていた記憶があります。

実際に開発する

実際のディレクトリ構成はこんな感じになりました。

sample
├── app
│   ├── appInit.go
│   ├── controllers
│   │   ├── hogeController.go
│   │   └── ・・・
│   ├── db.go
│   └── models
│       ├── hoge.go
│       └── ・・・
├── gae
│   ├── app.yaml
│   ├── init.go
│   ├── static
│   │   ├── main.css
│   │   ├── main.js
│   └── templates
│       ├── hoge.tmpl
│       └── ・・・
└── main.go

ローカルで極力goapp serveをやりたくない、ローカル環境時はIntelliJやGogland等のIDEのデバッガーを使いたい。みたいなことを感じたため、ローカルでビルドする際とgaeでデプロイする際に別の口を用意してそこからapp/app_init.goにアクセスして初期化を行う感じにしました。

とりあえず先にGAEへのデプロイのためのapp.yamlを書くとこんな感じになると思います。env_variables内の各値はCloud SQLの設定値をそのまま引っ張ってくるだけです。handlersにはGAE上でのstaticディレクトリ参照を書くようにしています。

version: 1
application: sample
runtime: go
api_version: go1.8

handlers:
- url: /static
  static_dir: static

- url: /.*
  script: _go_app

env_variables:
  CLOUDSQL_CONNECTION_NAME: <CLOUD_SQL_CONNECTION_NAME>
  CLOUDSQL_USER: <CLOUDSQL_USER>
  CLOUDSQL_PASSWORD: <CLOUDSQL_PASSWORD>

app/app_init.goは次のような感じになっています。

package app

import (
    "github.com/gin-gonic/gin"
    "net/http"
    "github.com/jinzhu/gorm"

    "sample/app/controllers"
)

var IsGAE = false
var db *gorm.DB

func Init(gae bool) *gin.Engine {
    IsGAE = gae

    InitDB(gae)
    db = ConnectDB(IsGAE)

    router := gin.Default()
    router.GET("/hello", func(c *gin.Context) {
        c.String(200, "Hello, e10dokup")
    })

    // 静的ファイルの設定

    if gae {
        router.LoadHTMLGlob("./templates/*")
    } else {
        router.Static("/static", "./gae/static")
        router.LoadHTMLGlob("./gae/templates/*")
    }

    return router
}

init関数の中でついでにdbのマイグレーションもしてしまう感じにしました。DBへの接続はgaeであるかどうかのboolとAppEngine SDK自体が持っているローカルか本番用かの判定を行う関数を使って与えるURIを切り替えているだけです。

package app

import (
    "fmt"
    "os"

    "google.golang.org/appengine"

    _ "github.com/go-sql-driver/mysql"

    "github.com/jinzhu/gorm"
    "sample/app/models"
)

func ConnectDB(gae bool) *gorm.DB {

    var uri string

    if gae {
        connectionName := os.Getenv("CLOUDSQL_CONNECTION_NAME")
        user := os.Getenv("CLOUDSQL_USER")
        password := os.Getenv("CLOUDSQL_PASSWORD")

        if appengine.IsDevAppServer() {
            uri = "<LOCAL_MYSQL_URI>/<TABLE_NAME>"
        } else {
            uri = fmt.Sprintf("%s:%s@cloudsql(%s)/<TABLE_NAME>", user, password, connectionName)
        }

    } else {
        uri = "<LOCAL_MYSQL_URI>/<TABLE_NAME>"
    }

    db, err := gorm.Open("mysql", uri)
    if err != nil {
        panic(err)
    }

    return db
}

func InitDB(gae bool) {
    db := ConnectDB(gae)

    db.AutoMigrate(&sample.Hoge{})
}

これをローカルで立ち上げるためのmain.goか、GAEで立ち上げるためのgae/init.goでInit関数を呼ぶときに引数のboolを分けて呼ぶという感じでした。GAE/Goだとmainパッケージのmain()関数を含むファイルが使えなかったり、init()関数が最初に呼ばれたりと色々勝手が違いますよね。あとgin周りだとGAEではrouter.Run()関数ではなくてhttp.Handle()関数に渡すようにしないと動かなかったりしました。

// main.go
package main

import (
    "sample/app"
)

func main() {
    router := app.Init(false)
    router.Run(":8888")
}


// gae/init.go
package gae

import (
    "talknotifier/app"
    "net/http"
)

func init() {
    router := app.Init(true)
    http.Handle("/", router)
}

ModelとかControllerを扱うエンドポイントを作る

後はModelとControllerを立ててinit()関数内にルーティングを生やしていく感じで実装しました。

// models/hoge.go

package models

type Hoge struct {
    ID int `gorm:"NOT NULL;UNIQUE" json:"id"`
    Name string `gorm:"NOT NULL" binding:"required" json:"name"`
}


// controllers/hogeController.go

package controllers

import (
    "github.com/gin-gonic/gin"
    "sample/app/models"
    "github.com/jinzhu/gorm"
)

func GetAll(c *gin.Context, db *gorm.DB) {
    var hogeArray []models.Hoge
    db.Select("*").Find(&hogeArray)
    c.JSON(200, hoges)
}

// appInit.go - init()内

router.GET("/api/hoge", func(c *gin.Context) {
    controllers.GetAll(c, db)
})

REST APIじゃなくてHTMLテンプレートのような静的ファイルを返したいときはgih.Context.HTMLで返してあげるようにします

router.GET("/", func(c *gin.Context) {
    c.HTML(http.StatusOK, "index.tmpl", gin.H{})
})

起動・デプロイ

ローカルで動かしたいときには普通に go build してから出力されたファイルを実行するようにしたり、IntelliJ/Goglandの設定をしてデバッガを走らせたりしていました。

GAEで動かしたいときには goapp deploy gae をすればGoogle Cloud SDKのインストール時に設定していたアカウントにGAEのインスタンスが立ち上がって稼働するので、結構お気軽に立てられていいなーって感じでやってました。GAEデプロイ中のインスタンスでログを取ろうとするとよくあるlog.Printf()関数ではなくgoogle.golang.org/appengine/logのlog.Infof()関数とかにappengine.Contextを引数で渡す必要があったりします。うーん…。

まとめ

当時は大慌てで実装したので色んなところに気づかず実装したりしていましたが、いざ振り返って自分のコードを見ると慣れていないにしても結構無茶苦茶なコードを書いてるなぁ…って思いました。あとGAEなんだかんだ便利ですね。とりあえず雑に動くものをデプロイしたいときにぱぱっと使えるのはいいなって思います。ネイティブ開発だと動く実機を用意したりしないと行けないのでコストが嵩む嵩む…。

結構腰を据えてGolang勉強したいなーってこの開発で思えるようになったので @puhitaku と @orisano にもらったプログラミング言語GoとGo言語によるWebアプリケーション開発を使ってちょくちょく勉強しています。お二人まじでありがとう。