🔦

defer f.Close() のエラーハンドリングで怒られる問題

2023/09/03に公開

defer f.Close() がエディタに怒られる問題というものがあります。

小さい話で恐縮なのですが、goを書いていると日常よく遭遇してしまうため、案外無視できないのです。fに限らず io.Closer 全般に当てはまります。

これを快適にさばくためにちょっとした工夫をしたので紹介します。

f.Close()のハンドリングを素で書くとどうなるか

まず、defer f.Close() がエディタに怒られる問題の実例を見て見ましょう(以下goland)。

エラーハンドリングについて警告が出る設定だと、deferのときも警告されることになります。
これがどうにも鬱陶しいのです。。。

ではエディタに任せたらどう修正してくれるか? golandの世界では修正候補は以下の様になっています。

  • 明示的に無視する
  • 明示的にハンドリングする

どっちかになります。これによってどういうコードになるかをみて見ましょう。

明示的に無視する

func readMyData (path string)error{
	f,err := os.Open(path)
	if err != nil {
	    return err
	}
	defer func(f *os.File) {
		_ = f.Close()
	}(f)
	
	// ...
}

ちょっと冗長ですよね。。。

明示的にハンドリングする

func readMyData (path string)error{
	f,err := os.Open(path)
	if err != nil {
	    return err
	}
	defer func(f *os.File) {
		err := f.Close()
		if err != nil {
			log.Println(err)
		}
	}(f)
	
	// ....
}

すごく冗長に感じますよね。。。あと、ハンドリングするといっても、deferを使う場合だと選択肢が結構限定されており、logしたり、監視レイヤに通知を飛ばすような処理が多いかと思います。

そもそもこのエラーはどう扱うべきか?

ちょっとさかのぼって考えてみたいと思います。
そもそも論としてcloseのエラーってどう処理されるべきなんでしょうか。

私は大雑把にいって2つの方向性があると思います。

  • Closeのエラーがフェータルな状況を招くため、明確にハンドリングされる必要がある
  • Closeにエラーが出ても、実際上大した影響はない。せいぜいlogしておけばいいくらいだ。

この2つはやはり分けて考えたほうがよいと思います。

私の理解では、前者の場合には、むしろdeferしないのが正解になると思います。つまりこういう書き方になります。

func readMyData(path string) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	// do something  //
	
	if err := f.Close();err!=nil{
		return err
	}

}

通常系のエラーとして普通に返すわけですね。このようにすると、その関数が失敗したという前提の、エラーハンドリングのフローに乗ってきます。

具体例としてぱっと思いついたものは、GCPのCloudStorageへの書き込み処理があります。データを書き込んだあとにWriterをCloseするのですが、Closeしないとデータが書き込まれません。 こういう場合は、データが書き込めないというフェータルな結果を招きますので、明示的にハンドルするべきと思います。

一方、後者の大して影響がない場合の具体例として思いつくのは、たとえば http.Response.Body です。http.Response.Bodyは、データを全て読み取り、かつ、Closeしなさいというのが、公式のお作法です。ただし、Closeに失敗しても、データが読めている限りは、処理は続行可能ですので、フェータルではないのです。

というわけで後者の場合は、エディタの警告は見たくないですが、かといって、冗長に書くのも割に合いません。さくっと処理したいです。

工夫

前置きが長くなりましたが、これを解消したいので、ちょっとした関数を書きました。要するにハンドリングしていればよいので、closeするための関数をはさめばよいことになります。

シンプルに作ると以下の様になります。

func shortClose(c io.Closer) {
	if err := c.Close(); err != nil {
		log.Printf("cannnot close : %v", err)
	}
}

これを使うとエディタはこうなりました。

警告されませんね! かつ、もし問題が起きてもlogの通知は確保されます。

もうちょっと詳しく情報をとりたければこんな書き方もありえます。

func richClose(c io.Closer) {
	err := c.Close()
	if err != nil {
		pc, f, l, ok := runtime.Caller(1)
		if !ok {
			log.Printf("runtime.Caller() failed")
			log.Printf("richClose %v", err)
			return
		}
		fname := ""
		fn := runtime.FuncForPC(pc)
		if fn != nil {
			fname = fn.Name()
		}
		msg := fmt.Sprintf("%v,%v,%v : %v", f, l, fname, err)
		log.Printf("richClose %v", msg)
	}
}

個人的にはだいぶストレス減りました!

Discussion