$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

Goの学習のため書籍を三冊読んだ

A Tour of Goが終わり、もう少しGo自体の深掘りをしたいためGoの書籍をいくつか読んでみたのでメモ。今回読んだ書籍は以下の三冊。

それぞれの感想としては

  • 実用Go言語:業務で使っていると起こるような色んなHowToを大量に提供してくれている。今の段階で全部読むというより、必要に応じて再度読むインデックス的な使い方が良さそう。クックブック的。
  • Go言語プログラミングエッセンス:最近出た本のため、最近のGoに即した内容を学べる。Go Modulesやcontext周りが自分は曖昧になっていたので、その辺りが色々知れた。また非同期処理まわりのパターンは非常に勉強になった
  • 改訂2版 みんなのGo言語:少し前に出版されたので内容は古めではあるが、Goらしいコードは何か、linterなどはどう使えば良いか、Goでのプロジェクトレイアウトはどうすると良いかなどが学べた

三冊読んで印象に残ったところをピックアップする。

  • 関数のオプション引数の作り方はいくつかパターンがある。別名関数、構造体利用、Functional Option、ビルダーパターン
  • シンプルなインターフェースを使うAPIをコアとして作り、ラップして使いやすいAPIを提供するのがGoらしい
    • io.Writerとio.Readerを受け取るNormalizeをベースに作り、fileを受け取るNormalizeFile、文字列を受け取るNormalizeStringを提供するなど
  • 型を明示しないconstはuntyped constと呼ばれ、使われる場所で型が決まる
  • go.modをexamplesディレクトリに配置すると、トップディレクトリをgo getしたときにexamples以下は無視してくれて便利
    • ただしreplaceを使ってexamples -> topの依存はローカルで完結させた方がいい
  • contextは親子関係を持てて、親のcancelは子に伝搬される

読書ノート

実用Go言語

- 慣例としてパッケージ名は小文字で構成される1つの単語 272
    - 例外としてテストでは`パッケージ名_test`を使うこともある。公開インターフェースにしかアクセスできないため、Exampleテストやブラックボックスに良い 301
- 1つのメソッドのみを持つインターフェースではerという接尾辞がついた名前が多い 301
    - io.Reader、fmt.Stringer
- constの制約 380
    - コンパイル時に確実に結果がわかっている必要がある
    - 構造体、マップ、スライスなどは使えない。関数の返り値も使えない
- 定数でerror型のインスタンスを提供 400

type errDatabase int
func (e errDatabase) Error() string {
    return "Database Error"
}
const (
    ErrDatabase errDatabase = 0
)

- iotaは参照されるたびにインクリメントした値に設定される定数 490
    - 行を変えると変化するので注意
    - iotaの初期値は0だが、ゼロ値との区別がつかないので1オリジンにするといい
    - コンパイル時に決まるため、別プロセスから参照される場合はずれが生じることがある。使わない方が良い 544
        - stringerなどを使うことで使いやすくなる
- フラグとiota 523

type CarOption uint64
const (
    GPS CarOption = 1 << iota
    SunRoof
    HeatedSeat
)

o CarOption := SunRoof | HeatedSeat

- 関数のオプション引数の作り方 752
    - 別名の関数によるオプション引数
    - 構造体によるオプション引数。例: http.Client
    - ビルダーを利用したオプション引数。。コマンドライン引数パーサでよく見かける
    - Functional Optionパターン
    - 初手は構造体パターン、その後必要に応じてビルダーやFunctional
- deferの落とし穴 1075
    - 関数を抜けた時に実行されるので、forループなどで使ってしまうとリソース消費量が増える
    - deferした結果起こるエラーもハンドリングしたい場合取りこぼす。名前付き返り値を使うなどの工夫が必要
- 大量文字列結合にはstrings.Builderが便利 1136
- 型のファクトリ関数を用意することで、機能の使い方の例を示せる 1588
- スライスへの型定義によって呼び出し側ロジックを整理できる 1610

type Consumers []Consumer
func (c Consumers) ActiveConsumer() Consumers {
    resp := make([]Consumer, 0, len(c))
    for _, v := range c {
        if v.ActiveFlg {
            resp = append(resp, v)
        }
    }
    return resp
}

consumers, err. := GetConsumers(ctx, key)
activeConsumers := consumers.ActiveConsumer()

- 値レシーバーとポインタレシーバーは、状態変更の有無で決める 2101
- タグを省略することもできるが、コードを探索するために省略しない方が良い 2351
- 構造体を設計する際のポイント3つ 2638 ☆
    - 1.ポインターとして扱うか、値として扱うか、両方良いか
        - 内部にポインターを持つなら、コピーされると変になるので、ポインタとして使うと良い
            - ポインタを使うなら明示的なコピーメソッドが必要 2672
        - 値として使うとスタックメモリ上にインスタンスが作られるため高効率 2695
    - 2.値なら、イミュータブルか、ミュータブルか
    - 3.値として扱える時、ゼロ値での動作を保証するか
    - どのように使って欲しいかを制約する方法はないので、Exampleなどで示す方が良い 2778
    - 初手は「ポインタ」「ミュータブル」「ゼロ値は保証せずファクトリ関数で」が良い 2780
- シンプルなインターフェースを使うAPIをコアとして作り、ラップして使いやすいAPIを提供するのがGoっぽい 3142
    - io.Writerとio.Readerを受け取るNormalize
    - fileを受け取るNormalizeFile
    - 文字列を受け取るNormalizeString
- プラグイン的に使えるのはinit()関数を使った実装の切り替え 3487

func init() {
    sql.Register("mysql", &MySQLDriver{})
}

- Go 1.13からfmt.Errorfで%wを使うことでエラーをラップできる 3608
- errors.Isは一致判定、errors.Asは型判定 3707
- エラーハンドリングとしては 3835
    - 1. 呼び出し元に関数の引数などの情報を付与してエラーを返す
        - ラップをすることで分かりやすくなる
    - 2. ログを出力して処理を継続
    - 3. リトライを実施
    - 4. リソースをクローズ
- log.Fatalなどでexitしてしまうと、deferが呼び出されないので注意
- `.`と`_`で始まる名前とtestdataと言う名前のフォルダはコンパイル対象から外してくれる
- GoのモジュールをOSSとして公開するとタグの位置を変えられない。なぜならgo.sumが変になるから 4312
- Standard Go Project Layoutはかなり複雑なので、従い過ぎるのは良くない 4365
- パッケージ分割における共通パッケージの分割戦略 4410
- 開発ツールのインストールにはgo installを使う方法と、tools.goを使う方法 4460
- 1レポジトリ・マルチモジュールにした場合、別モジュールの参照はローカルを使ってくれない。replaceなどを使っていい感じに解決できる 4507
    - フォークとかでもreplaceで対応できる
- プライベートレポジトリに管理されたモジュールを参照するには 4542
- パッケージの読み込み順 4770
    - Goのimportは深さ優先探索 4770
- Goのメジャーバージョンは2つまでサポート 4929
- JSON関連
    - JSONのフィールドをパースして構造体を生成してくれる、JSON-to-Go
    - JSONのomitemptyでは数値の0とゼロ値を区別できない。その時はポインタ型を利用する 5819
    - json.Marshalerなどを実装することで、変換をカスタマイズできる 5899
    - typeフィールドの値によって動的に値が決まるような時のやり方 5996
        - json.RawMessageで一旦保持して遅延評価するやり方が、型的にも綺麗
- データベース関連
    - sql.DB構造体はデフォルトでコネクションをプールする。ゴルーチンセーフ 6896
    - deferを使うと簡単にロールバックできる 7115

tx, err := db.Begin()
if err != nil {
    return err
}
defer tx.Rollback()

...

return tx.Commit()

- HTTP関連
    - HandlerFuncの実現手法 8142
    - HTTPミドルウェアの実現は、Plackとだいたい同じ 8629

type HandlerFunc func(ResponseWriter, *Request)
// HandlerFunc型のServeHTTPは関数型の値であるレシーバー自身fに移譲している
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}


テスト関連
- テストヘルパーは、`(package)_test.go`に置く、testutilという場所に通常ファイル名で置くという方法がある 10634
    - あるパッケージの機能をモックするようなヘルパーの場合は、`(package)test`という名前も(例: httptest)
- go testの-parallelはt.Parallel()の並列数。-pは複数パッケージテストのプロセス並列数
- assertionにはtestifyやgo-cmpを使える 11005
    - go-cmpにはIgnoreFieldsという関数もある

Go言語プログラミングエッセンス

- 型を明示しないconstはuntyped constantと呼び、使われる場所でそれぞれ型が決まる 64
- forは普通にwhileとして使える 74
- 配列から1要素を削除するテクニック 79
    - https://github.com/shibayu36/go-playground/blob/main/playground/delete_element_test.go
- deferではdefer時点での変数がキャプチャされる 99
    - 無名関数呼び出しの時、関数内部の変数はキャプチャされていないので注意 102
- forでgoroutineを起動するときに、変数を渡すテクニック 109
    - i := iなどループ内でバインディングする。簡単だが値型のコピーが走る
    - 無名関数の引数として渡す。値が大きいならポインタで渡すと効率的
- go.modをexamplesディレクトリに配置すると、トップディレクトリをgo getしたときにexamples以下は無視してくれて便利 126
    - この時はreplaceを使ってexamples -> topの依存はローカルで完結させた方がいい
- おまじないとして_へ代入しておくと、エラー検知しやすい 135

// type fooがinterface Iを満たすか調べられる
var _ I = (*foo)(nil)

- `go:generate` によるstringer自動実行 158
    - `go:generate`の後にはコマンドをおける
- pathパッケージはURLなど仮想的なパス、path/filepathは物理的なパスを扱う 171
- contextは習慣的に第一引数 182
    - context.TODOを使うと移行処理をスムーズにできる
- `go:build ignore`をつけておくと、タグをつけられていないファイルと干渉しないので、サンプルコードとして同じディレクトリ内に複数のmain関数をおくときに便利 191
- Go 1.16から`go:embed`のために外部ツールを使う必要がなくなった
    - ディレクトリも扱える 200

- オプショナル引数パターン: Functional Options Pattern 206 ☆

logger := log.New(f, "", log.LstdFlags)
svr := server.New("localhost", 8888,
    server.WithTimeout(time.Minute),
    server.WithLogger(logger),
)

- オプショナル引数パターン: Builder Pattern 213 ☆

svr := server.NewBuilder("localhost", 8888).
    Timeout(time.Minute).
    Logger(logger).
    Build()

- 非同期パターン色々 284 ☆
    - ジェネレータ、合流処理、先着処理、タイムアウト処理、停止処理、スロットリング
    - struct{}は空のstructでサイズは0バイト。非同期パターンに便利
    - https://github.com/shibayu36/go-playground/blob/main/playground/generator_test.go
    - https://github.com/shibayu36/go-playground/blob/main/playground/throttling_test.go
- Goのツールは、`.`や`_`から始まるディレクトリは無視 319
    - 気づき: なので_scripts/とかの名前になるんだね
- Go 1.17からt.Setenv()を使える 331
    - 気づき: t.Setenv()はCleanup()を使ってテスト関数の終了とともに元に戻している。この辺は他でも参考になりそう
    - c.Cleanup(func() {
            os.Setenv(key, prevValue)
        })
- encoding.TextMarshalerとencoding.TextUnmarshalerを実装した型はflagパッケージで扱える 421
- CLIを作るときはテストを作りやすくするため、`func run(args []string) int`として、CLI引数を受け取り終了コードを返すようにし、mainからそれを呼ぶと良い 424
- Goのフレームワークの一覧と特徴 478
- モジュール名とrootパッケージ名は一致する必要はないので、`github.com/mattn/go-hsd`のpackageをhsdとしておくことで、import時の読み込みをhsdにできる 495
- ORMのベンチマーク 546
- 気づき: Cloud RunではGo使いやすそ〜 554

改訂2版 みんなのGo言語

- runtimeパッケージを利用して各種メトリクスを取得できる 77
- ビルド時に自動的にldflagsでmain.versionを埋める場合、git describe --tagsを利用すると良い 149

GIT_VER=`git describe --tags`
go build -ldflags "-X main.version=${GIT_VER}"

- go-latestを使うことで、バイナリ利用者に最新バージョンを促せる 150
- bufioを使うことで順次処理を簡単に実装できる 158
    - Goでは自動バッファリングは行われないので自分でいい感じにやる必要がある。bufioならやってくれる 160
- io.MultiWriterによってWriteをいくつかのwriterに流せる 178
- math/randのSeedに与えるのはUnixNanoかcrypt/randからの出力を使う 184
- コンテキストは親子関係を持てる。親のcancelは伝播する 203
- SIGTERMなどのハンドリング 213
    - 独自のシグナル定義もできる
- Goのレポジトリ構成は、ライブラリをメインの成果物とするか、バイナリをメインの成果物とするかで変える 230
    - 気づき:もしかしたら最近はバイナリでもcmd/以下にしてることもある?