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

Fungsi #

Fungsi adalah unit komposisi utama di Go. Tidak ada class, tidak ada inheritance — cara kamu mengorganisasi dan menggunakan kembali logika adalah melalui fungsi. Tapi fungsi di Go jauh lebih ekspresif dari yang terlihat di permukaan: mereka bisa mengembalikan banyak nilai sekaligus, bisa disimpan di variabel dan dioper sebagai argumen, bisa membentuk closure yang “mengingat” state dari scope pembuatannya, dan punya defer yang mengubah cara berpikir tentang resource cleanup. Memahami semua aspek ini adalah kunci menulis Go yang benar-benar idiomatik.

Anatomi Fungsi #

Deklarasi fungsi di Go mengikuti urutan yang konsisten:

//  keyword  nama      parameter           return type
//     ↓      ↓            ↓                   ↓
    func  tambah   (a int, b int)           int    {
        return a + b
    }

Setiap bagian punya aturannya:

// Fungsi paling sederhana — tanpa parameter, tanpa return
func sayHello() {
    fmt.Println("Hello!")
}

// Dengan parameter
func greet(name string) {
    fmt.Printf("Halo, %s!\n", name)
}

// Dengan return value
func square(n int) int {
    return n * n
}

// Dengan multiple parameters dan return value
func divide(a, b float64) float64 {
    return a / b
}

Parameter — Pass by Value #

Semua parameter di Go di-pass by value — fungsi menerima salinan dari nilai yang dikirim, bukan referensi ke nilai aslinya. Perubahan pada parameter dalam fungsi tidak mempengaruhi variabel asli di pemanggil:

func doubleIt(n int) {
    n = n * 2  // hanya memodifikasi salinan lokal
    fmt.Println("dalam fungsi:", n)
}

func main() {
    x := 5
    doubleIt(x)
    fmt.Println("di luar fungsi:", x)  // tetap 5!
}
// Output:
// dalam fungsi: 10
// di luar fungsi: 5

Untuk memodifikasi nilai asli, gunakan pointer (dibahas di artikel Tipe Data) atau kembalikan nilai baru:

// Cara idiomatik Go: kembalikan nilai baru, jangan mutasi
func double(n int) int {
    return n * 2
}

x := 5
x = double(x)  // assign hasil ke x
fmt.Println(x)  // 10

Penyingkatan Parameter Bertipe Sama #

Ketika beberapa parameter berurutan punya tipe yang sama, tipe cukup ditulis sekali di akhir:

// Verbose — tipe ditulis berulang
func add(a int, b int, c int) int { return a + b + c }

// Idiomatic — tipe disingkat
func add(a, b, c int) int { return a + b + c }

// Campuran — tipe berbeda tetap ditulis masing-masing
func createUser(name, email string, age int, active bool) *User {
    return &User{Name: name, Email: email, Age: age, Active: active}
}

Multiple Return Values #

Ini adalah salah satu fitur Go yang paling membedakannya dari kebanyakan bahasa lain. Fungsi bisa mengembalikan lebih dari satu nilai:

// Dua return values
func minMax(nums []int) (int, int) {
    if len(nums) == 0 {
        return 0, 0
    }
    min, max := nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min { min = n }
        if n > max { max = n }
    }
    return min, max
}

func main() {
    minimum, maksimum := minMax([]int{3, 1, 4, 1, 5, 9, 2, 6})
    fmt.Println(minimum, maksimum)  // 1 9
}

Pola (result, error) — Konvensi Standar Go #

Multiple return values paling sering digunakan untuk mengembalikan hasil sekaligus error. Ini adalah konvensi yang sangat konsisten di seluruh standard library dan ekosistem Go:

// Error selalu menjadi return value TERAKHIR — ini konvensi wajib
func parseAge(s string) (int, error) {
    age, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("parseAge: input tidak valid %q: %w", s, err)
    }
    if age < 0 || age > 150 {
        return 0, fmt.Errorf("parseAge: umur %d di luar rentang valid", age)
    }
    return age, nil
}

func main() {
    // Selalu tangani kedua return values
    age, err := parseAge("25")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Umur:", age)

    // Abaikan salah satu dengan blank identifier — hati-hati dengan error!
    age2, _ := parseAge("30")  // hanya aman jika YAKIN tidak error
    fmt.Println("Umur 2:", age2)
}
Jangan abaikan error dengan _ sembarangan. result, _ := someFunc() adalah kode yang valid secara sintaks tapi berbahaya — jika fungsi gagal, result akan berisi zero value dan program mungkin berperilaku salah tanpa pesan error yang jelas. Selalu tangani error kecuali ada alasan yang sangat kuat.

Named Return Values #

Return values bisa diberi nama, menjadikannya variabel yang bisa digunakan langsung dalam body fungsi:

// Tanpa named return — tidak jelas nilai mana yang mana dari signature
func getStats(nums []float64) (float64, float64, float64) { ... }

// Dengan named return — signature lebih deskriptif
func getStats(nums []float64) (min, max, avg float64) {
    if len(nums) == 0 {
        return  // "naked return" — mengembalikan min, max, avg yang masih zero
    }
    min, max = nums[0], nums[0]
    sum := 0.0
    for _, n := range nums {
        if n < min { min = n }
        if n > max { max = n }
        sum += n
    }
    avg = sum / float64(len(nums))
    return  // naked return — mengembalikan min, max, avg yang sudah diisi
}

Named return paling berguna untuk:

  • Mendokumentasikan arti setiap return value di signature
  • Fungsi pendek di mana naked return masih mudah dipahami
  • Defer yang perlu memodifikasi return value
// Defer memodifikasi named return value — pola yang powerful
func readFileWithCleanup(path string) (content []byte, err error) {
    f, err := os.Open(path)
    if err != nil {
        return  // naked return: content=nil, err=error dari Open
    }
    defer func() {
        if cerr := f.Close(); cerr != nil && err == nil {
            err = cerr  // assign ke named return 'err'
        }
    }()

    content, err = io.ReadAll(f)
    return  // naked return: content dan err dari ReadAll
}
Hindari naked return pada fungsi panjang. Ketika fungsi lebih dari 15-20 baris, naked return menyulitkan pembaca karena harus scroll ke atas untuk melihat nama return variables. Pada fungsi panjang, lebih baik return eksplisit: return content, err.

Variadic Function #

Variadic function menerima jumlah argumen yang tidak ditentukan. Parameter variadic ditandai dengan ... sebelum tipenya dan selalu menjadi parameter terakhir:

// numbers bertipe []int di dalam fungsi
func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

func main() {
    fmt.Println(sum())              // 0    — valid, numbers = []int{}
    fmt.Println(sum(1))             // 1
    fmt.Println(sum(1, 2, 3))       // 6
    fmt.Println(sum(1, 2, 3, 4, 5)) // 15

    // Spread slice ke variadic dengan operator ...
    angka := []int{10, 20, 30}
    fmt.Println(sum(angka...))      // 60
}

Variadic dengan Parameter Biasa #

// Parameter biasa dulu, variadic di akhir
func logWithPrefix(prefix string, messages ...string) {
    for _, msg := range messages {
        fmt.Printf("[%s] %s\n", prefix, msg)
    }
}

logWithPrefix("INFO", "server started", "listening on :8080")
logWithPrefix("ERROR", "database connection failed")
// Output:
// [INFO] server started
// [INFO] listening on :8080
// [ERROR] database connection failed

Perbedaan Variadic dan Slice Parameter #

// Variadic — pemanggil tidak perlu membuat slice
func sumVariadic(nums ...int) int { ... }
sumVariadic(1, 2, 3)     // ✓ natural
sumVariadic(angka...)    // ✓ spread slice

// Slice parameter — pemanggil harus membuat slice
func sumSlice(nums []int) int { ... }
sumSlice([]int{1, 2, 3}) // harus bungkus dengan []int{}
sumSlice(angka)          // ✓ slice langsung

Gunakan variadic ketika pemanggil lebih sering memberikan nilai langsung satu per satu. Gunakan slice parameter ketika data sudah dalam bentuk slice.


Fungsi sebagai First-Class Citizen #

Di Go, fungsi adalah nilai seperti int atau string. Kamu bisa menyimpannya di variabel, mengopernya sebagai argumen, dan mengembalikannya dari fungsi lain.

Tipe Fungsi #

Setiap fungsi punya tipe yang didefinisikan oleh signature-nya:

// Tipe: func(int, int) int
add := func(a, b int) int { return a + b }

// Tipe: func(string) bool
isLong := func(s string) bool { return len(s) > 10 }

// Tipe: func() error
connect := func() error { return db.Connect() }

// Mendefinisikan named function type
type Transformer func(string) string
type Predicate func(int) bool
type Handler func(http.ResponseWriter, *http.Request)

Fungsi Anonim #

Fungsi anonim adalah fungsi tanpa nama — bisa disimpan ke variabel, dioper sebagai argumen, atau langsung dipanggil:

func main() {
    // Disimpan ke variabel
    multiply := func(a, b int) int {
        return a * b
    }
    fmt.Println(multiply(3, 4))  // 12

    // IIFE — Immediately Invoked Function Expression
    result := func(x int) int {
        return x * x
    }(5)
    fmt.Println(result)  // 25

    // Dioper sebagai argumen
    angka := []int{3, 1, 4, 1, 5, 9}
    sort.Slice(angka, func(i, j int) bool {
        return angka[i] < angka[j]  // ascending
    })
    fmt.Println(angka)  // [1 1 3 4 5 9]
}

Closure — Fungsi yang Mengingat State #

Closure adalah fungsi yang “menangkap” dan “mengingat” variabel dari scope tempat ia dibuat, bahkan setelah scope itu selesai. Variabel yang ditangkap disebut captured variable:

func makeCounter(start int) func() int {
    count := start  // count ditangkap oleh closure di bawah
    return func() int {
        count++
        return count
    }
}

func main() {
    counter1 := makeCounter(0)
    counter2 := makeCounter(100)

    fmt.Println(counter1())  // 1
    fmt.Println(counter1())  // 2
    fmt.Println(counter1())  // 3
    fmt.Println(counter2())  // 101  — state terpisah dari counter1
    fmt.Println(counter1())  // 4    — counter1 lanjut dari terakhir
}

Captured variable di-share antar semua closure yang menangkapnya dari scope yang sama — ini mengapa closure sangat berguna untuk state yang tersembunyi.

Closure sebagai Middleware atau Decorator #

// Logger decorator — membungkus fungsi apapun dengan logging
func withLogging(name string, fn func() error) func() error {
    return func() error {
        start := time.Now()
        fmt.Printf("[%s] mulai...\n", name)

        err := fn()  // panggil fungsi asli

        elapsed := time.Since(start)
        if err != nil {
            fmt.Printf("[%s] gagal dalam %v: %v\n", name, elapsed, err)
        } else {
            fmt.Printf("[%s] selesai dalam %v\n", name, elapsed)
        }
        return err
    }
}

// Penggunaan
processData := withLogging("processData", func() error {
    time.Sleep(100 * time.Millisecond)
    return nil
})
processData()
// Output:
// [processData] mulai...
// [processData] selesai dalam 100.12ms

// Rate limiter berbasis closure
func rateLimiter(maxPerSec int) func() bool {
    tokens := maxPerSec
    lastRefill := time.Now()
    return func() bool {
        now := time.Now()
        elapsed := now.Sub(lastRefill).Seconds()
        tokens += int(elapsed * float64(maxPerSec))
        if tokens > maxPerSec {
            tokens = maxPerSec
        }
        lastRefill = now

        if tokens <= 0 {
            return false
        }
        tokens--
        return true
    }
}

Higher-Order Function — Map, Filter, Reduce #

Higher-order function adalah fungsi yang menerima atau mengembalikan fungsi lain. Pola ini sangat umum di Go idiomatik:

// Map — transformasi setiap elemen
func mapInts(s []int, fn func(int) int) []int {
    result := make([]int, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

// Filter — pilih elemen yang memenuhi predikat
func filterInts(s []int, fn func(int) bool) []int {
    var result []int
    for _, v := range s {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

// Reduce — agregasi semua elemen menjadi satu nilai
func reduceInts(s []int, initial int, fn func(int, int) int) int {
    result := initial
    for _, v := range s {
        result = fn(result, v)
    }
    return result
}

func main() {
    angka := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // Kalikan semua dengan 2
    doubled := mapInts(angka, func(n int) int { return n * 2 })
    fmt.Println(doubled)  // [2 4 6 8 10 12 14 16 18 20]

    // Ambil hanya yang genap
    genap := filterInts(angka, func(n int) bool { return n%2 == 0 })
    fmt.Println(genap)  // [2 4 6 8 10]

    // Jumlahkan semua
    total := reduceInts(angka, 0, func(acc, n int) int { return acc + n })
    fmt.Println(total)  // 55

    // Chain: ambil yang genap, kalikan 3, jumlahkan
    hasil := reduceInts(
        mapInts(
            filterInts(angka, func(n int) bool { return n%2 == 0 }),
            func(n int) int { return n * 3 },
        ),
        0,
        func(acc, n int) int { return acc + n },
    )
    fmt.Println(hasil)  // (2+4+6+8+10)*3 = 90
}

defer — Cleanup yang Terjamin #

defer menunda eksekusi sebuah statement hingga fungsi yang mengandungnya selesai — baik selesai secara normal, maupun karena return atau bahkan panic. Ini menjamin cleanup code selalu dieksekusi:

func bacaFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()  // PASTI dipanggil saat bacaFile() selesai

    return io.ReadAll(f)
}

func withDB(fn func(*sql.DB) error) error {
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        return err
    }
    defer db.Close()  // koneksi selalu ditutup

    return fn(db)
}

Urutan LIFO — Last In, First Out #

Beberapa defer dalam satu fungsi dieksekusi dalam urutan terbalik dari pemanggilan:

func main() {
    fmt.Println("mulai")
    defer fmt.Println("defer 1 — dieksekusi ketiga")
    defer fmt.Println("defer 2 — dieksekusi kedua")
    defer fmt.Println("defer 3 — dieksekusi pertama")
    fmt.Println("selesai")
}
// Output:
// mulai
// selesai
// defer 3 — dieksekusi pertama
// defer 2 — dieksekusi kedua
// defer 1 — dieksekusi ketiga

Urutan LIFO ini sangat berguna untuk lock/unlock yang bersarang — unlock pertama selalu lock terakhir:

func safeTransaction(mu1, mu2 *sync.Mutex) {
    mu1.Lock()
    defer mu1.Unlock()  // dieksekusi kedua (setelah mu2)

    mu2.Lock()
    defer mu2.Unlock()  // dieksekusi pertama
    
    // operasi kritis
}

Argumen Defer Dievaluasi Segera #

Ini adalah gotcha yang sering mengejutkan: argumen fungsi dalam defer dievaluasi saat defer dipanggil, bukan saat fungsi defer dieksekusi:

func contoh() {
    x := 10
    defer fmt.Println("nilai x:", x)  // x=10 dievaluasi SEKARANG

    x = 20
    fmt.Println("x sekarang:", x)
}
// Output:
// x sekarang: 20
// nilai x: 10  ← bukan 20! karena x=10 sudah di-capture saat defer dipanggil

// Jika ingin menggunakan nilai terbaru, gunakan closure
func contoh2() {
    x := 10
    defer func() {
        fmt.Println("nilai x:", x)  // x dievaluasi saat closure dijalankan
    }()

    x = 20
    fmt.Println("x sekarang:", x)
}
// Output:
// x sekarang: 20
// nilai x: 20  ← closure membaca x terbaru

Rekursi #

Fungsi di Go bisa memanggil dirinya sendiri. Rekursi berguna untuk masalah yang punya sifat rekursif alami seperti tree traversal, divide and conquer, atau perhitungan matematis:

// Fibonacci rekursif — mudah dipahami tapi tidak efisien (O(2^n))
func fib(n int) int {
    if n <= 1 {
        return n
    }
    return fib(n-1) + fib(n-2)
}

// Fibonacci dengan memoization — efisien O(n)
func fibMemo(n int, memo map[int]int) int {
    if n <= 1 {
        return n
    }
    if v, ok := memo[n]; ok {
        return v
    }
    result := fibMemo(n-1, memo) + fibMemo(n-2, memo)
    memo[n] = result
    return result
}

// Tree traversal rekursif
type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

func inorderTraversal(node *TreeNode, result *[]int) {
    if node == nil {
        return
    }
    inorderTraversal(node.Left, result)
    *result = append(*result, node.Val)
    inorderTraversal(node.Right, result)
}

Best Practice Penamaan dan Ukuran Fungsi #

PENAMAAN FUNGSI:
  ✓ Gunakan kata kerja atau frasa kata kerja: getUser, calculateTotal, sendEmail
  ✓ Exported: PascalCase — GetUser, CalculateTotal
  ✓ Unexported: camelCase — getUser, calculateTotal
  ✓ Nama singkat untuk fungsi lokal: parse, build, validate
  ✗ Hindari redundansi dengan package: user.GetUser → user.Get

UKURAN FUNGSI:
  ✓ Satu fungsi, satu tanggung jawab (Single Responsibility)
  ✓ Idealnya muat dalam satu layar (< 40-50 baris)
  ✓ Jika fungsi butuh lebih dari 3 level indentasi, pecah jadi subfungsi
  ✗ Hindari "god function" yang melakukan terlalu banyak hal sekaligus

PARAMETER:
  ✓ Maksimal 3-4 parameter — lebih dari itu, pertimbangkan struct
  ✓ Gunakan struct untuk konfigurasi dengan banyak opsi opsional
  ✗ Hindari parameter boolean yang mengubah perilaku fungsi secara drastis

Contoh Program Lengkap #

Program berikut membangun pipeline pemrosesan data teks menggunakan berbagai konsep fungsi:

package main

import (
    "fmt"
    "strings"
    "unicode"
)

// Tipe fungsi untuk pipeline
type TextProcessor func(string) string
type TextFilter func(string) bool

// Pipeline — jalankan serangkaian processor secara berurutan
func pipeline(processors ...TextProcessor) TextProcessor {
    return func(text string) string {
        result := text
        for _, proc := range processors {
            result = proc(result)
        }
        return result
    }
}

// Processor factory functions — mengembalikan TextProcessor
func trimmer() TextProcessor {
    return strings.TrimSpace
}

func normalizer() TextProcessor {
    return func(s string) string {
        // Normalisasi spasi berulang
        words := strings.Fields(s)
        return strings.Join(words, " ")
    }
}

func lowercaser() TextProcessor {
    return strings.ToLower
}

func replacer(old, new string) TextProcessor {
    return func(s string) string {
        return strings.ReplaceAll(s, old, new)
    }
}

func censor(words []string) TextProcessor {
    badWords := make(map[string]bool)
    for _, w := range words {
        badWords[strings.ToLower(w)] = true
    }

    return func(s string) string {
        tokens := strings.Fields(s)
        for i, t := range tokens {
            // Ambil hanya huruf untuk pengecekan
            clean := strings.Map(func(r rune) rune {
                if unicode.IsLetter(r) {
                    return unicode.ToLower(r)
                }
                return -1
            }, t)
            if badWords[clean] {
                tokens[i] = strings.Repeat("*", len(t))
            }
        }
        return strings.Join(tokens, " ")
    }
}

// Filter factory functions
func minLength(n int) TextFilter {
    return func(s string) bool {
        return len(strings.TrimSpace(s)) >= n
    }
}

func notEmpty() TextFilter {
    return func(s string) bool {
        return strings.TrimSpace(s) != ""
    }
}

// Proses batch teks dengan pipeline dan filter
func processTexts(texts []string, proc TextProcessor, filters ...TextFilter) []string {
    var results []string

    for _, text := range texts {
        processed := proc(text)

        // Terapkan semua filter — closure menangkap filters
        passes := true
        for _, filter := range filters {
            if !filter(processed) {
                passes = false
                break
            }
        }

        if passes {
            results = append(results, processed)
        }
    }

    return results
}

// Statistik teks menggunakan closure untuk accumulator
func makeStatsCollector() (func(string), func() map[string]int) {
    stats := map[string]int{
        "total":    0,
        "words":    0,
        "chars":    0,
        "maxWords": 0,
    }

    collect := func(text string) {
        words := len(strings.Fields(text))
        stats["total"]++
        stats["words"] += words
        stats["chars"] += len(text)
        if words > stats["maxWords"] {
            stats["maxWords"] = words
        }
    }

    getStats := func() map[string]int {
        // Return salinan agar tidak bisa dimodifikasi dari luar
        result := make(map[string]int)
        for k, v := range stats {
            result[k] = v
        }
        return result
    }

    return collect, getStats
}

func main() {
    rawTexts := []string{
        "  Hello,   World!  ",
        "",
        "  Go is   AWESOME for backend  ",
        "   ",
        "The quick brown fox jumps over the lazy dog",
        "  spam   content   here  ",
        "Building scalable systems with Go",
    }

    // Bangun pipeline dengan beberapa processor
    proc := pipeline(
        trimmer(),
        normalizer(),
        lowercaser(),
        replacer("go", "golang"),
        censor([]string{"spam"}),
    )

    // Proses dengan filter
    results := processTexts(
        rawTexts,
        proc,
        notEmpty(),
        minLength(10),
    )

    // Kumpulkan statistik menggunakan closure
    collect, getStats := makeStatsCollector()

    fmt.Println("=== Hasil Pemrosesan Teks ===\n")
    for i, text := range results {
        fmt.Printf("%d. %s\n", i+1, text)
        collect(text)
    }

    stats := getStats()
    fmt.Printf("\n=== Statistik ===\n")
    fmt.Printf("Total teks diproses : %d\n", stats["total"])
    fmt.Printf("Total kata          : %d\n", stats["words"])
    fmt.Printf("Total karakter      : %d\n", stats["chars"])
    fmt.Printf("Kata terbanyak      : %d kata\n", stats["maxWords"])
    if stats["total"] > 0 {
        fmt.Printf("Rata-rata kata/teks : %.1f\n",
            float64(stats["words"])/float64(stats["total"]))
    }

    // Demonstrasi defer
    fmt.Println("\n=== Demo Defer ===")
    func() {
        fmt.Println("Fungsi dimulai")
        defer fmt.Println("Cleanup 1 — dieksekusi terakhir pertama")
        defer fmt.Println("Cleanup 2 — dieksekusi paling pertama")
        fmt.Println("Fungsi selesai")
    }()
}

Ringkasan #

  • Pass by value — fungsi menerima salinan parameter; gunakan pointer atau return value baru untuk memodifikasi data asli.
  • Parameter bertipe sama bisa disingkat: func f(a, b, c int) daripada func f(a int, b int, c int).
  • Multiple return values — pola (result, error) adalah konvensi standar Go; error selalu menjadi return value terakhir.
  • Named return berguna untuk dokumentasi signature dan defer yang memodifikasi return — hindari naked return pada fungsi panjang.
  • Variadic function dengan ...T — pemanggil bisa kirim nilai langsung; spread slice dengan slice....
  • Fungsi adalah first-class citizen — bisa disimpan di variabel, dioper sebagai argumen, dikembalikan dari fungsi lain.
  • Closure menangkap variabel dari scope pembuatannya — berguna untuk state tersembunyi, factory function, dan middleware.
  • defer menjamin cleanup selalu dieksekusi; beberapa defer dieksekusi LIFO; argumen defer dievaluasi eager saat defer dipanggil.
  • Higher-order function (map, filter, reduce) memungkinkan komposisi logika yang bersih dan dapat diuji secara terpisah.
  • Satu fungsi, satu tanggung jawab — jika fungsi terlalu panjang atau punya terlalu banyak parameter, pecah jadi fungsi-fungsi lebih kecil.

← Sebelumnya: Perulangan   Berikutnya: Struct →

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