20190228のGoに関する記事は6件です。

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
end
region1_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

https://github.com/rerost/redblocks-go/blob/27bd7d9590e6de38653ebe9235b3584a259fec83/pkg/compose/update.go#L28

~/.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

https://github.com/rerost/redblocks-go/blob/8a56b44f78b815bcf9cc2b9a4dad26e8ef457e2f/pkg/compose/update.go#L29

終わりに

RedBlocksのGo実装はまだ部分的に機能が足りないのですが、まあまあ使えるものにはなったかと思います。
以上です。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 の全体は以下のとおりです。

Dockerfile
FROM 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=on

Go 1.12 では、Go Modules を使用するために必要な環境変数 GO111MODULEon にしています。

RUN apk add --no-cache \
        alpine-sdk \
        git \
    && go get github.com/pilu/fresh

パッケージとして一通りビルドに必要なものが入っている alpine-sdk と Go Modules でライブラリ取得時に内部的に git を利用しているので git をインストールしています。
また、Go 開発のホットリロードで利用する fresh をインストールしています。

EXPOSE 8080

8080 ポートで Listen するアプリケーションなので、EXPOSE 命令で 8080 ポートを明示しています。

CMD ["fresh"]

最後に fresh で起動することで、ホットリロードで起動する Dockerfile の完成です。

docker-compose

開発環境の docker-compose ファイルは以下のとおりです。

docker-compose.yml
version: '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.prod
FROM 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.modgo.sum ファイルをコピーしています。
これは、コピー後に go mod download を実行してライブラリをインストールすることで、次回以降ライブラリの追加、 go.modgo.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 myapp

docker top コマンドで実行ユーザーを確認してみます。

$ docker top myapp
PID                 USER                TIME                COMMAND
13606               10001               0:00                /go/bin/app

USER10001 で実行されていることが分かります。

番外編

本番環境の例では、ビルドステージにデフォルトのイメージを使用して作成しました。
ただし、デフォルトのイメージを採用することによるデメリットも存在します。
以下に示すのは、デフォルトの 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.prod
FROM 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-sdkgit をインストールし、ファイルを全てコピーするように変更しています。

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
    }

    // ゴニョゴニョ...(レスポンスとして返す)
}

複数テーブルから必要な情報を抜き出して、クライアントが期待する形式にできました?‍??

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goモジュールモードでモジュール内に作ったモジュールを扱う

GoModule、大変便利ですね。
便利なのですがちょっと大きめのシステムを組もうとした時に頭を悩ませることが出てくるかと思います。

ちょっと大きめのシステムになると機能別にディレクトリを作って構成を分割したいという要求が出てくるかと思うのですが、
素直にGoModuleを利用するとこういったケースを上手く対応することができないのです。

想定する構成

sampleモジュールの中に、module1、module2というsampleモジュールから利用するためのサブモジュールが定義されています。

sample/
  module1/
    module1.go
    go.mod
  module2/
    module2.go
    go.mod
  main.go
  go.mod

importする

このような構成にした時、このモジュール内にあるモジュールは呼び出し側からは以下のようにインポートします。

main.go
package main

import (
    "github.com/sample/module1"
    "github.com/sample/module2"
)

このように書くとサブモジュールのコードを取得するためにgithub1へアクセスしてコードを取得してきます。
githubにアクセスするということはgithubへコミットされていないコードは反映されないということです。

一般的に、サブモジュールはその場で変更したら即座に利用側へ反映されてほしいはずです。

モジュール内モジュールは、ローカルのコードを参照するようにする

この問題は、go.modファイルにreplaceというキーワードを利用してモジュール名とパスの関連をつくることで解決することができます。

go.mod
replace (
    github.com/sample/module1 => ./module1
    github.com/sample/module2 => ./module2
)

このように書いておくと、GoModuleは以下のようなrequire文を追記してくれ、無事モジュール内モジュールのコードはローカルのコードを参照してビルドしてくれるようになります。

go.mod
replace (
    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
)

参考リンク


  1. この例はgithubを利用しているため、githubと記述しています。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby から FFI で Go にバイト列のポインタを渡す

Ruby を書いていると、バイナリ処理を Go で書いて FFI 呼び出すことでいい感じにしたくなることがあります。

バイナリの受け渡しをする際は、String を使うと NULL 文字で終端されてしまう 1 ため、ポインタで渡してあげる必要があります。

C での書き方は ffi/ffi の wiki に載っていますが、Go での書き方は調べてもあまり出てこないので、ここに書いておきます。

main.go
package 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.rb
require '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

  1. Binary data · ffi/ffi Wiki https://github.com/ffi/ffi/wiki/Binary-data 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【はじめての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-definition

main.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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む