Aplikasi yang sama hampir selalu berjalan di lebih dari satu lingkungan: komputer lokal saat develop, staging sebelum rilis, dan server produksi. Perbedaannya bukan di kode, melainkan di nilai seperti port, alamat database, dan token API. Configuration management adalah praktik memuat nilai-nilai itu dari luar kode, memberi default yang aman, lalu memvalidasinya sebelum aplikasi mulai menerima permintaan. Pada chapter ini kita membangun loader konfigurasi dengan standard library: struct Config, pembacaan environment variable, validasi fail-fast, dan pencetakan yang menyembunyikan secret.

Struct Config dan Fungsi Muat #

Konfigurasi yang baik bertype, bukan sekadar map string. Kita mendefinisikan struct dengan field bertipe tepat, lalu satu fungsi Muat yang bertanggung jawab mengisi struct tersebut:

go
type Config struct {
	AppEnv      string // dev, staging, prod
	Port        int
	DatabaseDSN string
	APIToken    string // rahasia: jangan pernah dicetak mentah
}

func Muat() (Config, error) { /* lihat demo */ }

Struct ini berperan sebagai kontrak tunggal: seluruh aplikasi hanya membaca field dari satu struct, bukan memanggil os.Getenv tersebar di mana-mana. Bila suatu saat nilai harus berasal dari file YAML atau layanan konfigurasi, hanya fungsi Muat yang berubah; tiga puluh pemanggil field tidak tersentuh.

Sumber Nilai: Environment, Default, dan Flag #

Praktik yang dipegang komunitas (dikenal dari Twelve-Factor App) menempatkan environment variable sebagai sumber utama. Nilai dari environment tidak masuk ke repository, berubah per lingkungan tanpa kompilasi ulang, dan cocok dengan container serta CI. Pola pembacaannya selalu tiga lapis:

Flag dari package flag berguna untuk program CLI yang ingin bisa dioverride per pemanggilan, misalnya -port 9090; untuk server, environment variable saja biasanya cukup. Untuk kenyamanan lokal, tim sering memakai file .env berisi APP_ENV=dev dan semacamnya; file itu dibaca library pihak ketiga seperti godotenv yang menyalin isinya ke environment sebelum Muat dipanggil, sehingga kode loader Anda tidak berubah sama sekali. File .env sendiri wajib masuk .gitignore agar secret tidak terbawa ke repository.

Demo: Loader dengan Nilai Tersembunyi #

Berkas config-demo.go memuat konfigurasi, memvalidasinya, dan mencetak versi yang sudah disensor. Environment diatur lewat os.Setenv untuk mensimulasikan variable yang biasanya diisi oleh sistem operasi atau pipeline CI:

go
package main

import (
	"fmt"
	"os"
	"strconv"
	"strings"
)

type Config struct {
	AppEnv      string // dev, staging, prod
	Port        int
	DatabaseDSN string
	APIToken    string // rahasia: jangan pernah dicetak mentah
}

func Muat() (Config, error) {
	cfg := Config{
		AppEnv:      ambil("APP_ENV", "dev"),
		Port:        8080,
		DatabaseDSN: ambil("DATABASE_DSN", "postgres://app@localhost/app_dev"),
		APIToken:    os.Getenv("API_TOKEN"),
	}
	if v := os.Getenv("PORT"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil {
			return Config{}, fmt.Errorf("PORT harus angka, dapat %q", v)
		}
		cfg.Port = n
	}

	// validasi fail-fast: salah konfigurasi sebaiknya gagal saat start.
	switch cfg.AppEnv {
	case "dev", "staging", "prod":
	default:
		return Config{}, fmt.Errorf("APP_ENV tidak valid: %q (harus dev/staging/prod)", cfg.AppEnv)
	}
	if cfg.AppEnv == "prod" && cfg.APIToken == "" {
		return Config{}, fmt.Errorf("API_TOKEN wajib diisi di lingkungan prod")
	}
	return cfg, nil
}

func ambil(kunci, cadangan string) string {
	if v := os.Getenv(kunci); v != "" {
		return v
	}
	return cadangan
}

func (c Config) Redacted() string {
	return fmt.Sprintf("AppEnv=%s Port=%d DatabaseDSN=%s APIToken=%s",
		c.AppEnv, c.Port, c.DatabaseDSN, tutup(c.APIToken))
}

func tutup(rahasia string) string {
	if rahasia == "" {
		return "(kosong)"
	}
	return strings.Repeat("*", 8)
}

func main() {
	// mensimulasikan environment yang diatur oleh sistem operasi/CI.
	os.Setenv("APP_ENV", "prod")
	os.Setenv("PORT", "9090")
	os.Setenv("API_TOKEN", "YOUR_API_TOKEN")

	cfg, err := Muat()
	if err != nil {
		fmt.Println("gagal memuat konfigurasi:", err)
		os.Exit(1)
	}
	fmt.Println("konfigurasi:", cfg.Redacted())

	// fail-fast: token hilang di prod harus tertangkap saat start.
	os.Unsetenv("API_TOKEN")
	_, err = Muat()
	fmt.Println("muat ulang tanpa token:", err)
}

Jalankan program dengan perintah berikut dari direktori chapter:

bash
go run config-demo.go

Output yang Dihasilkan:

text
konfigurasi: AppEnv=prod Port=9090 DatabaseDSN=postgres://app@localhost/app_dev APIToken=********
muat ulang tanpa token: API_TOKEN wajib diisi di lingkungan prod

Mari kita telaah bagian-bagian penting program di atas:

  1. Baris 17-23 (func Muat() ... cfg := Config{): nilai default diberikan di satu tempat: dev untuk APP_ENV, port 8080, DSN lokal untuk DATABASE_DSN. Developer baru bisa langsung go run tanpa menyetel apa pun.
  2. Baris 24-30 (if v := os.Getenv("PORT") ...): PORT harus dikonversi ke int. Konversi gagal ditangani di sini juga — "sembilan ribu" harus ditolak saat start, bukan meledak sebagai panic di tengah serving.
  3. Baris 32-40 (switch cfg.AppEnv ...): inti fail-fast. Nilai AppEnv yang salah dan token kosong di prod sama-sama menghentikan aplikasi sebelum layanan mendengarkan satu port pun.
  4. Baris 51-54 (func (c Config) Redacted ...): satu-satunya cara yang diizinkan untuk mencetak konfigurasi. Token dicetak sebagai delapan tanda bintang, sehingga log startup aman dibagikan.
  5. Baris 63-67 (os.Setenv ...): simulasi environment produksi. Di aplikasi nyata baris-baris ini tidak ada; pipeline CI atau orchestrator yang menyetel variable tersebut.
  6. Baris 76-79 (os.Unsetenv ...): skenario kesalahan nyata diuji secara otomatis. Output kedua memperlihatkan pesan error validasi yang akan dilihat operator ketika secret tertinggal.

Per Lingkungan dan Secret yang Terpisah #

Perbedaan antar lingkungan sebaiknya sekecil mungkin dan dinyatakan lewat variable yang sama, bukan file konfigurasi berbeda yang saling menyimpang. Nilai dev, staging, dan prod boleh berubah; struktur konfigurasinya tidak. Secret seperti APIToken diperlakukan lebih ketat lagi: diletakkan di sistem manajemen secret terpisah, tidak pernah di file yang masuk repository, dan tidak pernah dicetak mentah — praktik yang sudah kita bahas pada chapter Secure Configuration .

Konfigurasi memberi aplikasi angka dan alamat, tetapi belum bicara tentang masalah bisnis yang diselesaikan. Chapter berikutnya masuk ke jantung arsitektur: domain modeling dengan entity, value object, aggregate, dan invariant.

Referensi #

  • Package os — Getenv, Setenv, dan Unsetenv untuk environment variable.
  • Package flag — flag command line sebagai lapisan override konfigurasi.
  • Package fmt — format output konfigurasi yang rapi.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

config-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"os"
	"strconv"
	"strings"
)

type Config struct {
	AppEnv      string // dev, staging, prod
	Port        int
	DatabaseDSN string
	APIToken    string // rahasia: jangan pernah dicetak mentah
}

func Muat() (Config, error) {
	cfg := Config{
		AppEnv:      ambil("APP_ENV", "dev"),
		Port:        8080,
		DatabaseDSN: ambil("DATABASE_DSN", "postgres://app@localhost/app_dev"),
		APIToken:    os.Getenv("API_TOKEN"),
	}
	if v := os.Getenv("PORT"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil {
			return Config{}, fmt.Errorf("PORT harus angka, dapat %q", v)
		}
		cfg.Port = n
	}

	// validasi fail-fast: salah konfigurasi sebaiknya gagal saat start.
	switch cfg.AppEnv {
	case "dev", "staging", "prod":
	default:
		return Config{}, fmt.Errorf("APP_ENV tidak valid: %q (harus dev/staging/prod)", cfg.AppEnv)
	}
	if cfg.AppEnv == "prod" && cfg.APIToken == "" {
		return Config{}, fmt.Errorf("API_TOKEN wajib diisi di lingkungan prod")
	}
	return cfg, nil
}

func ambil(kunci, cadangan string) string {
	if v := os.Getenv(kunci); v != "" {
		return v
	}
	return cadangan
}

func (c Config) Redacted() string {
	return fmt.Sprintf("AppEnv=%s Port=%d DatabaseDSN=%s APIToken=%s",
		c.AppEnv, c.Port, c.DatabaseDSN, tutup(c.APIToken))
}

func tutup(rahasia string) string {
	if rahasia == "" {
		return "(kosong)"
	}
	return strings.Repeat("*", 8)
}

func main() {
	// mensimulasikan environment yang diatur oleh sistem operasi/CI.
	os.Setenv("APP_ENV", "prod")
	os.Setenv("PORT", "9090")
	os.Setenv("API_TOKEN", "YOUR_API_TOKEN")

	cfg, err := Muat()
	if err != nil {
		fmt.Println("gagal memuat konfigurasi:", err)
		os.Exit(1)
	}
	fmt.Println("konfigurasi:", cfg.Redacted())

	// fail-fast: token hilang di prod harus tertangkap saat start.
	os.Unsetenv("API_TOKEN")
	_, err = Muat()
	fmt.Println("muat ulang tanpa token:", err)
}