blog.takurinton.dev

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() // 問題なし

はあ、疲れた。

まとめ

もっと賢くなりましょう。