🔖

Go初心者必見!テストも安心、どこからでも設定ファイルを正しく読み込む裏ワザ

に公開

なぜ設定ファイルが読み込めなくなるの?

ディレクトリと変更前のコード

ディレクトリは以下の通り

├─Root
│  ├─main.go
│  ├─config.ini
│  ├─config
│  │  ├─config.go   <---このconfig.goにconfig.iniを読み込むコードを書いています
│  ├─test
│  │  ├─config_test.go

↓変更前のコードは以下の通り

config.go
func LoadConfig(section string)(*Config, error){
    cfg, err := ini.Load("config.ini")  //<---これが今回問題の箇所
    
    if err != nil{
        return nil, fmt.Errorf("iniファイル読込エラー:%w", err)
    }
    
    return &Config {
        Logfile:    cfg.Section(section).Key("logfile").String(),
        User:  cfg.Section(section).Key("user").String(),
        Password: cfg.Section(section).Key("password").String(),
        DBname: cfg.Section(section).Key("dbname").String(),
    }, nil
}

問題となる場合

main.goからconfig.goを実行すると、main.goとconfig.iniはカレントディレクトリに居るので、
cfg, err := ini.Load("config.ini")
は問題なく実行できる。
ただ、テスト等の時に「カレントディレクトリ(現在の作業ディレクトリ)」が変わります

そのため、
ini.Load("config.ini") のようにカレントディレクトリからの相対パスで設定ファイルを指定していると、
「ファイルが見つからない!」というエラーが発生しがちです。
でも、、、テストのたびに書き換えるの面倒じゃーん
という事で、以下の手法をご提案します。

対処法

config.goから見て、どこにconfig.iniがいるかを指してPathを通す

config.goから見て、config.iniがどこにいるか探す関数を作成

config.go
func getProjectRoot() string {
    _, filename, _, _ := runtime.Caller(0)
    return filepath.Dir(filepath.Dir(filename))
}

説明

  • runtime.Caller(0)
    • 現在の関数(getProjectRoot)が呼ばれた場所のファイルパスを取得します。
    • 第1引数(0)は「呼び出し元のスタックフレームの深さ」を表し、0は「自分自身」を指します。
  • filename
    • この関数が書かれているファイルのフルパスが格納されます。
    • 今回はconfig.goで呼ばれてるから、root/config/config.goと取得できる
  • filepath.Dir(filename)
    • filenameの親ディレクトリ(=この関数が書かれたファイルのあるディレクトリ)を取得します。
  • filepath.Dir(filepath.Dir(filename))
    • 上記、filenameの親ディレクトリ(=この関数が書かれたファイルのあるディレクトリ)のさらに親ディレクトリ(=プロジェクトルート)ということ。
    • 今回は、config.goから見たconfig.ini場所を取得したい(config.goの親ディレクトリ=config--->のさらに親ディレクトリ=Root)

config.iniがどこにいるか探す関数をiniファイルの読み込みに使用

LoadConfig()の中で、config.goから見たconfig.iniの絶対pathを指定して読込実行

config.go
gofunc LoadConfig() {
    root := getProjectRoot()
    cfgpath := filepath.Join(root, "config.ini")
    cfg, err := ini.Load(cfgpath)
    *// (以下、エラー処理や設定の利用などが続く)*
}

説明

  • root := getProjectRoot()
    • プロジェクトルートのパスを取得します。
  • cfgpath := filepath.Join(root, "config.ini")
    • プロジェクトルート直下のconfig.iniファイルのパスを組み立てます。
  • cfg, err := ini.Load(cfgpath)
    • config.iniファイルを読み込み、cfgに設定情報が格納されます。
    • ini.Loadgithub.com/go-ini/iniパッケージの関数です。
  • (エラー処理など)
    • 実際にはここでerrをチェックし、エラー処理を行うことが多いです。

3. まとめ

  • getProjectRoot()
    • プロジェクトルートのパスを取得するための関数。
  • LoadConfig()
    • プロジェクトルート直下のconfig.iniを読み込む関数。
  • この方法を使うと、どこから実行してもconfig.iniを確実に読み込める
    • テストやコマンド実行時のカレントディレクトリの違いに強くなります。

絶対Path直書きではなぜダメなの?

結局上記の説明って、絶対PathをLoadConfig()に渡してるよね?

であれば、

例えば、絶対Pathが"/Root/config/config.ini"の場合、

では、そもそも、
func LoadConfig(section string)(*Config, error){
cfg, err := ini.Load("/Root/config/config.ini")

と直書きすればよかったのでは?

Discussion