これは、Money Forward Engineering 2 Advent Calendar 2022 23日目の記事です。
最近読んだGoの本について感想と紹介を書きたいと思います
100 Go Mistakes and How to Avoid Them
読んだ本
https://www.manning.com/books/100-go-mistakes-and-how-to-avoid-them
英語の本です
Amazonだと電子書籍版で5800円でした
https://www.amazon.co.jp/dp/B0BBHQD8BQ/
どんな本なのか?
Go言語でプログラムを書くときにやりがちな失敗を100つ集めてその修正方法が解説されている本です
Required reading for Go developers before they touch code in production. It’s the Go equivalent of Effective Java.
Go言語で開発する人が製品品質のコードに触れる前に必ず読むべき本です。Effective Java に相当する Go の本です
とはいえEffective Goは無料で読めるのでまずはこれを読むのが良いのではないでしょうか?
https://go.dev/doc/effective_go
Effective Goのような形式でより詳しく書くトピックを知りたいという人に向いていると思います
それぞれのトピックごとに間違えやすいことと、より良い書き方が解説されています
- Code and project organization(コードとプロジェクト構成)
- Data types(データタイプ)
- Control structures(制御構造)
- Strings(文字列)
- Functions and methods(関数とメソッド)
- Error management(エラーマネジメント)
- Concurrency: Foundations(並列処理の基本)
- Concurrenty: Practice(並列処理の実践)
- The standard library(基本ライブラリ)
- Testing(テスト)
- Optimizations(最適化)
対象
Go言語の入門書を読み終わった人が対象くらいのレベル感です
導入
第1章ではGo言語の良さと難しさが書かれています
Go言語はコードが読みやすく、表現力もあり、保守管理しやすいというチーム開発に向いた言語です
その一方で使いこなすのが難しいとも書かれています
Go: Simple to learn but hard to master(Go言語は簡単に学べるが簡単にマスターすることは出来ない)
Simple doesn’t mean easy(シンプル≠簡単)
ここで言うGo言語はシンプルというのは学ぶことや理解することが易いということで、なんでも簡単に実現できるということではないということです
Go言語自体を理解するのは簡単でも問題を解決するためのコードを簡単には書くことが出来ないと言うことで、そのときにやりがちな失敗をこの本では紹介されています
特に気になったトピック
インターフェース汚染
インターフェースを使いすぎてしまうことをインターフェース汚染と定義している
インターフェースは抽象化が必要となるまで使う必要がなく、単に関数を定義するためだけに使うものではない
インターフェースがあるとコードの流れを追うことが不必要に難しくなってしまう
これは私もだいぶ心当たりがあり、実装が1つのパターンしかない場合でもインターフェースを定義してしまっていることが多かったので、今後見直してみようと思っています
Go言語でのインターフェースは暗黙的に実装を満たすかどうか判定されるため、使わる側に定義しない
JavaやC#のインターフェースやC言語のヘッダーファイルなどは関数を提供する側がそのインターフェースも同時に定義していたので、そちらの言語から来た人にとっては間違いやすい部分となる
Go言語の場合以下のほうが好ましい
そのためinterfeceの実装を満たす構造体を作成する場合などはInterfaceで返却するのではなく、実装で返却する
type a interface {
F()
}
type A struct {
}
func (a *A) F() {
}
この場合、New関数は
func NewA() a { // interfaceを返す
return &A{}
}
ではなく
func NewA() *A { // オブジェクトの実体を返す
return &A{}
}
のほうが良いということ
オブジェクトで返却しておけば、利用側はInterfaceで受けることも可能なので自由度が増すと思われる
util パッケージを作らない
便利関数をまとめたutil的なパッケージを作りがちである
package util
func NewStringSet(...string) map[string]struct{} {
// ...
}
func SortStringSet(map[string]struct{}) []string {
// ...
}
Go言語の場合、パッケージ名がアプリケーション設計に重要な要素となるためutilという意味のない名前を付けず、この場合はstringset
というような名前を付けるほうが適当である
私も考えるのが面倒な場合はよくutilパッケージを作ってしまっていたため、もっと実態にあった名前をつけていきたいと思いました
Sliceによるメモリリーク
大きなサイズのsliceを作成した後、そのsliceを縮小しても大きなスライスで確保したメモリは開放されない
package main
import (
"fmt"
"runtime"
)
type Foo struct {
v []byte
}
func main() {
foos := make([]Foo, 1_000)
printAlloc()
for i := 0; i < len(foos); i++ {
foos[i] = Foo{
v: make([]byte, 1024*1024),
}
}
printAlloc()
two := keepFirstTwoElementsOnly(foos)
runtime.GC()
printAlloc()
runtime.KeepAlive(two)
}
func keepFirstTwoElementsOnly(foos []Foo) []Foo {
return foos[:2]
}
func printAlloc() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%d KB\n", m.Alloc/1024)
}
178 KB
1024189 KB
1024191 KB
1024kbyteのsliceを持つオブジェクトを1000個作成したのちに、すべてのsliceを2byteまで縮小したがメモリの使用率は変わっていないことが確認できる
たしかにsliceの縮小はCapacityはそのままで利用している領域を示すマーカーを移動させるだけとは他の資料などで知っていたが、これによってメモリリークすることを意識して使えてはいなかったなあと思いました
Mapも同様にメモリリークすることに注意する
大きなサイズのMapを作成し、要素を追加後にすべてのMapの要素を削除してもメモリ上は一定のサイズが残り続ける
Mapに大量に要素を追加する場合は想定されるサイズで初期化したほうがいい
mapはその仕様からサイズが大きくなるときにコピーが発生し、処理に時間がかかる
サイズを指定しないで作成すると何度もサイズアップの処理が実行され処理が遅くなる
func BenchmarkNoSize(b *testing.B) {
m := map[int]int{}
for i := 0; i < b.N; i++ {
m[i] = i
}
}
func BenchmarkWithSize(b *testing.B) {
m := make(map[int]int, b.N)
for i := 0; i < b.N; i++ {
m[i] = i
}
}
BenchmarkNoSize-16 7047072 213.0 ns/op 99 B/op 0 allocs/op
BenchmarkWithSize-16 14849763 95.65 ns/op 43 B/op 0 allocs/op
サイズを指定して作成するほうが断然速いですね
テスト
テスト用の様々な設定やTipsが紹介されていました
テストオプションについてはほとんど意識して使っていなかったので使っていきたいと思いました
とくに最近テスト時間がかかるようになってきたのでParallel
などは有効に使えるといいなと思います
ビルドタグを使う
テストの種類に応じてビルドタグを付けておくとテストの種類ごとに実行できて便利である
テストファイル
//go:build integration
テスト実行
go test --tags=integration -v
Shortモードを使う
ショートフラグを設定することでその設定に応じてテストをスキップすることができる
func TestLongRunning(t *testing.T) {
if testing.Short() {
t.Skip("skipping long-running test")
}
// ...
}
go test -short -v .
parallelフラグを使う
func TestFoo(t *testing.T) {
t.Parallel()
// ...
}
テストに対してt.Parallel()
を設定しておくとテスト実行時に対象のテストを並列で実行してくれる
shuffleフラグを使う
go test -shuffle=on -v .
テストをランダムな順番で実行してくれる
まとめ
いろいろな事例が幅広く書かれているので、どんな人でも学ぶところがあると思える本ではないでしょうか
書かれていることを軽く紹介しましたが、簡単に紹介できる部分のみです
並列処理はエラー処理についてはかなりのページが割かれていますが、簡単には紹介できない感じなのでスルーしました
興味を持たれた方は実際の本で確かめてみてください