- 投稿日:2020-10-16T18:33:34+09:00
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.Uploader
のUpload()
を使うことになります。
アップロードするオブジェクトの内容は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.Writer
とs3manager.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{}) errorAWS 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 自体もまだまだ発展途上なので今後さらに機能が充実することを期待したいですね。
- 投稿日:2020-10-16T18:05:12+09:00
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を検索してインストールします。
初期設定
Ctrl + Shift + P
でメニューを出して、
Remote-Containers:Add Development Cotainers Configuration Files...
を選択します。
私の環境ではすでにdocker-compose.yml及びDockerFileで環境を構築しているため、そちらの設定を元に初期設定を行うことができます。
また、From a predefined container configuration definition...
を選択すると、あらかじめ用意されている、設定を使用することも可能です。
今回は、既存のdocker-composeファイルを使用して初期設定を行います。
.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.ymlversion: '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
を選択初回起動時では、ツール類が
/go/bin
にインストールされていないのでインストールします。goplsを使用するので、以下の通知からインストールするか、
go get -v golang.org/x/tools/gopls
を実行します
その他ツールに関しては、
Go:Install/Update Tools
からgocode
とgocode-gomod
以外をインストールします。
/go/bin
配下に全てインストールされていることが確認できました。ここまでで、コンテナ上で開発することができるようになります。
当然ながら、コンテナ単位での管理となるため、コンテナの削除を行うと、インストールしたものについては、消えるので、再設定が必要となります。デバッグ
コンテナ内でのデバッグも可能です。
マウントした、ソースコードと同じディレクトリに.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.ymlversion: '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参考
- Go勉強(1) mac+VSCode+Go環境を設定:https://qiita.com/oruharo/items/545378eae5c707f717ed
- VS Code Dev Container Guidebook:https://techbookfest.org/product/4696850535809024?productVariantID=5428870601768960
- 投稿日:2020-10-16T16:32:54+09:00
フロントエンジニアが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
を実行これで自動起動されます
- 投稿日:2020-10-16T02:12:37+09:00
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. フォルダ構成の説明
まずはフォルダ構成を説明します。
projectproject ├─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/errors3.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.goconfig := "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.gousers, 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開発とか楽に行えますね。
- 投稿日:2020-10-16T00:01:46+09:00
値を二つの方法でトグルする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}]}