😶🌫️
Go勉強( ..)φメモメモ
書籍から
Go100Tips
コードとプロジェクト構成
- まとめ
- 正常系をまっすぐ落とせ
- 抽象化(インターフェース)をあらかじめ作成することはせず、具体的な必要性が発生してから作成せよ
- インターフェースはそのインターフェースを使う側が定義せよ
- 外部はコアのニーズに応えよ
- 関数オプションパターンを使え
- パッケージ名は「何を提供するか」に基づいて名前を付けよ(
utils禁止)
- 意図しない変数シャドウイング(変数隠蔽)
- a
- 不必要にネストしたコード
- 正常系をまっすぐ落とせ(My thought)
- 「ハッピーパスを左側に揃え、1列目を下まで目を通せば、期待する実行の流れをすぐに確認できるはず。 Mat Ryer」
- ifブロックがリターンするなら、elseブロックは省略せよ
- ifもelseも簡単なreturnだけなら書く(My thought)
- init関数の誤用
- 依存先パッケージのinit関数から実行される
- importしたパッケージが依存元、importをされたパッケージが依存先
- 同じパッケージ内では先に書かれているinit関数が実行される
- 直接使用しなくても特定のパッケージの初期化が必要なときは
import _ "package"と書くことで初期化を実行できる - 特徴
-
initはエラーを返せない(ので、パニックを起こすぐらいしかできない) -
init関数の単体テストができない(ので、テストには結合テストが必要になる) - 他の関数で使用するのにグローバル変数への代入が必要になる
-
- 役立つ例
- 静的な設定の定義など(エラーを発生させない、単体テストが不要なもの)
- 依存先パッケージのinit関数から実行される
- ゲッターとセッターの多用
- Goではゲッターとセッターの強制はない
- 利点
- フィールドの取得・設定をカプセル化できる(チェック、計算、ミューテックスでの保護などができる)
- 内部表現を隠蔽して、公開するものを変更できる
- ゲッターにGetをつけないのはEffective Goに言及あり
- インタフェース汚染
- 原則
- 抽象化をあらかじめ作成することはせず、具体的な必要性が発生してから作成せよ
- インタフェース汚染
- コードを不必要に抽象化して、読むのが難しくなってしまう現象のこと
- ユースケース(一般的な)
- 実装に必要な「共通の振る舞い」を定義し、使う側はそれのみを用いて実装する
- 「具体的な実装と分離」するために用いる(Testableへ)
- 自パッケージで使う予定の「振る舞いに制限」する
- 「インタフェースが大きければ大きいほど、抽象化は弱くなる。 Rob Pike」
- 「インタフェースで設計するのではなく、インタフェースを発見するのです。 Rob Pike」
- 原則
- 生産者側のインタフェース
- インターフェースはそのインターフェースを使う側が定義せよ。つまり、実装側がインタフェースを定めるな。
- インタフェースを返す
- 「他のパッケージに定義されたインタフェース」を実装した状態で、他のパッケージからその実装関数を呼び出すと循環参照が起こる
- 原則
- インタフェースの代わりに構造体を返す
- 可能な限りインタフェースを受け付ける
- 例外
- 言語設計者が元から便利であると判断した抽象化物(
error,io.Readerなど) - クライアントに役立つとあらかじめ分かっているとき
- 言語設計者が元から便利であると判断した抽象化物(
- 「自分のやることは保守的に、他人から受け入れることは寛容に。 Transmission Control Protocol」
- anyは何も伝えない
-
anyはinterface{}のエイリアスである(Go 1.18~) - 基本は使わない
- 例外
- どんな型でも受け取り返す必要のある場合のみ(
json.Marshal()など)
- どんな型でも受け取り返す必要のある場合のみ(
-
- ジェネリクスをいつ使うべきかで混乱する
- a
- 型埋め込みで起こり得る問題点を意識していない
- 構造体のフィールドに別の構造体が名前なしで宣言されているときに「(その構造体が)埋め込まれている」という
- 埋め込まれた型のフィールド・メソッドがプロモートされる
- 直接アクセスできるようになる
- 埋め込みと継承との差異
- 埋め込みではメソッドはそのままの実装だが、継承ではオーバーライドできる
- 関数オプションパターンを使わない
- APIのオプション設定方法3種
- Config構造体を使う...構造体の意味を理解しないといけない
- ビルダーパターンを使う...BuilderでConfigを作成する、エラー処理が複雑になる
- 関数オプションパターンを使う...クロージャを返す関数を呼び出し、設定関数に渡し内部で呼び出すことで設定する
- クロージャ(closure)...本体の外からの変数を参照する無名関数のこと
- APIのオプション設定方法3種
- プロジェクトの誤った構成
- 公式の規約は存在しない
- 標準的なプロジェクト構成「project-layout」
- Tips
- あまりに小さなパッケージの乱立は避けよ(理解困難になる)
- あまりに巨大なパッケージの作成は避けよ
- パッケージ名には「何を提供するか」に基づいて名前を付けよ
- 小文字の単語1つであるべき
- 公開すべきか迷ったらデフォルトで「公開しない」ようにする
-
json.Unmarshalの使用などのためにフィールドの公開は必要かもしれない
-
- ユーティリティパッケージの作成
-
utils,common,shared,baseといった共有パッケージはよくあるバッドプラクティス - パッケージ名を適切に命名し意味を与えよ
- 何を含むかではなく、何を提供するかに基づいて命名せよ
- 結果として、関数名を短くできる
-
- パッケージ名の衝突を無視する
- パッケージ名と変数名は異なるようにせよ
- 変数で上書きすると、スコープ内でそのパッケージにアクセスできなくなるため
- 組み込み関数と変数名は異なるようにせよ
- 解決策
- 変数名を変える
- パッケージ名を変える
import aliasName "<package_name>"
- パッケージ名と変数名は異なるようにせよ
- コードのドキュメントがない
- 原則
- 公開されている要素は文書化されなければならない
- 公開される要素の名前で始まるコメントを書け
- 各コメントは句点で終わる完全な文で書け
- 利用者が利用方法を理解するために十分な情報を提供せよ
- 各パッケージを文書化せよ
-
// Package <package_name>の形式で内容を簡潔に書く
-
- 原則
- リンターを使わない
- リンターとフォーマッターを使おう、実行を自動化しよう
データ型
- 8進数リテラルで混乱を招く
- a
- 整数のオーバーフローを無視する
- a
- 浮動小数点を理解していない
- a
- スライスの長さと容量を理解していない
- a
- 非効率なスライスの初期化
- a
- nilスライスと空スライスに混乱する
- 定義
- 長さが0なら、スライスは空である
-
nilと等しいならnil
- (帰結)
nilの長さは0なので、空である - 注意点
-
nilの初期化には割当てが必要ないが、空には必要である - ライブラリによっては
nilと空を区別するので、使用前に確認すること
-
- 初期化の3つの方法
-
var s []string- 最終的な長さが確定していない場合
-
[]string(nil)-
nilで空スライスを作成するシンタックスシュガー
-
-
make([]string, length)- 最終的な長さが分かっている場合
-
- 定義
- スライスが空か否かを適切に検査しない
-
len(<slice_name>) != 0でガードする-
nilでも空でもlen(<slice_name>)は0であるため
-
- (バグを防ぐため)
nilと空を区別しない実装を行うべき- 下記のGo Wikiにも言及あり
- https://go.dev/wiki/CodeReviewComments#declaring-empty-slices
- この原則はmapでも同様
-
- スライスのコピーを正しく行わない
- a
- スライスへの
appendによる予期しない副作用- a
- スライスとメモリリーク
- a
- 非効率なマップの初期化
- マップの内部構造
- ハッシュテーブルの構造
- バケット(8個のキーと値の組からなる)
- バケットへのポインタの配列
- ハッシュテーブルの構造
- 要素の格納方法
-
hash(<key>)の値を計算し、その値をインデックスとするバケットへアクセスする - 「キーと値の組」を格納する(順に)
- バケットがいっぱいの場合は、別のバケットを作成し現バケットとリンクさせ、新しいバケットへ「キーと値の組」を格納する
-
- 要素の見つけ方
-
hash(<key>)を計算し、対応するバケットを見つける - 与えられた
<key>が見つかるまでバケットへ順次繰り返し処理を行う
-
- マップの拡大戦略
- 以下の場合に配列を2倍にする。配置済みの要素も拡大されたマップへ再配置する。
- 「要素数÷配列の大きさ(loadfactor)」が6.5より大きいとき(6.5は内部的な現在値)
- バケットオーバーフローしている要素数がとても多いとき
- 以下の場合に配列を2倍にする。配置済みの要素も拡大されたマップへ再配置する。
- 問題
- マップの拡大を繰り返すと時間がかかる
- 解決策
- 初期化時に
make(map[string]int, 1_000_000)のように初期サイズを与える- 与えられた初期サイズに適切な数のバケットをもつマップが作成されるため
- 100万要素だとパフォーマンスに約50%の差が生まれる
- 初期化時に
- マップの内部構造
- マップとメモリリーク
- 問題
- 128bytes以下の要素のmapはGCが実行されても思ったより小さくならない
- 解決策2つ
- 定期的に現在のmapのコピーを再作成し、前のmapを解放する
- コピー後からGCまでの間、2倍のメモリが必要
- mapが配列へのポインタを保持するようにする
- ポインタは8/4bytesなのでメモリ消費量が抑えられる
- 定期的に現在のmapのコピーを再作成し、前のmapを解放する
- 注意
- キー・値が128bytesを超える場合はmapのバケットに自動的にポインタを保存する
- 問題
- 値の比較の誤り
- a
制御構造
-
rangeループで要素がコピーされることを無視する- 「Goでは、代入されるものはすべてコピーです。」
-
rangeループでは各要素が変数へコピーされて実行される(ため、元のスライスへ変更は反映されない) - スライスへ変更を反映する方法3つ
-
range+インデックスで要素へアクセスして更新する- 書き込みと読み込みが高速
-
for+インデックスで要素へアクセスして更新する- 更新する要素の制御を行うのに向いている
- 「ある型のスライス」型を「その型のポインタのスライス」型へ変更し
rangeを使用する- 予測性に欠けるため、最適化が働かずCPU効率が悪い
-
-
rangeループでの引数の評価方法を無視する
文献
- 公式ドキュメント群の日本語訳へのリンク集
Discussion