- 投稿日:2019-11-29T19:40:43+09:00
Go Modules 依存パッケージを一括アップデートする
はじめに
Goのパッケージ管理はGo Modulesを使っていますが、何もわからない・・・
最新版に追従するためnpm update
みたいなことをしたいけどわからない・・・アップデート方法
@propellaさんの記事を参考に
go get -u
を実行してみました。$ go get -u go get .: path /Users/naoki/go/src/github.com/oke-py/usn-api is not a package in module rooted at /Users/naoki/go/src/github.com/oke-py/usn-apiどうやらうまくいっていません。検索して見つけたIssueコメントを参考にします。
$ go get -u all go: finding github.com/gofrs/uuid v3.2.0+incompatible go: finding github.com/aws/aws-lambda-go v1.13.3 go: finding github.com/jmespath/go-jmespath latest go: finding github.com/guregu/dynamo v1.4.1 go: finding golang.org/x/net latest go: finding github.com/cenkalti/backoff v2.2.1+incompatible go: finding github.com/aws/aws-sdk-go v1.25.43 go: downloading github.com/aws/aws-sdk-go v1.25.43 go: downloading github.com/guregu/dynamo v1.4.1 go: downloading github.com/aws/aws-lambda-go v1.13.3 go: extracting github.com/aws/aws-lambda-go v1.13.3 go: extracting github.com/guregu/dynamo v1.4.1 go: downloading github.com/cenkalti/backoff v2.2.1+incompatible go: downloading golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 go: extracting github.com/cenkalti/backoff v2.2.1+incompatible go: downloading gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 go: extracting gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 go: extracting golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 go: extracting github.com/aws/aws-sdk-go v1.25.43 go: finding gopkg.in/yaml.v2 v2.2.7 go: finding gopkg.in/check.v1 latest go: downloading gopkg.in/yaml.v2 v2.2.7 go: extracting gopkg.in/yaml.v2 v2.2.7 go: downloading gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 go: extracting gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15
git diff
やgit status
を実行するとgo.mod
とgo.sum
が更新されたことがわかります。おわりに
これで正しいのか自信がありませんが、やりたいことはできました。
参考
- 投稿日:2019-11-29T17:11:43+09:00
Go: DepからGo Modulesへの移行
はじめに
Go modules
はGo1.13(August 2019)から正式に導入されたGo言語公式の依存パッケージ管理ツールです。Go modules
以前は、dep
が依存パッケージ管理のツールとしてはデファクトでした。Go modules
はGo1.11から使用は可能でしたが、環境変数(ENV GO111MODULE=on)を設定したり色々と手間がかかる事前準備が必要でした。Go1.13からはデフォルトでGo modules
が組み込まれています。そもそもGo Modulesとは何?
Goの新しい依存管理システムである。
モジュールは、一つのユニットとしてバージョン管理されている関連Goパッケージの集まり。1.11からはmoduleモードとGOPATHモードが使えるようになっていましたが、1.13からはmoduleモードがデフォルトでONになっています。Goのオープンソースモジュールのエコシステムをより良く活用できるように、moduleモードがデフォルトで動作するようにしているそうです。
Golang.org: modules
Golang.org: Wiki-Modulesモジュールとは
モジュール対応モードでは,標準ライブラリを除くパッケージを「モジュール(module)」として管理する。 パッケージが git 等のバージョン管理ツールで管理されている場合はバージョン毎に異なるモジュールと見なされる。 つまりモジュールの実体は「パッケージ+バージョン」ということになる。
ただしコード上ではパッケージとモジュールの間に区別はなく,したがってソースコードを書き換える必要はない。 モジュールはソースコードではなくgo.mod
ファイルで管理される。depとGo Modulesの機能差マトリクス
項目 dep go modules 実行する場所 GOPATH
以下である必要がある*Goプログラムのルートディレクトリ
*GOPATH以下にあるソースコードでもgo.mod
ファイルがあればモジュール対応モードで管理が可能管理ファイル Gopkg.toml
・Gopkg.lock
go.mod
・go.sum
Vendoring depがvendoring対応のツール go mod vendor
というコマンドでvendoring対応ができるdockerの場合はvendoring利用がおすすめ Gitでの管理 Gopkg.toml
とGopkg.lock
をGit管理する通常は、 go.mod
とgo.sum
は共にGit管理するパッケージ管理 リポジトリの最新リビジョンのみが対象 リポジトリのバージョンタグまたはリビジョン毎に管理。Semantic Versioningに対応 Semantic Versioningとは、vX.X.Xというようなバージョン番号の定義方法 依存packageの場所 $GOPATH/src以下プログラムルートのvendorディレクトリ $GOPATH/pkg/mod以下 *go modで取得したバイナリなどは$GOPATH/pkg/mod/以下にキャッシュされている
*CIの高速化でビルドキャッシュをするときはこのディレクトリをキャッシュする必要がある
*キャッシュを削除するときはgo clean -cacheコマンドで削除
*go mod vendorコマンドでdepのようにvendorディレクトリに依存関係を保存することができる
*go mod tidyでgo.modから不要な依存関係を削除パッケージ管理で、GOPATHの依存がなくなる点くらいで双方の機能差はほぼ無し。
「$GOPATHからの呪縛からの解放」という最大のメリットで、何よりも代えがたいポイントのように思えます。GOPATHモード(GOPATH mode)とモジュールモード(module-aware mode)
バージョン1.11以降からは、Go言語コンパイラは以下の2つのモードのどちらかで動作するようになっていました。
モード GOPATHモード バージョン 1.10 までの動作モード。標準ライブラリを除く全てのパッケージの管理とビルドを $GOPATH 以下のディレクトリで行う。パッケージの管理はリポジトリの最新リビジョンのみが対象となる モジュール対応モード 標準ライブラリを除く全てのパッケージをモジュールとして管理する。モジュールの管理とビルドは任意のディレクトリで可能で,モジュールはリポジトリのバージョンタグまたはリビジョン毎に管理される 環境変数 $GO111MODULE
モード切替は
$GO111MODULE
という環境変数で切り替えます。1.12まではauto
が規定値になっている。 なお1.13 からはモジュール対応モードが既定になります。設定値
設定値 auto $GOPATH 以下のディレクトリにあるパッケージは GOPATH モードで,それ以外はモジュール対応モードで動作する off 常に GOPATH モードで動作する on 常にモジュール対応モードで動作する depからの移行
プロジェクトルートの
Gopkg.lock
ファイルを自動に読み込んで、moduleの初期設定を行ってくれる設計になっています。(depから移行するこが前提になっていますね )
Migrating to Go Modules などが参考になると思います。
- 既存のパッケージに go.mod ファイルを追加する
- Gopkg.lock ファイルを読んで go.mod ファイルに組み込んでくれる
$ go mod init github.com/my-repo/nice-project go: creating new go.mod: module github.com/my-repo/nice-project go: copying requirements from Gopkg.lock*もちろん、まっさらな状態から
go module
で初期化と依存パッケージのダウンロードを最初から行ってもいいです。補足:Dockerでの設定
dockerで開発している場合は、ビルド前に依存パッケージをダウンロード・インストールする必要があります。ポイントだけをピックアップしたDockerfileのイメージです。
# Goのバージョンは1.13以上 FROM golang:1.13-alpine as golang-build # Goビルドに必要なアプリケーションをインストールする RUN ..... WORKDIR /go/src/github.com/my-repo/nice-project # go moduleで依存パッケージを取得してダウンロードする # ローカルの、go.modとgo.sumをコピー COPY go.mod go.sum ./ # 依存ライブラリをダウンロードする RUN go mod download # Goアプリケーションのビルド ADD . /go/src/github.com/my-repo/nice-project RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build main.goビルド前に、go moduleを使って依存ライブラリ・モジュールをダウンロードしておきます。
とても少ない記述量で実現できてしまいます。まとめ
depを使っているときはは、CIのビルドの時間を短縮のために
dep ensure -vendor-only=true
として新しいパッケージを追加する際に、Gopkg.lockとGopkg.tomlを更新していたり手間がかかっていました。modules
でキャッシュディレクトリを設定しておけば、ビルド時間も短縮できるためトリッキーなファイルの運用がなくなりました。1.13がリリースされて4ヶ月位経ちますが、dep
で苦労していたり、切替をお考えの方がいれば、modules
への移行の参考になれば幸いです。
- 投稿日:2019-11-29T15:57:15+09:00
[Go]context.WithTimeoutとcontext.WithDeadlineの違い
context.WithDeadline
コメントを引用すると、以下のように書いてある
WithDeadline returns a copy of the parent context with the deadline adjusted to be no later than d. If the parent's deadline is already earlier than d, WithDeadline(parent, d) is semantically equivalent to parent. The returned context's Done channel is closed when the deadline expires, when the returned cancel function is called, or when the parent context's Done channel is closed, whichever happens first.
Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.context.WithTimeout
WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
つまりcontext.WithTimeoutは
WithDeadline(parent, time.Now().Add(timeout)
を読んでいるだけのよう
https://golang.org/src/context/context.go#L463すこし違うのは、
context.WithDeadline
ではtime.Time
型を引数にとるのに対して、context.WithTimeout
ではtime.Duration
型を引数にとる。つまり、
- context.WithDeadline => キャンセルされる時間を指定する
- context.WithTimeout => 現在時間からキャンセルされるまでの時間を指定する
の違いがある。// 以下の二つは同じ d := time.Now().Add(time.Second) ctx, cancel := context.WithDeadline(ctx, d) // ---------- d := time.Second ctx, cancel := context.WithTimeout(ctx, d)余談
context.WithDeadlineのコメントに書いてあるように親のcontextのDeadlineはWithTimeoutでも同じなので、子のcontextが親のcontextのDeadline以降のDeadlineを指定することはできない
https://golang.org/src/context/context.go#L395試しに書いたコード
package main import ( "context" "fmt" "time" ) func main() { ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() start := time.Now() if err := parentFunc(ctx); err != nil { fmt.Printf("error: %v\n", err) } fmt.Printf("time passed %v\n", time.Since(start)) /* error: parent error: childFunc error: heavyFunc error: context deadline exceeded time passed 1.001596379s */ } func parentFunc(ctx context.Context) error { // Deadline of parent is 1 sec. d := time.Second ctx, cancel := context.WithTimeout(ctx, d) defer cancel() if err := childFunc(ctx); err != nil { return fmt.Errorf("parent error: %w", err) } return nil } func childFunc(ctx context.Context) error { // Deadline of child is 2 sec. d := 2 * time.Second ctx, cancel := context.WithTimeout(ctx, d) defer cancel() if err := heavyFunc(ctx); err != nil { return fmt.Errorf("childFunc error: %w", err) } return nil } func heavyFunc(ctx context.Context) error { select { case <-time.After(10 * time.Second): fmt.Println("finish calculation") return nil case <-ctx.Done(): return fmt.Errorf("heavyFunc error: %w", ctx.Err()) } }
- 投稿日:2019-11-29T14:20:16+09:00
ニフクラ用の Terraformカスタムプロバイダを書いてみた(未完)
概要
先日記事に書いたニフクラ用Terraform Provider について、中途半端なところではありますが開発を終了しました。
ニフクラを利用する職場を離れたため、今後は動作検証もできないので、ここまでとなりました。予定通りロードバランサー、付替IPアドレスまでは追加できたので、作成可能なリソースは最終的に以下のようになりました。
リソース ステータス 備考 サーバー ok インポートやコピーは作っていません ディスク ok ネットワーク・セキュリティ ok SSHキーインポートのみ OSイメージ ok ロードバランサー ok SSL関連は未検証 マルチロードバランサー n/a ファイアウォール ok SSL証明書 n/a 付替IPアドレス ok 追加NIC n/a オートスケール n/a 基本監視 n/a プライベートLAN ok ルーター ok dhcp関連とNAT関連とWebプロキシは作っていません 拠点間VPNゲートウェイ ok サーバーセパレート n/a バックアップ ok RDB ok イベント通知は作っていません NAS n/a サンプルイメージ
examples_v0.12.14+
のコードを実行すると、とりあえず以下のような環境ができあがる状態になっています。
disableにしていますが、RDB やバックアップ、カスタマイズイメージも作成可能です。
備忘録
作成中はずっと Terraform v0.12.9 と examples のサンプルでテストしていました。
無事予定していたところまで完成したので、終了しようと思ったところ、Terraform のバージョンがいつのまにか v0.12.16 まで上がっていました。
せっかくなので最新バージョンで動作確認しようと思ったところ、エラーの嵐が・・・。
v0.12.14以降は変数の記載の仕方が厳格になったのですね。
そのため急遽 examples を修正して、examples_v0.12.14+
を作りました。
これで動く・・・と思いきや、Error: rpc error: code = Unavailable desc
といったエラーが大量にでました。
「今さらどうしよう・・・なぜ・・・」と思いましたが、hashicorp 関連といえばもうひとつあるぞ、と気づきました。
作成しているコード側で、github.com/hashicorp/terraform-plugin-sdk/
を使っているんですよね。
go.mod
を見ると v1.1.1 でしたが、最新版は v1.4.0 まで上がっていました。
go.mod
の記載を修正して再度 build したものを利用したところ、問題は無くなりました。
今後利用される方がいらっしゃった場合に、このあたりも気にしていただくと、しばらくはバージョンが上がってもちゃんと動くのかな、と思っています。今後について
私の開発は終了ですが、もし続きを作ろうかなとか、修正したいなとお考えになる方がいらっしゃったときのために、環境を分ける方法を簡単に記載しておきます。
以下のように自分の環境を作っていただいたらよいのかな、と思っています。
- github等で nifcloud-sdk-go と terraform-provider-nifcloud のリポジトリを作成
- 以下のようにして、自分の環境を作成
mkdir -p $GOPATH/src/github.com/YOURACCOUNT cd $GOPATH/src/github.com/YOURACCOUNT git clone https://github.com/shztki/nifcloud-sdk-go.git cd nifcloud-sdk-go git checkout -b XXXXXX find ./ -type f | xargs sed -i "s/shztki/YOURACCOUNT/g" git add . git commit -m "path change" git push -u origin XXXXXX cd $GOPATH/src/github.com/YOURACCOUNT git clone https://github.com/shztki/terraform-provider-nifcloud.git cd terraform-provider-nifcloud git checkout -b XXXXXX find ./ -type f | xargs sed -i "s/shztki/YOURACCOUNT/g" git add . git commit -m "path change" git push -u origin XXXXXX
- branch を作成している部分は、分けるつもりが無ければ不要です。
- sed での置換については、すべてのファイルが変更になるので注意。
.git
以下も変わるので、git remote set-url origin https://~
が不要になります。- github等のデフォルトブランチは自分が利用するブランチにしてください。
最後に
お読みいただきありがとうございました。
中途半端なところで終了してしまいましたので、業務に取り入れる、などは開発を継続できるような方でないと難しいと思いますが、個人でこっそり構築タスクを効率化する、程度であれば、ちょっとはお役に立てそうかな、と考えたりもしております。
私の試行錯誤した流れが、多少でもお役に立てれば幸いです。
どうぞよいニフクラライフを!
- 投稿日:2019-11-29T14:02:34+09:00
ASTでドキュメントコメント(Docフィールド)を取得する際にハマった
"go/parser"
でパースしたASTから、Docフィールドを取ろうとしてちょっとはまったのでメモGitHub のIssueによると 1
type の前につけたドキュメントコメントは、ast.TypeSpec
ではなく、ast.GenDecl
のDoc
フィールドにつくよという話。// doc comment type Foo struct { }// このコメントは GenDecl.Doc につく type A struct { } // このコメントは GenDecl.Doc につく type ( // このコメントが TypeSpec.Doc につく B struct { } )動作確認のソース Gist
- 投稿日:2019-11-29T12:06:08+09:00
Github actionsでgo mod download, test, lint
Github Actions
https://github.co.jp/features/actions
GithubのCI/CDサービスモチベーション
現職ではほとんどcircleciで回しているが、Github actionsが便利ということを聞いたので試した。
go.modのキャッシュの問題などあり、実際にやってみて記事としてこれ持って来ればOKみたいのが見当たらず、
githubでpublic repoを徘徊したので、最低限自分にあったやり方をまとめておきたいworkflow
stepなどはほぼ他のciサービスと同じだと思うので割愛
実際のyamlは以下github/workflows/ci.yaml// 好きな名前 name: golang_ci // フック on: [push] jobs: build: name: all runs-on: ubuntu-latest steps: # GOPATHなどのセットアップ - name: set up uses: actions/setup-go@v1 with: go-version: 1.13 id: go # repositoryの中身にアクセスするためのチェックアウト - name: checkout uses: actions/checkout@v1 # cache store - name: cache uses: actions/cache@v1 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: download modules # キャッシュが保存されていればそれを使う if: steps.cache.outputs.cache-hit != 'true' run: go mod download - name: install lint run: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.18.0 - name: lint run: golangci-lint run - name: test run: go test ./... -v基本的にactionを指定して環境を構築 -> 実行という流れ
参照できるactionは以下で
- public repository
- workflowと同一repository
- Dockerhubで公開されているDockerコンテナイメージ感想
circleciと比べてみての感想になってしまうが以下のようなメリデメリを感じた
メリット
- actionsがOSSで定義されていてわかりやすい。自前のactionsも使えるのでいざとなればフォークして...
- やるべきではないという意見もあるかもしれないが、workflow配下をgithub GUI上で操作してコミット -> 反映というサイクルがまわしやすい(CIなど開発環境の整備のためのGithub上でのコミットは良いと思っている)
- 従量課金だが個人利用Freeであれば
2000分/月
まで無料枠らしいのでだいたいおさまる(https://github.co.jp/features/actions)- 1repository 20並列の使用制限(https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#usage-limits)
- 他CIのように開発者tokenとかを外部に託さなくて良い(当たり前)
デメリット
- 参考にできる実例が少ない これは仕方ない。実際に公開repositoryでgo.modのキャッシュ関連を
All Github
で調べても100件はなさそうだった- sshがちょっとめんどくさそう workflowに変更を入れないとsshできない様子
手軽にsshしたいなと感じたくらいで、あとはCI構築のためには最適なGithub上でのtry and error、確認が手軽にできるのが嬉しい
今後個人repoなら使っていくかもしれない
- 投稿日:2019-11-29T11:17:08+09:00
04. 元素記号
04. 元素記号
"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.
Go
package main import ( "fmt" "strings" ) func main() { var src = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."; var dw = map[int]bool{1: true, 5: true, 6: true, 7: true, 8: true, 9: true, 15: true, 16: true, 19: true}; // 1文字の単語番号 var res map[string]int = map[string]int{}; // 単語に分割 words := strings.Split(src, " ") for i, word := range words { idx := i + 1 // 配列の exists的関数がなさそうなので Map を使用... if dw[idx] { // 1文字を map へ保存 res[word[0:1]] = idx } else { // 2文字を map へ保存 res[word[0:2]] = idx } } // 結果を表示 fmt.Println(res) }python
# -*- coding: utf-8 -*- src = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can." dw = [1, 5, 6, 7, 8, 9, 15, 16, 19] # 1文字の単語番号 res = {} # 単語に分割 words = src.split(" ") for i in range(len(words)): # 単語番号 idx = i + 1 # 1文字の単語番号の場合 if idx in dw: # 1文字を map へ保存 res[words[i][0:1]] = idx else: # 2文字を map へ保存 res[words[i][0:2]] = idx # 結果を表示 print(res)Javascript
var src = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can." var dw = [1, 5, 6, 7, 8, 9, 15, 16, 19]; // 1文字の単語番号 var res = new Map(); // 単語を空白で分割し単語数処理 var words = src.split(' '); for (var i = 0; i < words.length; i++) { // 単語番号 var idx = i+1; // 1文字の単語指定の場合 (ES2017) if (dw.includes(i+1)) { // 1文字を map へ保存 res.set(words[i].substring(0,1),idx); } else { // 2文字を map へ保存 res.set(words[i].substring(0,2),idx); } } // 結果表示 console.log(res);まとめ
if文で or を書くのがイマイチかと思い 1文字の単語番号をまとめてみた。
Goの配列などの存在チェックがイマイチ?。次の「05. n-gram」 は問題のの意味がよくわからない、まずは問題の理解から初めて見る。
他の人のを確認したら Map の内容が [文字 => 単語番号] と逆だったため修正しました。
その他
もともと、C言語やPHPが長いので Goの行末の ";" 癖が治らない。w
- 投稿日:2019-11-29T10:10:13+09:00
DockerでGoのRestAPIを作ってみた(ホットリロード対応)
dockerでGolangのAPIを作りたいと思いましてやってみました。
ちなみに、dockerはホットリロードさせたかったので、docker-composeを使っています。環境
それぞれのバージョンはこちら。
Dockerについての説明とかは省きます。$ docker-compose --version docker-compose version 1.24.1, build 4667896b $ go version go version go1.13.4 darwin/amd64■ echo 4.1.11 (ルーティングが便利になるかな、と)
公式はこちら https://echo.labstack.com/guide
以下によると、RestAPIに最適化されているらしー。
https://rightcode.co.jp/blog/become-engineer/go-flamework■ oxequa/realize
ホットリロードを行うために入れてみた。ファイルの準備
適当なフォルダに以下の3ファイルを用意します。
Docker
Dockerfile
# 公式 golang ランタイムをイメージとして使用 FROM golang:1.13 # ソース入れる用のディレクトリ作成(名前はなんでもいいけど、この後の「app」は全部合わせる) RUN mkdir /app # ソース入ってるのここだよ WORKDIR /app # ホスト側のカレントにあるファイルをソースフォルダにコピーするよ COPY . /app # echo と oxequa/realize のライブラリ取ってくるよ(ぼちぼち時間かかるよ...) RUN go get -u github.com/labstack/echo/... RUN go get github.com/oxequa/realizedocker-compose.yml
docker-compose.ymlversion: '3' services: api: build: . #buildが指定されている場合は作成イメージの名前になる #build指定なし:このイメージを元に作成される(ex.mysqlとか) image: sample_api:0.1 # ポートフォワーディング(ホスト側:docker側) # ホスト側のブラウザで実行するときは http://localhost:1000 で見れるようになる ports: - "1000:1323" # (ホスト側:docker側)dockerの「/app」フォルダとホストのカレントを繋げるよ。 # ホストの変更が反映されるようになる volumes: - .:/app # デフォルトのコマンドをこれに変える。ホットリロードを有効にする? command: realize start --run --no-config tty: trueGolang
server.go
server.gopackage main import ( "net/http" "github.com/labstack/echo" ) func main() { e := echo.New() routing(e) e.Logger.Fatal(e.Start(":1323")) } /* ルーティングを行う */ func routing(e *echo.Echo) { e.GET("/", hello) e.GET("/:name", greeting) } /* http:/~/ の時 */ func hello(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "hello"}) } /* http:/~/(名前) の時 */ func greeting(c echo.Context) error { /* c.Param("name") とすることで、URLの:name と対応させて取得 */ return c.JSON(http.StatusOK, map[string]string{"message": "hello " + c.Param("name")}) }Docker起動
上記ファイルを保管したフォルダに移動して、コマンド実行
(「-d」オプションつけてバックグラウンド実行しない理由は後述。つけても大丈夫)$ docker-compose up
これで、ローカルのブラウザから
http://localhost:1000
ってしたら
{message:hello}
が。
http://localhost:1000/taro
ってしたら
{message:hello taro}
って帰ってくるはず。詰まった時の小ネタ
docker-compose up でやってみる
どういうことかというと、「-d」を取ることでエラーの内容を教えてくれる。あずきはコマンド実行後、ターミナルをそのまま使いたかったので、おまじないのように「-d」をつけてました。
で、エラーで動かないんだけど(docker ps にあがってこない)なんでか分かんなかった。原因は同じフォルダ内に「func main()~」が書かれたファイルがいたってことだった。
試行錯誤しながら作ってたので、バックグランド実行だと気づかなくて何時間も無駄にした。でも、苦労したことは忘れない。きっと。
参考
https://qiita.com/y-ohgi/items/671de11f094f72a058b1
https://www.fox-hound.tech/1179/
https://qiita.com/prgseek/items/e557a371d7bd1f57b9b1
https://qiita.com/TsutomuNakamura/items/7e90e5efb36601c5bc8a
http://docs.docker.jp/engine/articles/dockerfile_best-practice.html
https://qiita.com/zembutsu/items/9e9d80e05e36e882caaa
API関連
https://ken-aio.github.io/post/2019/01/30/golang-echo/
- 投稿日:2019-11-29T03:42:58+09:00
ストレージ層のテスト
ではstore構造体にtodoManagerインターフェイスを実装していきます。
store.gofunc (s *store) projectTodo(t todo) (string, error) { return "", nil }テーブル定義
まずは保存用のテーブルを定義します。シンプルに以下の感じでいいでしょう。
sql.gopackage schema var schemas = map[int]string{ 0: ` ... `, 1: ` BEGIN; CREATE TABLE todo ( id UUID NOT NULL PRIMARY KEY, title STRING NOT NULL, description STRING NOT NULL ); UPDATE schema_version SET md_curr = false, md_update = EXTRACT(EPOCH FROM current_timestamp)::INT WHERE md_curr = true; INSERT INTO schema_version VALUES (1); COMMIT; `, }スキーマのバージョンを更新するクエリにも注目してみてください。
store構造体のテスト
以下のようにテストを書いていきます。
store_test.gopackage main import "testing" func TestStore_ProjectTodo(t *testing.T) { t.Run("project a new todo", func(t *testing.T) {}) }テスト用データベースのセットアップ
テスト時にはローカルにCockroachDBのインスタンスを立ち上げ、ダミーのDBに対してテストを実行していきたいと思います。
なのでセットアップ用に以下のユーティリティ関数、変数を用意します。store_test.govar testDB = fmt.Sprintf("test-%s", uuid.New().String()) func openDB(t *testing.T) *sql.DB { connStr := "postgres://root@localhost:26257/?sslmode=disable" db, err := sql.Open("postgres", connStr) require.NoError(t, err) _, err = db.Exec(fmt.Sprintf(`CREATE DATABASE IF NOT EXISTS "%s"; USE "%s"`, testDB, testDB)) require.NoError(t, err) return db } func closeDB(t *testing.T, db *sql.DB) { require.NoError(t, db.Close()) }テスト実行のたびにデータベースを作成することになるためパフォーマンスはよいとは言えませんが、問題となる規模になるまではこれで十分でしょう。
ローカルでのCockroachDB起動のため以下のdocker-compose.yaml
を用意します。docker-compose.yamlversion: '3' services: cockroachdb: image: cockroachdb/cockroach container_name: cockroachdb ports: - "26257:26257" - "8080:8080" command: ["start", "--insecure"]テストの実行
準備が整ったためテストを書きます。
一気に書いてしまいます。以下のような感じです。store_test.gofunc TestStore_ProjectTodo(t *testing.T) { t.Run("project a new todo", func(t *testing.T) { db := openDB(t) defer closeDB(t, db) sut, err := newStore(db, defaultSchemaVersion) require.NoError(t, err) input := todo{ title: "foo title", description: "foo description", } id, err := sut.projectTodo(input) require.NoError(t, err) var got todo require.NoError(t, db.QueryRow( `SELECT title, description FROM todo WHERE id = $1`, id, ).Scan(&got.title, &got.description)) assert.Equal(t, input, got) }) }
main.go
でデフォルトのスキーマバージョンを更新しておくのもお忘れなく。var ( gitHash = "overriden at compile time" - defaultSchemaVersion = 0 + defaultSchemaVersion = 1 )ではdocker-composeでローカル環境を立ち上げて、テストを実行してみます。
$ docker-compose up Creating network "qiita-advent-calendar-2019_default" with the default driver Creating cockroachdb ... done Attaching to cockroachdb ... $ make test go test -v -cover -timeout 30s ./... === RUN TestServer_CreateTodo === RUN TestServer_CreateTodo/project_a_new_todo === RUN TestServer_CreateTodo/error_in_projection --- PASS: TestServer_CreateTodo (0.00s) --- PASS: TestServer_CreateTodo/project_a_new_todo (0.00s) --- PASS: TestServer_CreateTodo/error_in_projection (0.00s) === RUN TestStore_ProjectTodo === RUN TestStore_ProjectTodo/project_a_new_todo --- FAIL: TestStore_ProjectTodo (0.21s) --- FAIL: TestStore_ProjectTodo/project_a_new_todo (0.21s) require.go:794: Error Trace: store_test.go:47 Error: Received unexpected error: pq: error in argument for $1: could not parse string "" as uuid Test: TestStore_ProjectTodo/project_a_new_todo FAIL coverage: 22.9% of statements FAIL github.com/KentaKudo/qiita-advent-calendar-2019 0.897s ? github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service [no test files] ? github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema [no test files] FAIL make: *** [test] Error 1失敗です。いいですね。
store.projectTodoの実装
ではいよいよ
store.projectTodo
を以下のように実装します。store.gofunc (s *store) projectTodo(t todo) (string, error) { id := uuid.New().String() if _, err := s.db.Exec( `INSERT INTO todo (id, title, description) VALUES ($1, $2, $3)`, id, t.title, t.description, ); err != nil { return "", err } return id, nil }再度テストを実行。
$ make test go test -v -cover -timeout 30s ./... === RUN TestServer_CreateTodo === RUN TestServer_CreateTodo/project_a_new_todo === RUN TestServer_CreateTodo/error_in_projection --- PASS: TestServer_CreateTodo (0.00s) --- PASS: TestServer_CreateTodo/project_a_new_todo (0.00s) --- PASS: TestServer_CreateTodo/error_in_projection (0.00s) === RUN TestStore_ProjectTodo === RUN TestStore_ProjectTodo/project_a_new_todo --- PASS: TestStore_ProjectTodo (0.55s) --- PASS: TestStore_ProjectTodo/project_a_new_todo (0.55s) PASS coverage: 24.7% of statements ok github.com/KentaKudo/qiita-advent-calendar-2019 1.269s coverage: 24.7% of statements ? github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service [no test files] ? github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema [no test files]通りました:)
main.goの更新
最後に
main.go
内でserver構造体に依存注入するようにしましょう。
利便性のためnewServer()
関数も定義しておきます。server.gofunc newServer(todoMgr todoManager) *server { return &server{ todoMgr: todoMgr, } }main.gofunc main() { app := cli.App(appName, appDesc) ... app.Action = func() { ... store, err := newStore(db, *schemaVersion) if err != nil { log.WithError(err).Fatalln("init store") } lis, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(*grpcPort))) if err != nil { log.Fatalln("init gRPC server:", err) } defer lis.Close() gSrv := initialiseGRPCServer(newServer(store)) ... } if err := app.Run(os.Args); err != nil { log.WithError(err).Fatal("app run") } }CircleCI設定の更新
忘れていました。CircleCI上でもCockroachDBを立ち上げてテストできるようにします。
$ git diff diff --git a/.circleci/config.yml b/.circleci/config.yml index 52436a0..fbeabaa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,6 +4,8 @@ jobs: working_directory: ~/qiita-advent-calendar-2019 docker: - image: circleci/golang:1 + - image: cockroachdb/cockroach + command: ["start", "--insecure"] steps: - checkout - run: make all @@ -12,6 +14,8 @@ jobs: working_directory: ~/qiita-advent-calendar-2019 docker: - image: circleci/golang:1 + - image: cockroachdb/cockroach + command: ["start", "--insecure"] steps: - checkout - run: make allついでに
やっぱりインテグレーションテストもしてみたかったのでBloomRPCでデバッグしてみました。
デプロイをしなおしたらport-forwardでポッドを繋いで、
$ kubectl -n qiita scale --replicas=0 deployment qiita-advent-calendar-2019 $ kubectl -n qiita scale --replicas=1 deployment qiita-advent-calendar-2019 $ kubectl -n qiita port-forward qiita-advent-calendar-2019-5bc6786c75-jwl5r 8090:8090 Forwarding from 127.0.0.1:8090 -> 8090 Forwarding from [::1]:8090 -> 8090BloomRPCでリクエストの送信。
DBの覗き見。
$ kubectl -n qiita exec -it cockroachdb-0 -- /cockroach/cockroach sql --url postgres://root@localhost:26257 --insecure ... root@localhost:26257/defaultdb> use qiita_advent_calendar_2019_db; SET Time: 998.634µs root@localhost:26257/qiita_advent_calendar_2019_db> select * from todo; id | title | description +--------------------------------------+------------------+-------------------------------------------------+ 8644af1a-d16a-4b92-911f-19e03905dffe | wash your hands! | wash your hands when you get back from outside! (1 row) Time: 26.989411msいいですねいいいですね。
というわけで
CreateTodo
エンドポイントの実装が完了しました。
盛りだくさんでしたがいかがでしたか?明日からはトピックがまた変わります。お楽しみに。
- 投稿日:2019-11-29T02:46:18+09:00
インターフェイス、テスト、モック
昨日までPOSTメソッドとか言ってましたが実際には
rpc CreateTodo
ですね、失礼しました。
実装していきます。実装していく過程で、テストやモックなどもみていきたいと思います。
では参りましょう。インターフェイスの定義
まずインターフェイスを定義し、server構造体からどう呼び出したいかを決めます。
今回はTodoを作成したいので以下のようにserver.go
に定義しました。server.go... var _ service.TodoAPIServer = (*server)(nil) type ( todo struct{} todoManager interface { projectTodo(todo) (string, error) } server struct { todoMgr todoManager } ) func (*server) GetTodo(context.Context, *service.GetTodoRequest) (*service.GetTodoResponse, error) { ...todo構造体を受け取り、保存したらそのidを返します。
サーバーはこのtodoManager
インターフェイスへ依存することにします。モックの作成
ではテストを書くために、このインターフェイスからモックを作成しましょう。
モックのコードジェネレートには github.com/golang/mock/mockgen を使います。
今回は直接Makefileにタスクを追加していきましょう。まずはツールインストール用のタスク。mockgen-install: GO111MODULE=off go get github.com/golang/mock/gomock go install github.com/golang/mock/mockgen次にコード生成用のタスク。
mockgen: mockgen -source=server.go -package=main -destination server_mock.go -mock_names todoManager=MockTodoManagerでは試してみます。
$ make mockgen-install GO111MODULE=off go get github.com/golang/mock/gomock go install github.com/golang/mock/mockgen $ make mockgen mockgen -source=server.go -package=main -destination server_mock.go -mock_names todoManager=MockTodoManager無事
server_mock.go
が生成されているようです。テスト
では
server_test.go
を作成し、テストを書いていきましょう。server_test.gopackage main import "testing" func TestServer_CreateTodo(t *testing.T) { }個人的にはTestSuite構造体を作ってテストを書くのがお気に入りです。
server_test.gotype serverTestSuite struct { sut *server ctrl *gomock.Controller todoMgr *MockTodoManager } func newServerTestSuite(t *testing.T) serverTestSuite { ctrl := gomock.NewController(t) todoMgr := NewMockTodoManager(ctrl) return serverTestSuite{ sut: &server{todoMgr: todoMgr}, ctrl: ctrl, todoMgr: todoMgr, } }まずは保存が成功するパターンをテストしてみましょう。
server_test.gofunc TestServer_CreateTodo(t *testing.T) { t.Run("project a new todo", func(t *testing.T) { s := newServerTestSuite(t) defer s.ctrl.Finish() input := &service.CreateTodoRequest{} want := &service.CreateTodoResponse{} got, err := s.sut.CreateTodo(context.Background(), input) require.NoError(t, err) assert.Equal(t, want, got) }) }ここで期待すべきはserver構造体が
todoMgr.projectTodo()
を呼び出すことなので、以下のようにアサーションを追加します。server_test.gofunc TestServer_CreateTodo(t *testing.T) { t.Run("project a new todo", func(t *testing.T) { s := newServerTestSuite(t) defer s.ctrl.Finish() input := &service.CreateTodoRequest{} todoID := uuid.New().String() want := &service.CreateTodoResponse{} s.todoMgr.EXPECT(). projectTodo(todo{}). Return(todoID, nil) got, err := s.sut.CreateTodo(context.Background(), input) require.NoError(t, err) assert.Equal(t, want, got) }) }inputとwantも値を埋めていきましょう。
server_test.gofunc TestServer_CreateTodo(t *testing.T) { t.Run("project a new todo", func(t *testing.T) { s := newServerTestSuite(t) defer s.ctrl.Finish() input := &service.CreateTodoRequest{ Todo: &service.Todo{ Title: "foo todo", Description: "foo description", }, } todoID := uuid.New().String() want := &service.CreateTodoResponse{ Success: true, Id: todoID, } s.todoMgr.EXPECT(). projectTodo(todo{}). Return(todoID, nil) got, err := s.sut.CreateTodo(context.Background(), input) require.NoError(t, err) assert.Equal(t, want, got) }) }最後にtodo構造体を更新してタイトルと説明文が保存されるようにします。
server.gotype ( todo struct { title string description string } ... )server_test.gofunc TestServer_CreateTodo(t *testing.T) { t.Run("project a new todo", func(t *testing.T) { s := newServerTestSuite(t) defer s.ctrl.Finish() input := &service.CreateTodoRequest{ Todo: &service.Todo{ Title: "foo todo", Description: "foo description", }, } todoID := uuid.New().String() want := &service.CreateTodoResponse{ Success: true, Id: todoID, } s.todoMgr.EXPECT(). projectTodo(todo{ title: input.Todo.Title, description: input.Todo.Description, }). Return(todoID, nil) got, err := s.sut.CreateTodo(context.Background(), input) require.NoError(t, err) assert.Equal(t, want, got) }) }いい感じです。テストを実行してみます。
$ make test go test -v -cover -timeout 30s ./... === RUN TestServer_CreateTodo === RUN TestServer_CreateTodo/project_a_new_todo --- FAIL: TestServer_CreateTodo (0.00s) --- FAIL: TestServer_CreateTodo/project_a_new_todo (0.00s) server_test.go:57: Error Trace: server_test.go:57 Error: Not equal: expected: &service.CreateTodoResponse{Success: true, Id: "1fbf35f0-9b75-4d28-95fe-ea0dc3d67866", } actual : &service.CreateTodoResponse{Success: false, Id: "", } Diff: --- Expected +++ Actual @@ -1,2 +1,2 @@ -(*service.CreateTodoResponse)(&CreateTodoResponse{Success:true,Id:1fbf35f0-9b75-4d28-95fe-ea0dc3d67866,}) +(*service.CreateTodoResponse)(&CreateTodoResponse{Success:false,Id:,}) Test: TestServer_CreateTodo/project_a_new_todo server_test.go:58: missing call(s) to *main.MockTodoManager.projectTodo(is equal to {foo todo foo description}) /Users/kenta/.ghq/github.com/KentaKudo/qiita-advent-calendar-2019/server_test.go:49 server_test.go:58: aborting test due to missing call(s) FAIL coverage: 9.1% of statements FAIL github.com/KentaKudo/qiita-advent-calendar-2019 0.682s ? github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service [no test files] ? github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema [no test files] FAIL make: *** [test] Error 1いいですね、期待通り失敗しています。
server.CreateTodoの実装
では、
server.go
を以下のように更新して、もう一度実行してみます。server.gofunc (s *server) CreateTodo(ctx context.Context, req *service.CreateTodoRequest) (*service.CreateTodoResponse, error) { id, err := s.todoMgr.projectTodo(todo{ title: req.Todo.Title, description: req.Todo.Description, }) if err != nil { return nil, err } return &service.CreateTodoResponse{ Success: true, Id: id, }, nil }$ make test go test -v -cover -timeout 30s ./... === RUN TestServer_CreateTodo === RUN TestServer_CreateTodo/project_a_new_todo --- PASS: TestServer_CreateTodo (0.00s) --- PASS: TestServer_CreateTodo/project_a_new_todo (0.00s) PASS coverage: 17.4% of statements ok github.com/KentaKudo/qiita-advent-calendar-2019 0.695s coverage: 17.4% of statements ? github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service [no test files] ? github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema [no test files]無事テストが通りました:)
todoMgrがエラーを返すパターンもテストしますが、目新しいことはないので説明は割愛します。コミットを確認してみてください。
残るはstore構造体にtodoManagerインターフェイスを実装する部分ですが、少し長くなってきたのでまた明日にします。では。
- 投稿日:2019-11-29T00:34:43+09:00
構造体へのインターフェースの埋め込み
構造体へのインターフェースの埋め込みについての知識に漏れがあったので、調べたことをまとめました。
Goでは、インターフェースの明示的宣言はなく、インターフェースの持つメソッドを実装することでそのインターフェースを満たしていることになります(ダックタイピング)。
しかし、実はインターフェースを構造体に埋め込むことでも、例えその構造体はインターフェースの持つメソッドを持っていなくても、そのインターフェースを満たしていることになります。
下記のコードでは、user構造体はUserInterfaceを持っているのでUserInterfaceのもつFullName()を持っていなくてもUserInterfaceを満たしていることになります。よって、下記のコードはビルドが通ります。
func main() { taro := testUser{ firstName: "Taro", lastName: "Yamada", } outputUser(taro) //testUserがUserInterfaceを満たしているため、taroをoputputUserに渡せる } type UserInterface interface { FullName() string } type testUser struct { UserInterface // interfaceを埋め込むことで、メソッドを実装しなくてもそのinterfaceを満たせる firstName, lastName string } func outputUser(u UserInterface) { fmt.Println(u.FullName())//userはFullNameメソッドを持っていないがコンパイルエラーにならない }しかし、ouputUserメソッド内で実際には実装されていないFullName()メソッドを呼んでいるため、実行時にはpanicが起きます。
❯ go run main.go panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10992c7] goroutine 1 [running]: main.main() /Users/mmm/go/src/github.com/masalennon/test/main.go:13 +0x87 exit status 2上記のコードに、FullNameメソッドを追加すれば、ちゃんと実行されます。
func (u user) FullName() string { return u.firstName + " " + u.lastName } // Taro Yamadaおわり