This episode manages Gin application configuration: environment variables with os.Getenv, .env files via godotenv, and Viper for centralized configuration. You'll also learn to structure a config struct and implement graceful shutdown with http.Server and signal.NotifyContext.

An application shouldn't load secrets or settings from code. This episode 10 dissects configuration & environment: how to read environment variables, load .env files for development, manage complex configuration with Viper, and — just as importantly — stop the server cleanly with graceful shutdown.
Why is this part crucial? Database connections, ports, and tokens should be able to differ between development, staging, and production without changing code. Graceful shutdown ensures in-flight requests finish before the process dies, so users don't hit errors mid-processing.
The most basic way to read configuration is os.Getenv. For variables that must exist, combine it with os.LookupEnv:
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
dbURL, ok := os.LookupEnv("DATABASE_URL")
if !ok {
log.Fatal("DATABASE_URL wajib diisi")
}os.Getenv("PORT") returns an empty string if the variable doesn't exist, so you need a fallback. os.LookupEnv returns two values so you know for sure whether a variable is defined — important for required variables like DATABASE_URL.
In development, setting dozens of variables through the terminal isn't practical. godotenv loads a .env file into the environment:
go get github.com/joho/godotenvPORT=8080
DATABASE_URL=postgres://arman:rahasia@localhost:5432/belajargin?sslmode=disable
REDIS_ADDR=localhost:6379
LOG_LEVEL=infofunc main() {
if err := godotenv.Load(); err != nil {
log.Println("tidak ada .env, pakai environment system")
}
cfg := config.Load()
// ...
}Call godotenv.Load() at the beginning of main before reading configuration. Never commit a .env file — use .env.example as a template. In production, variables are injected directly by the orchestrator or platform, not from a file.
For more complex configuration, viper combines config files, environment variables, and flags:
go get github.com/spf13/viperviper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
log.Printf("config file tidak terbaca: %v", err)
}viper.AutomaticEnv() lets every key be overridden by an environment variable following the same naming pattern. For example, the key database.url can be overridden by the variable DATABASE.URL or another pattern per your configuration. This establishes a priority: environment beats file.
Collect all settings into one struct so it can be injected:
type Config struct {
Port string
DatabaseURL string
MaxConns int
RedisAddr string
LogLevel string
}
func Load() Config {
return Config{
Port: os.Getenv("PORT"),
DatabaseURL: os.Getenv("DATABASE_URL"),
MaxConns: mustInt("MAX_CONNS", 10),
RedisAddr: os.Getenv("REDIS_ADDR"),
LogLevel: os.Getenv("LOG_LEVEL"),
}
}The function config.Load() Config becomes the single place that reads the environment. Handlers and services no longer need to call os.Getenv — they receive Config through constructors, following the injection pattern from episode 8.
With injection, handlers can be tested with any config:
NewServer(cfg config.Config) injects the config into every layer. Because all dependencies come from one place, switching environments only means changing environment values, not code.
engine.Run blocks the goroutine directly, making a clean stop difficult. Replace it with http.Server and handle signals:
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server gagal: %v", err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("shutdown gagal: %v", err)
}
log.Println("server berhenti dengan bersih")signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) cancels the context when the user presses Ctrl+C or an orchestrator sends SIGTERM. Then srv.Shutdown(shutdownCtx) waits for active requests to finish for up to 10 seconds before closing connections. This is the pattern we'll reuse in episode 21 for the production architecture.
Key takeaways:
os.Getenv with a fallback, os.LookupEnv for required variables.godotenv loads .env files for development; never commit them.viper combines config files and environment with environment taking priority.config.Config and inject it into handlers.http.Server + signal.NotifyContext for graceful shutdown.srv.Shutdown gives active requests time to finish before the process exits.In the next episode, episode 11, we'll dissect error handling & logging — capturing errors with c.Error, custom error types, an error handler middleware with unified JSON responses, and integrating slog into Gin middleware for structured logs.