Godotenv vs Viper: Choosing the Right Config Solution for Your Go Project
Managing configuration properly is one of the most underrated parts of building a solid Go application. Whether you're deploying to development, staging, or production, your app needs a clean way to read database credentials, API keys, and other environment-specific settings — without hardcoding them into your source code.
In the Go ecosystem, two libraries come up again and again when developers talk about configuration management: Godotenv and Viper. They solve a similar problem but at very different levels of complexity. This article breaks down what each one does, how to use them, and — most importantly — when you should reach for one over the other.
Why environment configuration matters
This follows the twelve-factor app methodology: configuration that varies between environments (database hosts, credentials, feature flags) should live outside your codebase, not be baked into it. This gives you a few concrete benefits:
- You can promote the exact same build from staging to production without recompiling.
- Secrets never end up committed to version control.
- Switching environments becomes a matter of changing a file or a set of variables, not editing code.
Both Godotenv and Viper help you achieve this — they just differ in scope.
Godotenv: simple, focused, and easy to reason about
Godotenv is a Go port of Ruby's popular dotenv library. Its entire job is to load key-value pairs from a .env file into your process's environment variables. Nothing more, nothing less.
Installation
go get github.com/joho/godotenv
Example .env file
DB_HOST=localhost
DB_PORT=5432
Usage
package main
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
)
func main() {
if err := godotenv.Load(); err != nil {
log.Fatal("Error loading .env file")
}
dbHost := os.Getenv("DB_HOST")
dbPort := os.Getenv("DB_PORT")
fmt.Println("dbHost:", dbHost)
fmt.Println("dbPort:", dbPort)
}
Run it:
$ go run main.go
dbHost: localhost
dbPort: 5432
That's it. godotenv.Load() reads .env from the current directory and populates os.Environ(), so the rest of your code can keep using the standard os.Getenv you're already familiar with.
Best for: small services, CLI tools, and projects that only need a flat list of key-value settings without validation, nested structures, or multiple file formats.
Viper: a complete configuration toolkit
Viper — maintained by spf13, the creator of Cobra and Hugo — is built for applications with more demanding configuration needs. It's used in production by projects like Hugo, Docker Notary, and DigitalOcean's doctl.
Unlike Godotenv, Viper isn't limited to .env files. It supports:
- Setting defaults
- Reading from JSON, TOML, YAML, HCL,
.env, and Java properties files - Live watching and reloading config files
- Reading from environment variables
- Reading from remote config stores like etcd or Consul
- Reading from command-line flags
- Layering all of the above with a defined precedence order
Installation
go get github.com/spf13/viper
Example config file (env/app.env)
DB_HOST=localhost
DB_PORT=5432
Usage
package main
import (
"fmt"
"github.com/spf13/viper"
)
func main() {
viper.AddConfigPath("env")
viper.SetConfigType("env")
viper.SetConfigName("app")
if err := viper.ReadInConfig(); err != nil {
panic(fmt.Errorf("fatal error config file: %w", err))
}
fmt.Println("DB_HOST from config:", viper.GetString("DB_HOST"))
fmt.Println("DB_PORT from config:", viper.GetString("DB_PORT"))
}
Run it:
$ go run main.go
DB_HOST from config: localhost
DB_PORT from config: 5432
A quick breakdown of the setup calls:
| Call | Purpose |
|---|---|
viper.AddConfigPath("env") | Tells Viper which directory to search for the config file |
viper.SetConfigType("env") | Declares the file format (env, yaml, json, etc.) |
viper.SetConfigName("app") | Sets the file's base name, so it looks for app.env |
viper.ReadInConfig() | Actually locates and parses the file |
Once loaded, viper.GetString(), viper.GetInt(), and similar getters let you pull typed values out of the config by key.
Best for: larger applications that need multiple config formats, environment layering, remote config, or live reload — especially services deployed across several environments with different operational needs.
Godotenv vs Viper at a glance
| Godotenv | Viper | |
|---|---|---|
| Primary purpose | Load .env into environment variables | Full configuration management |
| Supported formats | .env only | JSON, TOML, YAML, HCL, .env, Java properties |
| Live reload | No | Yes |
| Remote config (etcd/Consul) | No | Yes |
| Command-line flag binding | No | Yes (via pflag) |
| Setting defaults | No | Yes |
| Learning curve | Minimal | Moderate |
| Dependency footprint | Very small | Larger |
| Typical use case | Small services, scripts, CLIs | Medium-to-large apps, multi-environment deployments |
Which one should you choose?
There's no universally "correct" answer — it depends on the shape of your project:
- Reach for Godotenv if you just need to keep secrets out of your codebase and load a handful of environment variables from a single
.envfile. It's fast to set up and has almost no learning curve. - Reach for Viper if your application needs to support multiple config file formats, merge configuration from several sources (files, environment variables, flags, remote stores), or hot-reload settings without restarting the app.
It's also worth noting these aren't mutually exclusive — some teams use Godotenv purely for local development convenience (loading a .env file so os.Getenv works without extra shell setup) while using Viper's broader feature set for how the application actually consumes configuration at runtime.
Conclusion
Both godotenv and viper solve the same underlying problem — keeping configuration out of your code — but at very different scales. Godotenv is the right call when you want something simple that just works. Viper is the right call when your configuration needs start to outgrow a single flat file.
Start with what your project actually needs today. It's easy to migrate from Godotenv to Viper later if your configuration requirements grow — but adding Viper's full feature set to a project that only ever needed three environment variables is unnecessary overhead.
Have you used Godotenv or Viper in a real project? What tipped the decision for you? Let me know in the comments.
