Fungsi di Go adalah nilai, artinya fungsi dapat disimpan dalam variabel, dikirim sebagai argumen, dan dikembalikan oleh fungsi lain. Topik ini membahas tipe fungsi (function type), penulisan fungsi tanpa nama, dan closure, yaitu fungsi yang menangkap variabel di sekitarnya. Setelah membacanya, pola pengiriman perilaku sebagai argumen dan pola fungsi pembuat fungsi dapat ditulis serta dibaca dengan benar.
Fungsi sebagai nilai#
Sebuah fungsi bernama dapat diperlakukan sebagai nilai yang bertipe sesuai tanda tangannya. Program berikut mendeklarasikan tipe fungsi bernama OperasiSuhu, lalu mengirim dua fungsi konversi suhu sebagai argumen kepada fungsi lain.
package main
import "fmt"
// OperasiSuhu adalah tipe fungsi dari float64 ke float64.
type OperasiSuhu func(float64) float64
// terapkan menjalankan operasi pada suhu dalam derajat Celsius.
func terapkan(operasi OperasiSuhu, celsius float64) float64 {
return operasi(celsius)
}
func keFahrenheit(c float64) float64 {
return c*9/5 + 32
}
func keKelvin(c float64) float64 {
return c + 273.15
}
func main() {
fmt.Printf("25 C = %.2f F\n", terapkan(keFahrenheit, 25))
fmt.Printf("25 C = %.2f K\n", terapkan(keKelvin, 25))
}25 C = 77.00 F
25 C = 298.15 KEkspresi keFahrenheit tanpa tanda kurung menyebut fungsinya sebagai nilai, sedangkan keFahrenheit(25) memanggilnya. Fungsi terapkan menerima perilaku dari pemanggil dan menjalankannya melalui variabel parameternya. Pola ini disebut callback, yaitu fungsi yang dikirim agar dipanggil kembali oleh kode penerima, dan menjadi dasar banyak API pustaka standar seperti sort.Slice.
Pustaka standar memanfaatkan pola ini di banyak tempat. Fungsi sort.Slice mengurutkan data sesuai fungsi pembanding yang disediakan pemanggil, sehingga paket sort tidak perlu mengetahui makna datanya. Pemanggil menentukan urutan naik, urutan menurun, atau urutan menurut field tertentu tanpa menyentuh algoritma pengurutannya. Fungsi pembanding itu sendiri juga dapat dibentuk oleh closure apabila aturan urutannya bergantung pada keadaan di sekitarnya.
// tidak-dijalankan
sort.Slice(kecepatan, func(i, j int) bool {
return kecepatan[i] < kecepatan[j]
})Tipe fungsi dan fungsi tanpa nama#
Tipe fungsi ditulis lengkap dengan tanda tangannya, yaitu daftar tipe parameter dan tipe hasilnya. Tanda tangan yang sama juga dapat ditulis langsung sebagai tipe parameter, misalnya operasi func(float64) float64, dan deklarasi type hanya memperjelas penulisannya pada tanda tangan yang panjang. Dua tipe fungsi dianggap sama bila urutan tipe parameternya dan hasilnya sama, bukan karena namanya.
// tidak-dijalankan
type OperasiSuhu func(float64) float64
var konversi OperasiSuhu = func(c float64) float64 {
return c + 273.15
}Ekspresi func(c float64) float64 { ... } adalah fungsi tanpa nama (anonymous function), dibuat dan digunakan langsung tanpa deklarasi bernama. Nilai default dari variabel bertipe fungsi adalah nil, dan pemanggilan variabel yang masih nil menimbulkan panic. Karena itu, variabel fungsi perlu diisi sebelum dipanggil, atau diperiksa terhadap nil lebih dahulu.
Closure dan penangkapan variabel#
Fungsi tanpa nama dapat membaca dan mengubah variabel yang dideklarasikan di fungsi pengelilingnya. Pasangan fungsi dan variabel yang tertangkap itulah yang disebut closure. Variabel yang tertangkap tetap hidup selama fungsinya masih dapat dipanggil, sekalipun pemanggilan fungsi pengelilingnya sudah selesai.
package main
import "fmt"
// penghitungBaru mengembalikan fungsi baru yang menambah hitungan setiap
// dipanggil. Variabel hitungan tertangkap oleh closure yang dikembalikan.
func penghitungBaru(mulai int) func() int {
hitungan := mulai
return func() int {
hitungan++
return hitungan
}
}
func main() {
berikutnya := penghitungBaru(0)
fmt.Println(berikutnya(), berikutnya(), berikutnya())
penghitungLain := penghitungBaru(100)
fmt.Println(penghitungLain(), berikutnya())
}1 2 3
101 4Setiap pemanggilan penghitungBaru membuat variabel hitungan yang baru, sehingga berikutnya dan penghitungLain berhitung terpisah sebagaimana terlihat pada keluaran 101 4. Argumen pemanggilan fungsi dievaluasi dari kiri ke kanan, sehingga urutan cetaknya dapat diandalkan. Fungsi yang menangkap keadaan pribadi seperti ini sering disebut generator, yaitu pembuat deretan nilai secara berangsur.
Umur variabel yang tertangkap mengikuti fungsi yang menangkapnya. Kompilator Go menempatkan variabel semacam itu agar tetap hidup sekalipun pemanggilan fungsi pembuatnya sudah selesai, sehingga closure tidak pernah membaca variabel yang sudah dibebaskan dari memori. Mekanisme ini bekerja otomatis tanpa pengaturan apa pun dari sisi pemrogram.
Closure sebagai pembawa konfigurasi#
Penangkapan variabel juga menjadi cara ringkas membawa konfigurasi ke dalam fungsi. Fungsi pembuat menerima pengaturan sekali, lalu mengembalikan fungsi yang sudah membawa pengaturan itu tanpa perlu meneruskannya pada setiap pemanggilan. Program berikut membuat dua pelabel dengan satuan berbeda dari satu fungsi pembuat yang sama.
package main
import "fmt"
// pelabelBaru mengembalikan fungsi pelabel yang menangkap satuan.
func pelabelBaru(satuan string) func(float64) string {
return func(nilai float64) string {
return fmt.Sprintf("%.1f %s", nilai, satuan)
}
}
func main() {
suhuC := pelabelBaru("C")
kecepatanKnot := pelabelBaru("knot")
fmt.Println(suhuC(24.5), suhuC(23.0))
fmt.Println(kecepatanKnot(12.0), kecepatanKnot(9.5))
}24.5 C 23.0 C
12.0 knot 9.5 knotVariabel satuan tertangkap oleh fungsi yang dikembalikan pelabelBaru, sehingga suhuC dan kecepatanKnot menyimpan satuannya masing-masing untuk dipakai berulang kali. Pola ini menggantikan struct pengaturan pada fungsi sederhana, karena keadaannya tersembunyi di dalam closure. Fungsi pembuat semacam ini juga menjadi cara lazim menyusun fungsi berparameter panjang, yaitu bagian yang berubah disimpan oleh closure, sedangkan bagian yang tetap dibungkus lebih dahulu. Setiap pemanggilan pelabelBaru menghasilkan closure dengan keadaan yang terpisah, sama seperti perilaku penghitungBaru.
Kesalahan umum#
- Mengira closure menyimpan potret nilai pada saat dibuat. Closure menangkap variabelnya, bukan salinan nilainya, sehingga perubahan variabel setelah closure dibuat tetap terlihat oleh closure itu. Perbaikannya, salin nilai ke variabel lokal baru terlebih dahulu apabila potret nilai yang memang diinginkan.
- Membandingkan dua nilai fungsi dengan
==. Operator perbandingan untuk nilai bertipe fungsi tidak didefinisikan kecuali terhadapnil, sehingga perbandingan antarfungsi ditolak kompiler. Perbaikannya, bandingkan hasil pemanggilannya atau kirim fungsi pembanding sebagai argumen. - Lupa memanggil fungsi yang disimpan dalam variabel. Pernyataan
n := berikutnyamengisindengan nilai fungsi, bukan hasil hitungannya, dan gagal dikompilasi bilandipakai sebagai bilangan. Perbaikannya, sertakan tanda kurung, yaitun := berikutnya().
Ringkasan#
- Fungsi adalah nilai bertipe sesuai tanda tangannya dan dapat dikirim serta dikembalikan seperti nilai lain.
- Deklarasi
typemenamai tipe fungsi agar tanda tangan yang panjang mudah dibaca. - Fungsi tanpa nama dibuat langsung di dalam ekspresi dan lazim menjadi closure.
- Closure menangkap variabel, bukan potret nilai, sehingga keadaannya terus berbagi dengan fungsi pengeliling dan dapat membawa konfigurasi.
Referensi#
- First Class Functions in Go — The Go Blog — Tur kode pemakaian fungsi sebagai nilai untuk perhitungan statistik.
- The Go Programming Language Specification — The Go Authors — Bagian Function literals menetapkan semantik fungsi tanpa nama dan closure.