Pendahuluan
Jika anda pernah menulis kode Go, Anda mungkin pernah menjumpai tipe error
bawaan.
Kode Go menggunakan nilai error untuk mengindikasikan keadaan abnormal.
Misalnya, fungsi os.Open mengembalikan nilai error yang bukan nil saat
gagal membuka sebuah berkas.
func Open(name string) (file *File, err error)
Kode berikut menggunakan os.Open untuk membuka sebuah berkas.
Jika sebuah kesalahan terjadi, ia akan memanggil log.Fatal untuk mencetak
pesan kesalahan dan berhenti.
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// Lakukan operasi apa pun dengan *File f yang sudah dibuka.
Kita dapat menyelesaikan banyak hal dalam Go hanya dengan mengetahui tipe
error ini, namun dalam artikel ini kita akan melihat lebih dekat dan
mendiskusikan beberapa cara praktis tentang penanganan kesalahan dalam Go.
Tipe error
Tipe error adalah sebuah tipe interface.
Sebuah variabel error merepresentasikan nilai apa pun yang dapat
mendeskripsikan dirinya sendiri sebagai sebuah string.
Berikut deklarasi interface dari error:
type error interface {
Error() string
}
Tipe error, seperti halnya dengan tipe-tipe bawaan lainnya, telah
dideklarasikan
dalam
blok universal.
Implementasi error yang paling sering digunakan yaitu tipe errorString
dari paket
errors
yang tidak diekspor.
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
Kita dapat membuat nilai errorType dengan fungsi errors.New.
Fungsi tersebut menerima sebuah string yang kemudian dikonversi ke
errors.errorString dan mengembalikan sebuah nilai error.
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
Berikut cara menggunakan errors.New:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementasi
}
Fungsi Sqrt() yang menerima argumen sebuah nilai negatif akan mengembalikan
sebuah nilai error (yang merupakan representasi dari sebuah nilai
errors.errorString).
Yang memanggil fungsi Sqrt() dapat mengakses pesan kesalahan ("math: square
root of…") dengan memanggil method Error, atau cukup dengan mencetaknya:
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
Paket
fmt
memformat nilai error dengan memanggil method string Error() dari nilai
err tersebut.
Adalah tanggung jawab pengimplementasi eror untuk menyediakan konteks dari
kesalahan.
Pesan kesalahan yang dikembalikan oleh os.Open memiliki format
"open /etc/passwd: permission denied", bukan "permission denied" saja.
Pesan kesalahan yang dikembalikan oleh fungsi Sqrt() kita tidak memiliki
informasi tentang argumen yang tidak valid.
Untuk menambahkan informasi tersebut, salah satu fungsi yang berguna yaitu
Errorf dari paket fmt.
Fungsi Errorf memformat string menurut aturan Printf dan mengembalikan
sebuah error yang dibuat oleh errors.New.
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
Pada kebanyakan kasus, menggunakan fmt.Errorf sudah cukup bagus, namun
karena error adalah sebuah interface, kita dapat menggunakan struktur data
apa pun sebagai nilai error, untuk membolehkan pemanggil menginspeksi
detail dari kesalahan yang terjadi.
Misalnya, pemanggil dari Sqrt mungkin ingin memperbaiki argumen tidak valid
yang telah dikirim.
Kita dapat melakukan hal tersebut dengan mendefinisikan sebuah implementasi
error yang baru, bukan dengan menggunakan errors.errorString:
type NegativeSqrtError float64
func (f NegativeSqrtError) Error() string {
return fmt.Sprintf("math: square root of negative number %g", float64(f))
}
Pemanggil kemudian dapat menggunakan
tipe assertion
untuk memeriksa apakah
error adalah sebuah NegativeSqrtError dan melakukan penanganan secara
khusus, sementara itu pengguna fungsi Sqrt() yang menangani nilai error
dengan mengirim ke fmt.Println atau log.Fatal tidak mengalami atau melihat
perubahan perilaku apa pun.
Contoh lainnya, paket
json
menspesifikasikan tipe SyntaxError yang
dikembalikan oleh fungsi json.Decode saat menemukan kesalahan sintaksis
dari penguraian sebuah blob JSON.
type SyntaxError struct {
msg string // deskripsi dari error
Offset int64 // Offset saat kesalahan pembacaan terjadi
}
func (e *SyntaxError) Error() string { return e.msg }
Field Offset tidak ditampilkan dalam format pesan kesalahan, namun pemanggil
dari json.Decode dapat menggunakannya untuk menambahkan informasi berkas dan
baris pada pesan kesalahan mereka:
if err := dec.Decode(&val); err != nil {
if serr, ok := err.(*json.SyntaxError); ok {
line, col := findLine(f, serr.Offset)
return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
}
return err
}
(Contoh kode ini merupakan versi sederhana dari kode sebenarnya dari proyek Camlistore .)
Interface error hanya membutuhkan sebuah method Error;
implementasi kesalahan yang khusus bisa saja punya method-method tambahan.
Misalnya, paket net mengembalikan kesalahan bertipe error, mengikuti
konvensi, namun beberapa implementasi kesalahan memiliki method tambahan yang
didefinisikan oleh interface net.Error:
package net
type Error interface {
error
Timeout() bool // Apakah kesalahan karena waktu telah habis?
Temporary() bool // Apakah kesalahan sementara?
}
Kode dari klien dapat memeriksa net.Error dengan tipe assertion dan
membedakan antara kesalahan karena jaringan atau permanen.
Misalnya, sebuah web crawler bisa menunggu dan mencoba kembali saat
mengalami sebuah kesalahan sementara dan berhenti setelah mencoba beberapa
kali.
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
Menyederhanakan penanganan error yang berulang
Dalam Go, penanganan kesalahan sangat penting.
Konvensi dan rancangan bahasa Go mendorong kita untuk secara eksplisit
memeriksa kesalahan-kesalahan saat ia terjadi (yang berbeda dengan konvensi
pada bahasa pemrograman lainnya yang menggunakan "pelemparan" exception dan
terkadang "menangkap"-nya).
Pada beberapa kasus hal ini membuat kode Go menjadi panjang, namun untungnya
ada beberapa teknik yang dapat kita gunakan untuk mengurangi penanganan
kesalahan yang berulang.
Anggaplah kita punya sebuah aplikasi App Engine dengan handler HTTP yang menerima sebuah record dari datastore dan memformatnya dengan sebuah template.
func init() {
http.HandleFunc("/view", viewRecord)
}
func viewRecord(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
http.Error(w, err.Error(), 500)
return
}
if err := viewTemplate.Execute(w, record); err != nil {
http.Error(w, err.Error(), 500)
}
}
Fungsi tersebut mengurus kesalahan-kesalahan yang dikembalikan oleh fungsi
datastore.Get dan method viewTemplate.Execute.
Pada kedua kasus tersebut, kode di atas menampilkan sebuah pesan kesalahan
sederhana kepada user dengan HTTP status kode 500 ("Internal Server Error").
Jumlah baris pada kode tersebut tampak cukup bisa di-maintain, sampai kita
menambahkan beberapa handler HTTP lainnya dan akhirnya kita punya banyak
salinan kode penanganan error yang identik.
Untuk mengurangi duplikasi kita dapat mendefinisikan tipe appHandler
sendiri yang mengembalikan nilai error:
type appHandler func(http.ResponseWriter, *http.Request) error
Kemudian kita ganti fungsi viewRecord supaya mengembalikan error:
func viewRecord(w http.ResponseWriter, r *http.Request) error {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return err
}
return viewTemplate.Execute(w, record)
}
Kode tersebut lebih sederhana dari yang awalnya, namun paket
http
tidak mengenal fungsi-fungsi yang mengembalikan error.
Untuk memperbaiki hal tersebut kita dapat mengimplementasikan interface
http.Handler yaitu dengan menambahkan method ServeHTTP() pada tipe
appHandler:
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Error(), 500)
}
}
Method ServeHTTP() memanggil fungsi appHandler dan menampilkan pesan
kesalahan yang dikembalikan (jika ada) kepada user.
Perhatikan bahwa receiver method, fn, adalah sebuah fungsi.
(Go bisa melakukan hal tersebut!)
Method ServeHTTP akan memanggil fungsi fn dengan mengeksekusi ekspresi
fn(w, r).
Sekarang saat melakukan registrasi viewRecord pada paket http kita
menggunakan fungsi Handle (bukan HandleFunc) secara appHandler adalah
sebuah http.Handler (bukan http.HandlerFunc).
func init() {
http.Handle("/view", appHandler(viewRecord))
}
Dengan infrastruktur penanganan kesalahan dasar ini, kita dapat membuatnya lebih mudah digunakan. Tidak hanya menampilkan pesan kesalahan, akan lebih bagus memberikan pengguna sebuah pesan kesalahan dengan status kode HTTP yang sesuai, sementara tetap mencatat seluruh kesalahan ke App Engine developer console untuk tujuan pemeriksaan nantinya.
Untuk melakukan hal ini kita buat sebuah struct appError yang berisi sebuah
error dan beberapa field lainnya:
type appError struct {
Error error
Message string
Code int
}
Selanjutnya kita ubah tipe appHandler untuk mengembalikan nilai *appError:
type appHandler func(http.ResponseWriter, *http.Request) *appError
(Biasanya adalah sebuah kesalahan mengembalikan tipe konkret dari sebuah
error bukan sebuah nilai interface dari error, dengan alasan yang telah
didiskusikan dalam
Tanya Jawab Go,
namun untuk saat ini adalah pengecualian dan tepat untuk dilakukan karena
method ServeHTTP satu-satunya tempat yang tahu nilai dari error dan cara
menggunakan isinya.)
Selanjutnya kita buat method ServeHTTP dari appHandler supaya menampilkan
appError.Message kepada pengguna dengan status kode HTTP yang sesuai dan
mencatat keseluruhan Error ke developer console:
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e is *appError, not os.Error.
c := appengine.NewContext(r)
c.Errorf("%v", e.Error)
http.Error(w, e.Message, e.Code)
}
}
Terakhir, kita ubah viewRecord dengan penanda fungsi yang baru dan
membuatnya mengembalikan konteks lebih informatif saat menemui sebuah
kesalahan:
func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return &appError{err, "Record not found", 404}
}
if err := viewTemplate.Execute(w, record); err != nil {
return &appError{err, "Can't display record", 500}
}
return nil
}
Versi viewRecord di atas sama panjangnya dengan yang asli, namun sekarang
setiap baris memiliki makna tersendiri dan kita menyediakan penanganan
kesalahan yang lebih bersahabat.
Tidak hanya berakhir di sana; kita dapat meningkatkan lebih lanjut penanganan kesalahan dalam aplikasi kita. Berikut beberapa ide:
-
memberikan penanganan kesalahan sebuah template HTML
-
membuat debugging lebih mudah dengan menulis stack trace pada nilai kembalian HTTP saat user adalah administrator.
-
menulis sebuah fungsi constructor untuk
appErroryang menyimpan stack trace supaya dapat di debug lebih gampang. -
pemulihan dari kondisi panik di dalam
appHandler, mencatat kesalahan ke console sebagai "Critical", sementara menyampaikan kepada user bahwa "sebuah kesalahan kritis telah terjadi". Hal-hal tersebut adalah cara yang bagus untuk menghindari menampilkan kesalahan yang disebabkan oleh pemrograman kepada user. Lihat artikel Defer Panic dan Recover untuk lebih jelas.
Kesimpulan
Penanganan kesalahan yang baik adalah kebutuhan yang esensial dari perangkat lunak yang bagus. Dengan menggunakan teknik-teknik yang telah dijelaskan dalam artikel ini kita seharusnya dapat menulis kode Go yang lebih singkat dan dapat diandalkan.