Error adalah nilai

Rob Pike
12 Januari 2015

Poin diskusi paling sering di antara para pemrogram Go, khususnya yang baru dengan bahasa, yaitu bagaimana cara menangani error. Pembahasan sering kali menjadi keluhan saat beberapa urutan kode

if err != nil {
	return err
}

muncul. Kami baru-baru ini memindai semua proyek-proyek sumber terbuka yang dapat kami cari dan menemukan bahwa penggalan kode di atas terjadi hanya satu atau dua kali per halaman, lebih sedikit daripada apa yang orang percaya. Tetap saja, jika persepsi bahwa seseorang harus mengetik

if err != nil

setiap waktu, tampaknya ada sesuatu yang salah, dan biasanya target yang sering disalahkan adalah bahasa Go itu sendiri.

Sayangnya hal ini tidak benar, dan mudah untuk dikoreksi. Mungkin yang terjadi adalah para pemrogram yang baru dengan Go bertanya, "Bagaimana cara menangani error?", memelajari polanya, dan berhenti di sana. Pada bahasa pemrograman yang lain, ada yang menggunakan blok try-catch atau mekanisme lain untuk penanganan error. Oleh karena itu, pemrogram berpikir, saat saya menggunakan try-catch dalam bahasa lama saya, saya akan tulis if err != nil dalam Go. Seiring waktu kode Go tersebut menjadi banyak pengecekan seperti itu, dan hasilnya tampak janggal.

Apakah penjelasan tersebut benar atau tidak, cukup jelas bahwa pemrogram Go tersebut tidak memahami inti dasar tentang error: Error adalah nilai.

Nilai dapat diprogram, dan secara error adalah nilai, maka error dapat diprogram.

Tentu saja pernyataan yang sering berkaitan dengan nilai error yaitu pengecekan apakah ia nil, namun ada banyak hal lain yang dapat kita lakukan dengan nilai error, dan penerapan dari hal lain tersebut dapat membuat program kita lebih baik, mengeliminasi banyak kode yang bakal muncul bila setiap error di cek dengan perintah if.

Berikut contoh sederhana dari tipe Scanner pada paket bufio. Method Scan melakukan input/output (I/O), yang tentu saja dapat menyebabkan error. Namun method Scan tidak mengekspose error sama sekali. Method tersebut mengembalikan sebuah boolean, dan method yang terpisah yang dijalankan di akhir pemindaian, melaporkan apakah ada error atau tidak. Kode pada sisi klien akan seperti berikut:

scanner := bufio.NewScanner(input)
for scanner.Scan() {
	token := scanner.Text()
	// proses token
}
if err := scanner.Err(); err != nil {
	// proses error
}

Tentu saja, ada pengecekan nil pada error, namun ia muncul dan dieksekusi sekali. Method Scan bisa saja didefinisikan sebagai

func (s *Scanner) Scan() (token []byte, error)

dan kemudian contoh kode bisa menjadi (tergantung bagaimana token diterima),

scanner := bufio.NewScanner(input)
for {
	token, err := scanner.Scan()
	if err != nil {
		return err // atau bisa pakai break
	}
	// proses token
}

Hal ini tidak berbeda, tetapi ada satu keistimewaan penting. Dalam kode tersebut, klien harus memeriksa error di setiap iterasi, namun dalam API Scanner, penanganan error diabstraksikan dari elemen kunci API, yang mana mengiterasi token. Dengan API yang aslinya, kode pada klien tampak lebih alami: lakukan pengulangan sampai selesai, kemudian baru cek error. Penanganan error tidak menutupi alur kontrol.

Di belakangnya yang terjadi, tentu saja, yaitu saat Scan mengalami error I/O, ia akan mencatatnya dan mengembalikan false. Method terpisah, Err, melaporkan nilai error saat klien membutuhkannya. Meskipun tampak sepele, hal ini tidak sama dengan menulis

if err != nil

di mana saja atau menanyakan klien untuk memeriksa error untuk setiap token. Ini adalah contoh pemrograman dengan nilai error. Pemrograman yang sederhana, ya, namun tetap saja pemrograman.

Perlu ditekankan bahwa apa pun rancangannya, sangatlah penting supaya program memeriksa error saat mereka muncul. Diskusi saat ini bukan tentang bagaimana menghindari pengecekan error, namun tentang menggunakan bahasa untuk menangani error dengan apik.

Topik tentang kode pengecekan-error yang berulang-ulang, muncul saat saya menghadiri GoCon 2014 di Tokyo. Seorang gopher antusias, menggunakan akun @jxck di Twitter, mengeluhkan tentang pengecekan error. Dia memiliki kode yang secara semantik seperti ini:

_, err = fd.Write(p0[a:b])
if err != nil {
	return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
	return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
	return err
}
// dan seterusnya

Tampak sangat berulang. Dalam kode di dunia nyata, yang lebih panjang, banyak hal yang terjadi sehingga tidaklah mudah dengan mengganti hal ini dengan sebuah fungsi pembantu, namun dalam bentuk yang idealnya, sebuah fungsi yang berakhir dengan variabel error akan cukup membantu:

var err error
write := func(buf []byte) {
	if err != nil {
		return
	}
	_, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// dan seterusnya
if err != nil {
	return err
}

Pola ini bekerja dengan baik, namun membutuhkan sebuah closure di setiap fungsi saat melakukan penulisan (write); Sebuah fungsi pembantu terpisah lebih janggal digunakan karena variabel err perlu dijaga selama pemanggilan (coba lah).

Kita dapat membuat hal ini lebih jelas, lebih umum, dan bisa digunakan ulang dengan meminjam ide dari method Scan di atas. Saya menulis teknik ini dalam diskusi kita tetapi @jxck_ tidak tahu cara menggunakannya. Setelah berdiskusi lama, terhambat karena batasan bahasa, saya bertanya apakah dapat meminjam laptopnya dan memperlihatkannya dengan menulis beberapa kode.

Saya mendefinisikan sebuah objek yang disebut errWriter, seperti ini:

type errWriter struct {
	w   io.Writer
	err error
}

dan menambahkan sebuah method write. Method tersebut tidak perlu penanda Write yang standar, dan sengaja ditulis huruf kecil untuk memperlihatkan perbedaannya. Method write memanggil method Write dari Writer dan mencatat error yang pertama kali terjadi untuk referensi nantinya:

func (ew *errWriter) write(buf []byte) {
	if ew.err != nil {
		return
	}
	_, ew.err = ew.w.Write(buf)
}

Saat error terjadi, method write menjadi no-op (tidak beroperasi lagi) namun nilai error telah tersimpan.

Dengan tipe errWriter dan method write-nya, kode di atas dapat ditulis ulang menjadi:

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// dan seterusnya.
if ew.err != nil {
	return ew.err
}

Kode ini lebih jelas, dibandingkan dengan penggunaan closure, dan juga membuat urutan write lebih mudah dilihat dalam satu halaman. Tidak ada lagi kekusutan. Pemrograman dengan nilai error (dan interface) telah membuat kode lebih bagus.

Bisa saja bagian kode lain dalam paket yang sama dapat dibangun dengan ide ini, atau bahkan langsung menggunakan errWriter.

Juga, sekali errWriter ada, banyak hal yang dapat dilakukannya. Misalnya, ia bisa digunakan untuk menghitung byte. Ia bisa menggabungkan beberapa penulisan ke sebuah buffer yang kemudian dapat dikirim secara terpisah. Dan banyak lagi.

Pada kenyataannya, pola ini sering muncul dalam pustaka standar. Paket archive/zip dan net/http menggunakannya. Yang lebih menonjol, method Writer pada paket bufio sebenarnya implementasi dari ide errWriter. Walaupun bufio.Writer.Write mengembalikan error, hal ini semata-mata demi mengikuti interface dari io.Writer. Method Write pada bufio.Writer mirip dengan method errWriter.write kita di atas, dengan Flush yang melaporkan error, sehingga contoh kita di atas dapat ditulis seperti:

b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// dan seterusnya
if b.Flush() != nil {
	return b.Flush()
}

Ada satu kelemahan dari pendekatan ini, setidaknya pada beberapa aplikasi: kita tidak bisa mengetahui berapa banyak pemrosesan selesai sebelum error terjadi. Jika informasi tersebut penting, pendekatan yang lebih halus diperlukan. Terkadang, pengecekan ada-atau-tidak nya error pada akhirnya sudah cukup.

Kita telah melihat salah satu teknik untuk menghindari kode yang mengulang penanganan error. Ingatlah bahwa penggunakan errWriter atau bufio.Writer bukanlah satu-satunya cara untuk menyederhanakan penanganan error, dan pendekatan ini belum tentu sesuai dengan semua situasi. Pelajaran yang dapat diambil dari sini yaitu error adalah nilai dan kekuatan penuh dari bahasa pemrograman Go tersedia untuk memrosesnya.

Gunakan bahasa untuk menyederhanakan penanganan error Anda.

Namun ingat: Apa pun yang Anda lakukan, selalu periksa error!

Terakhir, untuk cerita lengkap tentang interaksi saya dengan @jxck_, termasuk video singkat yang dia rekam, kunjungi blognya.