Learning Golang - Application Configuration, Environment Variables, and Flags
Episode 7 of 19

Learning Golang - Application Configuration, Environment Variables, and Flags

This episode covers managing Go application configuration: reading environment variables with os.Getenv, .env files, CLI flags with the flag package, the modern cobra framework, and configuration best practices for local, staging, and production.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Hardcoding configuration values — ports, database credentials, API URLs — is one of the biggest sins in application development. Values that differ between local, staging, and production must be changeable without touching code.

Episode 7 teaches you how to manage configuration idiomatically in Go: reading environment variables, loading .env files during development, handling CLI flags with the built-in flag package, building larger CLIs with cobra, and designing a per-environment configuration strategy. By the end of the episode, your application will be ready to move between environments without changing a single line.

Environment Variables

Reading with os.Getenv

The simplest way to read configuration is environment variables via os.Getenv. A value that isn't set returns an empty string, so always provide a safe default.

Reading an environment variable
package main
 
import (
	"fmt"
	"os"
)
 
func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	fmt.Println("server berjalan di port", port)
}

To distinguish an unset variable from one set to empty, use os.LookupEnv, which returns two values: the value and its existence status.

Storing Secrets in the Environment

Credentials and secrets must never enter the repository. During deployment, environment variables are injected by the platform — Kubernetes through secrets, GitHub Actions through secrets, or the container runtime. Make sure .env and .env.local are in .gitignore.

Setting a variable in the terminal
export DATABASE_URL="postgres://user:pass@localhost:5432/app"
go run main.go

.env Files During Development

Loading a .env File

In a local environment, typing a dozen exports every time you open a terminal is very tedious. The common solution: store values in a .env file and load it during development. The popular package for this is github.com/joho/godotenv.

Example .env
PORT=8080
DATABASE_URL=postgres://user:pass@localhost:5432/app
LOG_LEVEL=debug
Loading .env
package main
 
import (
	"log"
	"os"
 
	"github.com/joho/godotenv"
)
 
func main() {
	if err := godotenv.Load(); err != nil {
		log.Println("file .env tidak ditemukan, pakai environment")
	}
	log.Println("port:", os.Getenv("PORT"))
}

godotenv.Load() loads the .env file into the process environment. Don't commit this file to the repository; provide a template in .env.example without secret values.

Configuration Priority

Set a clear priority order: values already set in the environment win, then the .env file, then defaults in code. With this order, production environments that use environment variables won't be overwritten by wrong defaults.

CLI Flags with the flag Package

Go's built-in flag package is enough for simple CLIs. flag.Int, flag.String, and similar functions register flags, and flag.Parse() processes the arguments.

CLI with the flag package
package main
 
import (
	"flag"
	"fmt"
)
 
func main() {
	port := flag.Int("port", 8080, "port server")
	debug := flag.Bool("debug", false, "mode debug")
	flag.Parse()
 
	fmt.Println("port:", *port, "debug:", *debug)
}

Run it with go run main.go -port 9090 -debug. Note that flags are stored as pointers, so you read them with the dereference *port.

flag and Environment Working Together

This combination gives maximum flexibility — pipeline scripts can pass flags, while containers can just use the environment.

Flag default from the environment
port := flag.Int("port", 8080, "port server")
if envPort := os.Getenv("PORT"); envPort != "" {
	fmt.Sscanf(envPort, "%d", port)
}

Cobra for Modern CLIs

For complex CLIs with many subcommands — like kubectl get pods or docker compose up — the cobra package is the industry standard. Cobra handles subcommands, flags, help, and autocomplete in a structured way.

Add cobra
go get github.com/spf13/cobra
Root command with cobra
package main
 
import (
	"fmt"
	"os"
 
	"github.com/spf13/cobra"
)
 
func main() {
	var nama string
	root := &cobra.Command{
		Use:   "salam",
		Short: "CLI sederhana dengan cobra",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Halo,", nama)
		},
	}
	root.Flags().StringVar(&nama, "nama", "dunia", "nama yang disapa")
 
	if err := root.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

Cobra organizes commands, subcommands, and flags in a single tree, with automatically generated help and completion.

Per-Environment Configuration Best Practices

Several practices proven in production:

  • Don't commit secrets: store them in a secret manager or platform tooling.
  • Use separate config files for non-secret per-environment values, for example config.local.yaml and config.prod.yaml.
  • Validate configuration at startup: the application fails fast with a clear message, rather than waiting for random runtime errors.
  • Limit the number of sources: environment, config files, and flags — don't add other sources without a strong reason.
  • Version your configuration: in distributed systems, record the configuration version so behavior changes between deployments are easy to diagnose.

With these patterns, teams can move from local to staging to production just by changing environment variables and config files.

Closing

Episode 7 equipped you with a comprehensive configuration strategy: reading environment variables with os.Getenv and os.LookupEnv, loading .env files with godotenv for development, handling CLI flags with the flag package, building modern CLIs with cobra, and per-environment configuration best practices.

Key takeaways:

  • Don't hardcode configuration; read it from the environment.
  • os.LookupEnv distinguishes an empty variable from an unset one.
  • .env is only for development and should never be committed.
  • Priority order: environment, config file, then defaults.
  • The flag package is enough for simple CLIs; cobra for complex ones.
  • Validate configuration early at startup with clear messages.

In the next episode we will discuss databases, persistence, and data access — database connections with database/sql and popular drivers, lightweight ORMs like sqlc, ent, or gorm, plus data access patterns, parameterized queries, and transaction management.

Learning Golang - Application Configuration, Environment Variables, and Flags | Learning Golang