- 投稿日:2019-02-28T22:39:11+09:00
RubyのライブラリをGoに移植したくなった話
TL;DR
- Rubyのライブラリである、RedBlocks をGoに移植している話
- Rubyの継承などはEmbeddingで解決した
- 仕方なくReflectionを使ってしまったが案外速度は問題なさそう
何を移植したくなったか
RubyにはRedisのset/zsetの操作(
SINTERSTORE
,ZINTERSTORE
)とデータの取得を宣言的に扱うためのライブラリであるRedBlocks というのが存在します。
Readmeからリンクされている[ブログ](を見てみるとclass RegionSet < RedBlocks::Set def initialize(region) raise ArgumentError unless Project::REGIONS.include?(region) @region = region end def key_suffix @region end def get Project.where(region_cd: @region).pluck(:id) end endregion1_set = RegionSet.new('hokkaido') region2_set = RegionSet.new('kanto') regions_set = RedBlocks::UnionSet.new([region1_set, region2_set]) regions_set.ids #=> [921, 324, 21, 39, 101]http://takenos.link/post/173086712782/redis-sorted-set-in-oop
このような形で、北海道もしくは関東の募集(=
Project
)が取得できます。実際Redisを直に触ってしまうとこのような計算はもっと複雑になってしまいます。
これを使い、Goでも同じように楽にRedisの操作がしたくなりました。何が問題か
問題点は基本的に2つありました。
1. Rubyの継承をどうGoで再現するか
2. クラス名(GoではStruct名)をどう取得するか基本的に
RedBlocks
は継承を前提としており、RedBlocks::Set
の場合、3つのメソッドを定義するだけでids
といったメソッドが自動で生えてきます。
これをGoでどうやるべきでしょうか。また、RedBlocksでは半自動でキー名を作成してきます。
基本的にキーはクラス名とユーザーが設定するキーからなります。RedBlocks::Set#key 。
そのうち、クラス名に関してはGoではどう取るべきかが悩ましいです。できたもの
RedBlocksをGoで実装したものがredblocks-go になります。
先程のRegionを作り、北海道または関東のものを取ってくる例がこちらになります。
func NewRegionSet(region string) set.Set { return regionSetImp{region} } type regionSetImp struct { region string } func (r regionSetImp) KeySuffix() string { return r.region } func (r regionSetImp) Get(ctx context.Context) ([]set.IDWithScore, error) { ... } func (r regionSetImp) CacheTime() time.Duration { return time.Second * 10 } store := store.NewRedisStore(newPool()) tokyo := compose.Compose(NewRegionSet("tokyo"), store) osaka := compose.Compose(NewRegionSet("kanto"), store) set := operator.NewUnionSet(store, time.Second*100, tokyo, kanto) set.IDs(ctx, options.WithPagenation(0, -1))どう移植したか
Rubyの継承をどうGoで再現するか
GoにはEmbeddingというのがあります。Goのドキュメントにもあります https://golang.org/doc/effective_go.html#embedding 。
redblocks-goの例を出すと、次のようになりますtype Set interface { KeySuffix() string Get(ctx context.Context) ([]IDWithScore, error) CacheTime() time.Duration NotAvailableTTL() time.Duration // NotAvailableTTL < CacheTime. For processing }https://github.com/rerost/redblocks-go/blob/master/pkg/set/set.go
type ComposedSet interface { set.Set Key() string Update(ctx context.Context) error Available(ctx context.Context) (bool, error) Warmup(ctx context.Context) error IDs(ctx context.Context, opts ...options.PagenationOption) ([]store.ID, error) IDsWithScore(ctx context.Context, opts ...options.PagenationOption) ([]store.IDWithScore, error) }https://github.com/rerost/redblocks-go/blob/master/pkg/compose/set.go
このように、ユーザーが定義すべきものを
Set
というinterface
にし、それをembed
したComposedSet
というinterface
を定義しています。
で、Set
からComposedSet
の必要なメソッドを定義する関数をCompose
としています。クラス名(GoではStruct名)をどう取得するか
正直速度の観点から、Reflection をできるだけ使いたくないと思っていました。
しかし、実際クラス名の取得だけだとベンチマークを走らせてみると10%程度の違いしかなさそうなので問題ないと思っています。~/.go/src/github.com/rerost/redblocks-go/pkg/compose (rerost/not-use-reflection) $ go test -bench BenchmarkKey goos: darwin goarch: amd64 pkg: github.com/rerost/redblocks-go/pkg/compose BenchmarkKey-4 5000000 277 ns/op PASS ok github.com/rerost/redblocks-go/pkg/compose 1.870s~/.go/src/github.com/rerost/redblocks-go/pkg/compose (master) user $ go test -bench BenchmarkKey goos: darwin goarch: amd64 pkg: github.com/rerost/redblocks-go/pkg/compose BenchmarkKey-4 5000000 309 ns/op PASS ok github.com/rerost/redblocks-go/pkg/compose 2.060s終わりに
RedBlocksのGo実装はまだ部分的に機能が足りないのですが、まあまあ使えるものにはなったかと思います。
以上です。
- 投稿日:2019-02-28T22:13:27+09:00
Go 1.12 の開発環境と本番環境の Dockerfile を考える
はじめに
Go 1.12 がリリースされましたね!
TLS 1.3 の対応や Go Modules の改良がされたようです。
Go 1.12 is released - The Go Blog今回は Go Modules が含まれている Go 1.12 を使用して、開発環境と本番環境の Dockerfile の作成を考えてみます。
開発環境は docker-compose、本番環境は Kubernetes などのオーケストレーションツールにデプロイすることを想定した Dockerfile の作成を考えます。アプリケーションは、Go Modules を利用してライブラリ管理し、外部に HTTPS 通信するアプリケーションを想定して Dockerfile を作成しています。
Listen ポートは 8080 ポートです。環境
- macOS High Sierra Version 10.13.6
- Docker for Mac 2.0.0.3 (Engine: 18.09.2)
ディレクトリ構成
以下のようなルートディレクトリに main.go が存在するようなアプリケーションを想定しています。
├── Dockerfile // 開発環境の Dockerfile ├── Dockerfile.prod // 本番環境の Dockerfile ├── docker-compose.yml // 開発環境の docker-compose ├── go.mod ├── go.sum └── main.go開発環境
開発環境はソースコードが随時変更されるため、
fresh
というライブラリを用います。
ソースコード自体は、docker-compose で指定するルートを docker volume に割り当て、ホットリロードを実現します。Dockerfile
開発環境の Dockerfile の全体は以下のとおりです。
DockerfileFROM golang:1.12.0-alpine3.9 WORKDIR /go/src/app ENV GO111MODULE=on RUN apk add --no-cache \ alpine-sdk \ git \ && go get github.com/pilu/fresh EXPOSE 8080 CMD ["fresh"]Dockerfile の詳細
開発環境の Dockerfile の詳細を追ってみます。
FROM golang:1.12.0-alpine3.9イメージは、執筆時点(2019/02)の最新の Alpine Linux の Go イメージを指定しています。
WORKDIR /go/src/app
WORKDIR
でアプリケーションを実行するディレクトリを指定しています。ENV GO111MODULE=onGo 1.12 では、Go Modules を使用するために必要な環境変数
GO111MODULE
をon
にしています。RUN apk add --no-cache \ alpine-sdk \ git \ && go get github.com/pilu/freshパッケージとして一通りビルドに必要なものが入っている
alpine-sdk
と Go Modules でライブラリ取得時に内部的にgit
を利用しているのでgit
をインストールしています。
また、Go 開発のホットリロードで利用するfresh
をインストールしています。EXPOSE 80808080 ポートで Listen するアプリケーションなので、
EXPOSE
命令で 8080 ポートを明示しています。CMD ["fresh"]最後に
fresh
で起動することで、ホットリロードで起動する Dockerfile の完成です。docker-compose
開発環境の docker-compose ファイルは以下のとおりです。
docker-compose.ymlversion: '3' services: app: build: . volumes: - ./:/go/src/app ports: - "8080:8080"docker-compose の詳細
開発環境の docker-compose の詳細を追ってみます。
build: .
build
で ビルドする Dockerfile のディレクトリを指定しています。volumes: - ./:/go/src/app
volumes
で Dockerfile でも指定したWORKDIR
のディレクトリに対してマウントしています。こうすることでコンテナ内から開発マシンのディレクトリが見えるようになり、fresh
のビルド対象になります。ports: - "8080:8080"
ports
は、8080 ポートで Listen するアプリケーションなので、開発マシンとコンテナのポートを割り当てています。開発環境の実行
以下の docker-compose コマンドで起動し、
localhost:8080
にアクセスすることで開発が始められます。$ docker-compose up -d本番環境
本番環境は可能な限り小さいイメージを作るために、マルチステージビルドを用います。
Dockerfile
本番環境の Dockerfile の全体は以下のとおりです。
Dockerfile.prodFROM golang:1.12.0 as builder WORKDIR /go/src/app ENV GO111MODULE=on RUN groupadd -g 10001 myapp \ && useradd -u 10001 -g myapp myapp COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/app FROM scratch COPY --from=builder /go/bin/app /go/bin/app COPY --from=builder /etc/group /etc/group COPY --from=builder /etc/passwd /etc/passwd COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt EXPOSE 8080 USER myapp ENTRYPOINT ["/go/bin/app"]Dockerfile の詳細
本番環境の Dockerfile の詳細を追ってみます。
FROM golang:1.12.0 as builderイメージは Alpine Linux ではなくデフォルトのイメージに変更し、マルチステージビルドを用いるために
as builder
というステージ名を指定しています。
デフォルトのイメージに変更したのは、Go Modules のインストールに必要なgit
や Go ライブラリのインストールや Go のビルドに必要なパッケージが含まれているためです。
マルチステージビルドについては、ステージ名に任意の名前が付けられます。
こうすることで、次のステージでビルドした成果物をコピーすることができます。RUN groupadd -g 10001 myapp \ && useradd -u 10001 -g myapp myappここでは、ユーザーグループとユーザーを作成しています。
これは、次のステージで利用するアプリケーションの実行ユーザーを作成するために実行しています。
また、次のステージで利用するscratch
イメージは shell が実行できないため、ビルドステージでユーザーを作成しています。COPY go.mod go.sum ./ RUN go mod download COPY . .開発環境では volume にマウントしていましたが、本番環境ではアプリケーションをそのままコンテナ内にコピーしています。
そして、COPY
命令で全てのファイルをコピーせず、事前にgo.mod
とgo.sum
ファイルをコピーしています。
これは、コピー後にgo mod download
を実行してライブラリをインストールすることで、次回以降ライブラリの追加、go.mod
とgo.sum
に変更がなくソースコードの変更のみであればソースコード全体をコピーするCOPY . .
の行から実行され、それより前のステップはキャッシュが活用されスキップされます。
こうすることでキャッシュが有効活用されるので、インストールのステップをスキップし、ビルド速度が向上します。RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/appクロスコンパイル用の環境変数を指定し、
-ldflags="-w -s"
でバイナリを削減しています。FROM scratch前述のとおり
scratch
イメージは、shell も入っていない最小のイメージです。
小さなイメージを作るには良いですが、attach も出来ないので取り回しが悪い点は否めません。
状況によって、Alpine Linux のイメージを使うと良いと思います。COPY --from=builder /go/bin/app /go/bin/app前のステージでビルドしたアプリケーションを次のステージにコピーしています。
--from=[NAME]
をコピーするファイルの前に宣言することでコピー元のステージが指定できます。COPY --from=builder /etc/group /etc/group COPY --from=builder /etc/passwd /etc/passwdここでは、ビルドステージからユーザーグループとユーザーのファイルをコピーしています。
このファイルをコピーすることで、ユーザーグループとユーザーを追加したことになるので、実行ユーザーを変更することが可能になります。COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crtここでは、HTTPS 通信をするために証明書をコピーしています。
HTTPS 通信をしないのであれば不要です。USER myapp作成したユーザーを
myapp
ユーザーに変更しています。
デフォルトでは root ユーザーで実行されるため、非 root ユーザーで実行するためにユーザーを変更しています。ENTRYPOINT ["/go/bin/app"]最後に
ENTRYPOINT
でアプリケーションを実行しています。
ENTRYPOINT
でコマンドを指定することで、引数を用いるアプリケーションであれば実行時に引数がそのまま指定できます。本番環境の実行
本来であれば CI でビルドし、CD でコンテナオーケストレーション環境にデプロイしますが、確認のために開発マシンで起動します。
Dockerfile をビルドします。
$ docker build -t myapp -f Dockerfile.prod .8080 ポートでビルドしたコンテナを起動します。
$ docker run --name myapp -p 8080:8080 -it -d myappdocker top コマンドで実行ユーザーを確認してみます。
$ docker top myapp PID USER TIME COMMAND 13606 10001 0:00 /go/bin/app
USER
が10001
で実行されていることが分かります。番外編
本番環境の例では、ビルドステージにデフォルトのイメージを使用して作成しました。
ただし、デフォルトのイメージを採用することによるデメリットも存在します。
以下に示すのは、デフォルトの Go と Alpine Linux の Docker イメージを比較したものです。golang 1.12.0-alpine3.9 d4953956cf1e 17 hours ago 347MB golang 1.12.0 c4f8e4c91496 17 hours ago 772MB見てわかるとおり二つのイメージには、400MB 程度のイメージサイズの差があります。
これはつまり、Docker イメージを pull するときのダウンロード速度に影響します。
同じベースイメージを使っていれば再利用されますが、Go のバージョンアップをするときにベースイメージを変更する必要があるため、その時点では全体のビルド速度が低下します。ビルドステージに Alpine Linux のイメージを使用した Dockerfile の全体は以下のとおりです。
Dockerfile.prodFROM golang:1.12.0-alpine3.9 as builder WORKDIR /go/src/app ENV GO111MODULE=on RUN addgroup -g 10001 -S myapp \ && adduser -u 10001 -G myapp -S myapp RUN apk add --no-cache \ alpine-sdk \ git COPY . . RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/app FROM scratch COPY --from=builder /go/bin/app /go/bin/app COPY --from=builder /etc/group /etc/group COPY --from=builder /etc/passwd /etc/passwd COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt EXPOSE 8080 USER myapp ENTRYPOINT ["/go/bin/app"]変更点としては、イメージをデフォルトから Alpine Linux のイメージに変更しています。
FROM golang:1.12.0-alpine3.9 as builderまた、Debian 系と Alpine Linux とでディストリビューションが異なるため、ユーザーグループとユーザー追加のコマンドが異なります。
RUN addgroup -g 10001 -S myapp \ && adduser -u 10001 -G myapp -S myappそして開発環境同様、
alpine-sdk
とgit
をインストールし、ファイルを全てコピーするように変更しています。RUN apk add --no-cache \ alpine-sdk \ git COPY . .Alpine Linux を採用するメリットは、イメージサイズの小ささによるダウンロードの速さです。
毎度イメージのダウンロードからパッケージのインストールまで行うのであれば、デフォルトのイメージに比べイメージが小さい分速く終わります。しかしながら番外編の Dockerfile にもデメリットがあり、本番環境の例で採用しなかったのは以下の理由からです。
Go Modules のインストールには、git
に依存しているためgit
のインストールが必要です。
また、Go ライブラリのインストールや Go のビルドにはgcc
などのパッケージが必要な場合があるため、alpine-sdk
をインストールしています。
Alpine Linux のイメージにはそれらがデフォルトでインストールされていないため、事前にインストールする必要があります。
しかしそれらをインストールしてしまうと Docker のキャッシュがリセットされ、ソースコードを変更する度にパッケージのインストールが再度実行されてしまいます。
本番環境の例で紹介したデフォルトのイメージであればgit
や必要なパッケージがインストールされているので、Docker のキャッシュを有効活用してソースコードのコピーより前のステップをスキップすることができます。
したがって、毎度のソースコードの変更が主なビルドを考えると Docker のキャッシュが有効なデフォルトのイメージが良いと思います。Go のライブラリを使用しないなど、パッケージをインストールしないのであれば、Alpine Linux を選択するメリットがあると思います。
さいごに
ここまで、Go 1.12 の開発環境と本番環境の Dockerfile の最適化を考えました。
次には Go 1.13 のリリースが控えていますが、Go Modules 自体は Go 1.13 からデフォルトで有効になるとのことなので、GO111MODULE
の環境変数が不要になる程度で概ね使えるのではないでしょうか。
また、Docker 18.09 で正式に追加されたBuildKit
を試していないので、BuildKit
を使うことでより最適化できるかもしれません。参考
Go v1.11 + Docker + fresh でホットリロード開発環境を作って愉快なGo言語生活
Create the smallest and secured golang docker image based on scratch
Non-privileged containers based on the scratch image
Use Multi-Stage Builds to Inject CA Certs
- 投稿日:2019-02-28T21:45:30+09:00
Goで、DBレコードから良い感じにJSON生成する�
こんな感じのレスポンスを期待されている時に、JSONをどう生成したかという話。
$ echo '{"books": [{"id":"1","title":"Story about Ruby","amount":20,"created_at":1551354984},{"id":"2","title":"Story about Pearl","amount": -30,"created_at":1551354984}]}' | jq . { "books": [ { "id": "1", "title": "Story about Ruby", "amount": 20, "created_at": 1551354984 }, { "id": "2", "title": "Story about Pearl", "amount": -30, "created_at": 1551354984 } ] }なんか変なデータ構造だけど、実際のプロジェクトコードの構造体名を変えてたりなので、まぁ気にせず?
// 複数テーブルからのデータを統合した情報 type BookWithAmount struct { Book *models.Book Amount int64 } // 蔵書一覧レスポンス type booksResponse struct { Books []bookRecord `json:"books"` } // 蔵書一覧レスポンスに含まれるレコード type bookRecord struct { ID string `json:"id"` Title string `json:"title"` Amount int64 `json:"amount"` CreatedAt int64 `json:"created_at"` } // レスポンス用に蔵書レコードを整形して返す func getBooksResponse(books []BookWithAmount) booksResponse { records := []bookRecord{} for _, b := range books { r := bookRecord{} r.ID = strconv.FormatInt(b.Book.ID, 10)// 文字列希望 r.Title = b.Book.Title r.Amount = map[bool]int64{true: b.Amount, false: -b.Amount}[b.Book.Type == Order]// 場合によってマイナス値に r.CreatedAt = b.Book.CreatedAt.Unix()// タイムスタンプ希望 records = append(records, r) } return booksResponse{Books: records} } func main() { // ごにょごにょ...(DBからレコード取得) // レスポンス整形 booksResponse := getBooksResponse(books) json, err := json.Marshal(booksResponse) if err != nil { return } // ゴニョゴニョ...(レスポンスとして返す) }複数テーブルから必要な情報を抜き出して、クライアントが期待する形式にできました???
- 投稿日:2019-02-28T13:30:49+09:00
Goモジュールモードでモジュール内に作ったモジュールを扱う
GoModule、大変便利ですね。
便利なのですがちょっと大きめのシステムを組もうとした時に頭を悩ませることが出てくるかと思います。ちょっと大きめのシステムになると機能別にディレクトリを作って構成を分割したいという要求が出てくるかと思うのですが、
素直にGoModuleを利用するとこういったケースを上手く対応することができないのです。想定する構成
sampleモジュールの中に、module1、module2というsampleモジュールから利用するためのサブモジュールが定義されています。
sample/ module1/ module1.go go.mod module2/ module2.go go.mod main.go go.modimportする
このような構成にした時、このモジュール内にあるモジュールは呼び出し側からは以下のようにインポートします。
main.gopackage main import ( "github.com/sample/module1" "github.com/sample/module2" )このように書くとサブモジュールのコードを取得するためにgithub1へアクセスしてコードを取得してきます。
githubにアクセスするということはgithubへコミットされていないコードは反映されないということです。一般的に、サブモジュールはその場で変更したら即座に利用側へ反映されてほしいはずです。
モジュール内モジュールは、ローカルのコードを参照するようにする
この問題は、
go.mod
ファイルにreplace
というキーワードを利用してモジュール名とパスの関連をつくることで解決することができます。go.modreplace ( github.com/sample/module1 => ./module1 github.com/sample/module2 => ./module2 )このように書いておくと、GoModuleは以下のようなrequire文を追記してくれ、無事モジュール内モジュールのコードはローカルのコードを参照してビルドしてくれるようになります。
go.modreplace ( github.com/sample/module1 => ./module1 github.com/sample/module2 => ./module2 ) require ( github.com/sample/module1 v0.0.0-00010101000000-000000000000 github.com/sample/module2 v0.0.0-00010101000000-000000000000 )参考リンク
この例はgithubを利用しているため、githubと記述しています。 ↩
- 投稿日:2019-02-28T10:51:33+09:00
Ruby から FFI で Go にバイト列のポインタを渡す
Ruby を書いていると、バイナリ処理を Go で書いて FFI 呼び出すことでいい感じにしたくなることがあります。
バイナリの受け渡しをする際は、String を使うと NULL 文字で終端されてしまう 1 ため、ポインタで渡してあげる必要があります。
C での書き方は ffi/ffi の wiki に載っていますが、Go での書き方は調べてもあまり出てこないので、ここに書いておきます。
main.gopackage main import ( "C" "bytes" "unsafe" ) //export hoge func hoge(pointer unsafe.Pointer, size C.int) *C.char { // ポインタからバイト列を取り出す var data []byte = C.GoBytes(pointer, size) // バイト列を使って何か処理する var result string = do_something(data) // Go の string を C の string に変換して返す return C.CString(result) } func do_something(data []byte) string { return "xxx" } func main() { }hoge.rbrequire 'ffi' module Hoge extend FFI::Library # 上記の Go を shared library にコンパイルしたファイル ffi_lib 'hoge.so' attach_function :hoge, [:pointer, :uint], :string class << self # ポインタを渡す必要があるので private にしてインターフェイスを call_hoge に統一する private :hoge # 文字列をポインタに変換してから hoge を呼ぶ def call_hoge(string) mem_buf = FFI::MemoryPointer.from_string(string) hoge(mem_buf, data.bytesize) end end end
Binary data · ffi/ffi Wiki https://github.com/ffi/ffi/wiki/Binary-data ↩
- 投稿日:2019-02-28T08:51:59+09:00
【はじめてのGO】gin + gormでシンプルなCRUDなREST APIを作成する
GO言語の勉強をはじめてまだ1ヶ月もないのですが、シンプルにCRUDするAPIを作りたいと思い、Go言語でのAPIの作り方(この記事ではginを使います)、DB接続、そしてcurlで叩いてCRUDするまでの過程をなぞっていきます。
使用するライブラリ
- gin ・・・ Go言語でのWAF(Web Application Framework)。API作るのに使います。
- gorm ・・・ORM。DBとのやりとりで使います。
- json ・・・ 標準ライブラリですが一応。marshal/unmarshalメソッドを用いてjsonと構造体の変換を担います。
各種ライブラリを試す
CRUD APIの作成に向けて、上記3つのライブラリについて先に簡易な説明をしていきます。
gin
ginはGo言語のWAFです。他にもGO言語には様々なWAFがあるようなのですが、ginの記事が多くあったので今回はこれを使ってみました。
試しに、Hello Worldを返すだけの簡易なAPIを作ってみます。
ますは、インストール。$ go get github.com/gin-gonic/gin
以下のようにポート8080で受け取るように作成します。
package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.GET("/hello", func(c *gin.Context) { c.String(http.StatusOK, "Hello world") }) r.Run(":8080") }実行し、curlで叩いてみましょう。
$go run main.go [GIN-debug] GET /hello --> main.main.func1 (3 handlers) [GIN-debug] Listening and serving HTTP on :8080この状態でもう一つターミナルを立ち上げて、curlで叩きます。
$ curl localhost:8080/hello Hello world
バッチリ返ってきました。こんな風にしてAPIを作っていきます。
gorm
次はORMであるgormです。
gormを使う前に、db接続の部分をみてみましょう。package main import ( "fmt" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) func gormConnect() *gorm.DB { DBMS := "mysql" USER := <DB user> PASS := <DB Password> PROTOCOL := tcp(<DBのIPアドレス>:<PORT>) DBNAME := <DB NAME> CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME db, err := gorm.Open(DBMS, CONNECT) if err != nil { panic(err.Error()) } fmt.Println("db connected: ", &db) return db } func main() { db := gormConnect() defer db.Close() db.LogMode(true) }接続ができたら、次はmigrationです。usersテーブルを作成します。
usersのモデルを作成します。type User struct { gorm.Model Name string `json:"name"` Age int `json:age` Birthday time.Time `json:birthday` }
gorm.Model
はベースとなるモデル定義で、IDやCreatedAtなどを自身のモデルに埋め込めます。
http://doc.gorm.io/models.html#model-definitionmain.goに下記を追記します。
db.Set("gorm:table_options", "ENGINE=InnoDB") db.AutoMigrate(&User{})実行すると、mysqlにusersテーブルが作成されます。
mysql> show columns from users; +------------+------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+------------------+------+-----+---------+----------------+ | id | int(10) unsigned | NO | PRI | NULL | auto_increment | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | | deleted_at | timestamp | YES | MUL | NULL | | | name | varchar(255) | YES | | NULL | | | age | int(11) | YES | | NULL | | | birthday | timestamp | YES | | NULL | | +------------+------------------+------+-----+---------+----------------+SELECTやUPDATE等については他にもたくさん記事があるので、そちらを参考にしました。
参考にした記事:
【GORM】Go言語でORM触ってみた
Go言語のGORMを使ってみた①記事の下部に最終的なCRUD APIのソースを置くので、詳細はそちらをご参照ください。
json(Marshal/Unmarshal)
次は、jsonと構造体の変換についてです。
encoding/jsonパッケージのMarshal/Unmarshalというメソッドを用います。Unmarshal
jsonから構造体に変換します。
まずは、jsonファイルを作成します。test.json[ { "name": "Yamada", "age": 22, "birthday": "1996-08-06T00:00:00+09:00" } ]次に、このjsonファイルを読み込み、Unmarshalします。Unmarshalは構造体に記載したjsonタグに対応したフィールドにマッピングされます。
//jsonファイルの読み込み func main() { jsonBytes, err := ioutil.ReadFile("test.json") if err != nil { log.Fatal(err) } //json(bytes)→構造体 users := []User{} if err := json.Unmarshal(jsonBytes, &users); err != nil { fmt.Println("Unmarshal error:", err) return } fmt.Println("user name ->", users[0].Name) //YAMADA fmt.Println("user age->", users[0].Age) //22 fmt.Println("user birthday->", users[0].Birthday) //1996-08-06 00:00:00 +0900 JST }Marshal
次は反対に構造体→jsonをやってみます。
func main() { //User型の構造体→json user := User{ Name: "Itou", Age: 10, Birthday: time.Date(2009, 2, 19, 12, 0, 0, 0, time.Local), } jsonBytes, err := json.Marshal(user) if err != nil { fmt.Println("Marshal error:", err) return } fmt.Println("user ->", string(jsonBytes)) }結果は以下のようになります。
$ go run main.go user -> {"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"name":"Itou","Age":10,"Birthday":"2009-02-19T12:00:00+09:00"}必要なものは整ったので、早速APIを作っていきましょう!
CRUDのREST APIを作成する
CREATE
まずはCREATEから。
CreaetdAtやUpdatedAtはバック側でtimeを用いて追記するようにしました。//CREATE r.POST("/user", func(c *gin.Context) { user := User{} now := time.Now() user.CreatedAt = now user.UpdatedAt = now err := c.BindJSON(&user) if err != nil { c.String(http.StatusBadRequest, "Request is failed: "+err.Error()) } db.NewRecord(user) db.Create(&user) if db.NewRecord(user) == false { c.JSON(http.StatusOK, user) } })無事に登録されれば、jsonで作成したレコードを返すようにしています。
curlで叩いみると正しく帰ってきているのがわかります。$ curl localhost:8080/user curl -X POST -H "Content-Type: application/json" -d '{"name":"sensuikan1973", "age":15, "birthday": "2007-02-04T03:18:45.000Z"}' {"ID":2,"CreatedAt":"2019-02-23T23:28:48.832877+09:00","UpdatedAt":"2019-02-23T23:28:48.832877+09:00","DeletedAt":null,"name":"sensuikan1973","age":15,"birthday":"2007-02-04T03:18:45Z"}また、以下を記載しておくと、実際に発行されたクエリを確認できるので記載しておきましょう。
db.LogMode(true)READ
次はデータの取得です。全データ取得と1レコードの取得でやってみます。
//READ //全レコード r.GET("/users", func(c *gin.Context) { users := []User{} db.Find(&users) // 全レコード c.JSON(http.StatusOK, users) }) //1レコード r.GET("/user/:id", func(c *gin.Context) { user := User{} id := c.Param("id") db.Where("ID = ?", id).First(&user) c.JSON(http.StatusOK, user) })全レコードはFindを用いて配列で取得、1レコードの例はidをparamから拾うようにし、Whereで指定した後、Firstで最初の1レコードだけを出すようにしています。
UPDATE
次はUpdateです。idをparamsから受け取り、更新するデータはjsonで取得しています。
//UPDATE r.PUT("/user/:id", func(c *gin.Context) { user := NewUser() id := c.Param("id") data := NewUser() if err := c.BindJSON(&data); err != nil { c.String(http.StatusBadRequest, "Request is failed: "+err.Error()) } db.Where("ID = ?", id).First(&user).Updates(&data) })DELETE
gormでは、削除フラグ
deleted_at
を立て、DELETE()を用いることで自動で論理削除として削除を実行してくれます。//DELETE r.DELETE("/user/:id", func(c *gin.Context) { user := NewUser() id := c.Param("id") db.Where("ID = ?", id).Delete(&user) })おわりに
今回、GoでCRUDなRESTful APIを作成するために、便利なライブラリとしてginとgormの説明、そして実際の作成を行いました。
gormの仕組みは非常にわかりやすかったのですが、クエリが複雑になってくると記載が難しそうな気がしました。今回のソースは以下に置いています。
まだ初学者ゆえ、修正点等あればご連絡頂けると幸いです。
https://github.com/daitasu/go-crud-first/blob/master/main.go参考記事
記事中にもいくつか記載しましたが、以下を参考にさせて頂きました。
【GORM】Go言語でORM触ってみた
Go言語のGORMを使ってみた①
Go言語製WAF GinでWebアプリを作ってみる
gorm Doc