20200708のGoに関する記事は3件です。

【Go】 deferに渡した関数内のエラーを呼び出し元に返す

はじめに

はじめまして。Go初学者です。
Gopher道場#8や自身の勉強にて、気づいた学びを共有しています。

今回は、defer内でのエラーハンドリング方法と、名前付き戻り値の利用方針についてまとめました。

結論から

deferに渡した関数のreturnは無視されるので、deferに渡した関数のエラーは 名前付き戻り値 に代入して呼び出し元に戻す。

If the deferred function has any return values, they are discarded when the function completes. (See also the section on handling panics.)

deferred関数に戻り値がある場合は、関数が完了したときに破棄されます。

defer

is 何

詳細は公式ドキュメントへ

いわゆる Finally 句の様なもので、関数が終了する前に呼ばれます。
defer キーワードの後に関数を渡すと、渡した順にスタックされていき、最後から順に実行されていきます。

func main() {
    defer func() {
        fmt.Println("defer 1")
    }()
    defer func() {
        fmt.Println("defer 2")
    }() 

    fmt.Println("hoge")
}
hoge
defer 2
defer 1

panicの場合でも、きちんと呼ばれてくれます。

func main() {
    defer func() {
        fmt.Println("defer 1")
    }()
    defer func() {
        fmt.Println("defer 2")
    }() 

    panic("panic!")
}
defer 2
defer 1
panic: panic!

goroutine 1 [running]:
main.main()
    /tmp/sandbox481037905/prog.go:15 +0x68

Program exited: status 2.

以下の様に、ファイルのクローズ処理などによく用いられるそうです。

func main() {
    file, _ := os.Create("hoge.txt")
    defer file.Close()

    // 書き込み処理
}

defer内でのreturnは無視される

下記の様な記述では、呼び出し元はerrを受け取る事ができません。

func main() {
    if err := hoge(); err != nil {
        fmt.Fprint(os.Stderr, err)
        os.Exit(1)
    }
    os.Exit(0)
}

func hoge() error {
    defer func() error {
        return fmt.Errorf("error occured!")
    }()

    return nil
}

では、先ほどの例でファイル操作をしている場合などに、エラー判定を呼び出し元に返すにはどのようにしたらよいのか?
この際に、名前付き戻り値を利用します。

名前付き戻り値

is 何

詳細は公式ドキュメントへ

Goでは戻り値に名称をつける事ができます。
また、付けられた名称は変数として関数内で利用する事が可能です。
名前付き戻り値を利用すると、returnのみでも名前を付けた変数が返却することができます。

func square(i int) (squared int) {
    squared = i * i
    return
}

良い所

戻り値に名前をつける事が出来るので、関数の利用者は作成者が何を返したいのかより明確になります。
IDEなどでも以下の様に表示されます。

image.png

ただ、そもそも関数名、引数名と引数の型、戻り値の型などを使って戻り値の意味が連想できないのはBadかなとは思いますが...

気になる所

私個人的には、returnで省略できる点については、ソースの可読性を下げる恐れがあると感じています。
なので、実際はreturn squaredの様に、明示的に記載することにしています。

deferに渡した関数内のエラーを呼び出し元に返す

本題です。
以下の様に、defer内で名前付き戻り値にエラーを設定することで、エラーを呼び出し元に返却する事が可能になります。

func main() {
    if err := file("hoge"); err != nil {
        fmt.Fprint(os.Stderr, err)
        os.Exit(1)
    }
    os.Exit(0)
}

func file(fileName string) (err error) { // errorに名前をつける
    file, _ := os.Create(fileName)
    defer func() {
        err = file.Close() // errに処理結果を代入
    }()

    // 書き込み処理

    return nil
}

おわりに

基本的に名前付き戻り値は使わずに実装が可能ですが、上記のパターンの場合は使わずに返却することは難しいようでした。
名前付き戻り値の使い所はこういうパターンもあるんですね:thinking:

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go言語】 time.Time - t.Sub(u)はu→t の正確な経過時間を返さない(かもしれない)

問題

time.TimeのSubメソッドを利用すると、2つのtime.Time間のDurationを取得することが出来ます。

例えば↓なカンジで処理時間を計測したりします。

start := time.Now()

(何か処理)

elapsed := time.Now().Sub(start)

fmt.Println(elapsed)

このコード自体間違いではないですが、今回実行していて明らかに実際の経過時間を返していない事象に遭遇し、軽くハマりました。
9:02にスタートし9:10に終了しているのに、elapsedに 3m などと出力されたり。

startとnowも出力↓

start:2020-07-08 09:02:00.243095 +0900 JST m=+0.003321332
now:2020-07-08 09:10:25.388989 +0900 JST m=+222.388991487
elapsed:3m42.385670155s

startとnowも確かに正しい日時が入っています。何が起きているのでしょうか・・・ ?
(鋭い人はこのログでピンと来てそうですね)

タネあかし

ログを見ると m=+222.388991487 みたいな出力が見られます。
これの単位を秒とみなすと、nowとstartの差分がちょうどelapsedと一致します。

これは実はmonotonic clockと呼ばれるもので、time.Timeは時刻を表す情報(wall clock)とは別に時間計測に使う為の情報(monotonic clock)を持つことが出来ます。time.Now() で作成したtime.Timeは内部にmonotonic clockを持ちます。

Subメソッドの様な時間経過を得る為の関数1はwall clockよりmonotonic clockを優先的に使用して計算を行います。それにより、例えば処理中にOS時刻を変更されたりしても、正しく経過時間を取得することが出来ます。

さてここまで見る限り処理時間の計測方法としては正しいと思われる(実際正しい)のですが、なぜズレが生じてしまったのでしょうか。

公式ドキュメントにそのものズバり書かれていました(-ω-;
https://golang.org/pkg/time/#hdr-Monotonic_Clocks

On some systems the monotonic clock will stop if the computer goes to sleep. On such a system, t.Sub(u) may not accurately reflect the actual time that passed between t and u.

「いくつかのシステムではコンピューターがスリープした場合monoritic clockが停止して、t.Sub(u)が正しい経過時間を返さないことがある」とのことでした。

そうです。今回私は実行中に何度かスリープ→回復していたのです。2

明示的にwall clockで計算したい場合は、t.Sub(u)のt、uどちらかのmonotonic clock情報を削除すればOKです。
monotonic clock情報は t.Round(0) の様にすれば削除出来ます。

最後に

monotonic clockについては、今まで「時間計測に使用される」、「t.Timeを==演算子で比較するのは危険」くらいの曖昧な知識しかありませんでした。

time.Timeのドキュメント見ると真っ先にmonotonic clockの説明が書かれているのに、全然読んでませんでした(猛省)。
公式ドキュメントはちゃんと読まなきゃですね。。

参考にした記事


  1. 他にtime.Since(start)、time.Until(deadline)、time.Now().Before(deadline)などが同様にmonotonic clockを利用します。 

  2. 実行環境はMacBook Pro(macOC10.15.5)です。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go getすると410 Goneのエラーが出る

背景

GitHub Enterpriseのリポジトリに作成したリポジトリをgo get仕様とすると、 410 Gone のエラーが出た。

$ go get git.enterprise.com/repo/package
verifying git.enterprise.com/repo/package@v0.1.1/go.mod:
git.enterprise.com/repo/package@v0.1.1/go.mod: reading
https://sum.golang.org/lookup/git.enterprise.com/repo/package@v0.1.1: 410 Gone

sum.golang.org を経由せずにダウンロードしたい。

intelliJのGoModuleの設定確認

Preference > 言語&フレームワーク > Go Moduleの設定を確認。
プロキシーに「direct」が入ってなかったので入れる。
image.png

GOPRIVATEの設定をして再度 go get

$ GOPRIVATE="git.dmm.com,direct"
$ go get -u git.enterprise.com/repo/package

intelliJの再起動

わたしは何故かintelliJの再起動までし無いと、IDE上で反映がされませんでしたので再起動をしました。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む