Chapter sebelumnya menutup dengan teka-teki: bagaimana fungsi keluaran makeMultiplier masih bisa memakai factor padahal makeMultiplier sudah selesai dieksekusi dan variabel lokalnya seharusnya sudah mati. Jawabannya adalah closure — nilai fungsi yang bersama-sama menangkap variabel lingkungan tempat fungsi tersebut dibuat. Closure membuat fungsi Go berkeadaan (stateful) tanpa struct dan tanpa variabel global. Pada chapter ini kita akan membuktikan mekanisme penangkapan itu dengan tiga eksperimen yang semuanya deterministik.

Apa Itu Closure #

Spec Go mendefinisikan function literal sebagai closure: referensi ke variabel lingkungan yang disimpan bersama fungsi. Masa hidup variabel yang ditangkap mengikuti masa hidup closure-nya, bukan masa hidup fungsi pembuatnya — garbage collector tidak membebaskan variabel selama masih ada closure yang memegangnya. Variabel ditangkap lewat referensi, bukan salinan nilai pada saat pembuatan; setiap pemanggilan closure membaca dan menulis variabel yang sama itu.

Counter Klasik #

Contoh kanoniknya adalah pembangkit counter. Fungsi makeCounter membuat variabel hitungan lalu mengembalikan fungsi tanpa nama yang menaikkan variabel tersebut:

go
// makeCounter mengembalikan closure yang menaikkan hitungannya sendiri.
func makeCounter() func() int {
	hitungan := 0
	return func() int {
		hitungan++
		return hitungan
	}
}

Tiga pemanggilan next() menghasilkan 1, 2, 3 — hitungan terus hidup di antara pemanggilan karena closure next menahannya. Pemanggilan makeCounter() kedua membuat hitungan yang benar-benar baru, sehingga pemanggilLain() kembali dari 1. Setiap hasil makeCounter adalah unit keadaan terpisah: inilah cara closure menggantikan “objek berstatus” tanpa deklarasi tambahan apa pun.

Dua Closure Berbagi Variabel #

Penangkapan lewat referensi berarti dua closure yang lahir di lingkungan sama membagi satu variabel. Pada program utuh, naik dan turun sama-sama menutup variabel skor milik main:

go
skor := 0
naik := func() {
	skor += 10
}
turun := func() {
	skor -= 5
}

Urutan naik(), naik(), turun() menghasilkan skor = 15 — ketiga nilai berinteraksi lewat variabel yang sama, bukan salinan masing-masing. Pola setter/getter berpasangan seperti ini adalah cikal bakal enkapsulasi state di Go, akan kita lihat lagi dalam bentuk method pada section Struct dan Method.

Loop Variable Sejak Go 1.22 #

Closure yang dibuat di dalam loop menangkap variabel loop. Sejak Go 1.22, setiap iterasi loop for mendapat variabel i sendiri, sehingga tiga closure di bawah masing-masing menangkap nilai berbeda:

go
// Setiap iterasi loop mendapat variabel i sendiri (Go 1.22+).
fungsi := make([]func() int, 0, 3)
for i := 1; i <= 3; i++ {
	fungsi = append(fungsi, func() int { return i * 10 })
}

Program Utuh #

Berkas closure-demo.go menjalankan ketiga eksperimen dalam satu program. Jalankan dari direktori chapter:

bash
go run closure-demo.go

Output yang Dihasilkan:

text
1
2
3
1
skor = 15
10
20
30

Mari kita telaah baris demi baris program di atas:

  1. Baris 6 (func makeCounter() func() int): tipe kembalian berupa fungsi — setiap pemanggilan menghasilkan closure baru.
  2. Baris 7 (hitungan := 0): variabel lokal yang akan ditangkap closure; masa hidupnya diperpanjang oleh closure yang memegang referensinya.
  3. Baris 8–11 (return func() int { ... }): function literal yang menaikkan lalu mengembalikan hitungan — fungsi dan variabel yang ditangkapnya menjadi satu paket nilai.
  4. Baris 24–30 (naik dan turun): dua closure menutup variabel skor yang sama milik main, terbukti dari hasil akhir 15.
  5. Baris 37–40 (loop + append): tiga closure dibuat di dalam loop; semantik Go 1.22 memberi masing-masing i terpisah sehingga hasilnya 10, 20, 30.

Counter, setter berpasangan, dan pembangkit fungsi — semuanya berdiri di satu mekanisme sederhana: fungsi yang membawa lingkungannya. Mekanisme berulang berikutnya berpindah dari variabel ke fungsi itu sendiri: sebuah fungsi yang memanggil dirinya sendiri, atau recursion.

Referensi #

Berkas Sumber

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

closure-demo.go

Unduh berkas
go
package main

import "fmt"

// makeCounter mengembalikan closure yang menaikkan hitungannya sendiri.
func makeCounter() func() int {
	hitungan := 0
	return func() int {
		hitungan++
		return hitungan
	}
}

func main() {
	next := makeCounter()
	fmt.Println(next())
	fmt.Println(next())
	fmt.Println(next())

	pemanggilLain := makeCounter()
	fmt.Println(pemanggilLain())

	// Dua closure membagi satu variabel yang sama.
	skor := 0
	naik := func() {
		skor += 10
	}
	turun := func() {
		skor -= 5
	}
	naik()
	naik()
	turun()
	fmt.Println("skor =", skor)

	// Setiap iterasi loop mendapat variabel i sendiri (Go 1.22+).
	fungsi := make([]func() int, 0, 3)
	for i := 1; i <= 3; i++ {
		fungsi = append(fungsi, func() int { return i * 10 })
	}
	for _, f := range fungsi {
		fmt.Println(f())
	}
}