Sentinel error hanya menjawab satu pertanyaan: apakah kegagalan ini kegagalan itu? Sering kali pemanggil butuh lebih dari jawaban ya-tidak — validasi form perlu tahu field mana yang salah, modul pembayaran perlu tahu rekening dan nominalnya. Untuk kebutuhan itu, Go membiarkan kita mendefinisikan tipe error sendiri berupa struct dengan field tambahan. Pada chapter ini kita akan membangun ErrorValidasi, memberinya method Error() dan Unwrap(), lalu mengambil isinya kembali dengan errors.As.
Tipe Error dengan Data Tambahan #
Titik awalnya struct biasa dengan field yang kita pilih sendiri:
type ErrorValidasi struct {
Field string
Pesan string
Sebab error
}Field Field dan Pesan membawa informasi yang teks pesan error tidak bisa simpan secara terstruktur, sedangkan Sebab menyimpan error asli yang memicu kegagalan. Berbeda dengan sentinel, setiap kejadian error bisa membawa data berbeda: sekali gagal di field umur, sekali lagi di field email.
Method Error dan Unwrap #
Struct di atas menjadi error begitu method Error() string dideklarasikan. Konvensi Go memakai pointer receiver *ErrorValidasi — dengan demikian tipe yang mengimplementasikan error adalah *ErrorValidasi, dan nilainya selalu dibuat lewat &ErrorValidasi{...}:
func (e *ErrorValidasi) Error() string {
if e.Sebab != nil {
return fmt.Sprintf("field %s: %s: %v", e.Field, e.Pesan, e.Sebab)
}
return fmt.Sprintf("field %s: %s", e.Field, e.Pesan)
}
func (e *ErrorValidasi) Unwrap() error {
return e.Sebab
}Method Unwrap() satu baris itu berperan besar: fungsi errors.Is dan errors.As memakainya untuk melanjutkan pencarian ke error penyebab. Tanpa Unwrap, rantai terputus tepat di ErrorValidasi.
Mengambil Detail dengan errors.As #
Sentinel menjawab “jenis kegagalan apa?”, tipe custom menjawab “apa isinya?”. Pemeriksaan isinya memakai errors.As dengan target berupa pointer ke tipe error kita:
var ev *ErrorValidasi
if errors.As(err, &ev) {
fmt.Println(ev.Field, ev.Pesan)
}Bila err atau salah satu penyebabnya bertipe *ErrorValidasi, errors.As mengisi ev dengan nilai tersebut sehingga field-nya bisa dibaca langsung. Perbandingan keduanya dirangkum berikut:
| Aspek | Sentinel error | Custom error type |
|---|---|---|
| Bentuk | satu var ErrX = errors.New(...) | struct + method Error() |
| Data yang dibawa | tidak ada, hanya identitas | field sebanyak yang diperlukan |
| Pemeriksaan | errors.Is | errors.As |
| Kebutuhan umum | keputusan bercabang | detail untuk log, pesan API, atau keputusan bisnis |
Program Utuh #
Berkas custom-error-demo.go menjalankan validasi umur dengan tiga masukan dan mengupas error hasilnya:
package main
import (
"errors"
"fmt"
"strconv"
)
// ErrUmurNegatif adalah sentinel penyebab untuk umur di bawah nol.
var ErrUmurNegatif = errors.New("umur tidak boleh negatif")
// ErrorValidasi membawa konteks field dan pesan, bukan sekadar teks.
type ErrorValidasi struct {
Field string
Pesan string
Sebab error
}
func (e *ErrorValidasi) Error() string {
if e.Sebab != nil {
return fmt.Sprintf("field %s: %s: %v", e.Field, e.Pesan, e.Sebab)
}
return fmt.Sprintf("field %s: %s", e.Field, e.Pesan)
}
func (e *ErrorValidasi) Unwrap() error {
return e.Sebab
}
func validasiUmur(teks string) error {
umur, err := strconv.Atoi(teks)
if err != nil {
return &ErrorValidasi{Field: "umur", Pesan: "bukan angka", Sebab: err}
}
if umur < 0 {
return &ErrorValidasi{Field: "umur", Pesan: "negatif", Sebab: ErrUmurNegatif}
}
return nil
}
func main() {
for _, masukan := range []string{"17", "-4", "dua puluh"} {
err := validasiUmur(masukan)
fmt.Printf("masukan=%q err=%v\n", masukan, err)
if err == nil {
continue
}
var ev *ErrorValidasi
if errors.As(err, &ev) {
fmt.Printf(" field=%q pesan=%q\n", ev.Field, ev.Pesan)
}
fmt.Println(" errors.Is ErrUmurNegatif:", errors.Is(err, ErrUmurNegatif))
}
}Jalankan program dari direktori chapter dengan perintah berikut:
go run custom-error-demo.goOutput yang Dihasilkan:
masukan="17" err=<nil>
masukan="-4" err=field umur: negatif: umur tidak boleh negatif
field="umur" pesan="negatif"
errors.Is ErrUmurNegatif: true
masukan="dua puluh" err=field umur: bukan angka: strconv.Atoi: parsing "dua puluh": invalid syntax
field="umur" pesan="bukan angka"
errors.Is ErrUmurNegatif: falseMari kita telaah baris demi baris program di atas:
- Baris 13-17 (
type ErrorValidasi struct): tiga field menyimpan nama field, ringkasan masalah, dan error penyebab — data yang tidak mampu ditampung sentinel. - Baris 19-24 (
func (e *ErrorValidasi) Error()): method dengan pointer receiver merangkai pesan; saatSebabterisi, teks penyebab ikut tercetak di belakang pesan utama. - Baris 26-28 (
func (e *ErrorValidasi) Unwrap()): method ini membuka error penyebab sehinggaerrors.Isdanerrors.Asdapat menjelajah rantai lebih dalam. - Baris 30-39 (
func validasiUmur): dua jalur gagal mengembalikan*ErrorValidasidenganSebabberbeda — error daristrconv.Atoidan sentinelErrUmurNegatif. - Baris 48-51 (
errors.As(err, &ev)): targetevbertipe*ErrorValidasi; bila pencarian sukses, fieldFielddanPesanterbaca langsung tanpa mem-parsing teks pesan. - Baris 52 (
errors.Is ErrUmurNegatif): hasilnyatruehanya untuk masukan-4karenaUnwrapmengembalikanErrUmurNegatifsebagai penyebab, danfalseuntuk masukan non-angka.
Perhatikan output masukan "dua puluh": pesan lengkapnya memuat rantai field umur: bukan angka: strconv.Atoi: .... Rantai itu terbentuk karena ErrorValidasi menyimpan error strconv sebagai Sebab. Bagaimana jika kita ingin menambahkan konteks lapisan demi lapisan tanpa mendefinisikan struct baru setiap kali? Chapter berikutnya, Error Wrapping
, memperkenalkan verb %w untuk hal tersebut.
Referensi #
- Working with Errors in Go 1.13
— artikel resmi tentang tipe custom dengan
Unwrap. - Dokumentasi errors: As
— aturan target
errors.As. - Error handling and Go — contoh tipe error dengan field tambahan.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
custom-error-demo.go
Unduh berkaspackage main
import (
"errors"
"fmt"
"strconv"
)
// ErrUmurNegatif adalah sentinel penyebab untuk umur di bawah nol.
var ErrUmurNegatif = errors.New("umur tidak boleh negatif")
// ErrorValidasi membawa konteks field dan pesan, bukan sekadar teks.
type ErrorValidasi struct {
Field string
Pesan string
Sebab error
}
func (e *ErrorValidasi) Error() string {
if e.Sebab != nil {
return fmt.Sprintf("field %s: %s: %v", e.Field, e.Pesan, e.Sebab)
}
return fmt.Sprintf("field %s: %s", e.Field, e.Pesan)
}
func (e *ErrorValidasi) Unwrap() error {
return e.Sebab
}
func validasiUmur(teks string) error {
umur, err := strconv.Atoi(teks)
if err != nil {
return &ErrorValidasi{Field: "umur", Pesan: "bukan angka", Sebab: err}
}
if umur < 0 {
return &ErrorValidasi{Field: "umur", Pesan: "negatif", Sebab: ErrUmurNegatif}
}
return nil
}
func main() {
for _, masukan := range []string{"17", "-4", "dua puluh"} {
err := validasiUmur(masukan)
fmt.Printf("masukan=%q err=%v\n", masukan, err)
if err == nil {
continue
}
var ev *ErrorValidasi
if errors.As(err, &ev) {
fmt.Printf(" field=%q pesan=%q\n", ev.Field, ev.Pesan)
}
fmt.Println(" errors.Is ErrUmurNegatif:", errors.Is(err, ErrUmurNegatif))
}
}