20201016のGoに関する記事は5件です。

1つのクラウドベンダーに一途なあなたも Go CDK の採用を検討すべき理由

Go CDK とは

Go CDK とは The Go Cloud Development Kit の略で、主要クラウドベンターが提供しているほぼ同一の機能を持ったサービスを統一的な API で扱うためのプロジェクトです(旧称 Go Cloud)。

例えばクラウドストレージサービスにオブジェクトを保存・取得する処理は Go CDK を使うことで次のように書くことができます1

  • Amazon S3
package main

import (
    "context"
    "fmt"
    "log"

    "gocloud.dev/blob"
    _ "gocloud.dev/blob/s3blob"
)

func main() {
    ctx := context.Background()

    bucket, err := blob.OpenBucket(ctx, "s3://bucket")
    if err != nil {
        log.Fatal(err)
    }
    defer bucket.Close()

    // 保存
    if err := bucket.WriteAll(ctx, "sample.txt", []byte("Hello, world!"), nil); err != nil {
        log.Fatal(err)
    }

    // 取得
    data, err := bucket.ReadAll(ctx, "sample.txt")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(data))
}
  • Google Cloud Storage
package main

import (
    "context"
    "fmt"
    "log"

    "gocloud.dev/blob"
    _ "gocloud.dev/blob/gcsblob"
)

func main() {
    ctx := context.Background()

    bucket, err := blob.OpenBucket(ctx, "gs://bucket")
    if err != nil {
        log.Fatal(err)
    }
    defer bucket.Close()

    // 保存
    if err := bucket.WriteAll(ctx, "sample.txt", []byte("Hello, world!"), nil); err != nil {
        log.Fatal(err)
    }

    // 取得
    data, err := bucket.ReadAll(ctx, "sample.txt")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(data))
}
  • Azure Blob Storage
package main

import (
    "context"
    "fmt"
    "log"

    "gocloud.dev/blob"
    _ "gocloud.dev/blob/azureblob"
)

func main() {
    ctx := context.Background()

    bucket, err := blob.OpenBucket(ctx, "azblob://bucket")
    if err != nil {
        log.Fatal(err)
    }
    defer bucket.Close()

    // 保存
    if err := bucket.WriteAll(ctx, "sample.txt", []byte("Hello, world!"), nil); err != nil {
        log.Fatal(err)
    }

    // 取得
    data, err := bucket.ReadAll(ctx, "sample.txt")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(data))
}

異なるクラウドベンダーを使用する場合のコードの違いはドライバーの import 部分と blob.OpenBucket() に与えている URL の scheme のみです。素晴らしいですね!

このように Go CDK を使うことでマルチクラウドなアプリケーションやクラウドポータビリティの高いアプリケーションを容易に実装することができます。

Go CDK についてより詳しく知りたい方は公式の情報をご参照ください。

大変便利な Go CDK ですが2020年10月現在のプロジェクトステータスは「API は alpha だけど production-ready」2 という感じらしいです。導入される際は自己責任でお願いします。

マルチクラウド・ポータビリティ以外にもある Go CDK のメリット

本題です。

「AWS しか使わない!ベンダーロックイン上等!」といった考えの人もいると思います。
本稿ではそういった方でも Go CDK を使うメリットは十分あるということを、「S3 のオブジェクト操作(保存・取得)」を例にご紹介したいと思います。

API が分かりやすい・扱いやすい

Go CDK の API は直観的に理解しやすく扱いやすい設計です。

Go CDK でのクラウドストレージへのオブジェクトの読み書きは blob.Bucket
NewReader() 及び NewWriter() によって得られる blob.Reader (io.Reader を実装) と blob.Writer (io.Writer を実装) を使います。
オブジェクトの取得(読み込み)を blob.Reader (io.Reader)、保存(書き込み)を io.Writer (io.Writer) で行えるというのは非常に直観的です。これにより、ローカルファイルを操作するかのような感覚でクラウド上のオブジェクトを扱うことができます。

AWS SDK を使う場合と比べてどうわかりやすくなるかを、具体例を挙げつつ見ていきます。

S3 にオブジェクトを保存する場合

AWS SDK の場合、s3manager.UploaderUpload() を使うことになります。
アップロードするオブジェクトの内容は io.Reader としてメソッドに渡します。ローカルにあるファイルをアップロードする場合なら os.File をそのまま渡せて便利なのですが、厄介なのはメモリ上にあるデータを何らかの形式でエンコードしてそのまま保存したい場合です。

例えば JSON エンコードしてそのまま S3 という処理は、AWS SDK では次のようになります。

package main

import (
    "encoding/json"
    "io"
    "log"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3/s3manager"
)

func main() {
    sess, err := session.NewSession(&aws.Config{
        Region: aws.String("ap-northeast-1"),
    })
    if err != nil {
        log.Fatal(err)
    }

    uploader := s3manager.NewUploader(sess)

    data := struct {
        Key1 string
        Key2 string
    }{
        Key1: "value1",
        Key2: "value2",
    }

    pr, pw := io.Pipe()

    go func() {
        err := json.NewEncoder(pw).Encode(data)
        pw.CloseWithError(err)
    }()

    in := &s3manager.UploadInput{
        Bucket: aws.String("bucket"),
        Key:    aws.String("sample.json"),
        Body:   pr,
    }
    if _, err := uploader.Upload(in); err != nil {
        log.Fatal(err)
    }
}

JSON をエンコードするための io.Writers3manager.UploadInput に渡す io.Reader とを繋ぐために io.Pipe() を使う必要があります。

Go CDK であれば、書き込みは blob.Writer (io.Writer) で行うのでそのまま json.NewEncoder() に渡すだけです。

package main

import (
    "context"
    "encoding/json"
    "log"

    "gocloud.dev/blob"
    _ "gocloud.dev/blob/s3blob"
)

func main() {
    ctx := context.Background()

    bucket, err := blob.OpenBucket(ctx, "s3://bucket")
    if err != nil {
        log.Fatal(err)
    }
    defer bucket.Close()

    data := struct {
        Key1 string
        Key2 string
    }{
        Key1: "value1",
        Key2: "value2",
    }

    w, err := bucket.NewWriter(ctx, "sample.json", nil)
    if err != nil {
        log.Fatal(err)
    }
    defer w.Close()

    if err := json.NewEncoder(w).Encode(data); err != nil {
        log.Fatal(err)
    }
}

もちろんローカルファイルをアップロードする場合もシンプルに書けます。
ファイルからファイルへとコピーするかのごとく io.Copy を使うだけです。

package main

import (
    "context"
    "io"
    "log"
    "os"

    "gocloud.dev/blob"
    _ "gocloud.dev/blob/s3blob"
)

func main() {
    ctx := context.Background()

    bucket, err := blob.OpenBucket(ctx, "s3://bucket")
    if err != nil {
        log.Fatal(err)
    }
    defer bucket.Close()

    file, err := os.Open("sample.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    w, err := bucket.NewWriter(ctx, "sample.txt", nil)
    if err != nil {
        log.Fatal(err)
    }
    defer w.Close()

    if _, err := io.Copy(w, file); err != nil {
        log.Fatal(err)
    }
}

ちなみに、s3blob の Writer は s3manager.Uploader を wrap する形で実装されているため s3manager.Uploader の持つ並列アップロード機能の恩恵を受けることができます。

S3 からオブジェクトを取得する場合

S3 から JSON を取得してデコードする場合を考えてみましょう。

AWS SDK の場合、s3.GetObject() を使います。
s3manager.Uploader と対になる s3manager.Downloader は出力先が io.WriterAt を実装している必要があるため、このケースでは使えないことに注意が必要です。

package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
)

func main() {
    sess, err := session.NewSession(&aws.Config{
        Region: aws.String("ap-northeast-1"),
    })
    if err != nil {
        log.Fatal(err)
    }

    svc := s3.New(sess)

    data := struct {
        Key1 string
        Key2 string
    }{}

    in := &s3.GetObjectInput{
        Bucket: aws.String("bucket"),
        Key:    aws.String("sample.json"),
    }
    out, err := svc.GetObject(in)
    if err != nil {
        log.Fatal(err)
    }
    defer out.Body.Close()

    if err := json.NewDecoder(out.Body).Decode(&data); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%+v\n", data)
}

Go CDK の場合はアップロードの時と逆になるように書くだけです。

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"

    "gocloud.dev/blob"
    _ "gocloud.dev/blob/s3blob"
)

func main() {
    ctx := context.Background()

    bucket, err := blob.OpenBucket(ctx, "s3://bucket")
    if err != nil {
        log.Fatal(err)
    }
    defer bucket.Close()

    r, err := bucket.NewReader(ctx, "sample.json", nil)
    if err != nil {
        log.Fatal(err)
    }
    defer r.Close()

    data := struct {
        Key1 string
        Key2 string
    }{}

    if err := json.NewDecoder(r).Decode(&data); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%+v\n", data)
}

取得したオブジェクトをローカルファイルに書き込む場合は s3manager.Downloader を使うことができます。

package main

import (
    "log"
    "os"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
    "github.com/aws/aws-sdk-go/service/s3/s3manager"
)

func main() {
    sess, err := session.NewSession(&aws.Config{
        Region: aws.String("ap-northeast-1"),
    })
    if err != nil {
        log.Fatal(err)
    }

    downloader := s3manager.NewDownloader(sess)

    file, err := os.Create("sample.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    in := &s3.GetObjectInput{
        Bucket: aws.String("bucket"),
        Key:    aws.String("sample.txt"),
    }
    if _, err := downloader.Download(file, in); err != nil {
        log.Fatal(err)
    }
}

Go CDK ではこの場合もアップロードの時と逆になるように書けば OK です。

package main

import (
    "context"
    "io"
    "log"
    "os"

    "gocloud.dev/blob"
    _ "gocloud.dev/blob/s3blob"
)

func main() {
    ctx := context.Background()

    bucket, err := blob.OpenBucket(ctx, "s3://bucket")
    if err != nil {
        log.Fatal(err)
    }
    defer bucket.Close()

    file, err := os.Create("sample.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    r, err := bucket.NewReader(ctx, "sample.txt", nil)
    if err != nil {
        log.Fatal(err)
    }
    defer r.Close()

    if _, err := io.Copy(file, r); err != nil {
        log.Fatal(err)
    }
}

ただしこの方法はシンプルですが欠点もあります。
s3manager.Downloder の場合は出力先に io.WriterAt を要求する代わりに並列ダウンロード機能を備えておりパフォーマンスに優れていますが、Go CDK の場合そのままでは並列ダウンロードを行うことができません。
Go CDK で並列ダウンロードを行たい場合は NewRangeReader() を使って自前で実装する必要があります。

Go CDK での並列ダウンロード実装例 (長いので折りたたみます)
package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "log"
    "os"
    "sync"

    "gocloud.dev/blob"
    _ "gocloud.dev/blob/s3blob"
)

const (
    downloadPartSize    = 1024 * 1024 * 5
    downloadConcurrency = 5
)

func main() {
    ctx := context.Background()

    bucket, err := blob.OpenBucket(ctx, "s3://bucket")
    if err != nil {
        log.Fatal(err)
    }
    defer bucket.Close()

    file, err := os.Create("sample.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    d := &downloader{
        ctx:         ctx,
        bucket:      bucket,
        key:         "sample.txt",
        partSize:    downloadPartSize,
        concurrency: downloadConcurrency,
        w:           file,
    }

    if err := d.download(); err != nil {
        log.Fatal(err)
    }
}

type downloader struct {
    ctx context.Context

    bucket *blob.Bucket
    key    string
    opts   *blob.ReaderOptions

    partSize    int64
    concurrency int

    w io.WriterAt

    wg     sync.WaitGroup
    sizeMu sync.RWMutex
    errMu  sync.RWMutex

    pos        int64
    totalBytes int64
    err        error

    partBodyMaxRetries int
}

func (d *downloader) download() error {
    d.getChunk()
    if err := d.getErr(); err != nil {
        return err
    }

    total := d.getTotalBytes()

    ch := make(chan chunk, d.concurrency)

    for i := 0; i < d.concurrency; i++ {
        d.wg.Add(1)
        go d.downloadPart(ch)
    }

    for d.getErr() == nil {
        if d.pos >= total {
            break
        }

        ch <- chunk{w: d.w, start: d.pos, size: d.partSize}
        d.pos += d.partSize
    }

    close(ch)
    d.wg.Wait()

    return d.getErr()
}

func (d *downloader) downloadPart(ch chan chunk) {
    defer d.wg.Done()
    for {
        c, ok := <-ch
        if !ok {
            break
        }
        if d.getErr() != nil {
            continue
        }

        if err := d.downloadChunk(c); err != nil {
            d.setErr(err)
        }
    }
}

func (d *downloader) getChunk() {
    if d.getErr() != nil {
        return
    }

    c := chunk{w: d.w, start: d.pos, size: d.partSize}
    d.pos += d.partSize

    if err := d.downloadChunk(c); err != nil {
        d.setErr(err)
    }
}

func (d *downloader) downloadChunk(c chunk) error {
    var err error
    for retry := 0; retry <= d.partBodyMaxRetries; retry++ {
        err := d.tryDownloadChunk(c)
        if err == nil {
            break
        }

        bodyErr := &errReadingBody{}
        if !errors.As(err, &bodyErr) {
            return err
        }

        c.cur = 0
    }
    return err
}

func (d *downloader) tryDownloadChunk(c chunk) error {
    r, err := d.bucket.NewRangeReader(d.ctx, d.key, c.start, c.size, d.opts)
    if err != nil {
        return err
    }
    defer r.Close()

    if _, err := io.Copy(&c, r); err != nil {
        return err
    }

    d.setTotalBytes(r.Size())

    return nil
}

func (d *downloader) getErr() error {
    d.errMu.RLock()
    defer d.errMu.RUnlock()

    return d.err
}

func (d *downloader) setErr(err error) {
    d.errMu.Lock()
    defer d.errMu.Unlock()

    d.err = err
}

func (d *downloader) getTotalBytes() int64 {
    d.sizeMu.RLock()
    defer d.sizeMu.RUnlock()

    return d.totalBytes
}

func (d *downloader) setTotalBytes(size int64) {
    d.sizeMu.Lock()
    defer d.sizeMu.Unlock()

    d.totalBytes = size
}

type chunk struct {
    w     io.WriterAt
    start int64
    size  int64
    cur   int64
}

func (c *chunk) Write(p []byte) (int, error) {
    if c.cur >= c.size {
        return 0, io.EOF
    }

    n, err := c.w.WriteAt(p, c.start+c.cur)
    c.cur += int64(n)

    return n, err
}

type errReadingBody struct {
    err error
}

func (e *errReadingBody) Error() string {
    return fmt.Sprintf("failed to read part body: %v", e.err)
}

func (e *errReadingBody) Unwrap() error {
    return e.err
}

s3manager.Downloader の実装を参考にしています


ローカル実行が容易になる

Go CDK は全てのサービスに対しローカル実装を提供するように開発が進められています。そのため、クラウドサービスの操作を簡単にローカル実装に差し替えることができます。
例えば開発用のローカルサーバなどでは全てのサービスをローカル実装に差し替えておくと AWS や GCP へのアクセスを発生させずに動作させることができるので便利です。

クラウドストレージを扱う gocloud.dev/blob パッケージの場合、fileblob というローカルファイルの読み書きを行う実装が提供されています。

以下はエンコードした JSON の出力先をオプションに応じて S3 とローカルとに切り替える例です。

package main

import (
    "context"
    "encoding/json"
    "flag"
    "log"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "gocloud.dev/blob"
    "gocloud.dev/blob/fileblob"
    "gocloud.dev/blob/s3blob"
)

func main() {
    var local bool
    flag.BoolVar(&local, "local", false, "output to a local file")
    flag.Parse()

    ctx := context.Background()

    bucket, err := openBucket(ctx, local)
    if err != nil {
        log.Fatal(err)
    }
    defer bucket.Close()

    data := struct {
        Key1 string
        Key2 string
    }{
        Key1: "value1",
        Key2: "value2",
    }

    w, err := bucket.NewWriter(ctx, "sample.json", nil)
    if err != nil {
        log.Fatal(err)
    }
    defer w.Close()

    if err := json.NewEncoder(w).Encode(data); err != nil {
        log.Fatal(err)
    }
}

func openBucket(ctx context.Context, local bool) (*blob.Bucket, error) {
    if local {
        return openLocalBucket(ctx)
    }

    return openS3Bucket(ctx)
}

func openLocalBucket(ctx context.Context) (*blob.Bucket, error) {
    return fileblob.OpenBucket("output", nil)
}

func openS3Bucket(ctx context.Context) (*blob.Bucket, error) {
    sess, err := session.NewSession(&aws.Config{
        Region: aws.String("ap-northeast-1"),
    })
    if err != nil {
        return nil, err
    }

    return s3blob.OpenBucket(ctx, sess, "bucket", nil)
}

そのまま実行すると S3 に sample.json が保存されますが、-local オプションを付けて実行するとローカルの output/sample.json に保存されます。
この際、オブジェクトのプロパティが output/sample.json.attrs として保存されます。これにより保存したオブジェクトのプロパティも問題なく取得できる仕組みになっています。

テスタビリティが圧倒的に向上する

AWS のような外部サービスの API を呼び出すようなコードではどのようにしてテストしやすい実装にするかということで常に頭を悩ませることになりますが、Go CDK ではその心配はありません。
通常は外部サービスを interface として抽象化して mock を実装し、テストでは mock に差し替える・・・ということになるかと思いますが、Go CDK ではすでに各サービスが適切に抽象化され、そのローカル実装が提供されているのでそのまま使うだけで OK です。

例えば以下のような エンコードした JSON をクラウドストレージにアップロードするための interface を実装する構造体をテストすることを考えてみます。

type JSONUploader interface {
    func Upload(ctx context.Context, key string, v interface{}) error

AWS SDK の場合、各種サービスクライアントの interface が提供されているのでそれを使うことでテスタビリティを担保します。
s3manager なら s3manageriface というパッケージで interface が提供されています。

type jsonUploader struct {
    bucketName string
    uploader   s3manageriface.UploaderAPI
}

func (u *jsonUploader) Upload(ctx context.Context, key string, v interface{}) error {
    pr, pw := io.Pipe()

    go func() {
        err := json.NewEncoder(pw).Encode(v)
        pw.CloseWithError(err)
    }()

    in := &s3manager.UploadInput{
        Bucket: aws.String(u.bucketName),
        Key:    aws.String(key),
        Body:   pr,
    }
    if _, err := u.uploader.UploadWithContext(ctx, in); err != nil {
        return err
    }

    return nil
}

このような実装にしておけばテストでは jsonUploader.uploader に適当な mock を入れておけば実際に S3 にアクセスせずにテストが可能です。ただしこの mock 実装は公式には提供されていないので、自分で実装するか適当な外部パッケージを見つける必要があります。

Go CDK の場合はそのまま実装するだけでテスタビリティの高い構造体となります。

type jsonUploader struct {
    bucket *blob.Bucket
}

func (u *jsonUploader) Upload(ctx context.Context, key string, v interface{}) error {
    w, err := u.bucket.NewWriter(ctx, key, nil)
    if err != nil {
        return err
    }
    defer w.Close()

    if err := json.NewEncoder(w).Encode(v); err != nil {
        return err
    }

    return nil
}

テストでは memblob というインメモリの blob 実装を使うと便利です。

func TestUpload(t *testing.T) {
    bucket := memblob.OpenBucket(nil)
    uploader := &jsonUploader{bucket: bucket}

    ctx := context.Background()

    key := "test.json"

    type data struct {
        Key1 string
        Key2 string
    }

    in := &data{
        Key1: "value1",
        Key2: "value2",
    }

    if err := uploader.Upload(ctx, key, in); err != nil {
        t.Fatal(err)
    }

    r, err := bucket.NewReader(ctx, key, nil)
    if err != nil {
        t.Fatal(err)
    }

    out := &data{}
    if err := json.NewDecoder(r).Decode(out); err != nil {
        t.Fatal(err)
    }

    if !reflect.DeepEqual(in, out) {
        t.Error("unmatch")
    }
}

まとめ

Go CDK 導入によるマルチクラウド対応やクラウドポータビリティ以外のメリットについてご紹介しました。
複数のクラウドベンダーを統一的に扱うという性質上、特定のクラウドベンダー固有の機能は使えないなど弱点は勿論あるので、各クラウドベンダーの SDK とは要件に合わせて使い分けることになるとは思います。

Go CDK 自体もまだまだ発展途上なので今後さらに機能が充実することを期待したいですね。


  1. 本稿のサンプルコードで使用している gocloud.devv0.20.0 です 

  2. https://github.com/google/go-cloud#project-status 

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

VSCodeのRemote ContainersでGoの開発環境を構築

はじめに

VSCodeの拡張機能である、Remote Containersを使って、Goの開発環境を構築したら快適だったので、紹介します。
尚、VSCodeおよび、Docker Desktopのインストール方法については割愛します。

Remote Containers

Remote Containersとは、VSCodeのリモート開発機能のうち、Dockerに特化した拡張機能です。
他にもSSH経由でリモート接続する「Remote SSH」、WSL(Windows Subsystem for Linux)を使用してリモート接続する「Remote WSL」があるようです。

ローカルのVSCodeからVSCodeServerを介して、Dockerコンテナにリモート接続することで、ローカル環境とは切り離された環境で開発をすることが可能です。
つまり、Docker上で、全てが完結するため、コード補完などの恩恵を受けつつ、ローカル環境を汚さなくて良い素晴らしい機能です。
公式サイト
※2020年10月時点ではプレビュー版となります。

インストール

VSCodeのExtentionsからRemote Containersを検索してインストールします。
image.png

初期設定

Ctrl + Shift + Pでメニューを出して、
Remote-Containers:Add Development Cotainers Configuration Files...を選択します。
image.png

私の環境ではすでにdocker-compose.yml及びDockerFileで環境を構築しているため、そちらの設定を元に初期設定を行うことができます。
また、From a predefined container configuration definition...を選択すると、あらかじめ用意されている、設定を使用することも可能です。
今回は、既存のdocker-composeファイルを使用して初期設定を行います。
image.png

image.png

.devcontainer

初期設定をすると.devcontainerフォルダが自動的に作成され、配下に
devcontainer.json
docker-compose.yml
が自動生成されます。
devcontainer.jsonが設定ファイルになります。

ここで、現在のディレクトリ構造は以下のようになりました。

- .devcontainer
 ├ devcontainer.json
  └ docker-compose.yml
- sample
  └ ソースコード
- docker-compose.yml
- docker-compose.dev.yml
- Dockerfile 
- Dockerfile.dev

自動生成されたファイルは以下。

devcontainer.json
// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
{
    "name": "Existing Docker Compose (Extend)",

    // Update the 'dockerComposeFile' list if you have more compose files or use different names.
    // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
    "dockerComposeFile": [
        "..\\docker-compose.dev.yml",
        "docker-compose.yml"
    ],

    // The 'service' property is the name of the service for the container that VS Code should
    // use. Update this value and .devcontainer/docker-compose.yml to the real service name.
    "service": "sample",

    // The optional 'workspaceFolder' property is the path VS Code should open by default when
    // connected. This is typically a file mount in .devcontainer/docker-compose.yml
    "workspaceFolder": "/workspace",

    // Set *default* container specific settings.json values on container create.
    "settings": {
        "terminal.integrated.shell.linux": null
    },

    // Add the IDs of extensions you want installed when the container is created.
    "extensions": []

    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    // "forwardPorts": [],

    // Uncomment the next line if you want start specific services in your Docker Compose config.
    // "runServices": [],

    // Uncomment the next line if you want to keep your containers running after VS Code shuts down.
    // "shutdownAction": "none",

    // Uncomment the next line to run commands after the container is created - for example installing curl.
    // "postCreateCommand": "apt-get update && apt-get install -y curl",

    // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
    // "remoteUser": "vscode"
}
docker-compose.yml
version: '3'
services:
  # Update this to the name of the service you want to work with in your docker-compose.yml file
  sample:
    # If you want add a non-root user to your Dockerfile, you can use the "remoteUser"
    # property in devcontainer.json to cause VS Code its sub-processes (terminals, tasks, 
    # debugging) to execute as the user. Uncomment the next line if you want the entire 
    # container to run as this user instead. Note that, on Linux, you may need to 
    # ensure the UID and GID of the container user you create matches your local user. 
    # See https://aka.ms/vscode-remote/containers/non-root for details.
    #
    # user: vscode

    # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer 
    # folder. Note that the path of the Dockerfile and context is relative to the *primary* 
    # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
    # array). The sample below assumes your primary file is in the root of your project.
    #
    # build:
    #   context: .
    #   dockerfile: .devcontainer/Dockerfile

    volumes:
      # Update this to wherever you want VS Code to mount the folder of your project
      - .:/workspace:cached

      # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details.
      # - /var/run/docker.sock:/var/run/docker.sock 

    # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
    # cap_add:
    #   - SYS_PTRACE
    # security_opt:
    #   - seccomp:unconfined

    # Overrides default command so things don't shut down after the process ends.
    command: /bin/sh -c "while sleep 1000; do :; done"

※一旦ファイルは生成されたものの、私は、docker-compose.ymlの2重管理になってしまうのが嫌だったので、.devcontainer配下のdocker-compose.ymlに全てを移しました。

Go向け設定

Goを開発するための設定を加えていきます。

extensions

ローカルにインストールしたVSCodeの拡張機能は、リモートコンテナ内部では使用できないので、コンテナ起動時に、自動的にインストールする拡張機能を設定します。
Goの拡張機能を入れます。この文字列は、拡張機能一覧で右クリックCopy Extension Idから取得できます。

devcontainer.json
    "extensions": [
        "golang.go"
    ]

settings

goplsを使用するので、githubを参考に、設定を追加します。
Gomod設定(GO111MODULE)に関しては、1.13以降不要となりましたが、設定しないとimport文にエラーが出てしまうので、設定します。

devcontainer.json
"settings": {
        "terminal.integrated.shell.linux": "/bin/bash",
        "go.useLanguageServer": true,
        "[go]": {
            "editor.formatOnSave": true,
            "editor.codeActionsOnSave": {
                "source.organizeImports": true,
            },
            // Optional: Disable snippets, as they conflict with completion ranking.
            "editor.snippetSuggestions": "none",
        },
        "[go.mod]": {
            "editor.formatOnSave": true,
            "editor.codeActionsOnSave": {
                "source.organizeImports": true,
            },
        },
        "gopls": {
            // Add parameter placeholders when completing a function.
            "usePlaceholders": true,
            // If true, enable additional analyses with staticcheck.
            // Warning: This will significantly increase memory usage.
            "staticcheck": false,
        },
        "go.toolsEnvVars":{
            "GO111MODULE":"on"
        }
    }

workspaceFolder

リモートコンテナ起動時のワークスペースを設定します。
docker-compose.yml側で、ボリュームを/go/src/配下にマウントするので、ワークスペースもそれに合わせて変更します。

devcontainer.json
"workspaceFolder": "/go/src/sample",

以上で設定は終了です。

起動

実際にコンテナを起動します。
Ctrl+Shift+Pから、Remote-Containers:Reopen in Containerを選択

image.png

初回起動時では、ツール類が/go/binにインストールされていないのでインストールします。

goplsを使用するので、以下の通知からインストールするか、
go get -v golang.org/x/tools/goplsを実行します
image.png

その他ツールに関しては、Go:Install/Update Toolsからgocodegocode-gomod以外をインストールします。
image.png

image.png

/go/bin配下に全てインストールされていることが確認できました。

image.png

ここまでで、コンテナ上で開発することができるようになります。
当然ながら、コンテナ単位での管理となるため、コンテナの削除を行うと、インストールしたものについては、消えるので、再設定が必要となります。

デバッグ

コンテナ内でのデバッグも可能です。
マウントした、ソースコードと同じディレクトリに.vscodeフォルダを作成して、launch.jsonを作成します。

launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Remote",
            "type": "go",
            "request": "launch",
            "host": "localhost",
            "program": "${workspaceRoot}",
            "args": []
        }
    ]
}

まとめ

最終的に作成したファイルを記載しておきます。何かのご参考になれば幸いです。

devcontianer.json
{
    "name": "Existing Docker Compose (Extend)",
    "dockerComposeFile": [
        "docker-compose.yml"
    ],
    "service": "sample",
    "workspaceFolder": "/go/src/sample",
    "settings": {
        "terminal.integrated.shell.linux": "/bin/bash",
        "go.useLanguageServer": true,
        "[go]": {
            "editor.formatOnSave": true,
            "editor.codeActionsOnSave": {
                "source.organizeImports": true,
            },
            "editor.snippetSuggestions": "none",
        },
        "[go.mod]": {
            "editor.formatOnSave": true,
            "editor.codeActionsOnSave": {
                "source.organizeImports": true,
            },
        },
        "gopls": {
            "usePlaceholders": true,
            "staticcheck": false,
        },
        "go.toolsEnvVars":{
            "GO111MODULE":"on"
        }
    },
    "extensions": [
        "golang.go"
    ]
}
docker-compose.yml
version: '3'
services:
  # Update this to the name of the service you want to work with in your docker-compose.yml file
  sample:
    image: golang:1.14
    volumes:
      - ./../:/go/src/:cached
    tty: true
    environment:
      - MYSQL_CONNECTION_STRING=${CONNECTION_STRING}
    networks:
      - default
      - db-network
networks:
  db-network:
    external: true

参考

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

フロントエンジニアがgoのAPIサーバーをサクッとsystemdで自動起動させてみた

systemdでサービス化した方がいいと言われたので、とりあえずやってみた。

①プロジェクトファイル(ここではsample)でgo build
②etc/systemd/systemにapp.serviceを作ります
③app.serviceに下記を記述

[Unit]
Description=Automatic start server demon

[Service]
ExecStart=/home/sample/sample
WorkingDirectory=/home/ec2-user/sample
Restart=always
Type=simple
User=ec2-user

[Install]
WantedBy=multi-user.target

【解説】
ExecStartで実行ファイルを指定
go buildしたらファイル名と同じバイナリが生成されるので、それを指定

WorkingDirectoryで実行するディレクトリを指定

Restart=always alwaysにすることで落ちても自動再起動してくれる

Type=simple 実行完了と判別するタイミングを指定

simple=コマンドが実行されたら
Type=forking=実行したコマンドが終了したら
Type=oneshot=コマンドが完了したら

User 実行するUser

WantedBy=multi-user.target enable時にこのユニットの.wantsディレクトリにリンクを作成する
これはよくわからなかった。

sudo systemctl deamon-reloadを実行
app.serviceの内容を更新

sudo systemctl enable /home/ec2-user/etc/systemd/system/app.serviceを実行
サービスの自動起動を有効にする
*絶対パスで指定すること

sudo systemctl start app.serviceを実行

これで自動起動されます

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

Db2コンテナをGoで操作する

こんにちは。
今回はDockerで立てたDb2コンテナをGoで操作する方法について紹介します。
Db2コンテナの立て方や、セットアップ時にデータを挿入する方法については、以下の記事で紹介しておりますので、ぜひ参考にしてください。

Db2のDBコンテナを立ててちょっとしたデータを挿入してみる
Db2/DBコンテナに初期テストデータを挿入した状態でセットアップ

今回は、Db2コンテナをデータを挿入した状態で立ち上げ、Goでの実装を中心に紹介します。

コード類はこちらに載せています。

概要

Db2コンテナにデータは挿入できたけど、実際にそのデータを取得してきて操作したり、データを更新したりするにはどうするんだという方向けの内容となっています。

今回はGo言語でDb2からデータを取ってくる方法をご紹介しようと思います。

開発環境

  • Win10 Enterprise
  • docker (v19.03.13)
  • ibmcom/db2 (v11.5.4.0)
  • Go (v1.12.9)
  • Git Bash (git version 2.27.0.windows.1)

前提

  • ibmcomのDb2コンテナの立ち上げ方がある程度分かる
  • Go言語がある程度わかる

Getting Started

開発環境はWindowsですが、MacでもLinuxでもできます。

今回は、Db2との疎通確認に重きを置いておりますので、API化などは行っていません。
単純にDb2からデータを取ってきて、コンソールに出力するだけのプログラムを書いていきます。(いつかGoでREST APIの実装も紹介します。)

1. フォルダ構成の説明

まずはフォルダ構成を説明します。

project
project
├─go
|  ├─model
|  |     ├─user.go
|  |     ├─tweet.go
|  |     └─reply.go
|  └─main.go
└─db  
   ├─data
   |    ├─users_insert.csv
   |    ├─tweets_insert.csv
   |    └─replys_insert.csv
   ├─sql
   |   ├─users_create.sql
   |   ├─tweets_create.sql
   |   └─replys_create.sql
   ├─createschema.sh
   ├─Dockerfile
   └─env.list
  • /go
    • /model
    • user.go ユーザーのDTOとDAO
    • tweet.go ツイートのDTOとDAO
    • reply.go
      リプライのDTOとDAO
    • main.go
      メインの関数
  • /db: データベースをセットアップするフォルダ
    • /data
      初期にデータベースに登録するテストデータのフォルダ
    • /sql
      テーブル作成するSQL文のフォルダ
    • createschema.sh
      データベースセットアップ時に呼ばれるテーブル作成用スクリプト
    • Dockerfile
      コンテナ定義
    • env.list
      Db2コンテナ用の構成情報

本当は、ドメイン駆動設計とかで、ユーザードメインとか、インフラストラクチャとか作ってカッコいい設計をしたいんですが、それはまたの機会ということで。

2. コンテナの立ち上げ

まずはDockerfileを用いて、コンテナイメージをビルドします。
実行するコマンドは以下です。

$ cd db
$ docker build -t test-db:v1.0 .

これで、コンテナイメージが出来上がるので、早速runしていきます。

$ docker run --name go-db --restart=always --detach --privileged=true -p 50000:50000 --env-file env.list test-db:v1.0

詳しい説明はこちらで紹介しています。

ここで大事なのはポートを50000:50000でポートフォワーディングしていることです。
クライアントに公開している50000ポートはDBと接続する時に指定する必要があるので、覚えておきます。

3. インポートするパッケージ

利用するパッケージ
* github.com/ibmdb/go_ibm_db
* github.com/pkg/errors

3.1. go_ibm_db

基本的にGoでDb2を利用する際は、github.com/ibmdb/go_ibm_dbというパッケージを利用します。

以下のコマンドを叩きます。

$ go get github.com/ibmdb/go_ibm_db

またデータベースを操作するにあたって、SQLを操作するためのドライバが必要になります。
色々操作があるので順にやります。

まず、落としてきたgithub.com/ibmdb/go_ibm_dbを見に行きます。
おそらくGOPATH配下に落とされていると思うので、こちらの階層を下ると、installerというフォルダにぶち当たります。
このフォルダ内setup.goがclidriverのダウンロードスクリプトになっています。

$ cd PathToInstaller/installer
$ go run setup.go

これでclidriverがinstaller配下にダウンロードできます。(パーミッションエラーが起きた方は、installerフォルダの権限を変えてみてください。)
結構時間がかかる気がします。

無事落とせてこれた方はPathToInstaller/installer/clidriver/binのパスを通す必要があるので、通しましょう。
これでgo_ibm_dbのセットアップは完了です。

もし余計なパッケージを環境に落としたくないという方は、go modでもできます。
しかしその場合も、sqlcli.hは必要になりますので、インストールしてきたinstallerをプロジェクトにコピーしてきて、、シェルスクリプトなどで、clidriver/binのパスを通し、moduleを指定してビルドすることで実行ファイルを生成できます。

3.2. errors

また、エラーの実装もするので、errorsパッケージも落としましょう。

$ go get github.com/pkg/errors

4. Goの実装

基本的に実装は本当に3で紹介した通りです。
main.goのmain関数を見ながら紹介します。

まずこのコード

main.go
  config := "HOSTNAME=localhost;DATABASE=USERDB;PORT=50000;UID=db2inst1;PWD=password"
    conn, err := sql.Open("go_ibm_db", config)
    if err != nil {
        fmt.Printf("DBとの接続に失敗しました。%+v", err)
    }
    defer conn.Close()

configにDB接続情報を格納します。HOSTNAMEとPORT以外はenv.listに乗せてある情報を使います。
その下のsql.OpenでDBとのコネクションを張ります。
一つ目の引数はドライバ名を指定します。今回はgo_ibm_dbです。
二つ目の引数はDB接続情報を指定します。エラーを取りうるので、エラー処理もかかせず行います。
コネクションは必ず終了する必要があるので、Goのプラクティスであるdeferを使ってコネクションを閉じましょう。

これでDb2コンテナとのコネクションが取得できました。
これを利用してデータを操作していきます。

まずはユーザーを全件取得して、情報をユーザー構造体に格納し、インスタンスの配列を作っています。

main.go
users, err := model.GetAllUser(conn)
if err != nil {
  fmt.Printf("取得に失敗 %+v", err)
}

ではユーザーDAOとDTOを定義しているuser.goを見ていきます。

user.go
// User is users entity
type User struct {
    id        string
    name      string
    mail      string
    password  string
    createdAt time.Time
    updatedAt time.Time
}

func (u *User) String() string {
    return fmt.Sprintf(
        "ユーザー名:%s",
        u.name,
    )
}

// GetID returns user's id
func (u *User) GetID() string {
    return u.id
}

ユーザー構造体はテーブル定義のカラムをフィールドに定義しています。
GetIDメソッドはユーザーのIDを取得するメソッドです。これは他のテーブルのクエリにIDを渡すためにユーザー構造体のフィールドがプライベートに指定されているため、書いています。
まぁここら辺は他の言語でも似たようなことやると思います。

その下、ユーザー全件取得メソッドですが、

user.go
// GetAllUser returns all user instances
func GetAllUser(conn *sql.DB) ([]User, error) {
    selectAllUserQuery := `SELECT * FROM users`

    selectAllUserPstmt, err := conn.Prepare(selectAllUserQuery)
    if err != nil {
        return []User{}, errors.Wrapf(err, "ステートメントの作成に失敗しました")
    }

    var users []User

    rows, err := selectAllUserPstmt.Query()
    if err != nil {
        return []User{}, errors.Wrap(err, "クエリ実行に失敗")
    }
    for rows.Next() {
        var user User
        if err := rows.Scan(
            &user.id,
            &user.name,
            &user.mail,
            &user.password,
            &user.createdAt,
            &user.updatedAt,
        ); err != nil {
            return []User{}, errors.Wrap(err, "結果読み込み失敗")
        }
        users = append(users, user)
    }
    return users, nil
}

ここは色んな書き方があるんですが、Prepare()メソッドでステートメントを用意してから、queryを実行する方法で書きます。

これを実行すると、取れてきたレコードがrowsに格納されます。
rowsはNextメソッドを持っていて、for文でそれぞれのレコードを回すことができます。
さらにrows.Scan()にユーザーインスタンスの情報を渡してあげると、そこにレコードの情報を格納してくれます。

これで、ユーザー情報をユーザーインスタンスに格納することができました。
ユーザーの配列を返します。

それではmainに戻ります。

次からはユーザーインスタンスからIDを取ってきて、TweetのWHERE句に渡して挙げて、ユーザーに紐づくレコードを取ってきています。
取ってきたtweetレコードからさらにIDを取ってきて、それに紐づくReplyを取得し出力、それをユーザーレコード分行うといった処理をしています。

main.go
// 件数少ないので3重for文で。
    for _, user := range users {
        fmt.Println(user.String())
        tweets, err := model.GetAllTweets(conn, user.GetID())
        if err != nil {
            fmt.Printf("取得に失敗 %+v", err)
        }
        for _, tweet := range tweets {
            fmt.Println(tweet.String())
            replys, err := model.GetAllReplys(conn, tweet.GetID())
            if err != nil {
                fmt.Printf("取得に失敗", err)
            }
            for _, reply := range replys {
                fmt.Println(reply.String())
            }
        }
    }

WHERE句にIDを渡すためにはSQL文をSELECT * FROM Tweets WHERE user_id = ?のように与えたいパラメータの箇所を?とします。
パラメータ分第2引数を与えることで、WHERE句をカスタムできます。

書き方は、
rows, err := selectAllTweetPstmt.Query(userID)
このような形です。

5. 実行結果

Windowsで実行すると、コンテナから値を受け取ってくる段階で、日本語箇所は文字化けして表示されてしまいます。
Db2で用いているコンテナがLinuxコンテナなので、文字コードがUTF-8のまま文字列が送られてくることに起因していると思われます。

実行結果は以下のようになります。

ユーザー名:hoge
ツイート本文:�����̓e�X�g�ł��B, 作成日:2020-10-09 12:00:00 +0900 JST
リプライユーザー名:fugaaaa, リプライ本文:�e�X�g�m�F���܂����B, 作成日:2020-10-11 12:00:00 +0900 JST
-----------------------
ユーザー名:fuga
ツイート本文:�����̓e�X�g�ł��B, 作成日:2020-10-10 12:00:00 +0900 JST
リプライユーザー名:hogeeee, リプライ本文:�e�X�g�m�F���܂����B, 作成日:2020-10-11 12:00:00 +0900 JST
-----------------------

まぁめっちゃ文字化けしてますね。
悲しいです。
このままだとあれなんで、Macで実行した結果も載せときます。

ユーザー名:hoge
ツイート本文:これはテストです。, 作成日:2020-10-09 12:00:00 +0900 JST
リプライユーザー名:fugaaaa, リプライ本文:テスト確認しました。, 作成日:2020-10-11 12:00:00 +0900 JST
-----------------------
ユーザー名:fuga
ツイート本文:これはテストです。, 作成日:2020-10-10 12:00:00 +0900 JST
リプライユーザー名:hogeeee, リプライ本文:テスト確認しました。, 作成日:2020-10-11 12:00:00 +0900 JST
-----------------------

こんな感じで、Db2から取得できています。

6. まとめ

文字コードの弊害がありながらも、GoでDb2コンテナに接続する手法を紹介しました。

これでAPI開発とか楽に行えますね。

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

値を二つの方法でトグルするAPIの実装(go)

やること

GoのwebフレームワークEchoになれるために練習するのが目的です。値をトグルするとは言っても、データベースのbool値をtrue<=>falseにする場合と、データベースのレコードをInsertしたりDeleteする場合(中間テーブルを使ってお気に入り機能を作るとか)があると思う。今回は、BoolTogglerモデルとして前者の実装をした後、UserモデルにBoolTogglerに対するのお気に入り機能をつけて後者を実装して行きます。(シュールで実用性に欠けるアプリケーションですが練習なので気にしないことにします...)
GitHubにてコード全体を乗せています。

DBのbool値をトグルするAPI

models

この記事の本題1です。モデルのbool型の列の値のTrue<=>Falseをトグルします。
こんなモデルを定義してマイグレーションしました。

models/bool_toggler.go

package models

import (
    "github.com/jinzhu/gorm"
)

type BoolToggler struct {
    // type gorm.Model struct {
            // ID        uint           `gorm:"primaryKey"`
            // CreatedAt time.Time
            // UpdatedAt time.Time
            // DeletedAt gorm.DeletedAt `gorm:"index"`
    // }
  gorm.Model
  Toggler bool `json:"toggler"`
}

Togglerという単純なbool値をもつ構造体です。api部分を実装します。

APIの実装

web/api/toggle_bool_toggler.go

package api

import (
    "github.com/labstack/echo"
    "github.com/valyala/fasthttp"
    "strconv"
    "hello/models"
    "hello/middlewares"
)

func ToggleBoolToggler() echo.HandlerFunc {
    return func(c echo.Context) error {
        // DB接続ミドルウェア
        dbs := c.Get("dbs").(*middlewares.DatabaseClient)
        // パスパラメータはstringなのでuintに変換する
        intId, _ := strconv.Atoi(c.Param("id"))
        uintId := uint(intId)

        boolToggler := models.BoolToggler{}
        if dbs.DB.First(&boolToggler, uintId).RecordNotFound() {
            // idの指定が誤っている場合はステータスコード404を返す。
            return c.JSON(fasthttp.StatusNotFound, "指定したidのboolTogglerが見つかりませんでした。")
        } else {
            // bool値を反転させて保存する
            boolToggler.Toggler = !boolToggler.Toggler
            dbs.DB.Save(&boolToggler)
            return c.JSON(fasthttp.StatusOK, boolToggler)
        }
    }
}

現在設定されているTogglerを返すエンドポイント

web/api/get_bool_toggler.go

package api

import (
    "github.com/labstack/echo"
    "github.com/valyala/fasthttp"
    "hello/models"
    "hello/middlewares"
    "strconv"
)

func GetBoolToggler() echo.HandlerFunc {
    return func(c echo.Context) error {
        dbs := c.Get("dbs").(*middlewares.DatabaseClient)
        intId, _ := strconv.Atoi(c.Param("id"))
        uintId := uint(intId)

        boolToggler := models.BoolToggler{}
        if dbs.DB.First(&boolToggler, uintId).RecordNotFound() {
            return c.JSON(fasthttp.StatusNotFound, "指定したidのboolTogglerが見つかりませんでした。")
        } else {
            return c.JSON(fasthttp.StatusOK, boolToggler.Toggler)
        }
    }
}

基本的にはさっきと同じです。データを更新する代わりにブール値だけを取り出してレスポンスに入れています。

routes/api.go

func Init(e *echo.Echo) {
    g := e.Group("/api")
    {
        g.PUT("/bool_toggler/:id/toggle", api.ToggleBoolToggler())
        g.GET("/bool_toggler/:id", api.GetBoolToggler()) 
    }
}

ここまで動作ををテストしてみます。

curl http://localhost:8080/api/bool_toggler/1
>> false

curl -XPUT http://localhost:8080/api/bool_toggler/1/toggle
>> {"ID":1,"CreatedAt":"2020-10-05T14:54:27Z","UpdatedAt":"2020-10-07T10:49:12.1435735Z","DeletedAt":null,"toggler":true}

curl http://localhost:8080/api/bool_toggler/1
>> true

// 未登録データ
curl http://localhost:8080/api/bool_toggler/3
>> "指定したidのboolTogglerが見つかりませんでした。

といった感じでうまく動作していることが確認できました。

お気に入り状態をトグルするAPI

models

中間テーブルで関連を表現するデータにおいて値をDeleteしたり、insertしたりするAPIを実装していきます。

単純なUserモデルを作るところから始めてきます。
中間テーブルによってリレーショナルな多対多なデータを単純なアプリケーションとして実装するのが目的なので、認証機能は作りません。とにかくシンプルにタイムスタンプと名前だけのモデルです。同様にマイグレーションします。

GORMで多対多のデータベースを作るには、https://gorm.io/ja_JP/docs/many_to_many.html

models/user.go

package models

import (
  "github.com/jinzhu/gorm"
)

type User struct {
    gorm.Model
    name string `json:"name"`
    // Favoritesという名前でbool_togglerを格納します
    Favorites []*BoolToggler `gorm:"many2many:user_favorite_togglers;"`
}

APIの実装

先にURI設計ですが認証機能がないため、

/api/favorite/users/:user_id/bool_togglers/:toggler_id

というURIで複数のパスパラメータからそれぞれの実体を参照することが必要だということがわかります。以下API部分ですがかなり複雑になってきたのでコメントを多めに残しました

web/api/toggle_favorite_bool_toggler.go

package api

import (
    "github.com/labstack/echo"
    "github.com/valyala/fasthttp"
    "strconv"
    "hello/models"
    "hello/middlewares"
)

func ToggleFavoriteToggler() echo.HandlerFunc {
    return func(c echo.Context) error {

        // response用のJSONを作る構造体
        type Response struct {
            UserId         uint
            BoolTogglerId  uint
            Favorite       bool
        }

        // DB接続
        dbs := c.Get("dbs").(*middlewares.DatabaseClient)

        // パスパラメータをuintに変換する
        intUserId, _ := strconv.Atoi(c.Param("user_id"))
        uintUserId := uint(intUserId)
        intTogglerId, _ := strconv.Atoi(c.Param("toggler_id"))
        uintTogglerId := uint(intTogglerId)

        // Response構造体をインスタンス化
        var resJSON Response
        resJSON.UserId = uintUserId
        resJSON.BoolTogglerId = uintTogglerId

        // AppendするときにIDを指定したboolTogglerを渡す
        var boolToggler models.BoolToggler
        boolToggler.ID = uintTogglerId

        // Preloadでuserのリレーションを有効化してselect
        user := &models.User{}
        dbs.DB.Preload("Favorites", "bool_toggler_id = ?", uintTogglerId).
        Find(&user, uintUserId)

        // まだお気に入りされていなかった場合は、新しいレコードを追加
        if len(user.Favorites) < 1 {
            dbs.DB.Model(&user).Association("Favorites").Append(&boolToggler)
            resJSON.Favorite = true

        // お気に入り済みだった場合は、既存のレコードを削除
        } else {
            dbs.DB.Model(&user).Association("Favorites").Delete(&boolToggler)
            resJSON.Favorite = false
        }
        return c.JSON(fasthttp.StatusOK, resJSON)
    }
}

routes/api.go

g.POST("/favorite/users/:user_id/bool_togglers/:toggler_id", api.ToggleFavoriteToggler()) //追記

一応curlしてみるとちゃんと値をトグルしています。

curl -XPOST http://localhost:8080/api/favorite/users/1/bool_togglers/1
{"UserId":1,"BoolTogglerId":1,"Favorite":false}
/go/src/app # curl -XPOST http://localhost:8080/api/favorite/users/1/bool_togglers/1
{"UserId":1,"BoolTogglerId":1,"Favorite":true}

さらに、mysqlのテーブルも確認すると意図した挙動を確認できました。

mysql> select * from user_favorite_togglers;
+---------+-----------------+
| user_id | bool_toggler_id |
+---------+-----------------+
|       1 |               1 |
+---------+-----------------+
1 row in set (0.00 sec)

// アクセス後
mysql> select * from user_favorite_togglers;
Empty set (0.00 sec)

user情報を返すエンドポイント

web/api/show_user.go

package api

import (
    "github.com/labstack/echo"
    "github.com/valyala/fasthttp"
    "hello/models"
    "hello/middlewares"
    "strconv"
)

func ShowUserInfo() echo.HandlerFunc {
    return func(c echo.Context) error {

        type ResToggler struct {
            BoolTogglerId uint
            Toggler       bool
        }

        type Response struct {
            UserId uint
            Name   string
            Favorites []ResToggler
        }

        dbs := c.Get("dbs").(*middlewares.DatabaseClient)
        // パスパラメータをuintに変換する
        intUserId, _ := strconv.Atoi(c.Param("id"))
        uintUserId := uint(intUserId)

        // Preloadでuserのリレーションを有効化してselect
        user := models.User{}
        dbs.DB.Preload("Favorites").Find(&user, uintUserId)

        var resJSON Response
        resJSON.UserId = user.ID
        resJSON.Name   = user.Name
        for _, v := range user.Favorites {
            toggler := ResToggler{
                BoolTogglerId: v.ID,
                Toggler: v.Toggler,
            }
            resJSON.Favorites = append(resJSON.Favorites, toggler)
        }
        return c.JSON(fasthttp.StatusOK, resJSON)
    }
}

Response用の構造体を定義しておくと、そのままJSONとして渡せるのがEchoフレームワークのいいところですね。
結果はこんな構造体を返します。

curl http://localhost:8080/api/user/1
{"UserId":1,"Name":"test_user01","Favorites":[{"BoolTogglerId":1,"Toggler":false}]}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む