環境変数の効率的な管理方法:os.Getenv vs os.LookupEnv と github.com/caarlos0/env の活用ガイド

TL;DR;
Go言語での環境変数管理には主にos.Getenvos.LookupEnvの2つの方法があります。os.Getenvはシンプルでデフォルト値の設定に適しており、os.LookupEnvは環境変数の存在確認が必要な場合に使います。より高度な環境変数管理にはgithub.com/caarlos0/envライブラリが推奨され、構造体タグによる型安全な設定が可能です。テスト時はt.Setenvを使用することで、環境変数の一時的な設定と自動復元が簡単に行えます。

Go言語の環境変数を取得する方法にはos.Getenvos.LookupEnvの2つの方法があります。

os.Getenv関数

  • 戻り値: 文字列のみ
  • 動作: 環境変数が存在しない場合、空文字列""を返す
  • 問題: 環境変数が存在しないのか、空文字列が設定されているのか区別できない
package main

import (
    "fmt"
    "os"
)

func main() {
    // 存在しない環境変数
    value1 := os.Getenv("NON_EXISTENT_VAR")
    fmt.Printf("NON_EXISTENT_VAR: '%s'\n", value1) // 出力: ''

    // 空文字列が設定された環境変数(事前にexport EMPTY_VAR=""で設定)
    value2 := os.Getenv("EMPTY_VAR")
    fmt.Printf("EMPTY_VAR: '%s'\n", value2) // 出力: ''

    // どちらも同じ結果になってしまう
}

os.LookupEnv関数

  • 戻り値: (string, bool)の2つの値
  • 動作: 環境変数の値と、存在するかどうかのbool値を返す
  • 利点: 環境変数の存在を明確に判定できる
package main

import (
    "fmt"
    "os"
)

func main() {
    // 存在しない環境変数
    value1, exists1 := os.LookupEnv("NON_EXISTENT_VAR")
    fmt.Printf("NON_EXISTENT_VAR: value='%s', exists=%t\n", value1, exists1)
    // 出力: NON_EXISTENT_VAR: value='', exists=false

    // 空文字列が設定された環境変数
    value2, exists2 := os.LookupEnv("EMPTY_VAR")
    fmt.Printf("EMPTY_VAR: value='%s', exists=%t\n", value2, exists2)
    // 出力: EMPTY_VAR: value='', exists=true

    // 値が設定された環境変数
    value3, exists3 := os.LookupEnv("PATH")
    fmt.Printf("PATH exists: %t\n", exists3)
    // 出力: PATH exists: true
}

実用的な使い分け例

package main

import (
    "fmt"
    "os"
)

func getConfig() {
    // os.Getenvの場合:デフォルト値を設定
    dbHost := os.Getenv("DB_HOST")
    if dbHost == "" {
        dbHost = "localhost" // 空文字列でもデフォルト値を使用
    }

    // os.LookupEnvの場合:存在チェック
    dbPort, exists := os.LookupEnv("DB_PORT")
    if !exists {
        dbPort = "5432"
        fmt.Println("DB_PORT not set, using default")
    } else if dbPort == "" {
        fmt.Println("DB_PORT is set but empty!")
        // 空文字列が明示的に設定された場合の処理
    }

    fmt.Printf("DB_HOST: %s, DB_PORT: %s\n", dbHost, dbPort)
}

  • os.Getenv: シンプルで、デフォルト値を簡単に設定したい場合に適している
  • os.LookupEnv: 環境変数の存在を明確に判定したい場合や、空文字列と未設定を区別したい場合に適している

一般的には、環境変数の存在チェックが重要な場合はos.LookupEnvを、単純にデフォルト値で十分な場合はos.Getenvを使用します。

サードパーティ:github.com/caarlos0/envの使用

Web開発をする場合、DBやSaasの接続情報など、複数の環境変数情報が必要になります。

OS パッケージのみを使って環境変数を扱おうとすると環境変数が増えるたびに OS.Getenv 関数を呼び出して変数に値を設定する必要があります。

また、OS パッケージで取得した環境変数の値は string 型なので何らかのスライスや数字型として環境変数を扱いたい場合はそれぞれのパース処理を書く必要も出てきます。

これらを簡略化するためにサードパーティのライブラリgithub.com/caarlos0/envを使うと良いです。

このライブラリは構造体のタグを使って環境変数を自動的にマッピングできる便利なツールです。

標準パッケージと比較して次のような優位な点があります。
Parse関数を一度呼ぶだけで複数の環境変数を読み込むことができる
●構造体へのtagsで環境変数とフィールドを細付けられる
string型以外の読み込みができる
●デフォルト値の設定をすることができる
●環境変数未設定の場合はerrorを返すことを指定できる

基本的な使用方法

package main

import (
    "fmt"
    "log"

    "github.com/caarlos0/env/v10"
)

type Config struct {
    // 必須の環境変数
    DatabaseURL string `env:"DATABASE_URL,required"`

    // デフォルト値付き
    Port int `env:"PORT" envDefault:"8080"`

    // オプション(デフォルト値なし)
    RedisURL string `env:"REDIS_URL"`

    // bool型
    Debug bool `env:"DEBUG" envDefault:"false"`
}

func main() {
    cfg := Config{}
    if err := env.Parse(&cfg); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Config: %+v\n", cfg)
}

os.Getenv/LookupEnvとの比較

従来の方法(os.Getenv使用)

package main

import (
    "fmt"
    "os"
    "strconv"
)

type Config struct {
    DatabaseURL string
    Port        int
    RedisURL    string
    Debug       bool
}

func loadConfigManual() Config {
    cfg := Config{}

    // 必須チェックを手動で行う
    cfg.DatabaseURL = os.Getenv("DATABASE_URL")
    if cfg.DatabaseURL == "" {
        panic("DATABASE_URL is required")
    }

    // 型変換を手動で行う
    portStr := os.Getenv("PORT")
    if portStr == "" {
        cfg.Port = 8080 // デフォルト値
    } else {
        port, err := strconv.Atoi(portStr)
        if err != nil {
            panic("Invalid PORT value")
        }
        cfg.Port = port
    }

    // オプション値
    cfg.RedisURL = os.Getenv("REDIS_URL")

    // bool変換
    debugStr := os.Getenv("DEBUG")
    cfg.Debug = debugStr == "true" || debugStr == "1"

    return cfg
}

envライブラリを使用した方法

package main

import (
    "log"
    "github.com/caarlos0/env/v10"
)

type Config struct {
    DatabaseURL string `env:"DATABASE_URL,required"`
    Port        int    `env:"PORT" envDefault:"8080"`
    RedisURL    string `env:"REDIS_URL"`
    Debug       bool   `env:"DEBUG" envDefault:"false"`
}

func loadConfigWithEnv() Config {
    cfg := Config{}
    if err := env.Parse(&cfg); err != nil {
        log.Fatal(err)
    }
    return cfg
}

高度な機能の例

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/caarlos0/env/v10"
)

type DatabaseConfig struct {
    Host     string `env:"DB_HOST" envDefault:"localhost"`
    Port     int    `env:"DB_PORT" envDefault:"5432"`
    Username string `env:"DB_USER,required"`
    Password string `env:"DB_PASS,required"`
}

type Config struct {
    // ネストした構造体
    Database DatabaseConfig `envPrefix:"DB_"`

    // スライス型
    AllowedHosts []string `env:"ALLOWED_HOSTS" envSeparator:","`

    // 時間型
    Timeout time.Duration `env:"TIMEOUT" envDefault:"30s"`

    // カスタムパーサー
    LogLevel string `env:"LOG_LEVEL" envDefault:"info"`

    // 環境変数の存在チェック
    SecretKey string `env:"SECRET_KEY,required,unset"`
}

func main() {
    cfg := Config{}

    // パースオプション
    opts := env.Options{
        RequiredIfNoDef: true, // デフォルト値がない場合は必須
    }

    if err := env.Parse(&cfg, opts); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Config: %+v\n", cfg)
}

実際の使用例(設定ファイル vs 環境変数)

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/caarlos0/env/v10"
)

type AppConfig struct {
    // サーバー設定
    ServerPort int    `env:"SERVER_PORT" envDefault:"8080"`
    ServerHost string `env:"SERVER_HOST" envDefault:"0.0.0.0"`

    // データベース設定
    DatabaseURL      string `env:"DATABASE_URL,required"`
    DatabasePoolSize int    `env:"DB_POOL_SIZE" envDefault:"10"`

    // 外部サービス
    RedisURL    string `env:"REDIS_URL"`
    RedisPrefix string `env:"REDIS_PREFIX" envDefault:"myapp"`

    // セキュリティ
    JWTSecret string `env:"JWT_SECRET,required"`

    // 機能フラグ
    EnableMetrics bool `env:"ENABLE_METRICS" envDefault:"true"`
    EnableDebug   bool `env:"DEBUG" envDefault:"false"`

    // 配列設定
    TrustedProxies []string `env:"TRUSTED_PROXIES" envSeparator:"," envDefault:"127.0.0.1"`
}

func main() {
    // 環境変数の例を設定
    os.Setenv("DATABASE_URL", "postgres://user:pass@localhost/db")
    os.Setenv("JWT_SECRET", "my-secret-key")
    os.Setenv("TRUSTED_PROXIES", "192.168.1.1,10.0.0.1")

    cfg := AppConfig{}
    if err := env.Parse(&cfg); err != nil {
        log.Fatal("設定の読み込みに失敗:", err)
    }

    fmt.Printf("サーバー: %s:%d\n", cfg.ServerHost, cfg.ServerPort)
    fmt.Printf("データベース: %s\n", cfg.DatabaseURL)
    fmt.Printf("デバッグモード: %t\n", cfg.EnableDebug)
    fmt.Printf("信頼できるプロキシ: %v\n", cfg.TrustedProxies)
}

メリットとデメリット

envライブラリのメリット

  • 簡潔なコード: 構造体タグで設定が完結
  • 型安全: 自動的な型変換とバリデーション
  • 必須チェック: requiredタグで必須項目を指定
  • デフォルト値: envDefaultで簡単に設定
  • 複雑な型対応: スライス、時間、カスタム型にも対応

envライブラリのデメリット

  • 外部依存: サードパーティライブラリに依存
  • 学習コスト: タグの記法を覚える必要がある
  • デバッグ: エラー時の詳細が分かりにくい場合がある

使い分けの指針

  • 小規模プロジェクト: os.Getenv / os.LookupEnv で十分
  • 中〜大規模プロジェクト: envライブラリで効率的に管理
  • 設定項目が多い: envライブラリが有効
  • 型変換が複雑: envライブラリが有効

このように、envライブラリは環境変数の管理を大幅に簡素化し、型安全性を提供する優れたツールです。

環境変数のテスト

t.Setenvメソッドについて詳しく説明します。

実は、Go 1.17以降ではt.Setenvの使用が推奨されています

t.Setenvメソッドの基本

func TestWithSetenv(t *testing.T) {
    // Go 1.17以降で利用可能
    t.Setenv("DATABASE_URL", "postgres://test@localhost/testdb")
    t.Setenv("SERVER_PORT", "8080")

    // テスト終了時に自動的に元の値に復元される
    cfg, err := LoadConfig()
    if err != nil {
        t.Fatal(err)
    }

    if cfg.DatabaseURL != "postgres://test@localhost/testdb" {
        t.Errorf("期待値と異なります")
    }
}

従来の方法 vs t.Setenv

従来の方法(非推奨になった理由)

func TestOldWay(t *testing.T) {
    // 手動でバックアップと復元
    original := os.Getenv("DATABASE_URL")
    defer func() {
        if original == "" {
            os.Unsetenv("DATABASE_URL")
        } else {
            os.Setenv("DATABASE_URL", original)
        }
    }()

    os.Setenv("DATABASE_URL", "postgres://test@localhost/testdb")

    // テスト実行
}

t.Setenvを使った方法(推奨)

func TestNewWay(t *testing.T) {
    // 自動的にバックアップと復元が行われる
    t.Setenv("DATABASE_URL", "postgres://test@localhost/testdb")
    t.Setenv("SERVER_PORT", "8080")
    t.Setenv("DEBUG", "true")

    // テスト実行
    cfg, err := LoadConfig()
    if err != nil {
        t.Fatal(err)
    }

    // アサーション
    if cfg.DatabaseURL != "postgres://test@localhost/testdb" {
        t.Errorf("DATABASE_URL: 期待値=%s, 実際=%s",
            "postgres://test@localhost/testdb", cfg.DatabaseURL)
    }
}

実践的な使用例

envライブラリとの組み合わせ

package config

import (
    "testing"
    "github.com/caarlos0/env/v10"
)

type AppConfig struct {
    DatabaseURL string   `env:"DATABASE_URL,required"`
    ServerPort  int      `env:"SERVER_PORT" envDefault:"8080"`
    Debug       bool     `env:"DEBUG" envDefault:"false"`
    Features    []string `env:"FEATURES" envSeparator:","`
}

func TestAppConfig(t *testing.T) {
    tests := []struct {
        name     string
        envVars  map[string]string
        expected AppConfig
        wantErr  bool
    }{
        {
            name: "すべての環境変数が設定されている",
            envVars: map[string]string{
                "DATABASE_URL": "postgres://user:pass@localhost/testdb",
                "SERVER_PORT":  "3000",
                "DEBUG":        "true",
                "FEATURES":     "auth,logging,metrics",
            },
            expected: AppConfig{
                DatabaseURL: "postgres://user:pass@localhost/testdb",
                ServerPort:  3000,
                Debug:       true,
                Features:    []string{"auth", "logging", "metrics"},
            },
        },
        {
            name: "デフォルト値が使用される",
            envVars: map[string]string{
                "DATABASE_URL": "postgres://user:pass@localhost/testdb",
            },
            expected: AppConfig{
                DatabaseURL: "postgres://user:pass@localhost/testdb",
                ServerPort:  8080, // デフォルト値
                Debug:       false, // デフォルト値
                Features:    nil,
            },
        },
        {
            name: "必須項目が不足",
            envVars: map[string]string{
                "SERVER_PORT": "3000",
            },
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // t.Setenvで環境変数を設定
            for key, value := range tt.envVars {
                t.Setenv(key, value)
            }

            cfg := AppConfig{}
            err := env.Parse(&cfg)

            if tt.wantErr {
                if err == nil {
                    t.Error("エラーが期待されていましたが発生しませんでした")
                }
                return
            }

            if err != nil {
                t.Fatalf("予期しないエラー: %v", err)
            }

            // 構造体の比較
            if cfg.DatabaseURL != tt.expected.DatabaseURL {
                t.Errorf("DatabaseURL: 期待値=%s, 実際=%s",
                    tt.expected.DatabaseURL, cfg.DatabaseURL)
            }
            if cfg.ServerPort != tt.expected.ServerPort {
                t.Errorf("ServerPort: 期待値=%d, 実際=%d",
                    tt.expected.ServerPort, cfg.ServerPort)
            }
            if cfg.Debug != tt.expected.Debug {
                t.Errorf("Debug: 期待値=%t, 実際=%t",
                    tt.expected.Debug, cfg.Debug)
            }
        })
    }
}

複雑な設定のテスト

func TestComplexConfig(t *testing.T) {
    t.Run("本番環境設定", func(t *testing.T) {
        t.Setenv("ENV", "production")
        t.Setenv("DATABASE_URL", "postgres://prod@prod-db:5432/proddb")
        t.Setenv("REDIS_URL", "redis://redis-cluster:6379")
        t.Setenv("LOG_LEVEL", "warn")
        t.Setenv("RATE_LIMIT", "1000")

        cfg, err := LoadConfig()
        if err != nil {
            t.Fatal(err)
        }

        if cfg.Env != "production" {
            t.Errorf("環境設定が正しくありません: %s", cfg.Env)
        }
    })

    t.Run("開発環境設定", func(t *testing.T) {
        t.Setenv("ENV", "development")
        t.Setenv("DATABASE_URL", "postgres://dev@localhost:5432/devdb")
        t.Setenv("LOG_LEVEL", "debug")
        t.Setenv("HOT_RELOAD", "true")

        cfg, err := LoadConfig()
        if err != nil {
            t.Fatal(err)
        }

        if cfg.Env != "development" {
            t.Errorf("環境設定が正しくありません: %s", cfg.Env)
        }
        if !cfg.HotReload {
            t.Error("開発環境ではホットリロードが有効になっている必要があります")
        }
    })
}

t.Setenvのメリット

1. 自動クリーンアップ

func TestAutoCleanup(t *testing.T) {
    // テスト前の値
    originalValue := os.Getenv("TEST_VAR")

    t.Setenv("TEST_VAR", "test_value")

    // テスト中
    if os.Getenv("TEST_VAR") != "test_value" {
        t.Error("環境変数が設定されていません")
    }

    // テスト終了後、自動的に元の値に復元される
    // 手動でのクリーンアップは不要
}

2. パラレルテスト対応

func TestParallelSafe(t *testing.T) {
    t.Run("テスト1", func(t *testing.T) {
        t.Parallel()
        t.Setenv("TEST_VAR", "value1")

        // このテストは他のテストと並列実行されても安全
        if os.Getenv("TEST_VAR") != "value1" {
            t.Error("値が正しくありません")
        }
    })

    t.Run("テスト2", func(t *testing.T) {
        t.Parallel()
        t.Setenv("TEST_VAR", "value2")

        // 他のテストの環境変数設定に影響されない
        if os.Getenv("TEST_VAR") != "value2" {
            t.Error("値が正しくありません")
        }
    })
}

3. シンプルなコード

func TestSimpleCode(t *testing.T) {
    // 従来の方法(煩雑)
    /*
    original := os.Getenv("DB_HOST")
    defer func() {
        if original == "" {
            os.Unsetenv("DB_HOST")
        } else {
            os.Setenv("DB_HOST", original)
        }
    }()
    os.Setenv("DB_HOST", "test-host")
    */

    // t.Setenvを使用(シンプル)
    t.Setenv("DB_HOST", "test-host")
    t.Setenv("DB_PORT", "5432")
    t.Setenv("DB_NAME", "testdb")

    // テスト実行
    config := loadDatabaseConfig()
    if config.Host != "test-host" {
        t.Error("ホスト名が正しくありません")
    }
}

注意点とベストプラクティス

1. Go 1.17以降限定

//go:build go1.17
// +build go1.17

func TestWithSetenv(t *testing.T) {
    t.Setenv("VAR", "value") // Go 1.17以降でのみ利用可能
}

2. サブテストでの使用

func TestSubtests(t *testing.T) {
    t.Run("サブテスト1", func(t *testing.T) {
        t.Setenv("ENV", "test1")
        // この設定はサブテスト終了時に自動クリーンアップされる
    })

    t.Run("サブテスト2", func(t *testing.T) {
        t.Setenv("ENV", "test2")
        // 前のサブテストの設定は影響しない
    })
}

簡単にまとめると以下の通りです。

  • Go 1.17以降の標準機能
  • 自動クリーンアップでメモリリークやテスト間の干渉を防ぐ
  • パラレルテスト対応
  • コードが簡潔で保守しやすい
  • エラーが起きにくい(手動復元の忘れがない)

Go 1.17以降を使用している場合は、t.Setenvを積極的に使用することを強く推奨します。

まとめ

Goにおける環境変数管理は、シンプルなアプローチから高度なサードパーティライブラリの利用まで幅広く対応しており、プロジェクトの規模や要件に応じて適切な方法を選択することが重要です。os.Getenvos.LookupEnvは標準ライブラリとして軽量で簡易的な方法を提供する一方で、複雑な型の変換や依存関係の管理が必要な場合にはgithub.com/caarlos0/envのような外部ライブラリが非常に有用です。

特に、環境変数を利用するシステムにおいては、環境変数が正しく設定されていない場合にアプリケーションが予期せぬ挙動をするリスクがあります。そのため、環境変数の存在確認やバリデーションを行う仕組みを導入することが推奨されます。例えば、envライブラリを使用すれば、環境変数が不足している場合にエラーを返す機能や、デフォルト値の設定、型安全な変換を簡単に実現できます。

また、テスト環境においては、t.Setenvを活用することで環境変数の設定やクリーンアップが効率的に行えるため、テストコードの保守性が向上します。特に、複数の環境で動作するアプリケーションを開発する際には、環境変数を適切に管理することで、設定の切り替えやデプロイがスムーズに行えます。

最終的には、以下のような指針に基づいて環境変数管理の方法を選択すると良いでしょう:

1. 環境変数の数が少なく、単純な設定の場合はos.Getenvos.LookupEnvを活用する。
2. 型変換やバリデーション、デフォルト値が必要な場合はenvライブラリなどのサードパーティツールを利用する。
3. テスト環境ではt.Setenvを活用して環境変数の設定・管理を簡略化する。
4. プロジェクトの規模が拡大する場合は、環境変数管理を一元化し、必要に応じて設定ファイルやシークレット管理ツール(例:HashiCorp Vault、AWS Secrets Manager)と組み合わせる。

適切な手法を選択することで、コードの簡潔さと可読性を保ちながら、安全でスケーラブルなアプリケーションの開発が可能になります。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

はじめまして、「知識を愛する者」愛知郎です。

五反田でエンジニアとして活動してます。

こちらでは、私が最近学んだこと発信しています。

目次