Практичный гайд по переменным окружения в Go

Привет, Хабр! Представляю вашему вниманию перевод статьи A no-nonsense guide to environment variables in Go автора Enda Phelan.

Переменные окружения — лучший способ хранения конфигурации приложения, поскольку они могут быть заданы на системном уровне. Это один из принципов методологии Twelve-Factor App, он позволяет отделять приложения от системы, в которой они запущены (конфигурация может существенно различаться между деплоями, код не должен различаться).

Использование переменных окружения


Всё, что необходимо для взаимодействия с переменными окружения есть в стандартной библиотеке os. Вот так можно получить значение переменной окружения PATH:

package main

import (
    "fmt"
    "os"
)

func main() {
    // Store the PATH environment variable in a variable
    path, exists := os.LookupEnv("PATH")

    if exists {
        // Print the value of the environment variable
    	fmt.Print(path)
   }
}

А так — установить значение переменной:

package main

import (
    "fmt"
    "os"
)

func main() {
    // Set the USERNAME environment variable to "MattDaemon"
    os.Setenv("USERNAME", "MattDaemon")

    // Get the USERNAME environment variable
    username := os.Getenv("USERNAME")

    // Prints out username environment variable
    fmt.Print(username)
}

Загрузка переменных окружения из файла .env


На девелоперской машине, где сразу запущено много проектов, хранить параметры в переменных окружениях не всегда удобно; логичнее будет разделить их между проектами с помощью env-файлов. Сделать это можно, например, с помощью godotenv — это портированная на Go Ruby-библиотека dotenv. Она позволяет устанавливать необходимые для приложения переменные окружения из файла .env.

Чтобы установить пакет запустим:

go get github.com/joho/godotenv

Добавим настройки в файл .env в корне проекта:


GITHUB_USERNAME=craicoverflow
GITHUB_API_KEY=TCtQrZizM1xeo1v92lsVfLOHDsF7TfT5lMvwSno

Теперь можно использовать эти значения в приложении:

package main

import (
    "log"
    "github.com/joho/godotenv"
    "fmt"
    "os"
)

// init is invoked before main()
func init() {
    // loads values from .env into the system
    if err := godotenv.Load(); err != nil {
        log.Print("No .env file found")
    }
}

func main() {
    // Get the GITHUB_USERNAME environment variable
    githubUsername, exists := os.LookupEnv("GITHUB_USERNAME")

    if exists {
	fmt.Println(githubUsername)
    }

    // Get the GITHUB_API_KEY environment variable
    githubAPIKey, exists := os.LookupEnv("GITHUB_API_KEY")
	
    if exists {
	 fmt.Println(githubAPIKey)
    }
}

Важно помнить, что если значение переменной окружения установлено на системном уровне, Go будет использовать именно это значение вместо указанного в env-файле.

Оборачиваем переменные окружения в конфигурационный модуль


Неплохо, конечно, иметь доступ к переменным окружения напрямую, как было показано выше, но вот поддерживать такое решение представляется довольно проблематичным. Имя переменной — это строка и, если оно изменится, то представьте себе головную боль, в которую выльется процесс обновления ссылок на переменную по всему приложению.

Чтобы решить эту проблему создадим конфигурационный модуль для работы с переменными окружения более централизованным и поддерживаемым способом.

Вот простой модуль config, который возвращает параметры конфигурации в структуре Config (также установим дефолтные значения параметров на случай, если переменной окружения в системе не окажется):

package config

import (
    "os"
)

type GitHubConfig struct {
    Username string
    APIKey   string
}

type Config struct {
    GitHub GitHubConfig
}

// New returns a new Config struct
func New() *Config {
    return &Config{
        GitHub: GitHubConfig{
	    Username: getEnv("GITHUB_USERNAME", ""),
	    APIKey: getEnv("GITHUB_API_KEY", ""),
	},
    }
}

// Simple helper function to read an environment or return a default value
func getEnv(key string, defaultVal string) string {
    if value, exists := os.LookupEnv(key); exists {
	return value
    }

    return defaultVal
}

Далее добавим типы в структуру Config, поскольку имеющееся решение поддерживает только строковые типы, а это не очень-то разумно для больших приложений.

Создадим хэндлеры для типов bool, slice и integer:

package config

import (
    "os"
    "strconv"
    "strings"
)

type GitHubConfig struct {
    Username string
    APIKey   string
}

type Config struct {
    GitHub    GitHubConfig
    DebugMode bool
    UserRoles []string
    MaxUsers  int
}

// New returns a new Config struct
func New() *Config {
    return &Config{
	GitHub: GitHubConfig{
	    Username: getEnv("GITHUB_USERNAME", ""),
	    APIKey:   getEnv("GITHUB_API_KEY", ""),
	},
	DebugMode: getEnvAsBool("DEBUG_MODE", true),
	UserRoles: getEnvAsSlice("USER_ROLES", []string{"admin"}, ","),
	MaxUsers:  getEnvAsInt("MAX_USERS", 1),
    }
}

// Simple helper function to read an environment or return a default value
func getEnv(key string, defaultVal string) string {
    if value, exists := os.LookupEnv(key); exists {
	return value
    }

    return defaultVal
}

// Simple helper function to read an environment variable into integer or return a default value
func getEnvAsInt(name string, defaultVal int) int {
    valueStr := getEnv(name, "")
    if value, err := strconv.Atoi(valueStr); err == nil {
	return value
    }

    return defaultVal
}

// Helper to read an environment variable into a bool or return default value
func getEnvAsBool(name string, defaultVal bool) bool {
    valStr := getEnv(name, "")
    if val, err := strconv.ParseBool(valStr); err == nil {
	return val
    }

    return defaultVal
}

// Helper to read an environment variable into a string slice or return default value
func getEnvAsSlice(name string, defaultVal []string, sep string) []string {
    valStr := getEnv(name, "")

    if valStr == "" {
	return defaultVal
    }

    val := strings.Split(valStr, sep)

    return val
}

Добавим в наш env-файл новые переменные окружения:


GITHUB_USERNAME=craicoverflow
GITHUB_API_KEY=TCtQrZizM1xeo1v92lsVfLOHDsF7TfT5lMvwSno
MAX_USERS=10
USER_ROLES=admin,super_admin,guest
DEBUG_MODE=false

Теперь можно использовать их в любом месте приложения:

package main

import (
    "fmt"
    "log"

    "github.com/craicoverflow/go-environment-variables-example/config"
    "github.com/joho/godotenv"
)

// init is invoked before main()
func init() {
    // loads values from .env into the system
    if err := godotenv.Load(); err != nil {
	log.Print("No .env file found")
    }
}

func main() {
    conf := config.New()

    // Print out environment variables
    fmt.Println(conf.GitHub.Username)
    fmt.Println(conf.GitHub.APIKey)
    fmt.Println(conf.DebugMode)
    fmt.Println(conf.MaxUsers)

    // Print out each role
    for _, role := range conf.UserRoles {
	fmt.Println(role)
    }
}

Готово!


Да, существуют пакеты, предлагающие готовое решение для конфигурации вашего приложения, но насколько они необходимы, если это так легко сделать самостоятельно?

А как вы управляете конфигурацией в вашем приложении?
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 11

    +1
    «он позволяет отделять приложения от системы», однако «они могут быть заданы на системном уровне». Два экземпляра приложения с разной конфигурацией через переменные окружения? Конфликт имен переменных? Явный конфиг лучше неявных переменных.
      0
      Два экземпляра приложения с разной конфигурацией через переменные окружения?

      MY_ENV_VARIABLE=1 ./app
      MY_ENV_VARIABLE=2 ./app

      0
      А как вы управляете конфигурацией в вашем приложении?

      Через конфиг файлы, например json, в GO же есть прям из коробки отличная возможность описать структуру и замапить ее.

        0
        Да, существуют пакеты, предлагающие готовое решение для конфигурации вашего приложения

        А то что у вас используется "github.com/joho/godotenv" это несчитово?


        Мы используем например "github.com/crgimenes/goconfig"

          0
          Использую переменные окружения для указания пути к файлам конфигурации. Такой вариант хорош, кроме всего прочего, и кроссплатформенностью — не приходится зашивать в код пути к файлам, которые могут зависеть от ОС.
            +1

            Есть еще простенькая библиотека https://github.com/caarlos0/env. Она позволяет мапить переменные окружения на структуру в стиле encoding/json

              0
              сначала вносил конфигы через ясон и хмл, потом пытался через флаги, потом пытался как переменные окружения…

              по моему, если бы разрешалось вносить параметры извне в соответствии с некоторым локальным файлом — это бы упростило жизнь и избавило бы от многих дискуссий.
                0
                Если это утилита, github.com/urfave/cli удобная библиотека для разруливания конфигурации.
                  0
                  Переменные окружения сами собой не появятся. Вопрос: а как вы устанавливаете переменные окружения? Как вы отслеживаете и храните историю изминений переменных окружения?
                    0

                    статью по диагонали читали? автор же пишет что они используют github.com/joho/godotenv и .env файл
                    Историю изменений такого обычно не ведут (это же всякие пароли и т.п.)

                    0
                    В go-awesome списке целая пачка библиотек для конфигурации через ENV-переменные, но фаворит вроде github.com/caarlos0/env

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое