unisbadri.com » Python Java Golang Typescript Kotlin Ruby Rust Dart PHP

Eksepsi #

Go tidak punya try-catch. Bukan karena keterbatasan, tapi karena keputusan desain yang sangat deliberat: error adalah nilai biasa, bukan mekanisme kontrol aliran yang istimewa. Di Java atau Python, exception bisa muncul dari fungsi mana saja dan “melompati” banyak lapisan kode sebelum ditangani — ini membuat alur error sulit diprediksi dan dipahami. Di Go, error dikembalikan eksplisit sebagai return value, harus ditangani oleh pemanggil langsung, dan alurnya selalu bisa dibaca dari atas ke bawah. Hasilnya lebih verbose, tapi juga lebih transparan — tidak ada “surprise exception” yang mengejutkan.

Interface error — Fondasi Semua Error di Go #

Semua error di Go mengimplementasikan interface yang sangat sederhana ini:

type error interface {
    Error() string
}

Hanya satu method. Artinya, tipe apapun yang punya method Error() string adalah error yang valid di Go. Ini membuat sistem error Go sangat extensible — kamu bisa membuat error yang membawa data apapun, selama ia bisa mendeskripsikan dirinya sebagai string.


Cara Membuat Error #

errors.New — Error Sederhana #

Untuk error yang hanya butuh pesan statis:

import "errors"

err := errors.New("file tidak ditemukan")
fmt.Println(err)         // file tidak ditemukan
fmt.Println(err.Error()) // file tidak ditemukan — memanggil method Error()

Sentinel Errors — Error yang Bisa Dibandingkan #

Sentinel error adalah variabel package-level yang merepresentasikan kondisi error spesifik. Mereka memungkinkan pemanggil memeriksa jenis error dengan errors.Is:

package database

import "errors"

// Sentinel errors — nama selalu diawali Err
var (
    ErrNotFound     = errors.New("record tidak ditemukan")
    ErrDuplicate    = errors.New("record sudah ada")
    ErrUnauthorized = errors.New("tidak memiliki akses")
    ErrConnFailed   = errors.New("koneksi database gagal")
)

func FindUser(id int) (*User, error) {
    user := db.Query(id)
    if user == nil {
        return nil, ErrNotFound  // kembalikan sentinel error
    }
    return user, nil
}
// Di pemanggil — bisa cek jenis error secara tepat
user, err := database.FindUser(42)
if errors.Is(err, database.ErrNotFound) {
    // tangani kasus "tidak ada" secara spesifik
    return nil, fmt.Errorf("user 42 tidak ada di sistem")
}
if err != nil {
    // error lain yang tidak diharapkan
    return nil, fmt.Errorf("gagal ambil user: %w", err)
}

fmt.Errorf — Error dengan Konteks #

fmt.Errorf membuat error dengan pesan yang diformat. Gunakan %w (bukan %v) untuk membungkus error asli sehingga masih bisa ditelusuri:

import "fmt"

// %v — hanya menyertakan string error, chain PUTUS
err1 := fmt.Errorf("gagal membaca file: %v", originalErr)

// %w — membungkus error asli, chain TERJAGA
err2 := fmt.Errorf("gagal membaca file: %w", originalErr)

// Dengan %w, originalErr masih bisa ditemukan dengan errors.Is
errors.Is(err2, originalErr)  // true
errors.Is(err1, originalErr)  // false — chain sudah putus

Custom Error Types — Error yang Membawa Data #

Ketika error perlu membawa informasi lebih dari sekadar string, buat tipe error kustom dengan mengimplementasikan interface error:

// Error untuk validasi input — membawa field mana yang bermasalah
type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validasi gagal pada field %q (nilai: %v): %s",
        e.Field, e.Value, e.Message)
}

// Error untuk HTTP — membawa status code
type HTTPError struct {
    StatusCode int
    Status     string
    Body       []byte
}

func (e *HTTPError) Error() string {
    return fmt.Sprintf("HTTP %d %s", e.StatusCode, e.Status)
}

// Error untuk operasi database — membawa query yang gagal
type DBError struct {
    Op    string   // operasi: "insert", "select", "update"
    Table string
    Err   error    // error asli dari driver
}

func (e *DBError) Error() string {
    return fmt.Sprintf("db %s pada tabel %q: %v", e.Op, e.Table, e.Err)
}

func (e *DBError) Unwrap() error {
    return e.Err  // wajib untuk errors.Is/As bekerja dengan chain
}

Menggunakan Custom Error Types #

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Value:   age,
            Message: "tidak boleh negatif",
        }
    }
    if age > 150 {
        return &ValidationError{
            Field:   "age",
            Value:   age,
            Message: "melebihi batas maksimum 150",
        }
    }
    return nil
}

func main() {
    err := validateAge(-5)
    if err != nil {
        // Ekstrak informasi detail dengan errors.As
        var valErr *ValidationError
        if errors.As(err, &valErr) {
            fmt.Printf("Field %q gagal: %s\n", valErr.Field, valErr.Message)
        }
    }
}

Error Wrapping — Membangun Error Chain #

Error wrapping memungkinkan kamu menambahkan konteks ke error sambil mempertahankan error aslinya. Hasilnya adalah error chain — serangkaian error yang terhubung:

func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        // Wrap: tambahkan konteks "di mana", pertahankan "apa"
        return nil, fmt.Errorf("readConfig(%q): %w", path, err)
    }
    return data, nil
}

func parseConfig(path string) (*Config, error) {
    data, err := readConfig(path)
    if err != nil {
        return nil, fmt.Errorf("parseConfig: %w", err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("parseConfig: unmarshal: %w", err)
    }
    return &cfg, nil
}

func startServer(configPath string) error {
    cfg, err := parseConfig(configPath)
    if err != nil {
        return fmt.Errorf("startServer: %w", err)
    }
    // ...
    return nil
}

Jika os.ReadFile gagal, error chain-nya akan terlihat seperti:

startServer: parseConfig: readConfig("/etc/app/config.json"): open /etc/app/config.json: no such file or directory

Setiap lapisan menambahkan konteks — sangat berguna untuk debugging di production.


errors.Is — Menelusuri Chain #

errors.Is memeriksa apakah error tertentu ada di dalam chain, menelusuri sampai ke ujung:

var ErrPermission = errors.New("akses ditolak")

func readSecretFile() error {
    return fmt.Errorf("readSecretFile: %w",
        fmt.Errorf("sistem operasi: %w", ErrPermission))
}

func main() {
    err := readSecretFile()

    // errors.Is menelusuri seluruh chain
    fmt.Println(errors.Is(err, ErrPermission))  // true — meski terbungkus dua kali

    // Perbandingan langsung tidak menemukan karena sudah dibungkus
    fmt.Println(err == ErrPermission)  // false
}

Implementasi Is() Kustom #

Untuk custom error type yang perlu logika pencocokan lebih kompleks dari kesamaan pointer:

type NotFoundError struct {
    Resource string
    ID       int
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s dengan ID %d tidak ditemukan", e.Resource, e.ID)
}

// Implementasi Is() kustom — cocok berdasarkan Resource saja
func (e *NotFoundError) Is(target error) bool {
    t, ok := target.(*NotFoundError)
    if !ok {
        return false
    }
    // Cocok jika Resource sama (abaikan ID)
    return e.Resource == t.Resource || t.Resource == ""
}

func main() {
    err := &NotFoundError{Resource: "User", ID: 42}
    target := &NotFoundError{Resource: "User"}  // ID kosong = wildcard

    fmt.Println(errors.Is(err, target))  // true — Resource sama
}

errors.As — Ekstrak Tipe Error Tertentu #

errors.As menelusuri chain dan mengekstrak error bertipe tertentu ke variabel target:

func processRequest(userID int) error {
    user, err := db.FindUser(userID)
    if err != nil {
        return fmt.Errorf("processRequest: %w",
            &DBError{Op: "select", Table: "users", Err: err})
    }
    _ = user
    return nil
}

func main() {
    err := processRequest(999)

    // errors.As — ekstrak *DBError dari chain manapun
    var dbErr *DBError
    if errors.As(err, &dbErr) {
        fmt.Printf("Operasi yang gagal: %s pada tabel %s\n",
            dbErr.Op, dbErr.Table)
    }

    // errors.As juga menelusuri melalui wrapping
    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Println("Validasi gagal:", valErr.Message)
    } else {
        fmt.Println("Bukan validation error")
    }
}

Perbedaan errors.Is vs errors.As #

errors.Is(err, target):
  → Memeriksa KESAMAAN nilai
  → target biasanya sentinel error (var ErrXxx = errors.New(...))
  → Menjawab: "apakah error ini adalah ErrNotFound?"

errors.As(err, &target):
  → Memeriksa KECOCOKAN TIPE dan mengekstrak nilainya
  → target adalah pointer ke tipe error konkret (*MyError)
  → Menjawab: "apakah ada *ValidationError dalam chain ini, dan jika ada, berikan ke saya"

Pola Error Handling Idiomatik #

Annotate at Boundary — Tambahkan Konteks di Batas Lapisan #

// ANTI-PATTERN: return error mentah tanpa konteks
func getUserPosts(userID int) ([]*Post, error) {
    posts, err := db.Query("SELECT * FROM posts WHERE user_id = ?", userID)
    if err != nil {
        return nil, err  // ✗ "sql: no rows" — tidak jelas konteksnya
    }
    return posts, nil
}

// BENAR: wrap dengan konteks di setiap boundary
func getUserPosts(userID int) ([]*Post, error) {
    posts, err := db.Query("SELECT * FROM posts WHERE user_id = ?", userID)
    if err != nil {
        return nil, fmt.Errorf("getUserPosts(userID=%d): %w", userID, err)
        // ✓ "getUserPosts(userID=42): sql: no rows" — jelas!
    }
    return posts, nil
}

Handle Once — Tangani Error Satu Kali #

// ANTI-PATTERN: log DAN return — error ditangani dua kali
func doWork() error {
    if err := step1(); err != nil {
        log.Println("step1 error:", err)  // log di sini
        return err                         // DAN return — double handling!
    }
    return nil
}

func main() {
    if err := doWork(); err != nil {
        log.Println("doWork error:", err)  // log lagi di sini — duplikasi!
    }
}

// BENAR: pilih satu — log ATAU return, tidak keduanya
func doWork() error {
    if err := step1(); err != nil {
        return fmt.Errorf("doWork.step1: %w", err)  // hanya wrap dan return
    }
    return nil
}

func main() {
    if err := doWork(); err != nil {
        log.Println("Error:", err)  // tangani SATU KALI di level tertinggi
    }
}

Jangan Abaikan Error #

// ANTI-PATTERN: mengabaikan error
file, _ := os.Create("output.txt")  // ✗ jika gagal, file adalah nil
file.Write([]byte("data"))          // panic: nil pointer dereference

// BENAR: selalu tangani
file, err := os.Create("output.txt")
if err != nil {
    return fmt.Errorf("gagal membuat file: %w", err)
}
defer file.Close()

panic dan recover #

panic menghentikan eksekusi normal, menjalankan semua defer dalam stack, lalu crash. recover menangkap panic agar program tidak crash total — hanya valid dipanggil dalam defer.

Kapan panic Sah Digunakan #

// 1. Bug pemrograman yang seharusnya tidak terjadi
func mustPositive(n int) int {
    if n <= 0 {
        panic(fmt.Sprintf("mustPositive: n harus > 0, got %d", n))
    }
    return n
}

// 2. Inisialisasi program yang gagal — tidak ada gunanya lanjut
func mustCompile(pattern string) *regexp.Regexp {
    re, err := regexp.Compile(pattern)
    if err != nil {
        panic(fmt.Sprintf("mustCompile: pattern tidak valid %q: %v", pattern, err))
    }
    return re
}

// Penggunaan di level package — panic terjadi sebelum main()
var emailRegex = mustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)

recover untuk Mencegah Crash #

// Middleware HTTP yang menangkap panic dari handler
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                // Tangkap panic, log, kembalikan 500
                log.Printf("PANIC: %v\n%s", rec, debug.Stack())
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// Safe wrapper — convert panic ke error
func safeCall(fn func() error) (err error) {
    defer func() {
        if r := recover(); r != nil {
            switch v := r.(type) {
            case error:
                err = fmt.Errorf("panic: %w", v)
            default:
                err = fmt.Errorf("panic: %v", v)
            }
        }
    }()
    return fn()
}
Kapan menggunakan panic vs error:

  Gunakan ERROR (return value) untuk:
    ✓ Kondisi yang bisa diantisipasi: file tidak ada, input invalid
    ✓ Kegagalan I/O: network timeout, DB error
    ✓ Kegagalan bisnis: saldo tidak cukup, produk habis

  Gunakan PANIC untuk:
    ✓ Bug pemrograman: index out of range, nil dereference
    ✓ Inisialisasi gagal: regex pattern salah, config wajib tidak ada
    ✓ Kondisi yang tidak mungkin terjadi jika kode benar

  JANGAN pakai panic sebagai pengganti error handling biasa

Multi-Error — Menggabungkan Beberapa Error #

Sejak Go 1.20, errors.Join memungkinkan menggabungkan beberapa error menjadi satu:

import "errors"

func validateUser(u User) error {
    var errs []error

    if u.Name == "" {
        errs = append(errs, errors.New("name tidak boleh kosong"))
    }
    if len(u.Name) > 100 {
        errs = append(errs, errors.New("name terlalu panjang (max 100)"))
    }
    if u.Email == "" {
        errs = append(errs, errors.New("email tidak boleh kosong"))
    }
    if !isValidEmail(u.Email) {
        errs = append(errs, fmt.Errorf("email %q tidak valid", u.Email))
    }
    if u.Age < 0 || u.Age > 150 {
        errs = append(errs, fmt.Errorf("age %d di luar rentang valid", u.Age))
    }

    return errors.Join(errs...)  // nil jika errs kosong
}

func main() {
    err := validateUser(User{Name: "", Email: "bukan-email", Age: -1})
    if err != nil {
        fmt.Println("Validasi gagal:")
        // errors.Join menghasilkan error yang bisa di-unwrap satu per satu
        for _, e := range errors.Unwrap(err).(interface{ Unwrap() []error }).Unwrap() {
            fmt.Println(" -", e)
        }
    }
}

Contoh Program Lengkap #

Program berikut mensimulasikan layanan pembayaran dengan error handling berlapis yang realistis:

package main

import (
    "errors"
    "fmt"
    "time"
)

// ── Sentinel Errors ───────────────────────────────────────────

var (
    ErrInsufficientFunds = errors.New("saldo tidak mencukupi")
    ErrAccountFrozen     = errors.New("akun dibekukan")
    ErrLimitExceeded     = errors.New("melebihi limit transaksi")
    ErrAccountNotFound   = errors.New("akun tidak ditemukan")
)

// ── Custom Error Types ────────────────────────────────────────

type TransactionError struct {
    Code      string
    AccountID string
    Amount    float64
    Reason    string
    Err       error
}

func (e *TransactionError) Error() string {
    return fmt.Sprintf("[%s] transaksi gagal untuk akun %s (Rp%.0f): %s",
        e.Code, e.AccountID, e.Amount, e.Reason)
}

func (e *TransactionError) Unwrap() error { return e.Err }

type AuditError struct {
    TransactionID string
    Err           error
}

func (e *AuditError) Error() string {
    return fmt.Sprintf("audit gagal untuk transaksi %s: %v", e.TransactionID, e.Err)
}

func (e *AuditError) Unwrap() error { return e.Err }

// ── Domain Models ─────────────────────────────────────────────

type Account struct {
    ID       string
    Name     string
    Balance  float64
    DailyLimit float64
    DailyUsed  float64
    Frozen   bool
}

type Transaction struct {
    ID        string
    From      string
    To        string
    Amount    float64
    Timestamp time.Time
    Status    string
}

// ── Repositories (simulasi) ───────────────────────────────────

type AccountRepo struct {
    accounts map[string]*Account
}

func NewAccountRepo() *AccountRepo {
    return &AccountRepo{
        accounts: map[string]*Account{
            "ACC001": {ID: "ACC001", Name: "Budi",  Balance: 5_000_000, DailyLimit: 10_000_000, Frozen: false},
            "ACC002": {ID: "ACC002", Name: "Sari",  Balance: 2_000_000, DailyLimit: 5_000_000,  Frozen: false},
            "ACC003": {ID: "ACC003", Name: "Ahmad", Balance: 1_000_000, DailyLimit: 3_000_000,  Frozen: true},
        },
    }
}

func (r *AccountRepo) FindByID(id string) (*Account, error) {
    acc, ok := r.accounts[id]
    if !ok {
        return nil, fmt.Errorf("AccountRepo.FindByID(%q): %w", id, ErrAccountNotFound)
    }
    return acc, nil
}

func (r *AccountRepo) UpdateBalance(id string, newBalance float64) error {
    acc, err := r.FindByID(id)
    if err != nil {
        return fmt.Errorf("AccountRepo.UpdateBalance: %w", err)
    }
    acc.Balance = newBalance
    return nil
}

// ── Audit Service ─────────────────────────────────────────────

type AuditService struct {
    logs []string
}

func (a *AuditService) Log(tx Transaction) error {
    // Simulasi: gagal jika amount sangat besar (bug palsu)
    if tx.Amount > 100_000_000 {
        return fmt.Errorf("AuditService.Log: nilai terlalu besar untuk diaudit")
    }
    a.logs = append(a.logs, fmt.Sprintf("[%s] %s → %s: Rp%.0f (%s)",
        tx.Timestamp.Format("15:04:05"),
        tx.From, tx.To, tx.Amount, tx.Status))
    return nil
}

// ── Payment Service ───────────────────────────────────────────

type PaymentService struct {
    repo  *AccountRepo
    audit *AuditService
}

func NewPaymentService(repo *AccountRepo, audit *AuditService) *PaymentService {
    return &PaymentService{repo: repo, audit: audit}
}

func (s *PaymentService) validateTransfer(from *Account, amount float64) error {
    if from.Frozen {
        return &TransactionError{
            Code:      "ACCOUNT_FROZEN",
            AccountID: from.ID,
            Amount:    amount,
            Reason:    "akun pengirim dibekukan",
            Err:       ErrAccountFrozen,
        }
    }
    if from.Balance < amount {
        return &TransactionError{
            Code:      "INSUFFICIENT_FUNDS",
            AccountID: from.ID,
            Amount:    amount,
            Reason:    fmt.Sprintf("saldo Rp%.0f < Rp%.0f", from.Balance, amount),
            Err:       ErrInsufficientFunds,
        }
    }
    if from.DailyUsed+amount > from.DailyLimit {
        remaining := from.DailyLimit - from.DailyUsed
        return &TransactionError{
            Code:      "LIMIT_EXCEEDED",
            AccountID: from.ID,
            Amount:    amount,
            Reason:    fmt.Sprintf("limit harian tersisa Rp%.0f", remaining),
            Err:       ErrLimitExceeded,
        }
    }
    return nil
}

func (s *PaymentService) Transfer(fromID, toID string, amount float64) (*Transaction, error) {
    // Ambil akun pengirim
    from, err := s.repo.FindByID(fromID)
    if err != nil {
        return nil, fmt.Errorf("Transfer: %w", err)
    }

    // Ambil akun penerima
    to, err := s.repo.FindByID(toID)
    if err != nil {
        return nil, fmt.Errorf("Transfer: %w", err)
    }

    // Validasi
    if err := s.validateTransfer(from, amount); err != nil {
        return nil, fmt.Errorf("Transfer: %w", err)
    }

    // Proses transfer
    tx := &Transaction{
        ID:        fmt.Sprintf("TRX-%d", time.Now().UnixNano()),
        From:      fromID,
        To:        toID,
        Amount:    amount,
        Timestamp: time.Now(),
        Status:    "SUCCESS",
    }

    from.Balance -= amount
    from.DailyUsed += amount
    to.Balance += amount

    if err := s.repo.UpdateBalance(fromID, from.Balance); err != nil {
        return nil, fmt.Errorf("Transfer: update pengirim: %w", err)
    }
    if err := s.repo.UpdateBalance(toID, to.Balance); err != nil {
        return nil, fmt.Errorf("Transfer: update penerima: %w", err)
    }

    // Audit log — error di sini tidak membatalkan transfer
    if err := s.audit.Log(*tx); err != nil {
        // Log error audit tapi jangan gagalkan transaksi
        fmt.Printf("⚠ Audit warning: %v\n", &AuditError{
            TransactionID: tx.ID,
            Err:           err,
        })
    }

    return tx, nil
}

// ── Helper untuk analisis error ───────────────────────────────

func describeError(err error) string {
    if err == nil {
        return "tidak ada error"
    }

    var txErr *TransactionError
    if errors.As(err, &txErr) {
        desc := fmt.Sprintf("TransactionError[%s]: %s", txErr.Code, txErr.Reason)

        // Identifikasi sentinel error di dalam
        switch {
        case errors.Is(err, ErrInsufficientFunds):
            desc += " (INSUFFICIENT_FUNDS)"
        case errors.Is(err, ErrAccountFrozen):
            desc += " (ACCOUNT_FROZEN)"
        case errors.Is(err, ErrLimitExceeded):
            desc += " (LIMIT_EXCEEDED)"
        }
        return desc
    }

    if errors.Is(err, ErrAccountNotFound) {
        return "Akun tidak ditemukan"
    }

    return fmt.Sprintf("Error umum: %v", err)
}

func main() {
    repo := NewAccountRepo()
    audit := &AuditService{}
    svc := NewPaymentService(repo, audit)

    testCases := []struct {
        name   string
        fromID string
        toID   string
        amount float64
    }{
        {"Transfer normal",              "ACC001", "ACC002", 1_000_000},
        {"Saldo tidak cukup",            "ACC002", "ACC001", 5_000_000},
        {"Akun dibekukan",              "ACC003", "ACC001", 500_000},
        {"Akun tidak ditemukan",         "ACC999", "ACC001", 100_000},
        {"Transfer ke akun tidak ada",   "ACC001", "ACC999", 100_000},
    }

    fmt.Println("=== Simulasi Transaksi Pembayaran ===\n")

    for _, tc := range testCases {
        fmt.Printf("▶ %s (Rp%.0f)\n", tc.name, tc.amount)

        tx, err := svc.Transfer(tc.fromID, tc.toID, tc.amount)
        if err != nil {
            fmt.Printf("  ✗ Gagal: %v\n", err)
            fmt.Printf("  ℹ Analisis: %s\n", describeError(err))
        } else {
            fmt.Printf("  ✓ Berhasil: ID=%s\n", tx.ID)
        }
        fmt.Println()
    }

    fmt.Println("=== Audit Log ===")
    for _, log := range audit.logs {
        fmt.Println(" ", log)
    }

    fmt.Println("\n=== Saldo Akhir ===")
    for _, id := range []string{"ACC001", "ACC002", "ACC003"} {
        acc, _ := repo.FindByID(id)
        fmt.Printf("  %s (%s): Rp%.0f\n", acc.ID, acc.Name, acc.Balance)
    }
}

Ringkasan #

  • Error adalah nilai — dikembalikan sebagai return value terakhir, bukan mekanisme exception; alur selalu bisa dibaca linier.
  • error interface hanya butuh Error() string — tipe apapun yang punya method ini adalah error yang valid.
  • Sentinel errors (var ErrXxx = errors.New(...)) untuk kondisi yang perlu dibandingkan secara tepat dengan errors.Is.
  • fmt.Errorf dengan %w untuk membungkus error dengan konteks sambil mempertahankan chain; %v memutus chain.
  • Custom error types ketika error perlu membawa data tambahan — implementasikan Unwrap() agar chain tetap bisa ditelusuri.
  • errors.Is memeriksa kesamaan nilai menelusuri seluruh chain; errors.As mengekstrak tipe tertentu dari chain.
  • Annotate at boundary — tambahkan konteks saat error melewati lapisan; handle once — tangani error di satu tempat saja.
  • panic untuk bug pemrograman dan kegagalan inisialisasi; recover dalam defer untuk mencegah crash di middleware.
  • errors.Join (Go 1.20+) untuk menggabungkan beberapa error menjadi satu — berguna untuk validasi yang mengumpulkan semua error.
  • Jangan abaikan error dengan _ kecuali benar-benar yakin — selalu pertimbangkan apa yang terjadi jika operasi gagal.

← Sebelumnya: Interface   Berikutnya: Array →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact