【Go 1.21】ループ変数が共有される問題への対策(プレビュー)が公開されました
TL;DR
- Goのfor文で宣言されるループ変数は、ループ(イテレーション)毎に同じアドレスに値が格納される仕様になっている
- この仕様のよってループ内で並行処理を実行するなど特定のケースで、どのループでも毎回同じ値が取得されてしまうといった意図しないバグが発生することがあった
- Go 1.21でこの問題への対策のプレビューが公開され、問題無ければ1.22で正式リリースされる予定です
- 既存プログラムがこの変更によって正常に動作しなくなる可能性は殆ど無いとされています
- 影響を受けるコードを検出するビルドのオプションが追加されたり、変更によって失敗するテストケースを特定するツールが提供されているので、それらで検証が可能です
※社内のテックブログに投稿した内容と同じです
【Go 1.21】ループ変数が共有される問題への対策(プレビュー)が公開されました - Money Forward Developers Blog
はじめに
先日Go 1.21がリリースされましたが、以前から多くのGoエンジニアを悩ませてきた「ループ変数が共有される問題」への対策が実験的に盛り込まれたので、今回はそちらについて紹介したいと思います。
Go 1.21 is released! - The Go Programming Language
※本記事で紹介するコードの実行結果は全てGo 1.21での挙動です ※本記事は主に以下ドキュメントの情報を基に作成しています(8月15日時点)
従来の仕様とその問題点について
ループ変数は同じアドレスに格納される
そもそも「ループ変数が共有される」とはどういったことなのかをサンプルコードを交えて紹介します。以下のコードはslice内の全要素をfor文で順次処理したものです。ループごとに各要素がループ変数v
に格納されて、その値とアドレス(ポインタ)を標準出力します。
func main() { values := []int{0, 2, 4, 6} for i, v := range values { fmt.Printf("index: %d, value: %d, address: %p\n", i, v, &v) } }
index: 0, value: 0, address: 0xc000012028 index: 1, value: 2, address: 0xc000012028 index: 2, value: 4, address: 0xc000012028 index: 3, value: 6, address: 0xc000012028
ループごとに異なる値が出力されるのは想定通りだと思いますが、注目すべき点はaddress
が全て同じであるところです。このようにGoではループごとに値が変わっていても、その値の格納場所に同じアドレスを使い回すようになっています。
この仕様自体はバグではなく想定されたもので、ループごとに新たなメモリ割り当てが発生しないのでパフォーマンス面だけでみると合理的な仕様だと言えます。しかし後述する特定のケースではこの仕様が影響して意図しない動作を引き起こしていました。
ケース1:goroutineでの並行処理
先程と同様にループ処理を組みますが、今度はgoroutineに渡したクロージャ内でループ変数を直接使って標準出力します。
func main() { var wg sync.WaitGroup values := []int{0, 2, 4, 6} for i, v := range values { wg.Add(1) go func() { // 実行されるごとに index/value/address が変更されていることを期待するが…… fmt.Printf("index: %d, value: %d, address: %p\n", i, v, &v) wg.Done() }() } wg.Wait() // 全ての goroutine が終了するまで待機 }
index: 3, value: 6, address: 0xc000012050 index: 3, value: 6, address: 0xc000012050 index: 3, value: 6, address: 0xc000012050 index: 3, value: 6, address: 0xc000012050
先程と異なり、全てのループで同じ結果(最後のループの結果)が出力されてしまいました。goroutineが起動してクロージャ内で標準出力の処理が行われる前に、後続のループ処理によってループ変数の値が上書きされたため、それぞれのループ開始時点の値が反映されなくなっています。
なので以下のように直前のgoroutineの処理が完了してから次のループに移るように書き換えれば、想定通りの値が出力されます。
func main() { var wg sync.WaitGroup values := []int{0, 2, 4, 6} for i, v := range values { wg.Add(1) go func() { fmt.Printf("index: %d, value: %d, address: %p\n", i, v, &v) wg.Done() }() wg.Wait() // 直前の goroutine の処理が終了してから次のループに移る } }
index: 0, value: 0, address: 0xc0000a8018 index: 1, value: 2, address: 0xc0000a8018 index: 2, value: 4, address: 0xc0000a8018 index: 3, value: 6, address: 0xc0000a8018
但し上記の方法だと別でgoroutineを起動する意味が無い(並行処理にならない)のでお勧め出来ません。
今回のケースであれば、ループ変数を直接使用せずクロージャの引数を経由すれば、同じアドレスを共有する問題を回避できるので想定通りに動きます。
func main() { var wg sync.WaitGroup values := []int{0, 2, 4, 6} for i, v := range values { wg.Add(1) go func(i, v int) { fmt.Printf("index: %d, value: %d, address: %p\n", i, v, &v) wg.Done() }(i, v) // クロージャの引数にループ変数を指定 } wg.Wait() }
その他のアプローチで最もシンプルな方法は、ループ処理内で新たに変数を宣言する方法です。
func main() { var wg sync.WaitGroup values := []int{0, 2, 4, 6} for i, v := range values { i, v := i, v // 新たに変数を宣言 wg.Add(1) go func() { fmt.Printf("index: %d, value: %d, address: %p\n", i, v, &v) wg.Done() }() } wg.Wait() }
ケース2:テストの並行実行
以下は奇数を判定する関数のテストです。t.Parallel()
でテストを並行実行しています。
func IsOdd(num int) bool { return num%2 != 0 } func TestIsOdd(t *testing.T) { tests := []struct { num int want bool }{ {num: 1, want: true}, {num: 2, want: true}, // 失敗する想定のテストケース {num: 3, want: true}, {num: 4, want: false}, } for _, tt := range tests { t.Run(fmt.Sprintf("num(%v)", tt.num), func(t *testing.T) { t.Parallel() // 並行実行 if got := IsOdd(tt.num); got != tt.want { t.Errorf("got = %v, want %v", got, tt.want) } }) } }
=== RUN TestIsOdd (中略) --- PASS: TestIsOdd (0.00s) --- PASS: TestIsOdd/num=1 (0.00s) --- PASS: TestIsOdd/num=2 (0.00s) --- PASS: TestIsOdd/num=4 (0.00s) --- PASS: TestIsOdd/num=3 (0.00s) PASS
1箇所、失敗する想定のテストケースがパスしてしまっています。こちらも先程と同様に、ループ変数が後続のループ処理で書き換えられてしまったことが原因です。
こちらもループ処理内で新たに変数を宣言すれば解消されます。
func TestIsOdd(t *testing.T) { tests := []struct { num int want bool }{ {num: 1, want: true}, {num: 2, want: true}, // 失敗する想定のテストケース {num: 3, want: true}, {num: 4, want: false}, } for _, tt := range tests { tt := tt // 新たに変数を宣言 t.Run(fmt.Sprintf("num=%v", tt.num), func(t *testing.T) { t.Parallel() if got := IsOdd(tt.num); got != tt.want { t.Errorf("got = %v, want %v", got, tt.want) } }) } }
=== RUN TestIsOdd (中略) --- FAIL: TestIsOdd (0.00s) --- PASS: TestIsOdd/num=1 (0.00s) --- PASS: TestIsOdd/num=4 (0.00s) --- PASS: TestIsOdd/num=3 (0.00s) --- FAIL: TestIsOdd/num=2 (0.00s) FAIL
ケース3:ループ変数のポインタを利用した処理
最後はintのポインタを格納するslice(ids)に、0から9の連番をポインタで格納する処理です。
func main() { var ids []*int for i := 0; i < 10; i++ { ids = append(ids, &i) } for i, v := range ids { fmt.Printf("index: %d, id: %d, address: %p\n", i, *v, v) } }
index: 0, id: 10, address: 0xc000012028 index: 1, id: 10, address: 0xc000012028 index: 2, id: 10, address: 0xc000012028 index: 3, id: 10, address: 0xc000012028 index: 4, id: 10, address: 0xc000012028 index: 5, id: 10, address: 0xc000012028 index: 6, id: 10, address: 0xc000012028 index: 7, id: 10, address: 0xc000012028 index: 8, id: 10, address: 0xc000012028 index: 9, id: 10, address: 0xc000012028
こちらも先程までのケースと同様、格納されたアドレスが全て同じなので、ループ最後の値で出力されてしまっています。
ループ処理内で新たに変数を宣言すれば解消されます。
func main() { var ids []*int for i := 0; i < 10; i++ { i := i // 新たに変数を宣言 ids = append(ids, &i) } for i, v := range ids { fmt.Printf("index: %d, id: %d, address: %p\n", i, *v, v) } }
index: 0, id: 0, address: 0xc0000a2000 index: 1, id: 1, address: 0xc0000a2008 index: 2, id: 2, address: 0xc0000a2010 index: 3, id: 3, address: 0xc0000a2018 index: 4, id: 4, address: 0xc0000a2020 index: 5, id: 5, address: 0xc0000a2028 index: 6, id: 6, address: 0xc0000a2030 index: 7, id: 7, address: 0xc0000a2038 index: 8, id: 8, address: 0xc0000a2040 index: 9, id: 9, address: 0xc0000a2048
従来の解決策
上記のサンプルコードでも例示しましたが、この問題への最もシンプルな解決策は、新たな変数を宣言する方法になります。この方法は以下の公式のFAQにもサンプルコードとともに紹介されています。
Even easier is just to create a new variable, using a declaration style that may seem odd but works fine in Go:
for _, v := range values { v := v // create a new 'v'. go func() { fmt.Println(v) done <- true }() }
What happens with closures running as goroutines?
Goエンジニアはこの仕様を考慮してループ処理を実装することが求められていましたが、見落としがちな仕様なので、新たにGoを利用する人はもちろん、使い慣れた人でも対策が漏れてしまい、バグを引き起こしてしまったという話はいくつも耳にしたことがあります。
その中でも特に大きな問題になったのが、Let's Encryptでのインシデントです。
Let's Encryptでのインシデント
無償でSSL証明書を発行しているLet's Encryptでは、ユーザーやドメインの検証に使用していたソフトウェアをGoで開発していますが、2020年にその検証ロジックにバグがあることが報告されました。
そのバグの原因となったのが、これまでに紹介したループ変数が共有されてしまう仕様によって引き起こされたものでした。
このバグは公式のwikiでも実際のコードと併せて紹介されています。 https://github.com/golang/go/wiki/LoopvarExperiment#what-is-the-problem-this-solves
これが原因で300万件近くの証明書が取り消されかけて(最終的に取り消さない判断になった)、複数のネットメディアで取り上げられたほど大きな問題に発展しました。
問題の大小はありますが同様の事例は様々報告されており、何年も前からこの仕様を変えるべきだという議論が行われていましたが、ついに今回対策の提案が承認されてGo1.21にプレビューとして組み込まれました。
今後の仕様変更と懸念事項
採用された提案内容
今回の提案内容では特にこれまでの記述方法を変えなくても、コンパイラが適切に判断して対策が適用されます。
先程紹介した「ケース1」「ケース2」のようなループ変数がクロージャやgoroutineに捕捉された場合や、「ケース3」のようにループを抜けた先でループ変数が参照される場合は、ループごとに変数が作成されるようになります。これによってループごとに保持された値が出力できます。
以前まで記述していた新たな変数の宣言(x := x
)は不要になります。
Go 1.21での検証方法
Go 1.21はプレビューなのでデフォルトでは従来通りの挙動ですが、ビルド時にGOEXPERIMENT=loopvar
を指定すると変更内容を反映してビルドが行われます。
GOEXPERIMENT=loopvar go install my/program GOEXPERIMENT=loopvar go build my/program GOEXPERIMENT=loopvar go test my/program GOEXPERIMENT=loopvar go test my/program -bench=....
The Go Playground
上でも先頭に// GOEXPERIMENT=loopvar
とコメントを付けると、このオプションが有効になります。\
go.dev
変更によって影響を受けるコードの場所を確認したい場合、-gcflags=all=-d=loopvar=2
を指定してビルドすると警告メッセージが表示されます。
$ go build -gcflags=all=-d=loopvar=2 cmd/go ... modload/import.go:676:7: loop variable d now per-iteration, stack-allocated modload/query.go:742:10: loop variable r now per-iteration, heap-allocated
Can I see a list of places in my code affected by the change?
またbisect
というツールを使えば、この変更でテストが失敗を引き起こすループを特定できます。
go install golang.org/x/tools/cmd/bisect@latest bisect -compile=loopvar go test
My test fails with the change. How can I debug it?
懸念1:既存プログラムへの破壊的な影響
これまでの仕組みが大きく変わるので、既存プログラムが正常に動作しなくなるなどの悪影響がまず気になります。
以下リンクにこれまで通り動かなくなるサンプルコードが紹介されています。従来の仕様に依存したコードは今回の変更で正常に動作しなくなる可能性があります。 Can this change break programs?
func main() { print(sum([]int{0, 2, 4, 6})) } func sum(list []int) int { m := make(map[*int]int) for _, x := range list { m[&x] += x // xがループ内で1つしか無いことに依存している } for _, sum := range m { return sum } return 0 }
Output(適用前): 12 Output(適用後): 0
func main() { var f func() for i := 0; i < 10; i++ { if i == 0 { f = func() { print(i) } // i=0しか有り得ない筈が、関数呼び出し時点の`i`を使用されるので、ループごとに値が異なる } f() } }
Output(適用前): 0123456789 Output(適用後): 0000000000
但し上記サンプルはGoの一般的なループ処理の記述方法とは大きくかけ離れた書き方で、考慮すべきケースは極めて稀だと思われます(issues上の議論の中でも実際に存在を確認できないという意見が多数)
Googleでは2023年5月初旬から本番利用しているツールに変更を反映しているようですが、現時点で問題は一度も報告されていないようです。
なおC#でも同様の変更を加えた経緯があるそうですが、特に変更によって大きな問題は発生しなかったようです
How often does the change break real programs?
懸念2:パフォーマンス低下
新たにメモリ割り当てされることになるのでパフォーマンス低下の懸念がありますが、通常のループ処理は従来通りコンパイルされる(変数共有)ので、それらのパフォーマンスに影響は出ません。影響を受けるループの場合でも、コンパイラのエスケープ解析で必要無いと判断されれば新しい割り当てが行われないようです。
Googleの内部環境で使用した際もパフォーマンスの問題は観測されなかったということなので、大抵のプログラムは影響を受けないと考えられます。
Will the change make programs slower by causing more allocations?
まとめ
今回調査してみて、少なくとも私の携わっているプロジェクトでは影響が殆ど無さそうなのが分かって安心しました。
新しい変数を都度宣言する従来の対策方法は、この仕様を知らない人からすると意図が読めませんし、うっかり忘れてしまう可能性もあるので、実装時に気にする必要がなくなるのは非常にありがたいです。
私自身、Goを使い始めて暫くした時にこの事象に遭遇して、かなり苦い思いをしたので、正式リリースを心待ちにしたいと思います。
【Go 1.21】愚直なループ処理から脱却できるslices/mapsパッケージ
TL;DR
- slices・mapsパッケージによって、これまでfor文で実装するしかなかったsliceやmapなどに対する汎用処理が簡単に実装できるようになった
- これまでGoには継承などの共通の振る舞いを実装する仕組みが存在しなかったが、Go 1.18で導入されたジェネリクスによって今回の汎用処理が実現できている
- 今後さらに関数の追加や、イテレータ用のパッケージの導入が検討されており、今後更に便利になりそう
slicesパッケージ
従来のsliceに対する汎用処理の実装方法
これまでGoではsliceに対する汎用処理が言語側でサポートされていませんでした。そのためsliceに対する処理は、for文によるループ処理を都度記述する必要がありました。以下はslice内に対象の数値が含まれているかチェックしたい場合のコード例です。
func main() { numbers := []int{1, 2, 3, 4, 5} target := 3 // ここから判定ロジック hasTarget := false for _, v := range numbers { if v == target { hasTarget = true break } } // ここまで7行 fmt.Println("Has a target:", hasTarget) }
Has a target: true
こういった汎用的な処理が言語側でサポートされている他言語と比べると、実装の手間や可読性などの観点でかなり見落とりします。以下はRubyで同じ処理を実装した場合のサンプルです。
numbers = [1, 2, 3, 4, 5] target = 3 hasTarget = numbers.include?(target) # 1行で判定可能 p "Has a target: #{hasTarget}"
Has a target: true
slicesパッケージでの実装例
Go1.21からsliceに対する汎用処理を提供するslicesパッケージが導入されています。以下は先ほどのコードを追加されたslicesパッケージの関数で書き換えたものです。
func main() { numbers := []int{1, 2, 3, 4, 5} target := 3 hasTarget := slices.Contains(numbers, target) // 1行で判定可能 fmt.Println("Has a target:", hasTarget) }
Has a target: true
ループ処理を自前で実装する必要がなくなったので、格段に見やすくなりましたし、実装しやすくなりました。
ジェネリクスによって様々な型に対応可能
先ほどのサンプルコードではint
のsliceを使いましたが、slicesパッケージの関数はジェネリクスを活用して実装されているため、様々な型に対応しています。先ほど使用したContains
関数の定義を見てみると以下のようになっています。
func Contains[S ~[]E, E comparable](s S, v E) bool
pkg.go.dev
型パラメータが[S ~[]E, E comparable]
と定義されています。comparable
は言語側で事前定義された型制約で、int
やstring
などの比較可能な型が受入可能です。なので以下のようにstring
やfloat64
など様々な型の配列を同じ関数で処理できます。
func main() { strings := []string{"a", "b", "c", "d", "e"} // stringの場合 targetStr := "f" hasStr := slices.Contains(strings, targetStr) fmt.Println("Has a target:", hasStr) // Has a target: false numbers := []float64{0.1, 0.5, 1.2, 1.5, 2.0} // float64の場合 targetNum := 2.0 hasNum := slices.Contains(numbers, targetNum) fmt.Println("Has a target:", hasNum) // Has a target: true }
コールバック関数によって複雑な仕様にも対応可能
ジェネリクスによって様々な型を受け入れることが可能になりましたが、構造体(sturct)などcomparable
を満たしていない型では先ほどの関数を利用できません。そういったケースでも柔軟に対応できるように、コールバック関数を渡せる関数も用意されています。
func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool
pkg.go.dev
これによって構造体(sturct)の比較も可能になっています。以下はslice内の構造体の一部が対象文字列と一致するかチェックするコードです。
func main() { type person struct { name string age int } persons := []person{ {name: "Tom", age: 20}, {name: "Bob", age: 30}, } target := "Bob" hasTarget := slices.ContainsFunc(persons, func(p person) bool { return p.name == target }) fmt.Println("Has a target:", hasTarget) // Has a target: true }
その他にも単純一致ではなく複雑な条件を指定することも可能です。以下はslice内に奇数が含まれるかチェックするコードです。
func main() { numbers := []int{2, 4, 6, 8} callbackFunc := func(n int) bool { return n%2 != 0 } hasOdd := slices.ContainsFunc(numbers, callbackFunc) fmt.Println("Has a odd:", hasOdd) // Has a target: false }
slicesパッケージの関数一覧
上記で例にあげたContains
関数以外にも様々なユースケースごとの関数が用意されています。
- 検索
- BinarySearch/BinarySearchFunc:対象要素を二分探索して存在有無(bool)とインデックス(int)を返す
- Contains/ContainsFunc:対象要素が含まれているか(bool)を返す
- Equal/EqualFunc:sliceを比較して全ての要素が同じか(bool)を返す
- Index/IndexFunc:対象要素のインデックス(int)を返す
- IsSorted/IsSortedFunc:ソート済みか(bool)を返す
- Max/MaxFunc:最大の要素を返す
- Min/MinFunc:最小の要素を返す
- データ操作 ※いずれも元のsliceを破壊的に変更せず、新たなsliceを返却します
- Clip:未使用容量を削除する
- Clone:コピーを返す(Shallow Copy)
- Compare/CompareFunc:重複する要素を削除する
- Delete/DeleteFunc:指定した範囲を削除する
- Grow:容量を指定した分だけ増やす
- Insert:指定した箇所に要素を挿入する
- Replace:指定した範囲を別で指定したsliceに置き換える
- Sort:昇順で並び替える
- SortFunc:指定した関数のロジックで並び替える
- SortStableFunc:指定した関数のロジックで並び替える(安定ソート)
- Reverse:逆順に並び替える
まだ他言語に比べると少なく感じますが、他にも新たな関数の追加が検討されています。 github.com
mapsパッケージ
mapに対する汎用的な処理については、mapsパッケージが提供しています。以下は削除の実装例です。
func main() { origin := map[int]string{ 1: "one", 10: "Ten", 1000: "THOUSAND", } // 削除 maps.DeleteFunc(origin, func(k int, v string) bool { return v == "THOUSAND" }) fmt.Println(origin) // map[1:one 10:Ten] }
- 検索
- Equal/EqualFunc:同じkey/valueのペアが含まれているか
- データ操作
- DeleteFunc:条件を満たすkey/valueのペアを削除する
- Clone:コピーを返す(Shallow Copy)
- Copy:対象mapを別に指定したmapへコピーする(keyが重複する場合は上書き)
こちらはslicesパッケージに比べるとボリューム少なめですが、他にも新たな関数の追加が検討されています。 github.com
検討中のイテレータ用のパッケージについて
今回の改修はsliceとmapのそれぞれに対して専用のパッケージを用意して実現していますが、こういったイテレータ全般に対する汎用処理を提供するパッケージの検討も進められています。
以下はexpパッケージ(実験的なパッケージ)への追加の提案内容ですが、他言語で良く見かけるようなイテレータに対する定番の処理が提案されています。
Package xiter implements basic adapters for composing iterator sequences:
[Concat] and [Concat2] concatenate sequences. [Equal], [Equal2], [EqualFunc], and [EqualFunc2] check whether two sequences contain equal values. [Filter] and [Filter2] filter a sequence according to a function f. [Limit] and [Limit2] truncate a sequence after n items. [Map] and [Map2] apply a function f to a sequence. [Merge], [Merge2], [MergeFunc], and [MergeFunc2] merge two ordered sequences. [Reduce] and [Reduce2] combine the values in a sequence. [Zip] and [Zip2] iterate over two sequences in parallel.
本ブログ投稿時点ではacceptされていませんが、順調に進めば次のバージョンに組み込まれるかもしれません。
まとめ
イテレータ関連の汎用処理が言語側でサポートされていないのは、Goの使いづらいポイントの一つでしたが、それが大きく改善のはとても嬉しいです(毎回for文を書く作業から解放される...)
今回追加されたslices・mapsパッケージや、新たに検討されているイテレータのパッケージの実装にはジェネリクスが非常に多く活用されていて、ジェネリクスの活用例や実装のお手本を知る良い機会になりました。ジェネリクスの具体的な活用方法を知りたい時は、これらの実装を参考にしてみるのも良いかもしれません。
複数セッションからINSERT ... ON DUPLICATE KEY UPDATEを実行した際にデッドロックが発生する問題(MySQL)
TL;DR
- MySQLにおいて、複数セッションから同時に
INSERT ... ON DUPLICATE KEY UPDATE
で複数行の挿入を実行するとデッドロックが発生する場合がある - この現象はMySQLの5.7.26(8系なら8.0.16)でのバグ修正に起因すると考えられる
- MySQL自体のバグでは無いため、サーバー側で対策する必要がある(以下対処例)
目次
INSERT ... ON DUPLICATE KEY UPDATE
でデッドロックが発生する
INSERT ... ON DUPLICATE KEY UPDATE
による複数行のBULK INSERT/UPDATEを複数プロセスのバッチ処理で実行した際、デッドロックが度々発生するという問題が発生しました。
今回はその再現検証と原因・対策について調査した内容をまとめています。
再現検証
- MySQLバージョン: 8.0.32
- 検証用テーブル
CREATE TABLE `tests` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `foo` int(11) NOT NULL, PRIMARY KEY (id), UNIQUE KEY `index_unique` (`foo`) ) ;
以下のクエリを複数セッションから繰り返し実行したところ、重複レコードが存在するタイミングでデッドロックが発生しました。
INSERT INTO tests (`foo`) VALUES (699422), (699421) ON DUPLICATE KEY UPDATE foo = VALUES (foo); INSERT INTO tests (`foo`) VALUES (699439), (699439) ON DUPLICATE KEY UPDATE foo = VALUES (foo);
エラーメッセージはこちらです。
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
ログ調査
デッドロックの詳細ログを表示した結果は以下の通りです。
mysql> SHOW ENGINE INNODB STATUS; 〜〜〜〜〜〜〜〜〜(略)〜〜〜〜〜〜〜〜〜〜〜 ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2023-07-03 05:46:41 281472373108672 *** (1) TRANSACTION: TRANSACTION 41226, ACTIVE 29 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 5 lock struct(s), heap size 1128, 4 row lock(s) MySQL thread id 6154, OS thread handle 281472697032640, query id 21610 172.19.0.1 mysql_user update INSERT INTO tests (`foo`) VALUES (699422), (699421) ON DUPLICATE KEY UPDATE foo = VALUES (foo) *** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 2093 page no 4 n bits 80 index PRIMARY of table `app`.`tests` trx id 41226 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 2093 page no 4 n bits 80 index PRIMARY of table `app`.`tests` trx id 41226 lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) TRANSACTION: TRANSACTION 41227, ACTIVE 15 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 6 lock struct(s), heap size 1128, 4 row lock(s) MySQL thread id 6147, OS thread handle 281472700202944, query id 21625 172.19.0.1 mysql_user update INSERT INTO tests (`foo`) VALUES (699422), (699421) ON DUPLICATE KEY UPDATE foo = VALUES (foo) *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 2093 page no 4 n bits 80 index PRIMARY of table `app`.`tests` trx id 41227 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 2093 page no 4 n bits 80 index PRIMARY of table `app`.`tests` trx id 41227 lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** WE ROLL BACK TRANSACTION (1) ------------ TRANSACTIONS ------------ Trx id counter 41229 Purge done for trx's n:o < 41187 undo n:o < 0 state: running but idle History list length 0 LIST OF TRANSACTIONS FOR EACH SESSION: ---TRANSACTION 562947829962320, not started 0 lock struct(s), heap size 1128, 0 row lock(s) ---TRANSACTION 562947829959896, not started 0 lock struct(s), heap size 1128, 0 row lock(s) ---TRANSACTION 562947829960704, not started 0 lock struct(s), heap size 1128, 0 row lock(s) ---TRANSACTION 562947829959088, not started 0 lock struct(s), heap size 1128, 0 row lock(s) ---TRANSACTION 562947829958280, not started 0 lock struct(s), heap size 1128, 0 row lock(s) ---TRANSACTION 41228, ACTIVE 171 sec 1 lock struct(s), heap size 1128, 1 row lock(s) MySQL thread id 6155, OS thread handle 281472364715968, query id 21619 172.19.0.1 mysql_user ---TRANSACTION 41227, ACTIVE 176 sec 6 lock struct(s), heap size 1128, 8 row lock(s) MySQL thread id 6147, OS thread handle 281472700202944, query id 21625 172.19.0.1 mysql_user -------- 〜〜〜〜〜〜〜〜〜(略)〜〜〜〜〜〜〜〜〜〜〜
デッドロック発生直前にperformance_schema.data_locks
で取得したロックのステータスは以下の通りです。
mysql> SELECT ENGINE_TRANSACTION_ID,INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATA FROM performance_schema.data_locks;
ENGINE_TRANSACTION_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
41226 | PRIMARY | RECORD | X,REC_NOT_GAP | GRANTED | 1 |
41226 | PRIMARY | RECORD | X | GRANTED | supremum pseudo-record |
41226 | PRIMARY | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
41227 | PRIMARY | RECORD | X,REC_NOT_GAP | GRANTED | 3 |
41227 | PRIMARY | RECORD | X | GRANTED | supremum pseudo-record |
41228 | PRIMARY | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
上記の表の見方は以下の通りです。
- ENGINE_TRANSACTION_ID:
- ロックを要求したトランザクションのID
- INDEX_NAME:
- NULL - テーブル・ロック
- PRIMARY - プライマリインデックス
- index_unique - セカンダリインデックス
- LOCK_MODE:
- LOCK_STATUS:
- GRANTED - トランザクションがロックを所有している
- WAITING - 競合するロックが解放されるまで待機している
- LOCK_DATA:
- supremum pseudo-record - インデックスの最大値を超える疑似値とその範囲に対してギャップロック
- NULL - テーブルロック
- その他
- primary indexの場合 - プライマリ・キーのすべてのカラム
- secondary indexの場合 - インデックスの定義で明示されているカラムの値の後に、残りのプライマリキーのカラムが続く
上記から待機状態のロックを抜き出すと以下の通りです。
ENGINE_TRANSACTION_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
41226 | PRIMARY | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
41228 | PRIMARY | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
インテンションロック(排他ロック)で待機状態となっており、これが競合してデッドロックが発生したと考えられます。
MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.1 InnoDB ロック
原因分析
以下のバグ報告のやり取りから、MySQLの5.7.26(8系なら8.0.16)で別のバグ修正のために追加されたロックの影響によって発生した事象だと考えられます。
MySQL Bugs: #98324: Deadlocks more frequent since version 5.7.26
しかし上記の変更は他のバグに対する修正であり、以下の通り今回のデッドロックについてはバグでは無いというのが公式の見解のため、サーバー側での対処が必要となります。
"Deadlock happens" is not a bug - deadlocks should be handled by a properly written application, as they are one of possible outcomes of each transaction.
"Deadlocks happen more often than before" is also not a bug (Yes, it might be a symptom of a bug if the change was a surprising result of some unrelated change we haven't thought through. But in our case, the deadlocks happen more often because we lock more often, and this was an intended change)
"I'd like deadlocks to happen less often" is a feature request in my opinion, unless one can point out where we make a mistake by taking a lock.
対処方法
リトライ処理を追加する
デッドロック発生時の一般的な対処方法としてリトライ処理が上げられます。
シンプルな処理なのでデッドロックの発生頻度が低いのであれば有効ですが、高頻度で発生するようであればパフォーマンス面で難があると考えられます。
クエリを分割する
以下3通りありますが、クエリを分割することで対処可能です。
一行づつINSERTする
シンプルですが、大量にレコードが存在する場合にはパフォーマンスに難があると思われます。
BEGIN; INSERT new row1; INSERT new row2; INSERT new row3; COMMIT;
UPDATEとINSERTを分ける
前述のINSERTよりは若干処理が煩雑ですが、大量のレコードの挿入が発生するなら、こちらの方がパフォーマンスは優れていると考えられます。
BEGIN; SELECT rows that conflict on secondary indexes; UPDATE conflicting rows; INSERT new rows; COMMIT;
重複レコードをDELETE後にINSERTを行う
前述のUPDATEとINSERTを分ける方法よりは、サーバー側の処理がシンプルに実装できるかもしれません。
但しAUTO_INCREMENTだとIDがどんどん採番されるのと、削除によるデータ喪失のリスクを考慮すると、前述のUPDATEとINSERTを分ける方法の方が無難かもしれません。
またDELETE文でWHERE句を利用した絞り込みを行う場合、条件に該当するレコードが存在しない場合はテーブルのすべての行がロック(ギャップロック)されてしまい、今度はそれがデッドロックの原因となり得るので注意が必要です。
BEGIN; DELETE conflicting rows; INSERT new rows; COMMIT;
トランザクション分離レベルをREAD COMMITTED
に変更する
インテンションロックはギャップロックの一種ですが、READ COMMITTED
にするとファントムリード(ファジーリード)を許容するため、ギャップロックは取得されずにデッドロックが発生しなくなります。
ファントムリード(ファジーリード)が許容できるのであれば、こちらの方法も良いかもしれません。
MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.4 ファントム行
テーブルロックする
アクセス頻度が低いテーブルなら良いかもしれないです。
但しアクセス頻度が高いテーブルであれば、待ち状態が頻繁に発生するので、パフォーマンス面で難があります。
今回のようなデッドロックが発生していて対策を要する状況であれば、既に一定のアクセス頻度が想定されるので、あまり良い選択肢では無いかもしれません。
サーバー側で並列ではなく直列にクエリが実行されるように制御する
無理に並列実行する必要が無いような時などは、単一プロセスで動かすようにして対処するのも方法かもしれません。
但しサーバー側で排他制御の仕組みを導入したりしてまで、サーバー側での制御に固執する必要は無いと思われます。
諦める(デッドロックを許容)
発生頻度が非常に少なく、ユーザー側で再実行が容易に出来たりするのであれば、あえて対策せず許容するのも選択肢として考慮しても良いと考えられます。
まとめ
- リトライする
- 一行づつINSERTする
- メリット:実装がシンプル
- デメリット:大量のレコードをINSERTする場合はパフォーマンスに難がある
- 採用シーン:INSERTするレコードが少数の場合
- UPDATEとINSERTを分ける
- メリット:DELETE後にINSERTを行うよりパフォーマンス・安全性ともに有利
- デメリット:リトライや一行づつINSERTに比べると実装コストが高い。
- 採用シーン:トランザクション分離レベルの変更が難しく、リトライで対処困難な場合
- 重複レコードをDELETE後にINSERTを行う
- メリット:UPDATE/INSERTに比べると実装がシンプルになりそう
- デメリット:AUTO_INCREMENT使用中は連番が歯抜けになる。データ喪失のリスク。ギャップロックが発生する可能性あり。
- 採用シーン:上記デメリットが許容できて、極力実装コストを下げたい場合(UPDATEとINSERTを分ける場合との比較)
- トランザクション分離レベルを
READ COMMITTED
に変更する - テーブルロックする
- メリット:別要因で発生するデッドロックも抑制できそう
- デメリット:アクセス頻度が多いテーブルだとパフォーマンスに悪影響を及ぼす
- 採用シーン:テーブルへのアクセス頻度が低い場合
- サーバー側で並列ではなく直列にクエリが実行されるように制御する
- メリット:大きな改修が不要
- デメリット:並列処理では無くなるので想定したパフォーマンスを出せなくなるかもしれない
- 採用シーン:単一プロセスでの実行が許容できる場合
- 諦める(デッドロックを許容)
- メリット:改修不要
- デメリット:何も解決しない
- 採用シーン:解決する必要が無い場合
参考リンク
Go1.20で追加された「unsafe.StringData、unsafe.String、unsafe.SliceData」による[]byteとstringの変換処理
TL;DR
- Go1.20からメモリ効率を重視した
[]byte
とstring
の変換処理として以下が提供されている- string -> []byte:
unsafe.Slice(unsafe.StringData(s), len(s))
- []byte → string:
unsafe.String(&b[0], len(b))
- string -> []byte:
目次
従来の変換方法
Goにおいて[]byte
とstring
で相互に変換を行う際、簡潔な方法として以下のように型キャストする実装方法があります。
// string -> []byte []byte(s) // []byte → string string(b)
しかし[]byte
とstring
は内部のデータ構造が異なるため、上記の方法では無駄なメモリアロケーションが発生し、パフォーマンス面に難があります。
その問題に対処するため、公式のドキュメントなどで明示されていないですが、以下のような手法がこれまで広く使われていました。
// string -> []byte *(*[]byte)(unsafe.Pointer(&s)) // []byte → string *(*string)(unsafe.Pointer(&b))
但し上記の方法は、本来想定されていない無理矢理な変換方法なので、使用方法を誤ればバグを生み出すリスクがありました。
新しい変換方法
この問題に対処するため、Go1.20にて「unsafe.StringData、unsafe.String、unsafe.SliceData」が追加されたことで、以下の方法が推奨されるようになりました。
// string -> []byte unsafe.Slice(unsafe.StringData(s), len(s)) // []byte → string unsafe.String(&b[0], len(b))
ベンチマーク
import ( "testing" "unsafe" ) func BenchmarkTest1(b *testing.B) { for i := 0; i < b.N; i++ { test1() } } func BenchmarkTest2(b *testing.B) { for i := 0; i < b.N; i++ { test2() } } func BenchmarkTest3(b *testing.B) { for i := 0; i < b.N; i++ { test3() } } func test1() { origin := "test" // string -> []byte b := []byte(origin) // []byte → string _ = string(b) } func test2() { origin := "test" // string -> []byte b := *(*[]byte)(unsafe.Pointer(&origin)) // []byte → string _ = *(*string)(unsafe.Pointer(&b)) } func test3() { origin := "test" // string -> []byte b := unsafe.Slice(unsafe.StringData(origin), len(origin)) // []byte → string _ = unsafe.String(&b[0], len(b)) }
$ go test -bench . -benchmem goos: darwin goarch: arm64 BenchmarkTest1-10 234380760 4.933 ns/op 0 B/op 0 allocs/op BenchmarkTest2-10 1000000000 0.2888 ns/op 0 B/op 0 allocs/op BenchmarkTest3-10 1000000000 0.4360 ns/op 0 B/op 0 allocs/op PASS ok command-line-arguments 2.790s
単純な型キャストと比べると、新しい方法での変換も非常にパフォーマンスが良いことが分かります。
活用事例
既にGinやTinyGoなどのライブラリでは取り入れられているようで、今後どんどん置換が進んでいきそうです。
参考リンク
Google Chromeなどの一部ツールは、サブドメイン付きのlocalhostのアドレス(subdomain.localhost)を自動的に127.0.0.1で名前解決してくれます
TL;DR
※2023年6月時点
- Google Chromeなどのツールでは、サブドメイン付きの
localhost
のURL(例:http://hoge.localhost:3000)を、自動で127.0.0.1
に名前解決してリクエストを行います - 一方でSafariなどの一部ツールでは非対応なので、それらでも同様に名前解決したい場合は、別途実行環境のDNSの設定を変更する必要がある
- 自動的な名前解決が主流のようだが、ツール毎の差分や今後の仕様変更の可能性を考慮するなら、自動的な名前解決に依存しない運用が望ましい
目次
- TL;DR
- 目次
- ブラウザやツールによってサブドメイン付きlocalhostの名前変換の仕様が異なる
- ※実行環境
- 非対応のツールでは実行環境にDNS設定を行う必要がある
- lvh.meというドメインを使えば、非対応のツールでも対応可能だが。。。
- RFC 6761での定義
- Google Chromeも以前は未対応だった
- 今後の仕様変更の可能性を考慮するなら、自動での名前解決に依存しない設計が望ましい
- 参考リンク
ブラウザやツールによってサブドメイン付きlocalhostの名前変換の仕様が異なる
はてなブログのようなユーザー毎に任意のサブドメインを提供するようなサービス(API)を構築する際、ローカル環境(localhost)でも様々なサブドメインに切り替えた動作検証をしたいと思います。
その場合ホストマシンのDNSの設定などを変更する必要があると思いきや、Chromeなどの一部のツールは特にDNS関連の設定を変更しなくても、自動で127.0.0.1
に名前解決してリクエストを行いました。
一方でSafariや各プログラミング言語のHTTPクライアントライブラリなどで実行すると、名前解決が行われずに、通信に失敗します。
以下は各ツールでのリクエスト結果です(2023年6月時点)
※実行環境
- 端末:MacBook Pro(M2)
- OS:Ventura 13.4.1
ブラウザ
- Google Chrome → 成功
- Edeg → 成功
- FireFox → 成功
- Safari → エラー
- Postman(APIクライアント) → 成功
CLI
- ping → エラー
$ ping hoge.localhost ping: cannot resolve hoge.localhost: Unknown host
- curl → 成功
$ curl 'http://hoge.localhost:3000/health' "OK" $ curl -V curl 7.88.1 (x86_64-apple-darwin22.0) libcurl/7.88.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.11 nghttp2/1.51.0 Release-Date: 2023-02-20 Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp Features: alt-svc AsynchDNS GSS-API HSTS HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz MultiSSL NTLM NTLM_WB SPNEGO SSL threadsafe UnixSockets
HTTPクライアントライブラリ
- Dio(Flutter) → エラー
flutter: *** DioError ***: flutter: uri: http://hoge.localhost:3000 flutter: DioError [unknown]: null Error: SocketException: Failed host lookup: 'hoge.localhost' (OS Error: nodename nor servname provided, or not known, errno = 8)
- net/http(Go) → エラー
Get "http://hoge.localhost:3000": dial tcp: lookup hoge.localhost: no such host
非対応のツールでは実行環境にDNS設定を行う必要がある
Macであれば、/etc/hosts
ファイルにドメイン名を追記することでIPアドレスとのマッピングが可能です。
- /etc/hosts
127.0.0.1 hoge.localhost 127.0.0.1 fuga.localhost
しかしホストPCに直接設定することになるので、チーム開発などをする際は、設定の差分に気を付ける必要があります。
lvh.meというドメインを使えば、非対応のツールでも対応可能だが。。。
実行環境のDNS設定を変更以外の方法を探すと、127.0.0.1
でDNSに登録されているlvh.me
というドメインが存在するそうで、それを使えばサブドメインを付けた状態でもループバックアドレスに名前解決できました。
lvh.meというループバックドメイン:Technical tips:Media hub
ただこのドメインは公的機関など信頼性のある団体ではなく、どこかの個人が管理しているようなので、突然使えなくなったり、情報漏洩などセキュリティ上のリスクが懸念されるので、商用サービスなどでの利用は控えた方が良さそうです。
dns - It is safe to use lvh.me instead of localhost for testing? - Stack Overflow
RFC 6761での定義
なおRFCの「localhost」に関するドメイン名の定義には、以下のように記述されていました。
6.3. Domain Name Reservation Considerations for "localhost."
The domain "localhost." and any names falling within ".localhost." are special in the following ways:
Users are free to use localhost names as they would any other domain names. Users may assume that IPv4 and IPv6 address queries for localhost names will always resolve to the respective IP loopback address.
Application software MAY recognize localhost names as special, or MAY pass them to name resolution APIs as they would for other domain names.
Name resolution APIs and libraries SHOULD recognize localhost names as special and SHOULD always return the IP loopback address for address queries and negative responses for all other query types. Name resolution APIs SHOULD NOT send queries for localhost names to their configured caching DNS server(s).
Caching DNS servers SHOULD recognize localhost names as special and SHOULD NOT attempt to look up NS records for them, or otherwise query authoritative DNS servers in an attempt to resolve localhost names. Instead, caching DNS servers SHOULD, for all such address queries, generate an immediate positive response giving the IP loopback address, and for all other query types, generate an immediate negative response. This is to avoid unnecessary load on the root name servers and other name servers.
https://www.rfc-editor.org/rfc/rfc6761#section-6.3
.localhost.
を含むドメイン名はループバックアドレス(127.0.0.1)で解決することが推奨されているので、この仕様が現時点でのスタンダードのようです。
Google Chromeも以前は未対応だった
またGoogle Chromeに関しては、数年前の時点ではこの自動での名前解決に対応していなかったようで、途中で仕様変更の提案を受けて仕様が変わったようです。
dns - Why does Chrome resolve websitename.localhost as localhost? - Webmasters Stack Exchange
今後の仕様変更の可能性を考慮するなら、自動での名前解決に依存しない設計が望ましい
前述の通り、自動的に名前解決する方針が主流のようですが、各ツールごとに仕様が揃っていなかったり、仕様が変わったりしているのが現状です。
ローカル環境での話なので、そこまで神経質に取り扱う必要は無いかもしれないですが、今後の仕様変更などを考慮するなら、自動での名前解決に依存しない方針で開発を進めた方が良いかもしれません。
参考リンク
Echo(Golang web framework)のディレクトリ構成の参考情報 - OSS「echo-sample」のコードリーディング
TL;DR
echo-sample
は、こんな構成だった
きっかけ
業務で、Golang
とそのWebフレームワークのEcho
で、APIサーバーを作る機会があったのですが、初めて触るのでディレクトリ構成で悩みました。
必要なのは、Clean Architectureなどでガッツリ分離する必要も無いくらいのシンプルな機能のみなので、OSSで程よい構成のお手本を探しました。
いくつかEchoを利用したリポジトリがある中で、echo-sample
が簡潔な構成で参考になりそうでした。
そこで、それぞれのファイルがどういった役割を担っているのか調査したので、結果をまとめました。
echo-sampleの概要
名前の通り、Echoを用いたアプリケーションのサンプルです。
POST
で新しいメンバーの登録、GET
でメンバーのデータを取得(個別or全件)出来るという非常に簡素なAPIです。
ディレクトリ構成
echo-sample ├── .gitignore ├── LICENSE ├── glide.lock ├── glide.yaml │ ├── main.go ・ ・ ・ 「①サーバー起動」 │ ├── api │ └── member.go ・ ・ ・ 「③レスポンスの制御」 │ ├── conf │ └── config.go ・ ・ ・ 「④'DBの認証情報を定義」 │ ├── db │ ├── ddl.sql ・ ・ ・ 「⓪DBの作成」 │ └── mysql.go ・ ・ ・ 「③'DBの設定」 │ ├── handler │ └── handler.go ・ ・ ・ 「③'''エラーハンドリング」 │ ├── middleware │ └── transaction.go ・ ・ ・ 「③’'DBの起動・トランザクション制御」 │ ├── model │ └── member.go ・ ・ ・ 「④ビジネスロジックの実装」 │ ├── route │ └── router.go ・ ・ ・ 「②ルーティング設定」 │ └── vendor ├── github.com │ └── ... └── golang.org/x └── ...
個々の構成毎の役割
パッケージ管理のglide
や外部ライブラリの置き場所のvendor
、.gitignore
とLICENSE
は自明なので飛ばします
main.go
echo-sample/main.go at master · eurie-inc/echo-sample · GitHub
役割
- サーバー起動
logrus
の読み込み及び設定
詳細
全ての起点となる/main.go
ですが、最低限の内容に留めていました。
サーバー起動を行っていますが、echoインスタンスの生成は、後述するroute
で行っています。
func main() { router := route.Init() router.Run(fasthttp.New(":8888")) }
また、ログ出力に標準logではなく、外部パッケージのlogrus
を利用しており、それの読み込み/設定も行っています。
標準logより高機能で良いらしいです。
func init() {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.JSONFormatter{})
}
route
echo-sample/route/router.go at master · eurie-inc/echo-sample · GitHub
役割
詳細
先ほど説明した通り、ここではechoインスタンスの生成を行っています。
func Init() *echo.Echo { e := echo.New() // 省略 }
インスタンス生成の中で、各種ミドルウェアの読み込みと、ルーティングの設定を行っています。
ミドルウェアについては、echo標準のものをいくつか読み込んでいます。
// Set Bundle MiddleWare e.Use(echoMw.Logger()) e.Use(echoMw.Gzip()) e.Use(echoMw.CORSWithConfig(echoMw.CORSConfig{ AllowOrigins: []string{"*"}, AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAcceptEncoding}, }))
また後述するmiddleware
で定義したミドルウェアも読み込んでいます。
その中で、後述するdb
を呼び出して、DB接続も行っています。
// Set Custom MiddleWare
e.Use(myMw.TransactionHandler(db.Init()))
また、エラーハンドリングを行う為に、後述するhandler
の設定も行っています。
e.SetHTTPErrorHandler(handler.JSONHTTPErrorHandler)
ルーティングでは、バージョン管理が容易に出来るように、Group
でパスを束ねていました。
なお、実際の処理は、後述するapi
で実装しているようです。
v1 := e.Group("/api/v1") { v1.POST("/members", api.PostMember()) v1.GET("/members", api.GetMembers()) v1.GET("/members/:id", api.GetMember()) }
middleware
echo-sample/middleware/transaction.go at master · eurie-inc/echo-sample · GitHub
役割
- DBのトランザクション 制御
詳細
transaction.go
という名前の通り、DBのトランザクション 制御を請負っているようです。
const ( TxKey = "Tx" ) func TransactionHandler(db *dbr.Session) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return echo.HandlerFunc(func(c echo.Context) error { tx, _ := db.Begin() c.Set(TxKey, tx) if err := next(c); err != nil { tx.Rollback() logrus.Debug("Transction Rollback: ", err) return err } logrus.Debug("Transaction Commit") tx.Commit() return nil }) } }
DBの操作にはdbr
というパッケージを利用しています。
handler
echo-sample/handler/handler.go at master · eurie-inc/echo-sample · GitHub
役割
- HTTPのエラーハンドリング
詳細
こちらでHTTPのエラーハンドリングを行っている様です。
なお、echo.Context
は、現在のHTTPリクエストのコンテキストを表します。
func JSONHTTPErrorHandler(err error, c echo.Context) { code := fasthttp.StatusInternalServerError msg := "Internal Server Error" if he, ok := err.(*echo.HTTPError); ok { code = he.Code msg = he.Message } if !c.Response().Committed() { c.JSON(code, map[string]interface{}{ "statusCode": code, "message": msg, }) } }
db
echo-sample/db/mysql.go at master · eurie-inc/echo-sample · GitHub
役割
- DBへの接続
詳細
ここではデータベースへの接続を行っています。
func Init() *dbr.Session { session := getSession() return session } func getSession() *dbr.Session { db, err := dbr.Open("mysql", conf.USER+":"+conf.PASSWORD+"@tcp("+conf.HOST+":"+conf.PORT+")/"+conf.DB, nil) if err != nil { logrus.Error(err) } else { session := db.NewSession(nil) return session } return nil }
ここでもdbr
というパッケージを利用しています。
なお、認証情報については後述するconf
から読み出しています。
conf
echo-sample/conf/config.go at master · eurie-inc/echo-sample · GitHub
役割
- DBの認証情報やサーバーの接続情報の定義
詳細
ここでは DBの認証情報やサーバーの接続情報の定義しています。
定数は全てここで管理している様です。
const ( USER string = "root" PASSWORD string = "mysql01" DB string = "sample" HOST string = "192.168.99.100" PORT string = "32769" )
api
echo-sample/api/member.go at master · eurie-inc/echo-sample · GitHub
役割
- レスポンスのフォーマットを指定
- modelのエラーハンドリング
詳細
api
では、model
の処理が正常に完了すれば、その処理結果をJSONとして返します。
また、処理がエラーになった場合は、HTTPエラーを返します。
実際のデータの取得や更新についてはmodel
で行っているようです。
func PostMember() echo.HandlerFunc { return func(c echo.Context) (err error) { m := new(model.Member) c.Bind(&m) tx := c.Get("Tx").(*dbr.Tx) member := model.NewMember(m.Number, m.Name, m.Position) if err := member.Save(tx); err != nil { logrus.Debug(err) return echo.NewHTTPError(fasthttp.StatusInternalServerError) } return c.JSON(fasthttp.StatusCreated, member) } } func GetMember() echo.HandlerFunc { return func(c echo.Context) (err error) { number, _ := strconv.ParseInt(c.Param("id"), 0, 64) tx := c.Get("Tx").(*dbr.Tx) member := new(model.Member) if err := member.Load(tx, number); err != nil { logrus.Debug(err) return echo.NewHTTPError(fasthttp.StatusNotFound, "Member does not exists.") } return c.JSON(fasthttp.StatusOK, member) } } func GetMembers() echo.HandlerFunc { return func(c echo.Context) (err error) { tx := c.Get("Tx").(*dbr.Tx) position := c.QueryParam("position") members := new(model.Members) if err = members.Load(tx, position); err != nil { logrus.Debug(err) return echo.NewHTTPError(fasthttp.StatusNotFound, "Member does not exists.") } return c.JSON(fasthttp.StatusOK, members) }
model
echo-sample/model/member.go at master · eurie-inc/echo-sample · GitHub
役割
- DBの操作
詳細
/model
は大きく分けると、以下の3つで構成されています。
- JSONでデータを取り扱う為の構造体
// メンバー(個別) type Member struct { Number int64 `json:"number"` Name string `json:"name"` Position string `json:"position"` CreatedAt int64 `json:"createdAt"` } ................................................ // メンバー(全件) type Members []Member
- memberの保存の為にインスタンスを生成する関数
// メンバー保存 func NewMember(member int64, name, position string) *Member { return &Member{ Number: member, Name: name, Position: position, CreatedAt: time.Now().Unix(), } }
- メンバー取得・保存用の関数
// メンバー保存 func (m *Member) Save(tx *dbr.Tx) error { _, err := tx.InsertInto("member"). Columns("number", "name", "position", "created_at"). Record(m). Exec() return err } // メンバー取得(個別) func (m *Member) Load(tx *dbr.Tx, number int64) error { return tx.Select("*"). From("member"). Where("number = ?", number). LoadStruct(m) } // メンバー取得(全件) func (m *Members) Load(tx *dbr.Tx, position string) error { var condition dbr.Condition if position != "" { condition = dbr.Eq("position", position) } return tx.Select("*"). From("member"). Where(condition). LoadStruct(m) }
メンバー取得・保存用の関数では、dbr
という外部ライブラリを用いて、db
へアクセスしています。
なお、完全に余談ですが、JSON形式のテキストを、Goの構造体の書式に変換してくれるツールがありました。 mholt.github.io
/db/ddl.sql
echo-sample/db/ddl.sql at master · eurie-inc/echo-sample · GitHub
役割
- DBの作成
詳細
最後に残ったddl.sql
は、データベース作成用のSQLが記述されています。
CREATE DATABASE IF NOT EXISTS sample; USE sample; DROP TABLE IF EXISTS member; CREATE TABLE IF NOT EXISTS member ( number INT PRIMARY KEY, name VARCHAR(255) NOT NULL, position CHAR(2) NOT NULL, created_at BIGINT NOT NULL );
参考リンク
AtCoder Beginner Contest 144「D - Water Bottle」(Ruby)
図を描いて三角関数を使えば、実装は比較的簡単 (図をキレイに書くスキルが欲しい・・・)
問題
問題文 高橋君は、底面が 1 辺 a
c m の正方形であり、高さが b
c m であるような直方体型の水筒を持っています。(水筒の厚みは無視できます。)
この水筒の中に体積 x
c m 3 の水を入れ、底面の正方形の 1 辺を軸として、この水筒を徐々に傾けます。
水を溢れさせずに水筒を傾けることができる最大の角度を求めてください。
制約 入力は全て整数 1 ≤ a ≤ 100 1 ≤ b ≤ 100 1 ≤ x ≤ a 2 b
考えたこと
こういった図形絡みの問題は、実際に図を描いた方が分かりやすいと思ったので、図を描いてみました。
水の体積と底面の正方形の 1 辺の長さから、水面までの高さを割り出せます。
底面の正方形の 1 辺を軸として水筒を徐々に傾けるので、傾きを考える時は面で考えて良さそう。
傾きを考える時、二つのパターンがありそう。
一つは、最大まで傾けても、水面が底面に達しない場合。
これはコップを傾けたことで水に接している左辺の長さが伸びると、右辺はそれに反比例して縮むことに着目して、各辺の長さを割り出しています。
もう一つは、最大まで傾けてると、水面が底面に達する場合。
これはコップを傾けて水が接している面の形が変わっても、面積は変わらないことに着目して各辺の長さを割り出しています。
それぞれ計算方法が異なるので、条件分岐が必要。
この二つのパターンを三角関数の公式に当てはめれば良さそう。
コード
a,b,x = gets.split(' ').map(&:to_i) if b-2*x.to_f/(a*a) <= 0 puts Math.atan2((b-x.to_f/(a*a))*2, a) * 180 / Math::PI else puts Math.atan2(b, 2*x.to_f/(a*b)) * 180 / Math::PI end
コード長 244 Byte
実行時間 7 ms
メモリ 1788 KB
AtCoder Beginner Contest 144「C - Walk on Multiplication Table」(Ruby)
平方根の活用がポイント
問題
問題文 高橋君は無限に広い掛け算表の上にいます。
掛け算表のマス ( i , j ) には整数 i × j が書かれており、高橋君は最初 ( 1 , 1 ) にいます。
高橋君は 1 回の移動で ( i , j ) から ( i + 1 , j ) か ( i , j + 1 ) のどちらかにのみ移ることができます。
整数 N が与えられるので、 N が書かれているマスに到達するまでに必要な移動回数の最小値を求めてください。
制約 2 ≤ N ≤ 10 12 N は整数である。
考えたこと
1回の移動で (i,j) から (i+1,j) か(i,j+1)
とあることから、縦方向か横方向に、それぞれ1マスづつしか動けないことが分かります。
現在地が(1, 1)なので、移動先が1であれば、移動する必要はありません。
つまり移動回数は、(i - 1) + (j - 1)
で求めることが出来ます。
そして整数 i×j
は、複数の組み合わせが想定されます。
移動回数がiとjの足し算なので、移動回数の最小値を求めるには、出来るだけ互いの値を小さく抑える必要があります。
マスの位置は掛け算で表されるので、どちらかの値を1にして、一方を最大の値にすれば、移動回数も最大になります。
互いの値の差が最大の時に移動回数が最大になるので、互いの値の差が最小になるケースを考える必要があります。
互いの値の差が最小になるので、両方の値が同じ場合です。
平方根()を用いれば、互いの値の差が無い数値を求めることが出来ます。
しかしNは整数なので、平方根の結果が整数にならない場合は、そこから最も違い数値を探す必要があります。
通りなので、全探索でも対応出来そうです。
コード
n = gets.to_i a = Math.sqrt(n).to_i while n % a != 0 a -= 1 end b = n / a puts a + b - 2
コード長 100 Byte
実行時間 45 ms
メモリ 3836 KB
モデルのバリデーションでBoolean型の入力チェックを行う方法(Rails)
結論
Boolean型の入力チェックは
presence: true
だと出来ないinclusion:{in: [true, false]}
であれば、チェック可能
基本的な入力必須のバリデーション
Railsのモデルのバリデーションにおいて、入力必須のチェックを行う場合、presence: true
が良く用いられます。
class User < ApplicationRecord validates :name, presence: true end
これはテキスト(string型、text型など)や数値(integer型など)などであれば有効です。
しかし、Boolean型(真偽値、true/false)では、正常に機能しません。
Boolean型ではpresence: true
は正常に機能しない
Boolean型のバリデーションにpresence: true
を利用すると、false
が入った時にも、エラーで弾かれてしまいます。
これはpresence: true
が、内部でblank?
メソッドを使って、属性の有無を判別しているのが原因です。
blank?
メソッドは、オブジェクトがnil, 空文字列('', ' '), 空配列([]), 空ハッシュ ({}), false
のときtrue
を返し、1つ以上の要素があればfalse
を返すメソッドです。
つまり、false
という値が入っていたとしても、入力されていないと判定されてしまうのです。
inclusion
を利用する
Boolean型に対して入力有無のチェックを行いたい場合は、inclusion
を利用します。
inclusion
は、指定した複数の値の中に、値が含まれているかどうかを検証します。
inclusion
で、true
もしくわfalse
を指定すれば、false
であってもtrue
が返ります。
class User < ApplicationRecord validates :is_admin, inclusion: { in: [true, false] } end
参考リンク
AtCoder Beginner Contest 142「D - Disjoint Set of Common Divisors」(Ruby)
問題
D - Disjoint Set of Common Divisors
実行時間制限: 2 sec / メモリ制限: 1024 MB
配点 : 400 点
問題文 正整数 A , B が与えられます。
A と B の正の公約数の中からいくつかを選びます。
ただし、選んだ整数の中のどの異なる 2 つの整数についても互いに素でなければなりません。
最大でいくつ選べるでしょうか。
考えたこと
まずAとBの公約数を求める必要があります。
調べたところ、Rubyには標準添付ライブラリに素因数分解の処理が用意されています。
require 'prime' 12.prime_division #=> [[2, 2], [3, 1]] #2の2乗、3の1乗 18.prime_division #=> [[2, 1], [3, 2]] #2の1乗、3の2乗
このように、配列で素因数分解された結果が出力できるので、これで公約数を求めることが出来ます。
あとは二つの数字の素因数分解した結果を見比べて、一致する数を集計するだけです。
なお、今回は「互いに素」ということなので、各配列の先頭部分の素数だけ見れば良いです。
提出コード
思いつくままに書いたので汚い・・・
require 'prime' a,b = gets.split.map(&:to_i) a_f = a.prime_division b_f = b.prime_division arr = [] delete_f = true a_f.each do |aa| while delete_f do b_f.delete_if do |bb| if aa[0] == bb[0] arr << aa[0] delete_f = false true elsif aa[0] < bb[0] false else true end end delete_f = false end delete_f = true end puts arr.size + 1
補足
素因数分解には、Linuxコマンドのfactor
コマンドを使用する方法もあります。
RubyでLinuxコマンドを呼び出す場合は、バッククォート(`)で括れば良いです
x = 12 `factor #{x}` #=> "12: 2 2 3\n"
これを使って、もっとシンプルにしたコードがこちらです。
a, b = gets.split.map(&:to_i) g = a.gcd(b) s = `factor #{g}`.split[1..-1].map(&:to_i) p s.uniq.size+1