Menu

Closure dan Fungsi Anonim di Golang, dengan Contoh

Fungsi anonim di Go bisa menangkap variabel dari scope di sekitarnya. Dari situ lahir closure: counter, generator, middleware, dan callback yang menyimpan state-nya sendiri.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Fungsi anonim

Function literal adalah fungsi tanpa nama. Function literal adalah ekspresi, jadi bisa ditaruh di mana pun sebuah nilai bisa ditaruh: variabel, argumen, nilai kembalian, field struct.

Go tidak punya penulisan singkat berupa arrow atau lambda. Bentuk lengkap func(params) results { body } adalah satu-satunya cara menulisnya.

Menangkap variabel

Function literal bisa memakai variabel dari fungsi yang melingkupinya. Variabel itu tidak disalin: closure dan kode di sekitarnya memakai satu variabel yang sama.

Variabel yang ditangkap tetap hidup selama masih ada closure yang merujuknya, bahkan setelah fungsi yang mendeklarasikannya sudah selesai. Compiler memindahkannya ke heap kalau perlu; kamu tidak pernah mengurus hal itu sendiri.

Generator dan counter

Mengembalikan closure dari sebuah fungsi memberi setiap fungsi yang dikembalikan state pribadinya sendiri.

Setiap pemanggilan counter() membuat n yang baru, jadi c1 dan c2 menghitung secara terpisah. Tidak ada kode di luar yang bisa membaca atau mereset n, sehingga ini menjadi bentuk kecil dari enkapsulasi.

Closure di dalam loop (Go 1.22 dan sesudahnya)

Sebelum Go 1.22, for loop hanya punya satu variabel loop untuk seluruh loop. Semua closure yang dibuat di dalamnya menangkap variabel tunggal itu dan melihat nilai terakhirnya. Itulah bug closure yang paling umum di Go.

Sejak Go 1.22, setiap iterasi mendapat salinannya sendiri, jadi kode di bawah ini mencetak 0 1 2:

Di Go 1.21 dan sebelumnya, loop yang sama yang ditulis sebagai for i := 0; i < 3; i++ (range atas integer juga baru ada di 1.22) mencetak 3 3 3. Aturan baru ini berlaku per modul, berdasarkan baris go di go.mod, jadi modul lama tetap memakai perilaku lama sampai baris go-nya dinaikkan ke 1.22 atau lebih baru. Kamu masih akan melihat i := i atau v := v di dalam loop pada kode lama: itu perbaikan manual dan sekarang sudah tidak diperlukan.

Aturan per iterasi hanya berlaku untuk variabel yang dideklarasikan oleh pernyataan for itu sendiri. Variabel yang dideklarasikan sebelum loop dan diubah di dalam badan loop tetap satu variabel bersama:

var last string
for _, s := range items {
	last = s
	handlers = append(handlers, func() { use(last) }) // every closure sees the final value
}

Closure dan goroutine

Goroutine yang dijalankan dengan function literal juga merupakan closure. Aturan berbagi yang sama berlaku, ditambah aturan konkurensi yang biasa: kalau beberapa goroutine menulis ke variabel yang ditangkap, kamu butuh mutex atau channel.

Kode ini selalu mencetak 5050. Setiap goroutine membaca i miliknya sendiri (semantik loop Go 1.22) dan menulis ke total bersama di bawah lock. Hapus mutex-nya dan hasilnya jadi tidak bisa ditebak; go run -race melaporkannya sebagai data race. Lihat goroutine untuk WaitGroup dan race detector.

Di mana closure muncul di kode sungguhan

  • Pengurutan dan pencarian: slices.SortFunc, slices.IndexFunc, sort.Slice semuanya menerima function literal.
  • Pembersihan yang ditunda: defer func() { ... }() menjalankan sebuah blok saat fungsi selesai dan bisa membaca variabel fungsi itu. Lihat defer.
  • Middleware HTTP: fungsi yang menerima handler lalu mengembalikan literal http.HandlerFunc baru yang membungkusnya.
  • Konfigurasi: functional options (func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } }) adalah closure atas argumennya.

Contoh middleware yang menangkap parameter sekaligus counter:

Kesalahan umum

  • Mengharapkan potret nilai. Closure membaca nilai variabel saat closure itu berjalan, bukan nilai saat closure dibuat. Kirim nilainya sebagai parameter kalau kamu butuh potret: go func(v int) { ... }(x).
  • Literal rekursif. Function literal tidak bisa merujuk dirinya sendiri lewat variabel yang sedang di-assign kepadanya dalam := yang sama. Deklarasikan variabelnya lebih dulu: var walk func(n int); walk = func(n int) { ... walk(n-1) }.
  • Penulisan dari goroutine tanpa sinkronisasi. Menangkap variabel bukanlah sinkronisasi. Lindungi penulisan ke data bersama.

Pertanyaan yang sering diajukan

Apa itu fungsi anonim di Go?

Function literal tanpa nama: func(x int) int { return x * 2 }. Kamu bisa menyimpannya ke variabel, mengirimnya sebagai argumen, mengembalikannya, atau langsung memanggilnya dengan menambahkan () setelah kurung kurawal penutup. Go tidak punya sintaks lambda terpisah; function literal itulah lambda-nya.

Apa itu closure di Go?

Function literal yang merujuk ke variabel yang dideklarasikan di luarnya. Fungsi itu menjaga variabel tersebut tetap hidup dan memakainya bersama scope di sekitarnya, jadi perubahan di dalam closure terlihat di luar, begitu juga sebaliknya. Variabel ditangkap sebagai referensi, bukan disalin.

Apakah closure di Go menangkap variabel sebagai nilai atau referensi?

Sebagai referensi. Closure dan kode di sekitarnya memakai variabel yang sama. Kalau kamu butuh potret nilainya, salin nilainya ke variabel baru sebelum membuat closure, atau kirim sebagai argumen.

Apakah bug variabel loop pada closure sudah diperbaiki di Go?

Ya, sejak Go 1.22. Setiap iterasi for loop sekarang mendeklarasikan variabel loop yang baru, jadi closure dan goroutine yang dibuat di dalam loop melihat nilai dari iterasi itu. Aturan ini berlaku untuk modul yang go.mod-nya menulis go 1.22 atau lebih baru. Kode lama sering punya i := i di dalam loop sebagai solusi sementara; itu tidak berbahaya, tetapi sudah tidak diperlukan.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai