This episode teaches how to manage application configuration: environment variables with os.Getenv, loading .env with godotenv, Viper for structured configuration, and config structs injected into handlers. You will also build graceful shutdown with signal.NotifyContext and http.Server.

Hard-coded values like ports, database URLs, and secrets are the enemy of production applications. Episode 10 teaches correct configuration: separating values from code, loading them from the environment, and injecting them across the application.
You'll also build one of the most commonly forgotten things: graceful shutdown. A server that receives a termination signal should stop politely — finishing in-flight requests — not be killed abruptly.
The most basic way:
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
dbURL := os.Getenv("DATABASE_URL")os.Getenv("PORT") returns an empty string if the variable isn't set, so you need a default value. This pattern is simple and portable — every modern platform supports environment variables.
export PORT=9090
export DATABASE_URL=postgres://postgres:secret@localhost:5432/chi_service
go run ./cmd/serverexport PORT=9090 sets the variable for the running process. In production, environment variables are injected by the platform — Docker, Kubernetes, or services like Vercel.
For local development, load a .env file:
go get github.com/joho/godotenvfunc main() {
godotenv.Load()
port := os.Getenv("PORT")
...
}godotenv.Load() reads the .env file in the working directory and populates the environment. Never commit a .env file — keep it as .env.example only.
Viper adds a layer: defaults, config files, and automatic env mapping all at once:
viper.SetDefault("PORT", "8080")
viper.SetDefault("DB_MAX_CONNS", 20)
viper.AutomaticEnv()
port := viper.GetString("PORT")
maxConns := viper.GetInt("DB_MAX_CONNS")viper.AutomaticEnv() automatically maps environment variables to keys of the same name. viper.GetString("PORT") reads the value with the default already defined.
Tidy all values into a single struct:
type Config struct {
Port string
DBURL string
MaxConns int
JWTSecret string
}
func loadConfig() Config {
return Config{
Port: viper.GetString("PORT"),
DBURL: viper.GetString("DATABASE_URL"),
MaxConns: viper.GetInt("DB_MAX_CONNS"),
JWTSecret: viper.GetString("JWT_SECRET"),
}
}loadConfig() gathers all environment reads in one place — handlers never call os.Getenv again.
cfg := loadConfig()
pool, _ := pgxpool.New(ctx, cfg.DBURL)
userRepo := repository.NewUserRepo(pool)
userHandler := handler.NewUserHandler(userRepo)
deps := Deps{Config: cfg, Users: userHandler}
http.ListenAndServe(":"+cfg.Port, Routes(deps))deps := Deps{Config: cfg, Users: userHandler} builds the dependency graph in main, then passes it to Routes from episode 8. Configuration flows downward, never accessed randomly inside handlers.
signal.NotifyContext turns OS signals into context cancellation:
func main() {
ctx, stop := signal.NotifyContext(context.Background(),
os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":" + cfg.Port, Handler: Routes(deps)}
go func() {
if err := srv.ListenAndServe(); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(),
10*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)
}signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) cancels the context when Ctrl+C or SIGTERM arrives. <-ctx.Done() waits for the signal, then srv.Shutdown(shutdownCtx) gives in-flight requests 10 seconds to finish.
Without graceful shutdown, a deployment restart cuts off in-flight requests — users get mysterious errors. With this pattern, the process waits for requests to finish, closes database connections, then exits with a zero code.
go run ./cmd/server &
kill -TERM $!kill -TERM $! sends SIGTERM to the server process — you should see a clean shutdown log, not a panic.
Key takeaways:
os.Getenv..env; Viper for structured configuration.Config struct and inject into handlers.viper.AutomaticEnv maps environment to keys automatically.signal.NotifyContext catches termination signals.srv.Shutdown gives requests time to finish before the process exits.In the next episode 11 we discuss failures and their traces: error handling and logging — custom error types, handlers that return errors to JSON, recovery middleware, and structured logging with slog.