🤖

🤖

:gijutsu_burogu:

Goのお作法を学んでセルフコードレビューしてみた

はじめに

業務以外で 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の中に閉じ込めた方がいいです。 理由として、err1err2といった変数が作られバグり安くなるからです。 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 パッケージを使うことで以上の問題を解決できます。

godoc.org

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
}

内部実装はとてもシンプルになっており驚きました。

github.com

構造体を初期化する時は属性名を指定しておく

このようにどちらの方法でも宣言できます。

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

参考

future-architect.github.io

future-architect.github.io

qiita.com

qiita.com