はじめに
業務以外で Go でツールを作ったり、Twitter Bot を作ったりしています。 しかし、コードレビューを受ける機会がなく良い Go の書き方が身につかないのではと不安になりました。 そこで、さまざまな記事を参考にしてコードの書き方を学んでみました。
Return function calls
最初に Go に触れた時、以下のエラー処理のお作法を新鮮に感じました。
if err != nil { return nil, err }
それなので、ついついなにも考えずにこれを使いまくってしまいます。 使わなくてもいいところにまで使ってしまっているかもしれません。 そうすると、以下のコードを書いてしまうこともあるでしょう。
func bar(arg string) (*Example, error) { v, err := foo(arg) if err != nil { return nil, err } return v, nil }
上のコードは、関数をそのまま返すこれで十分です。
func bar(argstring) (*Example, error) { return foo(arg) }
err のスコープを if に閉じ込める
以下のようエラー処理を書いてしまっていませんか。
err := PostTweet(content) if err != nil { return err }
err
のスコープをif
の中に閉じ込めた方がいいです。
理由として、err1
やerr2
といった変数が作られバグり安くなるからです。
err1
を返すべきところでerr2
を返してしまったりすることがあるかもしれません。
if err := PostTweet(content); err != nil { return err }
err
だけでなく複数の値を返す場合でも、このようにしたほうが良いようです。
基本的に main 関数以外では error を上流に返す
以下のようにpanic
でプログラムの途中で以上終了するのは推奨されていません。
if err != nil { panic(err) }
main
までエラーを伝播させていき、main
で一度だけエラー処理を行うのが良いです。
func main() { if err := run(); err != nil { log.Fatal(err) } } func run() error { // 省略 }
goroutine(並列処理)でのエラーの伝播
goroutine でエラーを伝播させていくときに困りました。
func getResponses() ([]*http.Response, error) { var responses []*http.Response mutex := sync.Mutex{} wg := sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) go func() { res, err := randomResponse() if err != nil { // TODO: エラーを伝播できない log.Fatal(err) } mutex.Lock() responses = append(responses, res) mutex.Unlock() wg.Done() }() } wg.Wait() return responses, nil }
以上はgetResponse
でエラーが発生していても伝播させることができていません。
エラーを伝播させるには、channel を作り外部で受け取るなど別で実装する必要があります。
また、1 つの goroutine でエラーが発生した場合に他の goroutine を終了させたいです。
これはcontext
を使って別で実装する必要があります。
errgroup パッケージを使うことで以上の問題を解決できます。
Go
メソッドとWait
メソッドで実現します。
func getResponsesErrGroups() ([]*http.Response, error) { var responses []*http.Response mutex := sync.Mutex{} eg, ctx := errgroup.WithContext(context.TODO()) for i := 0; i < 10; i++ { eg.Go(func() error { res, err := randomResponseContext(ctx) if err != nil { return err } mutex.Lock() responses = append(responses, res) mutex.Unlock() return nil }) } if err := eg.Wait(); err != nil { return nil, err } return responses, nil }
内部実装はとてもシンプルになっており驚きました。
構造体を初期化する時は属性名を指定しておく
このようにどちらの方法でも宣言できます。
type Sample struct { Foo string, Bar int, } sample1 := Sample{ "example", 123 } sample2 := Sample{ Foo: "example", Bar: 123 }
以下のように、構造体を変更します。
type Sample struct { Foo string, Bar int, Add string, // このフィールドを追加 }
すると、sample1
の宣言はコンパイルエラーが発生しますがsample2
の宣言はコンパイルエラーが発生しません。
個人的には、エラーが出た方が初期化忘れを防止できるので良いと思いましたが違うようです。 互換性を重視しエラーが出ない方がよく属性名付きで初期化するのが良いようです。
Go 1 and the Future of Go Programs - The Go Programming Language