📚 Series: Belajar Golang dari Nol sampai Deploy
← Part 3 Part 4: Interface & Error Handling Part 5: Goroutine →

Interface di Go

Interface di Go bersifat implicit — kamu tidak perlu menulis implements InterfaceName. Selama sebuah struct memiliki semua method yang diperlukan, struct tersebut secara otomatis mengimplementasi interface tersebut.

// Definisi interface
type Stringer interface {
    String() string
}

type Animal interface {
    Sound() string
    Name() string
}

// Struct yang mengimplementasi Animal
type Dog struct{ nama string }
type Cat struct{ nama string }

func (d Dog) Sound() string { return "Woof!" }
func (d Dog) Name() string  { return d.nama }

func (c Cat) Sound() string { return "Meow!" }
func (c Cat) Name() string  { return c.nama }

// Fungsi yang menerima interface
func describe(a Animal) {
    fmt.Printf("%s berkata: %s\n", a.Name(), a.Sound())
}

func main() {
    anjing := Dog{nama: "Rex"}
    kucing := Cat{nama: "Whiskers"}

    describe(anjing) // Rex berkata: Woof!
    describe(kucing) // Whiskers berkata: Meow!

    // Slice of interface
    hewan := []Animal{anjing, kucing}
    for _, h := range hewan {
        describe(h)
    }
}

Empty Interface dan Type Assertion

// interface{} (atau 'any' di Go 1.18+) menerima tipe apapun
func printApa(nilai interface{}) {
    fmt.Println(nilai)
}

printApa("teks")    // teks
printApa(42)        // 42
printApa(true)      // true

// Type assertion — cek tipe konkret dari interface
var i interface{} = "hello"

s, ok := i.(string) // safe assertion
if ok {
    fmt.Println("String:", s) // String: hello
}

n, ok := i.(int) // gagal tapi aman
if !ok {
    fmt.Println("Bukan int, n =", n) // Bukan int, n = 0
}

// Type switch
func cekTipe(val interface{}) {
    switch v := val.(type) {
    case string:
        fmt.Printf("String dengan panjang %d\n", len(v))
    case int:
        fmt.Printf("Integer: %d\n", v)
    case bool:
        fmt.Printf("Boolean: %v\n", v)
    default:
        fmt.Printf("Tipe tidak dikenal: %T\n", v)
    }
}

Error Handling di Go

Go tidak pakai try-catch. Error adalah nilai biasa yang di-return dari fungsi. Ini terasa verbose di awal, tapi membuat kode lebih jelas dan tidak ada kejutan tersembunyi.

import (
    "errors"
    "fmt"
    "strconv"
)

func bagi(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("pembagi tidak boleh nol")
    }
    return a / b, nil
}

func main() {
    hasil, err := bagi(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Hasil:", hasil)

    // Konversi string ke int — bisa error
    angka, err := strconv.Atoi("bukan angka")
    if err != nil {
        fmt.Println("Gagal konversi:", err)
    } else {
        fmt.Println("Angka:", angka)
    }
}

Custom Error

// Custom error dengan struct
type ValidationError struct {
    Field   string
    Message string
}

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

func validateTask(title string) error {
    if title == "" {
        return &ValidationError{Field: "title", Message: "tidak boleh kosong"}
    }
    if len(title) > 100 {
        return &ValidationError{Field: "title", Message: "maksimal 100 karakter"}
    }
    return nil
}

func main() {
    err := validateTask("")
    if err != nil {
        var valErr *ValidationError
        if errors.As(err, &valErr) {
            fmt.Println("Field:", valErr.Field)
            fmt.Println("Pesan:", valErr.Message)
        }
    }
}

fmt.Errorf dan %w (Error Wrapping)

func getTask(id int) (*Task, error) {
    task, err := db.FindByID(id)
    if err != nil {
        // Wrap error dengan context tambahan
        return nil, fmt.Errorf("getTask(%d): %w", id, err)
    }
    return task, nil
}

// Unwrap error dengan errors.Is dan errors.As
var ErrNotFound = errors.New("not found")

err := fmt.Errorf("database error: %w", ErrNotFound)
fmt.Println(errors.Is(err, ErrNotFound)) // true

Defer, Panic, dan Recover

// Defer — jalankan sebelum fungsi return (biasa untuk cleanup)
func bacaFile(path string) {
    file, err := os.Open(path)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer file.Close() // dipanggil saat fungsi selesai, apapun yang terjadi

    // ... proses file
    fmt.Println("File berhasil dibaca")
}

// Panic — error kritis yang menghentikan program
func mustDivide(a, b int) int {
    if b == 0 {
        panic("pembagi tidak boleh nol!") // program crash
    }
    return a / b
}

// Recover — tangkap panic (seperti try-catch, tapi jarang dipakai)
func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    result = mustDivide(a, b)
    return
}

💡 Tips: Gunakan panic hanya untuk situasi yang benar-benar tidak bisa dipulihkan (bug programmer). Untuk error normal seperti validasi atau file not found, selalu gunakan return error biasa.

Checklist Progress

← Part 3 Part 5: Goroutine →