Memahami Optional Params di Golang
***

Daftar Isi

Memahami Optional Params di Golang dengan Contoh Utility GeneratePassword

Halo BeeGangs, kali ini kita akan bahas salah satu pola yang sering banget dipakai di Golang untuk membuat fungsi lebih fleksibel tanpa bikin parameter-nya panjang dan berantakan: Optional Parameters.

Seperti yang kita tahu, Go tidak mendukung optional params seperti bahasa lain (misalnya Python atau TypeScript). Jadi, kalau mau bikin fungsi dengan banyak konfigurasi, kita punya beberapa pilihan:

  • Pakai struct sebagai input
  • Pakai variadic functions
  • Pakai Functional Options Pattern (favorit banyak orang)

Di artikel ini, aku bakal pakai contoh sederhana: sebuah fungsi GeneratePassword() yang bisa dikonfigurasi memakai parameter opsional: algo, salt, dan secretKey.

***

Kenapa Perlu Optional Params?

Gampangnya begini: ketika sebuah fungsi berkembang, sering ada kebutuhan untuk menambah konfigurasi baru. Kalau semua opsi dijadikan parameter, fungsi akan seperti ini:

1func GeneratePassword(password, algo, salt, secretKey string) string
2

Di banyak bahasa lain, kamu bisa bikin parameter opsional atau default value, contoh (pseudo):

1function generatePassword(password: string, algo = "sha256", salt?: string, secretKey?: string) {}
2

Di Go… nggak ada fitur ini. Go murni: kalau mau parameter, ya tulis semua:

1func GeneratePassword(password, algo, salt, secretKey string) string
2

Problem:

  • Semua param wajib diisi, padahal kadang cuma butuh password aja
  • Urutan param gampang ketuker (apalagi kalau string semua)
  • Kode pemanggilan jadi susah dibaca:

Padahal lebih enak kalau bisa nulis yang “bermakna”:

1GeneratePassword("bee123", WithSecretKey("supersecret"))
2

Nah, di sinilah Functional Options Pattern masuk.

Beberapa Pendekatan (dan Kekurangannya)

a. Versi “Kebelet” – Semua Jadi Param

1func GeneratePassword(password, algo, salt, secretKey string) string
2
  • ✅ Simpel
  • ❌ Wajib isi semua param
  • ❌ Sulit dibaca dan rawan tertukar
  • ❌ Kalau mau nambah param baru → function signature berubah → semua call site harus di-update

b. Pakai Struct Langsung

1type PasswordConfig struct {
2    Algo      string
3    Salt      string
4    SecretKey string
5}
6
7func GeneratePassword(password string, cfg PasswordConfig) string
8

Pemanggilan:

1GeneratePassword("bee123", PasswordConfig{
2    Algo:      "sha256",
3    Salt:      "noobee",
4    SecretKey: "supersecret",
5})
6
  • ✅ Lebih jelas (pakai field name)
  • ✅ Bisa punya default di dalam fungsi
  • ❌ Masih agak verbose di pemanggilan
  • ❌ Tiap kali panggil, harus bikin struct (walau bisa di-simplify)

Ini oke buat banyak kasus, tapi masih bisa kita buat lebih enak lagi.

c. Functional Options Pattern (Pendekatan yang Kita Pakai)

Idenya:

  • Kita punya config struct (PasswordConfig)
  • Kita punya tipe Option: function yang menerima pointer ke config dan memodifikasinya
  • GeneratePassword terima opts ...Option
  • Di dalam GeneratePassword, kita:
1out2 := GeneratePassword(
2    "bee123",
3    WithSalt("noobee"),
4    WithSecretKey("supersecret"),
5)

Keuntungan:

  • Bisa punya default yang jelas
  • Pemanggilan fleksibel: user bebas pilih opsi mana yang mau diisi
  • Kita bisa nambah opsi baru tanpa mengubah function signature

Lalu, gimana implementasinya?

Definisikan Config

1type PasswordConfig struct {
2    Algo      string
3    Salt      string
4    SecretKey string
5}
6

Ini “sumber kebenaran” konfigurasi untuk GeneratePassword.

Definisikan Tipe Option

1type Option func(*PasswordConfig)
2

Artinya: Option adalah fungsi yang menerima pointer ke PasswordConfig dan boleh mengubah isi config-nya.

Buat Helper WithXxx(...)

Di sinilah “optional param” muncul, tapi dalam bentuk fungsi.

1func WithAlgo(algo string) Option {
2    return func(c *PasswordConfig) {
3        c.Algo = algo
4    }
5}
6
7func WithSalt(salt string) Option {
8    return func(c *PasswordConfig) {
9        c.Salt = salt
10    }
11}
12
13func WithSecretKey(key string) Option {
14    return func(c *PasswordConfig) {
15        c.SecretKey = key
16    }
17}
18

Cara bacanya:

  • WithSalt("noobee") mengembalikan Option
  • Option itu ketika dieksekusi akan mengubah cfg.Salt di dalam GeneratePassword

3.4. Implementasi GeneratePassword

Di sini kita gabungkan semuanya.

1func GeneratePassword(password string, opts ...Option) string {
2    // 1. Default config
3    cfg := &PasswordConfig{
4        Algo:      "sha256",
5        Salt:      "",
6        SecretKey: "",
7    }
8
9    // 2. Apply semua option yang dikirim
10    for _, opt := range opts {
11        opt(cfg)
12    }
13
14    // 3. Bentuk input akhir sesuai config
15    finalInput := password + cfg.Salt + cfg.SecretKey
16
17    // 4. Pilih algoritma hash sesuai cfg.Algo
18    switch cfg.Algo {
19    case "sha256":
20        h := sha256.Sum256([]byte(finalInput))
21        return hex.EncodeToString(h[:])
22    default:
23        // fallback ke sha256 kalau belum di-support
24        h := sha256.Sum256([]byte(finalInput))
25        return hex.EncodeToString(h[:])
26    }
27}
28

Breakdown:

  1. cfg := &PasswordConfig{...} → Kita set default value. Misalnya default algo = sha256.
  2. for _, opt := range opts { opt(cfg) } → Di sinilah option-option seperti WithSalt(...), WithSecretKey(...) dijalankan dan mengubah config.
  3. finalInput := password + cfg.Salt + cfg.SecretKey → Contoh sederhana: kita gabungkan password, salt, dan secretKey, lalu di-hash.
  4. switch cfg.Algo { ... } → Di contoh ini, baru support sha256. Di real life, kamu bisa tambahkan sha512, argon2, dsb.

Sample Code

1package main
2
3import (
4    "crypto/sha256"
5    "encoding/hex"
6    "fmt"
7)
8
9// ====== CONFIG & OPTION ======
10
11type PasswordConfig struct {
12    Algo      string
13    Salt      string
14    SecretKey string
15}
16
17type Option func(*PasswordConfig)
18
19func WithAlgo(algo string) Option {
20    return func(c *PasswordConfig) {
21        c.Algo = algo
22    }
23}
24
25func WithSalt(salt string) Option {
26    return func(c *PasswordConfig) {
27        c.Salt = salt
28    }
29}
30
31func WithSecretKey(key string) Option {
32    return func(c *PasswordConfig) {
33        c.SecretKey = key
34    }
35}
36
37// ====== CORE FUNCTION ======
38
39func GeneratePassword(password string, opts ...Option) string {
40    // default config
41    cfg := &PasswordConfig{
42        Algo:      "sha256",
43        Salt:      "",
44        SecretKey: "",
45    }
46
47    // apply options
48    for _, opt := range opts {
49        opt(cfg)
50    }
51
52    finalInput := password + cfg.Salt + cfg.SecretKey
53
54    // simple: hanya support sha256
55    h := sha256.Sum256([]byte(finalInput))
56    return hex.EncodeToString(h[:])
57}
58
59// ====== DEMO ======
60
61func main() {
62    // 1. Tanpa optional params (pakai default)
63    fmt.Println("=== Default (tanpa salt & secretKey) ===")
64    out1 := GeneratePassword("bee123")
65    fmt.Println(out1)
66
67    // 2. Dengan salt dan secretKey
68    fmt.Println("\n=== Dengan Salt dan SecretKey ===")
69    out2 := GeneratePassword(
70        "bee123",
71        WithSalt("noobee"),
72        WithSecretKey("supersecret"),
73    )
74    fmt.Println(out2)
75
76    // 3. Hanya dengan secretKey
77    fmt.Println("\n=== Hanya dengan SecretKey ===")
78    out3 := GeneratePassword(
79        "bee123",
80        WithSecretKey("supersecret"),
81    )
82    fmt.Println(out3)
83
84    // 4. Hanya dengan salt
85    fmt.Println("\n=== Hanya dengan Salt ===")
86    out4 := GeneratePassword(
87        "bee123",
88        WithSalt("noobee"),
89    )
90    fmt.Println(out4)
91}
92

Output

Kalau kamu run dengan input persis di atas, contoh outputnya bisa seperti ini:

1=== Default (tanpa salt & secretKey) ===
248d22b9f8d3c069a995dcc3fb2faddfe192aaf98a6959ce3353dc5e17fc67e35
3
4=== Dengan Salt dan SecretKey ===
5f5199c4c0bfe9032b78418fa54e703afb05db057844ad4ff322dd6db442f4d73
6
7=== Hanya dengan SecretKey ===
89ddda548f01b133cb7e0ce4fbcd1d316a6aa665196e87d4e8a63961393215ca8
9
10=== Hanya dengan Salt ===
119f5f3e6ae9ca0f6b5ddfd8f61c350cdbcee48269ffdb5307f8c961520c3fecd8
12

Yang penting di sini: Input berbeda → kombinasi salt/secretKey berbeda → output hash berbeda, tapi cara memanggil fungsi jadi jelas dan fleksibel.

Mini Arsitektur: Posisi GeneratePassword di Aplikasi

Biar lebih kebayang, kita taruh utility ini di konteks aplikasi beneran. Misalnya sistem registrasi user.

Struktur Folder Sederhana

1.
2├── cmd
3│   └── api
4│       └── main.go          # entrypoint HTTP server
5├── internal
6│   ├── handler
7│   │   └── user_handler.go  # HTTP handler register/login
8│   ├── service
9│   │   └── user_service.go  # business logic
10│   └── security
11│       └── password.go      # GeneratePassword + options
12└── go.mod
13

Di internal/security/password.go kamu punya kode:

1package security
2
3// PasswordConfig, Option, WithSalt, WithSecretKey, GeneratePassword
4// (isi sama seperti contoh di atas, hanya beda package)
5

Flow Arsitektur (High-Level)

Flow registrasi user:

  1. HTTP Handler nerima request:
  2. Handler panggil
  3. userService
  4. DB simpan

Secara “diagram teks”:

1[HTTP Request]
2     |
3     v
4[User Handler]  --> parse JSON body
5     |
6     v
7[User Service]  --> validasi data, panggil GeneratePassword()
8     |
9     v
10[security.GeneratePassword] + optional params (salt, secretKey, algo)
11     |
12     v
13[Database]      --> simpan hash
14

Contoh implementasi:

1// internal/service/user_service.go
2package service
3
4import (
5    "context"
6
7    "myapp/internal/security"
8)
9
10type UserService struct {
11    // repo, logger, dll.
12}
13
14func (s *UserService) Register(ctx context.Context, email, plainPassword string) error {
15    // misalnya salt = email, secretKey = dari env
16    passwordHash := security.GeneratePassword(
17        plainPassword,
18        security.WithSalt(email),
19        security.WithSecretKey("SUPER-SECRET-FROM-ENV"),
20    )
21
22    // TODO: simpan ke database (pseudo)
23    // return s.repo.CreateUser(ctx, email, passwordHash)
24    return nil
25}
26

Dari arsitektur ini, kelihatan:

  • Layer lain cuma tahu: “kalau mau hash password, panggil
  • Detail teknis (pakai sha256, apa itu salt, dsb.) dikapsulasi di
  • Kalau suatu saat kamu mau ganti algo ke

Kapan Sebaiknya Pakai Functional Options?

Cocok dipakai ketika:

  • Fungsi punya banyak konfigurasi, tapi:
  • Kamu ingin:
  • Contoh use case:

Kurang perlu kalau:

  • Parameternya cuma 1–2 dan hampir selalu diisi semua
  • Fungsi jarang berubah dan simple
  • Kadang pakai struct biasa sudah cukup

7. Penutup

Jadi, BeeGangs, pattern Functional Options ini sebenarnya bukan fitur spesial bahasa, tapi “trik” desain API di Golang yang bikin:

  • Fungsi lebih fleksibel
  • Call site lebih mudah dibaca
  • Arsitektur lebih rapi dan scalable
***