Practical Guide to Environment Variables in Go

Hello, Habr! I present to you the translation of the article A no-nonsense guide to environment variables in Go by Enda Phelan.

Environment variables are the best way to store application configurations, as they can be set at the system level. This is one of the principles of the Twelve-Factor App methodology , it allows you to separate applications from the system in which they are running (the configuration can vary significantly between deployments, the code should not be different).

Using environment variables


All that is needed to interact with environment variables is in the os standard library . This is how to get the value of the PATH environment variable:

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)
   }
}

And so - set the value of the variable:

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)
}

Loading environment variables from a .env file


On a development machine, where many projects are launched right away, storing parameters in variable environments is not always convenient; it would be more logical to split them between projects using env files. This can be done, for example, by godotenv - is ported to Go Ruby-library dotenv . It allows you to set the environment variables necessary for the application from the .env file.

To install the package, run:

go get github.com/joho/godotenv

Add the settings to the .env file in the root of the project:


GITHUB_USERNAME=craicoverflow
GITHUB_API_KEY=TCtQrZizM1xeo1v92lsVfLOHDsF7TfT5lMvwSno

Now you can use these values ​​in the application:

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)
    }
}

It is important to remember that if the value of the environment variable is set at the system level, Go will use this value instead of the one specified in the env file.

Wrap environment variables in the configuration module


It’s nice, of course, to have access to environment variables directly, as shown above, but maintaining such a solution seems rather problematic. The variable name is a string, and if it changes, then imagine a headache that will result in the process of updating variable references throughout the application.

To solve this problem, we will create a configuration module for working with environment variables in a more centralized and supported way.

Here is a simple config module that returns configuration parameters in the Config structure (we also set the default values ​​of the parameters in case there is no environment variable in the system):

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
}

Next, add the types to the Config structure , since the existing solution only supports string types, which is not very reasonable for large applications.

Create handlers for the bool, slice and integer types:

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
}

Add new environment variables to our env file:


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

Now you can use them anywhere in the application:

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)
    }
}

Done!


Yes, there are packages that offer a turnkey solution for configuring your application, but how much are they needed if it is so easy to do it yourself?

And how do you manage the configuration in your application?

Also popular now: