DEV Community

Enda
Enda

Posted on • Edited on

68 13

A no-nonsense guide to environment variables in Go

Environment variables are the best way to set configuration values for your software application as they can be defined at system-level, independently of the software. This is one of the principles of the Twelve-Factor App methodology and enables applications to be built with portability.

Using environment variables

All you need to interact with environment variables is the standard os package. Here is
an example of how you can access the system 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)
}
}
view raw main.go hosted with ❤ by GitHub

It’s equally easy to set environment variables:

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)
}
view raw main.go hosted with ❤ by GitHub

Loading environment variables from a .env file

It is not always practical to set environment variables on development machines
where multiple projects are running.

godotenv is a Go port of the Ruby
dotenv library. This allows you to define
your application’s environment variables in a .env file.

To install the package run:



$ go get github.com/joho/godotenv


Enter fullscreen mode Exit fullscreen mode

Add your configuration values to a .env file at the root of your project:

GITHUB_USERNAME=craicoverflow
GITHUB_API_KEY=TCtQrZizM1xeo1v92lsVfLOHDsF7TfT5lMvwSno
Enter fullscreen mode Exit fullscreen mode

Then you can use these values in your 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)
}
}
view raw main.go hosted with ❤ by GitHub

It’s important to note that if an environment variable is already defined in the
system, then Go will prefer use that instead of the value in .env.


Wrapping environment variables in a configuration package

It's all well and good accessing environment variables directly like this, but having to maintain that doesn't seem fun, does it? Every value is a string - and imagine having to update every reference when an environment key is modified!

To deal with this, let’s create a configuration package to access environment variables in a much
more centralized and maintainable way.

Here is a simple config package which will return configuration values in a
Config struct. We have the option to define a default value, so when an
environment variable does not exist this will be used instead.

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
}
view raw config.go hosted with ❤ by GitHub

Next, we should add different types to the Config struct. The current implementation can only handle string types, which isn’t very practical for larger applications.

Let's add functions to handle 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
}
view raw config.go hosted with ❤ by GitHub

Update your .env file with these environment variables.

GITHUB_USERNAME=craicoverflow
GITHUB_API_KEY=TCtQrZizM1xeo1v92lsVfLOHDsF7TfT5lMvwSno
MAX_USERS=10
USER_ROLES=admin,super_admin,guest
DEBUG_MODE=false
Enter fullscreen mode Exit fullscreen mode

And now you can access these values from the rest of your 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)
}
}
view raw main.go hosted with ❤ by GitHub

And there you have it

There are several libraries out there that claim to offer a configuration
“solution” for your Go application. But is it really a solution when it’s just
as easy to make one yourself?

How do you manage configuration in your Go applications?

Did you enjoy this post? Be sure to check out more like it at https://endaphelan.me.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (4)

Collapse
 
clairmont32 profile image
Matthew Clairmont

Thanks for the read. Hopefully that's a dead API key now!

Collapse
 
craicoverflow profile image
Enda

Hehe! It's a made up one!

Collapse
 
verdaan profile image
Vardaan Aashish

Fantastic, thank you for the simple solution!

Collapse
 
tsherisherpa profile image
Tsheri Sherpa

great article

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay