Open1

実用Go言語: 読書メモ

ShimmyShimmy

1章

「Goらしさ」にふれる

iotaで列挙型を実現

ユニークな値を持つ定数

type CarType int

const (
	Sedan CarType = iota + 1
	Hatchback
	SUV
)

iotaの初期値は0。このまま利用すると初期化されていない状態の0なのか、最初の要素である0なのか区別がつかない。なのでiota + 1として1をオリジンにするのが一般的

組み合わせてフラグとして利用する定数

ファイルを開くモードのように、それぞれの定数を組み合わせて宣言し、組み合わせを表現するフラグとしてiotaを使う場合

import "fmt"

type CarOption int

const (
	GPS         CarOption = iota << 1 // 1 (0001 in binary)
	AWD                               // 2 (0010 in binary)
	Sunroof                           // 4 (0100 in binary)
	HeatedSeats                       // 8 (1000 in binary)
)

func main() {
	var o CarOption = Sunroof | HeatedSeats
	o = Sunroof | HeatedSeats

	if o&Sunroof != 0 {
		fmt.Println("Has Sunroof")
	}
}

if o&Sunroof != 0の箇所はビット積(AND)で計算した結果で存在確認をしている。
より具体的なサンプルだとGo言語 ビット演算でのフラグ管理が参考になる

文字列として出力可能にする

iotaはintベースの列挙型なので、ログ出力時に元の整数値が出力される。その対応方法として
iotaによる手法+go-generateで解決するのが良い。
具体的には次の2種類がある。詳細は上記のリンクを参考

  1. iota + Stringer
  2. iota + enumer

関数のオプション引数

関数やメソッドを実装する時に必要に応じてパラメーターを増減させたいことがある。Goでの実現方法について説明する
著者の記事ではないが、Goで関数のオプション引数を実装するパターン集に同様の内容が書かれている。

  1. 構造体の利用
    Goの標準ライブラリでよく見られる。例: http.Clientの初期化
  2. ビルダーを利用したオプション引数
    APIやロガー周り、コマンドライン引数パーサで見かける
  3. Functional Optionパターン
    一番凝っている方法

著者おすすめは、まずは構造体パターンを実装して提供する。必要になってからビルダー形式やFunctional Optionパターンを提供する

メモリ起因のパフォーマンス低下を解消する方法

Goでパフォーマンスに差が現れがちなポイントはスライスマップのメモリ確保である。パフォーマンス問題にあたると、まず調べるのがメモリ確保の頻度

スライスのメモリ確保を高速化

  • 予め必要な要素数が分かっているなら
    • s1 := make([]int, 1000)で実際のサイズを確保
  • 最大量の見込みがつく場合
    • s2 := make([]int, 0, 1000)でキャパシティを確保

マップのメモリ確保を高速化

予め必要な要素が分かっているなら
m := make(map[string]string, 1000)でキャパシティを確保

deferの落とし穴1

deferは関数ブロックを抜けたときに、そのキーワードの後ろにあった文を実行する。deferはリソースの明示的な開放を行うために主に利用され、リソース確保の直後に書く。一方で関数を抜けるまで終了処理が呼ばれないため、forループと一緒に使うと、リソースの消費量が線形に増えていくので、明示的に開放処理を呼ぶ必要がある。

bad.go
for _, fname := range files {
	f, err := os.Open(fname)
	if err != nil {
		return err
	}
	defer f.Close()
}
good.go
for _, fname := range files {
	f, err := os.Open(fname)
	if err != nil {
		fmt.Println("Error opening file:", err)
	}

	err = f.Close()
	if err != nil {
		fmt.Println("Error closing file:", err)
	}
}

deferの落とし穴2

Close()など、エラーを返す可能性がある場合、普通にdeferを呼ぶだけでは、エラーを取りこぼす。無名関数でくくってそのエラーを名前付きの返り値に代入することでエラーを呼び出し元に返す

func deferReturnSample(fname string) (err error) {
	var f *os.File
	f, err = os.Open(fname)
	if err != nil {
		return fmt.Errorf("os.Open: %w", err)
	}
	defer func() {
        // Closeのエラーを拾って名前付き返り値に代入
		err = f.Close()
	}()
	io.WriteString(f, "deferのエラーを拾うサンプル")
	return
}

日時の取り扱い

  • 日時のフォーマットはJSとの変換を考慮するとtime.RFC3390Nanoがおすすめ
  • 翌月など日数を自分で+-で計算しているロジックを見たら警戒する