- 投稿日:2019-11-27T23:39:53+09:00
fmtとポインタのTips
はじめに
標準出力する際に fmt.Print を用いることがあると思います。ポインタを fmt.Println に渡すと興味深い結果が表示されることがあります。その例を紹介します。
まずは以下の実装例を見てください。単に fmt.Println で time.Now() の変数とそのポインタ型の変数を標準出力しているだけです。
package main import ( "fmt" "time" ) func main() { t := time.Now() printTime(&t) } func printTime(t *time.Time) { fmt.Println(t) fmt.Println(*t) }
- 出力結果
上記の実装の出力結果は以下のようになります。ポインタの値を表示される想定でしたが、実際に表示されたのは time.Time の値でした。なぜでしょう。
2009-11-10 23:00:00 +0000 UTC m=+0.000000001 2009-11-10 23:00:00 +0000 UTC m=+0.000000001https://play.golang.org/p/8_kqljv7LVL
理由
fmtが表示するもの
まず fmt.Println が何を表示するのか確認します。Package fmt によると
%T
や%p
を用いる場合を除いて、指定の順番で表示するようになっています。fmt.Println
に効いてくるのは 5 番目の内容です。
- If an operand implements method String() string, that method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any).
String() メソッドを実装している場合は、オブジェクトを文字列に変換するために String() が呼ばれます。time.Time は以下のように String() メソッドを実装していました。そのため fmt.Print() で標準出力するときは String() メソッドの返り値の文字列が表示されます。
func (t Time) String() string { s := t.Format("2006-01-02 15:04:05.999999999 -0700 MST") // Format monotonic clock reading as m=±ddd.nnnnnnnnn. if t.wall&hasMonotonic != 0 { m2 := uint64(t.ext) sign := byte('+') if t.ext < 0 { sign = '-' m2 = -m2 } m1, m2 := m2/1e9, m2%1e9 m0, m1 := m1/1e9, m1%1e9 var buf []byte buf = append(buf, " m="...) buf = append(buf, sign) wid := 0 if m0 != 0 { buf = appendInt(buf, int(m0), 0) wid = 9 } buf = appendInt(buf, int(m1), wid) buf = append(buf, '.') buf = appendInt(buf, int(m2), 9) s += string(buf) } return s }ポインタのメソッドセット
time.Time は String() メソッドを保持しているので文字列を表示するのは分かりました。ではなぜ *time.Time が String() メソッドを保持しているのでしょうか?
Method sets から引用すると、以下が理由になります。
The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T).
つまり *T 型のメソッドセットは T 型と *T 型のメソッドセットを保持します。
よって今回の例では *time.Time 型の変数は time.Time 型で実装していた String() を保持しているので String() が返却する文字列を表示することができました。
参考
- 投稿日:2019-11-27T23:29:16+09:00
Go で DIP に従って依存関係を逆転させたいときは ifacemaker と moq の組み合わせが強い
はじめに
- Go で DIP(依存関係逆転の法則)に従って依存関係を逆転させたくなったとき ifacemaker を使って構造体からインターフェイスを生成して moq を使ってインターフェイスからモックを作るのが便利
例
- 構造体 User の値をメモリに登録・参照するリポジトリ UserMemoryRepository がある
- UserMemoryRepository をサービスなどから利用したくなったとして、直接参照するのではなく UserMemoryRepository を抽象化したインターフェイスを参照するようにしたいとする
Userの定義type User struct { ID int Name string }UserMemoryRepositoryの定義type UserMemoryRepository struct { } var userMemoryRepositoryMap = map[int]*User{} func (r *UserMemoryRepository) Get(id int) (*User, bool) { u, ok := userMemoryRepositoryMap[id] return u, ok } func (r *UserMemoryRepository) Put(u *User) error { userMemoryRepositoryMap[u.ID] = u return nil }構造体からインターフェイスを生成する
- UserMemoryRepository を抽象化して扱うために ifacemaker を使ってインターフェイスを生成する
実行コマンドifacemaker -f user.go -s UserMemoryRepository -i UserRepository -p user -o user_interface.go生成されたインターフェイスUserRepositorypackage user // UserRepository ... type UserRepository interface { Get(id int) (*User, bool) Put(u *User) error }インターフェイスからモックを生成する
- UserMemoryRepository のインターフェイスとして生成した UserRepository を使ったテストをしやすいように moq を使ってモックを生成する
実行コマンドmoq . UserRepository -out user_mock.go
生成されたモック
生成されたモック// Code generated by moq; DO NOT EDIT. // github.com/matryer/moq package user import ( "sync" ) var ( lockUserRepositoryMockGet sync.RWMutex lockUserRepositoryMockPut sync.RWMutex ) // Ensure, that UserRepositoryMock does implement UserRepository. // If this is not the case, regenerate this file with moq. var _ UserRepository = &UserRepositoryMock{} // UserRepositoryMock is a mock implementation of UserRepository. // // func TestSomethingThatUsesUserRepository(t *testing.T) { // // // make and configure a mocked UserRepository // mockedUserRepository := &UserRepositoryMock{ // GetFunc: func(id int) (*User, bool) { // panic("mock out the Get method") // }, // PutFunc: func(u *User) error { // panic("mock out the Put method") // }, // } // // // use mockedUserRepository in code that requires UserRepository // // and then make assertions. // // } type UserRepositoryMock struct { // GetFunc mocks the Get method. GetFunc func(id int) (*User, bool) // PutFunc mocks the Put method. PutFunc func(u *User) error // calls tracks calls to the methods. calls struct { // Get holds details about calls to the Get method. Get []struct { // ID is the id argument value. ID int } // Put holds details about calls to the Put method. Put []struct { // U is the u argument value. U *User } } } // Get calls GetFunc. func (mock *UserRepositoryMock) Get(id int) (*User, bool) { if mock.GetFunc == nil { panic("UserRepositoryMock.GetFunc: method is nil but UserRepository.Get was just called") } callInfo := struct { ID int }{ ID: id, } lockUserRepositoryMockGet.Lock() mock.calls.Get = append(mock.calls.Get, callInfo) lockUserRepositoryMockGet.Unlock() return mock.GetFunc(id) } // GetCalls gets all the calls that were made to Get. // Check the length with: // len(mockedUserRepository.GetCalls()) func (mock *UserRepositoryMock) GetCalls() []struct { ID int } { var calls []struct { ID int } lockUserRepositoryMockGet.RLock() calls = mock.calls.Get lockUserRepositoryMockGet.RUnlock() return calls } // Put calls PutFunc. func (mock *UserRepositoryMock) Put(u *User) error { if mock.PutFunc == nil { panic("UserRepositoryMock.PutFunc: method is nil but UserRepository.Put was just called") } callInfo := struct { U *User }{ U: u, } lockUserRepositoryMockPut.Lock() mock.calls.Put = append(mock.calls.Put, callInfo) lockUserRepositoryMockPut.Unlock() return mock.PutFunc(u) } // PutCalls gets all the calls that were made to Put. // Check the length with: // len(mockedUserRepository.PutCalls()) func (mock *UserRepositoryMock) PutCalls() []struct { U *User } { var calls []struct { U *User } lockUserRepositoryMockPut.RLock() calls = mock.calls.Put lockUserRepositoryMockPut.RUnlock() return calls }これまでのコード生成を go generate で実行する
- 構造体からインターフェイスの生成、インターフェイスからモックの生成、それぞれを go generate コマンドひとつでまとめて実行できるように
go:generate
コメントを追記してくUserMemoryRepositoryの定義の上にコメントを追記//go:generate ifacemaker -f user.go -s UserMemoryRepository -i UserRepository -p user -o user_interface.go //go:generate moq -out user_mock.go . UserRepository type UserMemoryRepository struct {モックを使ってテストする
- 生成したモックを使ってテストを書いてみる
- moq で生成したモック構造体を使う場合は初期化時に必要な関数の実装を関数リテラルで指定するだけなので直感的
package user import "testing" func TestUserRepository_Get(t *testing.T) { r := UserRepositoryMock{ GetFunc: func(id int) (*User, bool) { return &User{}, true }, } if _, ok := r.Get(100); !ok { t.Fatal("Get must ok.") } }おわりに
- testing はテスト用のミニ言語とか読むのも覚えるのもだるいからやめようぜっていう方向性なのに gomock が公式みたいな顔して独自の記法をゴリ押ししてくるのが納得いってないので moq を使ってやればいいとおもいました。
- 投稿日:2019-11-27T17:17:44+09:00
個人開発のWebサービスをリリースして2ヶ月で見つかったバグを一挙紹介する
この記事は、ひとり開発 Advent Calendar 2019の2日目の記事になります。
1日目の記事はhimataniさんのひとり開発でも諦めない、これからのプロダクトのつくり方でした。はじめに
こんにちは、ぷらす (@p1ass)です。
皆さんは個人開発をする時にしっかりとテストやデバッグをしていると言い切れますか?
私は趣味レベルで開発しているときはスピード優先でテストをないがしろにしていまうときがあります。しかし、大抵バグを埋め込んでしまい、後々大変なことになってしまいます。
この記事では、私が個人で開発しているMemoito(めもいと)を開発・運用していく上で見つかったバグ・不具合を自戒を込めて一挙紹介していきます。
この記事を読んで、今一度、自分の開発しているサービスを見直すきっかけになれば幸いです。
Memoitoとは
最初に、この記事で紹介していくバグが発見されたWebサービスであるMemoitoについて先に紹介しておきます。
MemoitoはTwitter連携を用いて、メモをフォローしている人と紐付けて保存することができるWebサービスです。
Twitterで気軽に連絡先を交換できるようになりましたが、徐々にフォローしている人が増えてきて、「この人誰だっけ?」となる経験はないでしょうか?
Memoitoはフォローしている人と紐付けてメモを保存できるので、スマホの標準メモアプリでメモするより簡単にメモを取ることができます。
勉強会やミートアップ、カンファレンス等で会った人のことを、その時話した内容と一緒にメモを取れば、後から見直すことができてとても便利です。
公式Twitterもあります。
Memoito(めもいと)はSNSの繋がりと紐付けてメモを保存できるWebサービスです!
— Memoito(めもいと)公式 (@MemoitoOfficial) September 30, 2019
フォローした理由をメモしておいたり、オフ会で会った時に話した内容をメモしたりできるので是非使ってみてください!https://t.co/r7rzQ9sXI4アーキテクチャ
サービスのアーキテクチャが分かっていた方が記事を読みやすいと思うので、軽く説明します。詳しいことは、今度書く予定の記事を参照してもらうとして、こんな感じのアーキテクチャになっています。
フロントエンド
- Nuxt.js
- Netlify
バックエンド
- Go
インフラ
- GKE
- CloudSQL
デプロイはCircleCIに寄せていて、Dockerイメージのpushや
kubectl apply
などを実行しています。また、GCPのインフラ構成定義にはTerraformを使っています。個人ではオーバーエンジニアリングですが、頑張って今風なアーキテクチャにしています。
見つかったバグ
さて、ここからは見つかったバグを紹介していきます。
リリース前の検証ユーザがDBに残っている
問題
リリース前に無効なIDを持つTwitterのユーザ情報をインサートしてしまっていて、それがDBに残ったままでした。
mysql> select id from twitter_users where id = 0; +----+ | id | +----+ | 0 | +----+ 1 row in set (0.04 sec)ここでの
id
はTwitter側の内部IDと同じ値なので、0になることはありません。
おそらく、ゼロ値の構造体をそのままインサートしてしまっていたが原因と考えられます。対応
直接本番DBのコンソールからrowを消しました。
ただ、この作業はさくっとSQLを1回発行するだけではダメでした。
SQLの制約がいくつか張られていたので、他のテーブルのrowを先に削除する必要がありました。間違って他のユーザのデータを消すわけにはいかないので、入念に調査をして、冪等性があるクエリでrowを削除するようにしました。
無効な値を持つレコードがDBに格納されている
問題
これも上と似ていますが、本来存在し得ない値が書き込まれていました。
たとえ話ですが、ある状態を表すカラム
status
は本来、[0, 2]の値を取るはずが、3が書き込まれている、といった状況でした。これは、JSON APIでPOSTされた値のバリデーションが漏れていたのが原因でした。
対応
全てのフィールドに対して、値が有効であるかを確認するバリデーション関数を実装し、テストもしっかりと書きました。
いくらクライアントサイドでバリデーションしているとはいえ、サーバ側でも実装しなきゃなという気持ちになりました(当たり前)。
1対1で対応しているはずのテーブルのレコード数が違う
問題
Memoitoは、将来的に他のSNSの対応を見据えているため、ユーザ情報は汎用的な
users
テーブルとTwitter固有の情報を格納しているtwitter_users
テーブルに分けて保存しています。現時点では、Twitterしか対応していないため、
users
テーブルとtwitter_users
テーブルのrowの数は一致するはずですが、なぜか一致していませんでした。mysql> select count(id) from users; +-----------+ | count(id) | +-----------+ | 200 | +-----------+ 1 row in set (0.05 sec) mysql> select count(id) from twitter_users; +-----------+ | count(id) | +-----------+ | 184 | +-----------+ 1 row in set (0.04 sec)これは、トランザクションが正しく貼れていなかったことが原因で、片方が失敗した場合に不整合が生じていました。
対応
まず、2つのテーブルに対するインサートを同じトランザクションで実行するようにアプリケーションを修正しました。
その後、おかしいrowを探し出し、SQLでパッチを当てていきました。
具体的には、
- 不必要な
users
のrowを探す- その
id
が使われているメモ(notes
テーブル)を探すnotes
のuser_id
を正しいものに変更する- 参照が全てなくなった不必要な
users
のrowを削除という手順で対応しました。
これが、一番修正が面倒くさいバグでした。
おかしなrowの洗い出しが大変でしたし、SQLの制約を考慮しつつパッチを当てなくてないけないのでなかなか骨の折れる作業でした?Twitterのプロフィールが更新されない (未解決)
問題
MemoitoではTwitterのプロフィールをこちら側のDBに保存しています。
登録ユーザはログインするたびに新しいプロフィールに更新されるようになっています。しかし、メモを取った相手は必ずしも登録ユーザではないため、プロフィールが更新されていませんでした。
結果として、プロフィールアイコンが404で表示されないという不具合がありました。
対応
実はこの不具合はまだ修正できていません。せっせと実装中です。
DBに保存されているユーザの全てのプロフィール情報をTwitter APIから取得して、プロフィール情報を更新する方法を考えています。
おそらく、一番オーソドックスな方法なのではないでしょうか。他に良い方法があれば教えて下さい。
おわりに
ここまで、リリースしてから見つかったバグを紹介してきました。
特に整合性周りがきちんと実装していないと、不具合が生まれがちなので次からはしっかりと考えて実装しようと思いました。(対応大変なので、、、)
皆さんもこの記事を反面教師として、今一度自分のアプリケーションを見直してみましょう。何かヤバいものが見つかるかもしれません、
最後になりますが、よかったらMemoito使ってみてください!
Memoito(めもいと)はSNSの繋がりと紐付けてメモを保存できるWebサービスです!
— Memoito(めもいと)公式 (@MemoitoOfficial) September 30, 2019
フォローした理由をメモしておいたり、オフ会で会った時に話した内容をメモしたりできるので是非使ってみてください!https://t.co/r7rzQ9sXI4明日はひとり開発 Advent Calendar 2019はbinnmtiさんの担当です。お楽しみに。
- 投稿日:2019-11-27T16:46:34+09:00
Goを書く前に知っておくべき3つのこと
Goを書く前に知っておくべき3つのこと
言語について学習する以外で知っておくとあとあと人生が楽になることがいくつかあるので個人的に必須だと思っていること3つをサクっとリストアップしてみました。
1. 環境の準備
Goの面倒な要素として
$GOPATH
という変数(未設定の場合は$HOME/go
)が指すディレクトリの下に全てのGoのソースコードが(少なくともエクスポートしたいものに関しては)入っている必要があります。変なこと(たとえばiCloudやDropBoxでコードをクラウドで同期するみたいなこと)をしない場合は未設定あるいはGOPATH=$HOME/go
と明示的に設定してしまっていいと思います。合わせてPATH=$PATH:$GOPATH/bin
も設定してしまいましょう。
$GOPATH
以下の階層は大雑把に以下のように構造化されています。
$GOPATH/
bin/
go get などでインストールされたバイナリ郡pkg/
標準ライブラリsrc/
その他のGoコードここでポイントになるのは
src
以下の構造です。本来であればsrc/myprojects
などとディレクトリを切りたいところですが、そうもいきません。というのもsrc
以下にはまずドメイン名が来るからです。「なんでや」と思われるかもしれませんがこの切り分けをすることでそのソースコードがどこから来ているかがわかりやすくなるというメリットがあります。Goをインストールしたての場合は
src
以下には何もないかと思います。そこで試しに以下のコマンドを実行してみてください。$ go get github.com/ktnyt/assert
すると、
$GOPATH/src/github.com/ktnyt/assert
というディレクトリが生成されます。同様にgo get doma.in/path...
というコマンドを実行すると$GOPATH/src/doma.in/path...
というディレクトリができます。なので、自分のコードは原則的に$GOPATH/src/github.com/<GitHub user name>
というディレクトリ以下に作りましょう。「現実的に github.com 以外のドメインからコード拾ってくることはあるの?」と思われるかもしれませんが実は結構あります。まず Go 公式のツールは大体
golang.org/x/tools/cmd/<toolname>
にあります。あとはGitHubのレポジトリから勝手にバージョンタグを拾って適切にインストールしてくれるgopkg.in
というサービスがあったり(例:gopkg.in/go-yaml/yaml.v2
はgithub.com/go-yaml/yaml
というレポジトリのバージョンタグでv2
,v2.x
, またはv2.x.x
とついている中で最新のものを拾ってくる)と実は色々あります。Go のディレクトリ構造やコード整理などについてより詳しいことは公式ドキュメントにまとまっているので一読することをおすすめします。
2. プロジェクトの構造
次に重要なのは個別のプロジェクトの構造です。Goのソースコードはプロジェクトディレクトリの直下に置いてください(
src
などのディレクトリを切ることは非推奨です)。なぜかというとプロジェクトディレクトリ以下にディレクトリを切るとそれらのディレクトリはサブパッケージ扱いになるからです(github.com/<username>/<package>/src
はsrc
というパッケージになってしまう)。大量のファイルができてしまうことが憂慮される場合は適切にディレクトリを切ってサブパッケージ化したりプロジェクトを分けたりとコードの境界を意識して開発をする必要があります。次にバイナリを配布したい場合はプロジェクトそのものを
main
パッケージにしてしまうことは基本的に避けましょう。cmd/<commandname>
というディレクトリを切ってgo get github.com/<usermame>/<package>/cmd/<commandname>
とさせましょう。もちろんcmd/<command1>
cmd/<command2>
のように複数のコマンドを作ることも可能です。他にも慣例的にいくつかのディレクトリを切ることがあります。例としてパッケージ内に参照範囲を限定する
internal
やバージョンをフリーズしてコードを配布するためのvendor
などがあります。Go プロジェクトの標準レイアウトは https://github.com/golang-standards/project-layout にテンプレートがあるので一度ご覧になることをおすすめします。3. goimports
Goを書くときにストレスになりがちなことのひとつにインポート管理があります。未インポートのAPIを参照すると当然コンパイルエラーになりますが未使用のパッケージがインポートされているのもまたコンパイルエラーになります。Goと同梱のフォーマッタである gofmt は便利ですが実は goimports というインポートを自動的に挿入・削除してくれる上位互換があります。
$ go get golang.org/x/tools/cmd/goimports
このコマンドを実行すれば
$GOPATH/bin
にバイナリがインストールされます。あとはお好みのエディタのフォーマッタコマンドに goimports を指定すればよしなにインポートを解決してくれるのでとても便利です。まとめ
TL;DR
mkdir -p $GOPATH/github.com/<GitHub Username>
- https://golang.org/doc/code.html
- https://github.com/golang-standards/project-layout
$ go get golang.org/x/tools/cmd/goimports
Happy Hacking!
- 投稿日:2019-11-27T16:00:00+09:00
01. 「パタトクカシーー」
01. 「パタトクカシーー」
「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.
Go
package main import "fmt" func main() { var src string = "パタトクカシーー"; var des string = ""; // rune 型へ変換 msrc := []rune(src); // 1.ループで処理 for i := 0; i <= 7; i++ { // 問題では 1,3,5,7 指定だが奇数判定で対応 if (i % 2) != 0 { des += string(msrc[i]); } } fmt.Println(des); }python
# -*- coding: utf-8 -*- src = u"パタトクカシーー" des = "" # 1.ループで処理(range) reverse = "" for i in range(len(src)): # 問題では 1, 3, 5, 7 指定だが奇数判定で対応 if i % 2 != 0 : des += src[i] print(des) # -*- coding: utf-8 -*- src = u"パタトクカシーー" des = "" # 1.ループで処理(range) reverse = "" for i in range(len(src)): # 1, 3, 5, 7 指定だが奇数判定で対応 if i % 2 != 0 : des += src[i] print(des) # 2.スライスで処理(ステップを指定) print(src[1::2])Javascript
var src = "パタトクカシーー"; var dsc = "" // 1.ループで処理 for (var i = 0; i < src.length; i++) { // 問題では 1, 3, 5, 7 指定だが奇数判定で対応 if (i % 2 != 0) { dsc += src[i]; } } console.log(dsc);まとめ
単純なループ処理しか思いつかない。
他のロジックって有るのかな?。100本ノックの記事探してみる。
1,3,5,7 を渡すと、切り出してくれる標準関数とか有るのかな?。
- 投稿日:2019-11-27T11:55:53+09:00
言語処理100本ノック 2015 をやってみた 00
言語処理100本ノック 2015 をやってみた
動機と目的
新たな言語を勉強してみるが使わないと忘れていく。
Qiita で 100本ノックの記事を見た。なるほど。やってみよう。
言語処理100本ノック 2015目標
毎日(たぶん)、数問を解くことでそれぞれの言語に慣れるようにしてみる。
Qiita に書くことで Markdown に慣れる。言語
- Go
- Python
- Javascript
を主にやってみる。
00.文字列の逆順
Go
package main import_ "fmt" func main() { var data string = "stressed" var reverse string // 1.ループ for i := len(data) - 1; i >= 0; i-- { reverse += string(data[i]) } fmt.Println(reverse) }python
# -*- coding: utf-8 -*- data = "stressed" reverse = "" # 1.逆カウントのループ i = len(data) - 1 while i >= 0: reverse += data[i] i-=1 print(reverse) # 2.range->reversed reverse = "" for i in reversed(range(len(data))): reverse += data[i] print(reverse) # 3.reversed->join print(''.join(reversed(data))) # 4.スライス print(data[::-1])Javascript
var data = "stressed"; var reverse = "" // 1.逆カウントのループ for (var i = data.length - 1; i >= 0; i--) { reverse += data[i]; } console.log(reverse); // 2.split->reverse->join console.debug(data.split("").reverse().join(""));まとめ
Javascript以外は日本語非対応。
後でマルチバイト対応予定。
- 投稿日:2019-11-27T11:55:53+09:00
00. 文字列の逆順
00.文字列の逆順
文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.
Go
package main import_ "fmt" func main() { var data string = "stressed" var reverse string // 1.ループ for i := len(data) - 1; i >= 0; i-- { reverse += string(data[i]) } fmt.Println(reverse) }python
# -*- coding: utf-8 -*- data = "stressed" reverse = "" # 1.逆カウントのループ i = len(data) - 1 while i >= 0: reverse += data[i] i-=1 print(reverse) # 2.range->reversed reverse = "" for i in reversed(range(len(data))): reverse += data[i] print(reverse) # 3.reversed->join print(''.join(reversed(data))) # 4.スライス print(data[::-1])Javascript
var data = "stressed"; var reverse = "" // 1.逆カウントのループ for (var i = data.length - 1; i >= 0; i--) { reverse += data[i]; } console.log(reverse); // 2.split->reverse->join console.debug(data.split("").reverse().join(""));まとめ
goは日本語非対応。
問題ページとトップページ分割。
- 投稿日:2019-11-27T11:12:45+09:00
Golang - Incorrect CPU Numbers of GOMAXPROCS in Docker
The runtime.GOMAXPROCS(??) will read the host machine's CPU numbers instead of container's (--cpus=??). We can use UBER's automaxprocs to solve it.
go get -u go.uber.org/automaxprocsimport _ "go.uber.org/automaxprocs" func main() { // Your application logic here. }https://github.com/uber-go/automaxprocs
https://mp.weixin.qq.com/s/Lk1EbiT7WprVOyX_dXYMyg
http://vearne.cc/archives/39195
https://colobu.com/2017/10/11/interesting-things-about-GOMAXPROCS/
- 投稿日:2019-11-27T04:23:04+09:00
CockroachDBへのアクセスとDBマイグレーション
今日はアプリからCockroachDBにアクセスしてみたいと思います。ついでにDBマイグレーションにも触れます。
DBコネクションの作成
DBへのアクセスには標準の
database/sql
パッケージを使います。使用するドライバーはPostgresのものを使います。
ということでmain.go
で以下のパッケージを読み込みます。main.goimport ( log "github.com/sirupsen/logrus" "github.com/utilitywarehouse/go-operational/op" "google.golang.org/grpc" + + _ "github.com/lib/pq" )
store.go
ファイルを作成し、DBに接続するための関数initDB()
を以下のように定義します。store.gopackage main import ( "database/sql" "github.com/pkg/errors" ) func initDB(connString string) (*sql.DB, error) { db, err := sql.Open("postgres", connString) if err != nil { return nil, err } if err := db.Ping(); err != nil { return nil, errors.Wrap(err, "ping") } return db, nil }
main.go
からの呼び出しはこんな感じです。main.gofunc main() { ... + dbURL := app.String(cli.StringOpt{ + Name: "db-url", + Desc: "cockroachdb url", + EnvVar: "DB_URL", + Value: "postgresql://root@localhost:26257/test?sslmode=disable", + }) + app.Action = func() { log.WithField("git_hash", gitHash).Println("Hello, world") + db, err := initDB(*dbURL) + if err != nil { + log.WithError(err).Fatalln("connect db") + } + defer db.Close() + lis, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(*grpcPort))) if err != nil { log.Fatalln("init gRPC server:", err) ...では
app.yaml
で環境変数DB_URL
を定義してリソースを更新し、起動時にエラーが出ないことを確認してみましょう。app.yamlenv: - name: SRV_PORT value: "8080" + - name: DB_URL + value: "postgres://qiita_advent_calendar_2019@cockroachdb-proxy:26257/qiita_advent_calendar_2019_db?sslmode=disable" ports: - containerPort: 8080 name: srv
$ kubectl apply -f kubernetes/app.yaml service/qiita-advent-calendar-2019 unchanged deployment.apps/qiita-advent-calendar-2019 configured $ kubectl -n qiita logs qiita-advent-calendar-2019-5bc6786c75-gbr5s qiita-advent-calendar-2019 time="2019-11-26T18:24:23Z" level=info msg="Hello, world" git_hash=bb1916d14726fbd4d3e98b333d54ccfad45f824d特にエラーがないため接続できているようです。
テーブルスキーマ定義
せっかくDBに接続できたので、テーブルを作成してみましょう。
internal/schema
パッケージを切り、テーブルスキーマ定義用のファイルsql.go
を作成します。internal/schema/sql.gopackage schema var schemas = map[int]string{ 0: ` BEGIN; CREATE TABLE schema_version ( id SMALLSERIAL NOT NULL PRIMARY KEY, md_insert BIGINT DEFAULT EXTRACT(EPOCH FROM current_timestamp)::INT, md_update BIGINT DEFAULT 0, md_curr BOOL DEFAULT 'true' ); INSERT INTO schema_version VALUES (0); COMMIT; `, }先に
schema_version
テーブルを書いてしまいました。
僕のチームでは特別マイグレーション用のライブラリを使用しておらず、このテーブルでスキーマのバージョン管理をしてます。
マイクロサービスにより、一つ一つのデータベースの規模は小さく、スキーマが更新される頻度はモノリスのアプリケーションに比べて少ないため、今のところはこれで事足りているという状況です。ではマイグレーション用の関数
Migrate()
をみていきましょう。
関数はinternal/schema/schema.go
に置いておきます。internal/schema/schema.gopackage schema import ( "database/sql" "github.com/pkg/errors" ) // Migrate sets the schema at the requested version func Migrate(db *sql.DB, expectedVersion int) error { currentVersion, err := Version(db) if err != nil { return err } switch { case expectedVersion == currentVersion: return nil case expectedVersion < currentVersion: return errors.Errorf("schema migrate: invalid request, can not migrate backwards, current: %v, expected: %v", currentVersion, expectedVersion) case expectedVersion > currentVersion: for ; expectedVersion > currentVersion; currentVersion++ { if _, err := db.Exec(schemas[currentVersion+1]); err != nil { return errors.Wrap(err, "exec migration") } } } return nil } // Version returns the current schema version func Version(db *sql.DB) (v int, err error) { if err := db.QueryRow("SELECT id FROM schema_version WHERE md_curr = true").Scan(&v); err != nil { if err.Error() == `pq: relation "schema_version" does not exist` { return -1, nil } } return v, errors.Wrap(err, "select version") }この関数では指定されたバージョンまでのSQLを順に適用します。
store構造体の定義とマイグレーションの実行
では
store.go
ファイルに戻り、DBアクセス用のメソッドを定義するstore
構造体を定義し、その初期化処理時に上記のMigrate()
関数を呼び出すようにしましょう。store.gopackage main import ( "database/sql" "github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema" "github.com/pkg/errors" ) ... type store struct { db *sql.DB } func newStore(db *sql.DB, version int) (*store, error) { if err := schema.Migrate(db, version); err != nil { return nil, errors.Wrap(err, "migrate db schema") } return &store{db: db}, nil }
main.go
ではこの初期化関数を呼び出すだけです。main.gopackage main ... var ( gitHash = "overriden at compile time" defaultSchemaVersion = 0 ) ... func main() { ... schemaVersion := app.Int(cli.IntOpt{ Name: "schema-version", Desc: "schema version", EnvVar: "SCHEMA_VERSION", Value: defaultSchemaVersion, }) app.Action = func() { ... _, err = newStore(db, *schemaVersion) if err != nil { log.WithError(err).Fatalln("init store") } ... }さてでは先ほどと同様にアプリをデプロイしなおしてエラーが出ないことを確認してみましょう。
$ kubectl -n qiita scale --replicas=0 deployment qiita-advent-calendar-2019 deployment.extensions/qiita-advent-calendar-2019 scaled $ kubectl -n qiita scale --replicas=1 deployment qiita-advent-calendar-2019 deployment.extensions/qiita-advent-calendar-2019 scaled $ kubectl -n qiita logs qiita-advent-calendar-2019-5bc6786c75-lx4zk qiita-advent-calendar-2019 time="2019-11-26T19:09:37Z" level=info msg="Hello, world" git_hash=395149f7219511f2d72a09cd935e53f56fd29255エラーが出ていません。マイグレーションが正常に終了したようです。
昨日同様に直接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: 867.116µs root@localhost:26257/qiita_advent_calendar_2019_db> show tables; table_name +----------------+ schema_version (1 row) Time: 6.743223ms root@localhost:26257/qiita_advent_calendar_2019_db> show columns from schema_version; column_name | data_type | is_nullable | column_default | generation_expression | indices | is_hidden +-------------+-----------+-------------+-------------------------------------------------------------------+-----------------------+-------------+-----------+ id | INT | false | unique_rowid() | | {"primary"} | false md_insert | INT8 | true | extract('epoch':::STRING, current_timestamp():::TIMESTAMPTZ)::INT | | {} | false md_update | INT8 | true | 0:::INT | | {} | false md_curr | BOOL | true | true | | {} | false (4 rows) Time: 21.007097ms root@localhost:26257/qiita_advent_calendar_2019_db> select * from schema_version; id | md_insert | md_update | md_curr +----+------------+-----------+---------+ 0 | 1574795377 | 0 | true (1 row) Time: 73.755494ms無事テーブルが作成されていていい感じです。
今日はちょっといろいろやりました。
駆け足になってしまいましたがこれでDBのセットアップは完了したので明日、POSTエンドポイントを実装したいと思います。