Go + Gin + GormでREST APIをさくっと実装してGAEにデプロイしてみました。
そもそもGoの本やGoに関する情報を読んだ感じ、多くのことが標準ライブラリでまかなえるため、基本はFWやORMを使わないのが「Goらしい手法」とのことなので、次は標準ライブラリのみでも実装してみたいと思います。
参考:https://employment.en-japan.com/engineerhub/entry/2018/06/19/110000
参考:https://golang.org/doc/effective_go.html
Ginとは
Ginとは軽量かつシンプルなインターフェイスが特徴のGoのWebアプリケーションフレームワークです。
また、GoのWebアプリケーションの中では比較的初期から開発されているので安定して使われています。
Gormとは
Goでよく使われているORMの一種です。
そもそもORM(O/Rマッピング)とは、オブジェクト指向言語におけるオブジェクトと、リレーショナルデータベースにおけるレコードとを対応づけることです。
ORMによって、リレーショナルデータベースのレコードがオブジェクトとして直感的に扱えるようになり、リレーショナルデータベースにアクセスするプログラムを記述する処理を容易にすることが可能になります。
プロジェクト構成
Getパラメータで本検索をして、その結果をJSON形式で返す簡単なAPIを実装していきます。
以下の記事を参考にプロジェクト構成を組んでみました。元々Railsを書いていたこともあり、この構成が今のところしっくりきました(今後勉強していくうちによりより構成が見つかる気がします)。
参考:https://qiita.com/Asuforce/items/0bde8cabb30ac094fcb4
sample-go-api
├── controller
│ └──book_controller.go
├── db
│ └── db.go
├── entity
│ └── book.go
├── main.go
├── server
│ └── server.go
├── service
│ └── book_service.go
├── .env.development
├── .env.production
├── .gcloudignore
├── .gitgnore
├── app.yaml
├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
DB・モデル実装
ローカル用にDockerでさくっとMySQLを立ち上げてDBを作成します。
・docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 | version: '3' services: mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: db MYSQL_USER: user MYSQL_PASSWORD: password ports: - "3306:3306" |
docker-compose.ymlができたら、起動します。
1 | $ docker-compose up -d |
次にGormとMySQLをgo getでインストールします。
1 2 | $ go get github.com/jinzhu/gorm $ go get github.com/jinzhu/gorm/dialects/mysql |
本モデルを作成します。
booksテーブルのカラムを定義します。
・entity/book.go
1 2 3 4 5 6 7 8 9 10 11 12 13 | package entity import ( "time" ) type Book struct { Id uint Title string `gorm:"size:128"` Category int Author string `gorm:"size:64"` CreatedAt time.Time } |
DB周りを実装していきます。今回はGAEに本番環境をデプロイするので、developmentとproductionと2つの環境を作りました。そのため、godotenvライブラリを使って実行する環境を取得します。
実行環境に合ったDB接続を行います。ローカルはDockerで立ち上げたMySQL、本番はCloud SQLを使っています。
DB初回起動時にマイグレーションが行われるようにしています。
・db/db.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | package db import ( "os" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" "github.com/joho/godotenv" "github.com/sample/sample-go-api/entity" ) var ( db *gorm.DB err error ) // DB初期化 func Init() { // 実行環境取得 env := os.Getenv("ENV") if "production" == env { env = "production" } else { env = "development" } // 環境変数取得 godotenv.Load(".env." + env) godotenv.Load() // DB接続 db, err = gorm.Open("mysql", os.Getenv("CONNECT")) if err != nil { panic(err) } autoMigration() } // DB取得 func GetDB() *gorm.DB { return db } // DB接続終了 func Close() { if err := db.Close(); err != nil { panic(err) } } // マイグレーション func autoMigration() { db.AutoMigrate(&entity.Book{}) } |
ローカル環境用のenv。
・.env.development
1 | CONNECT=user:password@tcp(localhost:3306)/db?parseTime=true |
本番環境用のenv。
・.env.production
1 | CONNECT=user:password@unix(/cloudsql/sample-project:sample-zone:sample-instance)/sample_db?parseTime=true |
Bookサービスの実装
検索処理を実装していきます。送られてきたGETパラメータからそれぞれのカラム検索を行います。
Gormを使っていますが、このくらいのSQLならわざわざORMを使わなくても良い気がしますね。
・service/book_service.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | package book import ( "github.com/sample/sample-go-api/db" "github.com/sample/sample-go-api/entity" ) type Service struct{} type book entity.Book type Parameter struct { title string category int author string } // 検索 func (s Service) Search(title string, category int, author string) ([]Book, error) { // DB接続 db := db.GetDB() // 本モデルから作成 var book []Book // パラメータセット p := Parameter{title, category, author} // DB接続確認 if err := db.Take(&book).Error; err != nil { return nil, err } // 本検索クエリ tx := db tx = tx.Find(&book) // タイトル if p.title != "" { tx = tx.Where("title = ?", p.title).Find(&book) } // カテゴリ if p.category != 0 { tx = tx.Where("category = ?", p.category).Find(&book) } // 著者 if p.author != "" { tx = tx.Where("author = ?", p.author).Find(&book) } return book, nil } |
Bookコントローラの実装
Ginをgo getでインストールしておきます。
1 | $ go get -u github.com/gin-gonic/gin |
コントローラを実装します。*gin.ContextでGETパラメータを取得してサービスの検索メソッドに投げています。
検索結果を受け取ってJSON形式で返しています。
・controller/book_controller.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package book import ( "fmt" "net/http" "strconv" "github.com/gin-gonic/gin" book "github.com/sample/sample-go-api/service" ) type Controller struct{} // 検索 GET /books func (pc Controller) Index(c *gin.Context) { // パラメータ取得 title := c.Query("title") category, _ := strconv.Atoi(c.Query("category")) author := c.Query("author") // 検索処理 var s book.Service p, err := s.Search(title, category, author) // 検索結果を返す if err != nil { c.AbortWithStatus(http.StatusNotFound) fmt.Println(err) } else { c.JSON(http.StatusOK, p) } } |
サーバ周り実装
ルーティング・CORS設定・サーバ起動を実装しています。
・server/server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | package server import ( "net/http" "github.com/gin-gonic/gin" book "github.com/sample/sample-go-api/controller" ) // 初期化 func Init() { r := router() r.Run() } // ルーティング func router() *gin.Engine { r := gin.Default() // CORS対応 r.Use(CORS()) // ルーティング u := r.Group("/books") { ctrl := book.Controller{} u.GET("", ctrl.Index) } return r } // CORS func CORS() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(http.StatusNoContent) return } c.Next() } } |
main.goの実装
最後にmain.goからDBとサーバを起動して完了です。後は.gitignoreにenvファイルを追加しておきましょう。
1 2 3 4 5 6 7 8 9 10 11 | package main import ( "github.com/sample/sample-go-api/db" "github.com/sample/sample-go-api/server" ) func main() { db.Init() server.Init() } |
GAEにデプロイする
完成したAPIをGAEにデプロイします。
Goのバージョンは1.12、インスタンスクラスは一番下のF1を指定します(デフォルトF1ですが)。
正規表現 /.* にパスが一致するURLへのすべてのリクエストはmainパッケージのhandle関数で処理されます。
ENVでproductionを指定することでCloud SQLに接続するようにしています。
参考:https://cloud.google.com/appengine/docs/standard/go/config/appref?hl=ja
1 2 3 4 5 6 7 8 | runtime: go112 service: sample-go-api-production instance_class: F1 handlers: - url: /.* script: auto env_variables: ENV: production |
以下のコマンドでGAEにデプロイして完了です。
1 | $ gcloud app deploy |