Goのdeferに注意する
2020-11-15
はじめに
今日開発してて遭遇したエラーについて話します。短めです。よろしくお願いします。
状況
GoでAPIサーバを開発してる時に、エラーハンドリングについての実装をしていた。
現状の問題としては、DBと接続する時にアプリケーションサーバ(NginxやらGolangやら)が生きてる状態でDBサーバ(今で言うRDS)が死んでる時にDBのIPアドレスとポートがクライアント側に渡されてしまうという問題が起きていた。
そこで、DBサーバが死んでいる時には無条件でinternal server errorを返すという実装をしてた。
問題点
コードで言うと、以下の点で問題が発生した。
DBに接続する部分のコード。
gormを使用して接続している。戻り値はうまく行ったらdbとnil、失敗したらnilとエラーを返すようにしてる。
// service/init.go
package service
import (
"os"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
)
func DBConn() (*gorm.DB, error) {
DBMS := "mysql"
HOSTNAME := os.Getenv("ホストネーム")
USERNAME := os.Getenv("ユーザーネーム")
DBNAME := os.Getenv("DBの名前")
PASSWORD := os.Getenv("パスワード")
PORT := os.Getenv("ポート")
CONNECT := USERNAME + ":" + PASSWORD + "@(" + HOSTNAME + ":" + PORT + ")/" + DBNAME + "?parseTime=true"
db, err := gorm.Open(DBMS, CONNECT)
if err != nil {
return nil, err // 接続失敗したらここでエラーを投げる
}
return db, nil
}
問題が発生した部分のコード
以下の部分で問題が発生した。
// controller/portfolio.go
func (p *Portfolio) GetPortfolio() (props Props, err error) {
db, err := DBConn()
defer db.Close()
if err != nil {
return
}
// DBの情報を取得するなんらかの処理
return
}
上のコードの中でも以下の部分に注目します。
db, err := DBConn()
defer db.Close()
if err != nil {
return
}
通常Goでは何かに接続してファイルなどを開く時に実行する関数の下にdeferをつけてクローズする関数を置きます。こうすることで、deferが最後に実行されるため安全に終わることができると思って、今回もそうしたつもりでした。
現実は違ったっぽい
しかし、これではうまくいきませんでした。
理由はdeferは最後に実行されるというよりは、呼び出された時点で定義がされ、それをただ単に最後に実行するだけだったということです。
つまり
db, err := DBConn() // dbにnilが入る
defer db.Close() // 関数が登録される
if err != nil { // エラーだ、処理終了
return
}
// ここでdeferが呼ばれるけど、登録された時点ではエラーハンドリングがされていないのでエラー
のような感じです。
初心者目線から見てですが、初心者がハマりやすいポイントかも、、、。と思いました。
そのため、以下のように修正しました。
db, err := DBConn() // dbがnil
if err != nil { // エラーだ、処理終了
return
}
defer db.Close() // 問題なし
はあ、疲れた。
まとめ
もっと賢くなりましょう。