Effective Goのまとめ

Effective Go読んだのでまとめ。 C言語 + オブジェクト指向言語の経験者向け。

Formatting

  • go fmt使え
  • indentはスペースでなくタブ

Commentary

  • 文法としては、C++と同じ
  • トップレベルコメントはJavadoc的な立ち位置。go docで出力する。普通のテキスト扱い。HTML使ったり自前の整形などはしないで、普通のテキストとして記述するのが良い。
  • すべてのパッケージはパッケージコメント、つまりパッケージ節の前にあるブロックコメントを持つべき
  • 関数宣言などにつけるコメントの最初の文は、宣言されている名前から始まる1文の要約であること
  • 変数宣言のグループ化とコメントを組み合わせると、個々の変数でなくまとめてコメントを付けれる

Names

  • パッケージ名
    • シンプルな名前に
      • 名前が衝突しても別名を付けれる
    • 慣習1:小文字でシングルワード
    • 慣習2:ディレクトリ名。src/encoding/base64だとしたら"base64"
    • 長い名前を付けるのではなく、関数のdocを書くほうがしばしば良い
  • Getters/Setter
    • 先頭に"Get"を付けない
    • Setterは"Set"付ける
  • Interface names
    • 1メソッドしかないのものは"-er"にする。Reader/Writerなど。
    • メソッド名が、Write, Close, Flush, Stringなどの標準的なシグネチャと機能に一致しているなら、それらの名前を付ける。そうでないなら、標準的な名前は避ける。
  • Snakecaseでなく、MixedCapsまたはmixedCaps

Semicolons

  • つけなくてOK。自動挿入されるので。
    • セミコロン使うと良いケースは、if err := file.Chmod(0664); err != nil { ... }、というようなエラーハンドリングくらい(筆者注記)

Control structures

  • for/if/switchがある
    • 特筆すべきはswitch
  • switch
    • caseには、定数以外も指定できる。ifで書くような条件文も書ける。変数の型による分岐もできる
    • 特に指定しなければfallthroughはなし
  • Labelを付けて、break

Functions

  • 多値返しできる。 C言語のようにポインタを引数に渡さなくても良い。
  • 戻り値に名前を付けて、変数として値を設定したり取得できる
  • "defer func(args)"で、deferの呼び出し元の関数の終了時にその関数を実行できる
    • 複数deferを記述した場合の関数実行順は、LIFO
    • 引数は即評価される。deferの呼び出し元の関数の終了時ではない。

Data

newによるallocate

  • 戻り値は*T
  • 中身はゼロ値

Constructors and composite literals

  • オブジェクト作成時にゼロ値だと困るときはコンストラクタ作る。名前は"New<型名>"。
  • オブジェクト作成をリテラル形式で書くと実装が簡潔になる
    • new(File)と&File{}は等価
  • C言語と違って、ローカル変数の参照を返せる

makeによるallocate

  • slice/map/channel用
    • これらをnewで作成しないのは、ゼロ値だと困るから。sliceでいえば、array、len、capも初期化したいから。

Arrays

  • C言語と違って値型。 関数呼び出し時、呼び出し元と変更内容を共有したい、値のコピーを避けたいときは、ポインタ型を使う。
  • 要素の型は同じでもサイズが違うなら、別の型扱い

Slices

  • 関数呼び出し時、Arraysと違い、呼び出し元と要素の変更は共有される。内部的には配列のポインタを保持しているため。
  • 関数呼び出し時、呼び出した関数の中でサイズを変更する場合は、結果をSliceで返すなどする。内部的には長さは値型として保持されているため。

Maps

  • 等価演算子が実装されている型ならkeyに使える
  • 存在しないkeyを参照するとゼロ値が返る
  • 存在チェックはをするなら"v, ok = map[key]"。okがtrueなら存在するkey。
  • 要素削除はdelete(map, key)。要素は存在しなくてもOK

Printing

正直あまり覚える必要ないと思う。

  • Printfなどで使う%vは重要。
    • Printlnは%vで値を表示してくれるっぽい。
    • %vは、値のデフォルトの文字列を返す
    • %+vは、構造体のフィールド名も出力
    • %#vは、go syntaxとして値を出力
  • %xは、文字列や配列にも使える

Append

  • sliceに要素追加:append(slice, a, b, c)
  • sliceにsliceを追加:append(slice, ...slice2)。"..."はリストを展開する意味。可変長引数にarrayやsliceを渡すとき使う。

Initialization

Constants

Variables

  • 初期化のタイミングは実行時

Init Functions

  • 変数宣言の初期化子を評価された後に実行
  • 使い所
    • 変数宣言では設定しきれない値の初期化
    • 状態の正しさを保証する

Methods

  • 値メソッドはポインタと値に対して呼び出すことができる
  • ポインタメソッドはポインタに対してのみ呼び出すことができる

Interfaces and other types

  • 型は複数のインタフェースを実装することができる
  • 型変換の方法
    1. 型switch
    2. value.(typeName)を使う
      • 具体例:"str, ok := value.(string)"。戻り値1つしか受けらないと変換失敗時にpanic
  • あるinterfaceを実装している型について、そのinterfaceとして使わないなら、コンストラクタの戻り値の型は、そのinterface型にする
  • ほとんど全てのものがメソッドを持つことができる
    • 構造体、組み込み型、関数など

The blank identifier

"_"の使いどころ。

  • 多値返しの関数の結果を受けるときに、値を捨てる
  • 開発段階で、パッケージの未使用importによるエラーを避ける
    • "var _ = fmt.Printf"のように書くと、fmt未使用のエラーを回避できる
    • この記述は、imoprt文の直後に置くのが慣習
  • パッケージのinit関数を実行させるため(そのinit関数による副作用が必要なとき)
  • ある構造体が特定のインターフェースを実装しているかチェックする
    • トップレベルで、"var _ json.Marshaler = (RawMessage)(nil)"のように書くと、RawMessageがjson.Marshalerを実装しているかチェックできる。
    • コード内に静的変換が存在しない場合にのみ使用する

Embedding

type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

このように書くと、ReadWriterは、ReaderWriterからReadやWriteを直接呼べる。

良い点

  • 内側の構造体のメンバへのアクセスが簡潔にできる
  • 内側の構造体が実装しているinterfaceを引き継げる
    • 例でいえば、ReadWriterはReaderとWriterの両方のinterfaceを実装している扱い

メンバ名が被った時のルール

  • 外側のメンバで上書き
  • 同じ階層のメンバ名が被るとエラー。ただし、そのメンバに参照しなければエラーにならない。

Concurrency

Share by communicating

基本:メモリ上での値の共有は避け、通信によって値を共有する

(Mutex避けるということ)

Goroutines

  • 複数のOSスレッドに多重化されているので、I/Oを待っている間などに1つのスレッドがブロックされても、他のスレッドは実行され続ける
  • "go func()"のように書く。func()の実行完了は待たないで次に進む。
  • 関数リテラルを使うと便利

Channels

  • makeで作る
  • 使い方はいろいろ。たとえば、サーバのリクエストの処理数の制限。

var sem = make(chan int, MaxOutstanding)

func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Create new instance of req for the goroutine.
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

req := reqは重要。これを忘れると、全てのgoroutineから見えるreqは、最新のreqの値になってしまう。ループ変数reqのスコープは、for文の1イテレーションではなくfor文終わるまでだから。

Channels of channels

  • channelにchannelをもたせることもできる

Parallelization

Channelを使うと並列処理できる。

const numCPU = runtime.NumCPU()

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // Drain the channel.
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}

runtime.NumCPU()の代わりにruntime.GOMAXPROCSというのも使える。これは、 実装者が指定したプログラムが同時に実行できるコア数。デフォルト値はruntime.NumCPU()。

A leaky buffer

Channelを使うと並列処理と関係ないリソース管理にも使えるよ、という話。

Errors

基本

  • 関数の多値返しを利用してエラーを返す + チェックする
  • エラー型には、組み込みinterfaceのerrorを実装させる
  • 可能であれば、エラー文字列は、エラーを発生させた操作やパッケージを示す接頭辞を持つなどして、その発生源が分かるようにする

panic

  • 回復不能なエラーのときに使う。初期化処理失敗時など。
  • panic時の挙動
    • 関数の戻り値としてnilが設定される
    • goroutineのスタックの巻き戻しが行われる。deferで指定した関数が呼ばれる

recover

  • panicから復旧させるときに使う。これをしないとプログラムはクラッシュする。
  • deferとセットで使う
  • 用途
    • 複数gorouttine を実行していて、問題が起きたgoroutineだけシャットダウンする
    • エラー処理を簡潔に書く
  • recover後の再度panicさせると、元のpanic要因と再panic要因の両方がクラッシュレポートに含まれる

A web server

"html/template"や"net/http"を使うと、Webサーバ作ったり、HTMLテンプレートが簡単に使える、というだけの話。

感想

他のEffective Xと違って、「Go言語入門」の一部。A Tour of Goの次に読んだ方が良い。