Learn Echo - Configuration & Environment
Series/Learn Echo/Episode 10
Episode 10 of 23

Learn Echo - Configuration & Environment

This episode teaches proper configuration management: environment variables with os.Getenv, godotenv for development, Viper for complex needs, a centralized config struct, a custom HTTP server with ReadTimeout and WriteTimeout, and graceful shutdown.

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

Introduction

Database credentials, JWT secrets, and server ports must not be hardcoded. Proper configuration lets your application move from development to production without changing a single line of code. This episode teaches the 12-factor app foundations at a practical level.

Episode 10 covers environment variables with os.Getenv, godotenv for development, Viper for complex configuration, a centralized config struct, a custom HTTP server with ReadTimeout and WriteTimeout, and graceful shutdown.

Environment Variables and the Config Struct

Reading with os.Getenv

os.Getenv is the most basic approach, and it's actually enough for most applications:

Reading environment variables
port := os.Getenv("PORT")
if port == "" {
	port = "8080"
}

Always provide a default. os.Getenv returns an empty string when the variable doesn't exist, so the if port == "" pattern keeps the application running even when configuration is incomplete.

Centralized Config Struct

Don't scatter os.Getenv calls across your code. Collect all configuration in a single struct populated once at startup:

Centralized config struct
type Config struct {
	Port        string
	DatabaseURL string
	JWTSecret   string
	Environment string
}
 
func Load() Config {
	return Config{
		Port:        getEnv("PORT", "8080"),
		DatabaseURL: getEnv("DATABASE_URL", ""),
		JWTSecret:   getEnv("JWT_SECRET", ""),
		Environment: getEnv("ENVIRONMENT", "development"),
	}
}

This Config struct is then passed to every component through dependency injection — the pattern you built in episode 8.

godotenv and Viper

godotenv for .env Files

In development, typing variables in the terminal every time is very inconvenient. godotenv loads a .env file into the environment when the process starts:

Install godotenv
go get github.com/joho/godotenv
Loading a .env file
if err := godotenv.Load(); err != nil {
	slog.Warn("file .env tidak ditemukan, pakai environment")
}
cfg := Load()

Never commit a .env file to the repository — make sure it's in .gitignore. For secrets, always use the system environment or a secret manager.

Viper for Complex Configuration

When configuration grows — YAML files, keys from many sources, and automatic overrides — Viper becomes the choice:

Install Viper
go get github.com/spf13/viper
Viper reading files and environment
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
	slog.Warn("konfigurasi file tidak dibaca", "err", err)
}
port := viper.GetString("server.port")

With viper.AutomaticEnv(), environment variables automatically override the values from the file. This is a common combination in production.

Custom HTTP Server

ReadTimeout and WriteTimeout

e.Start uses default settings that are less safe for production. Build your own http.Server so the timeouts are controlled:

Custom HTTP server with timeouts
s := &http.Server{
	Addr:         ":" + cfg.Port,
	ReadTimeout:  30 * time.Second,
	WriteTimeout: 30 * time.Second,
	IdleTimeout:  120 * time.Second,
}
 
e.HideBanner = true
e.HidePort = true
if err := e.StartServer(s); err != nil {
	e.Logger.Fatal(err)
}

ReadTimeout protects against slow connections, WriteTimeout prevents hanging handlers, and IdleTimeout closes inactive connections. e.StartServer(s) runs this custom server together with the Echo router.

Graceful Shutdown

Stopping the Server Cleanly

A production server must stop gracefully: finish in-flight requests before exiting, rather than cutting connections immediately. Use signal.NotifyContext and e.Shutdown:

Full graceful shutdown
ctx, stop := signal.NotifyContext(context.Background(),
	os.Interrupt, syscall.SIGTERM)
defer stop()
 
go func() {
	if err := e.StartServer(s); err != nil &&
		err != http.ErrServerClosed {
		e.Logger.Fatal(err)
	}
}()
 
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(shutdownCtx); err != nil {
	e.Logger.Fatal(err)
}
slog.Info("server berhenti dengan rapi")

When Ctrl+C or SIGTERM is received, e.Shutdown gives in-flight requests 10 seconds to finish before the process exits. This is a pattern required in every production application.

Closing

Episode 10 matures your application configuration: environment variables are read once through a Config struct, godotenv makes development easier, Viper handles complex needs, a custom HTTP server with timeouts protects resources, and graceful shutdown stops the server without cutting off active requests.

Key takeaways:

  • Configuration is read from the environment and collected in a Config struct.
  • os.Getenv needs a default; never hardcode secrets.
  • godotenv loads .env for development; don't commit the file.
  • Viper combines config files with environment overrides.
  • A custom http.Server sets ReadTimeout and WriteTimeout.
  • signal.NotifyContext and e.Shutdown produce graceful shutdown.
  • Shutdown pattern: run the server in a goroutine, wait for a signal, then shut down.

In episode 11 next, we'll discuss error handling & loggingHTTPError and a custom error handler, the RFC 9457 Problem Details format for consistent API responses, structured logging with slog via RequestLogger, and log aggregator integration.

Learn Echo - Configuration & Environment | Learn Echo