Spesifikasi Penanganan Kesalahan Golang
Jenis Kesalahan
Terdapat beberapa opsi untuk mendeklarasikan kesalahan. Sebelum memilih opsi yang paling cocok untuk kasus penggunaan Anda, pertimbangkan hal berikut:
- Apakah pemanggil perlu mencocokkan kesalahan untuk menanganinya? Jika ya, kita harus mendukung fungsi
errors.Is
atauerrors.As
dengan mendeklarasikan variabel kesalahan tingkat atas atau jenis kustom. - Apakah pesan kesalahan berupa string statis atau string dinamis yang memerlukan informasi kontekstual? Untuk string statis, kita dapat menggunakan
errors.New
, namun untuk yang terakhir, kita harus menggunakanfmt.Errorf
atau jenis kesalahan kustom. - Apakah kita melewatkan kesalahan baru yang dikembalikan oleh fungsi turunan? Jika ya, lihat bagian pembungkus kesalahan.
Cocokan Kesalahan? | Pesan Kesalahan | Panduan |
---|---|---|
Tidak | statis | errors.New |
Tidak | dinamis | fmt.Errorf |
Ya | statis | variabel var tingkat atas dengan errors.New |
Ya | dinamis | jenis kesalahan kustom |
Sebagai contoh, gunakan errors.New
untuk merepresentasikan kesalahan dengan string statis. Jika pemanggil perlu mencocokkan dan menangan kesalahan ini, ekspor sebagai variabel untuk mendukung pencocokan dengan errors.Is
.
Tidak Ada Pencocokan Kesalahan
// package foo
func Open() error {
return errors.New("tidak dapat membuka")
}
// package bar
if err := foo.Open(); err != nil {
// Tidak dapat menangani kesalahan.
panic("kesalahan tidak diketahui")
}
Pencocokan Kesalahan
// package foo
var ErrTidakDapatMembuka = errors.New("tidak dapat membuka")
func Open() error {
return ErrTidakDapatMembuka
}
// package bar
if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrTidakDapatMembuka) {
// menangani kesalahan
} else {
panic("kesalahan tidak diketahui")
}
}
Untuk kesalahan dengan string dinamis, gunakan fmt.Errorf
jika pemanggil tidak perlu mencocokkannya. Jika pemanggil memang perlu mencocokkannya, maka gunakan kesalahan error
kustom.
Tidak Ada Pencocokan Kesalahan
// package foo
func Open(file string) error {
return fmt.Errorf("file %q tidak ditemukan", file)
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
// Tidak dapat menangani kesalahan.
panic("kesalahan tidak diketahui")
}
Pencocokan Kesalahan
// package foo
type KesalahanTidakDitemukan struct {
File string
}
func (e *KesalahanTidakDitemukan) Error() string {
return fmt.Sprintf("file %q tidak ditemukan", e.File)
}
func Open(file string) error {
return &KesalahanTidakDitemukan{File: file}
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
var tidakDitemukan *KesalahanTidakDitemukan
if errors.As(err, &tidakDitemukan) {
// menangani kesalahan
} else {
panic("kesalahan tidak diketahui")
}
}
Perhatikan bahwa jika Anda mengekspor variabel atau jenis kesalahan dari sebuah paket, maka itu menjadi bagian dari API publik paket tersebut.
Pembungkusan Kesalahan
Ketika terjadi kesalahan saat memanggil metode lain, biasanya ada tiga cara untuk menanganinya:
- Mengembalikan kesalahan aslinya apa adanya.
- Menggunakan
fmt.Errorf
dengan%w
untuk menambahkan konteks ke kesalahan dan kemudian mengembalikannya. - Menggunakan
fmt.Errorf
dengan%v
untuk menambahkan konteks ke kesalahan dan kemudian mengembalikannya.
Jika tidak ada konteks tambahan yang perlu ditambahkan, kembalikan kesalahan aslinya apa adanya. Ini akan mempertahankan tipe kesalahan asli dan pesannya. Ini sangat cocok ketika pesan kesalahan yang mendasarinya sudah cukup untuk melacak dari mana kesalahan berasal.
Namun, tambahkan konteks ke pesan kesalahan sebanyak mungkin agar kesalahan yang ambigu seperti "connection refused" tidak terjadi. Sebagai gantinya, Anda akan menerima kesalahan yang lebih berguna, seperti "memanggil layanan foo: connection refused".
Gunakan fmt.Errorf
untuk menambahkan konteks ke kesalahan Anda dan pilih di antara kata kerja %w
atau %v
berdasarkan apakah pemanggil harus dapat mencocokkan dan mengekstrak penyebab akar.
- Gunakan
%w
jika pemanggil harus memiliki akses ke kesalahan yang mendasarinya. Ini adalah standar yang baik untuk sebagian besar pembungkusan kesalahan, tetapi perlu diingat bahwa pemanggil dapat mulai mengandalkan perilaku ini. Oleh karena itu, untuk kesalahan pembungkusan yang merupakan variabel atau tipe yang dikenal, cek dan uji mereka sebagai bagian dari kontrak fungsi. - Gunakan
%v
untuk menyamarkan kesalahan yang mendasarinya. Pemanggil tidak akan dapat mencocokkannya, tetapi Anda dapat beralih ke%w
di masa depan jika diperlukan.
Saat menambahkan konteks ke kesalahan yang dikembalikan, hindari menggunakan frasa seperti "failed to" agar konteksnya ringkas. Ketika kesalahan menyebar melalui tumpukan, akan ditumpuk lapis demi lapis:
Tidak Disarankan:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %w", err)
}
// failed to x: failed to y: failed to create new store: the error
Disarankan:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %w", err)
}
// x: y: new store: the error
Namun, setelah kesalahan dikirim ke sistem lain, harus jelas bahwa pesan tersebut adalah sebuah kesalahan (mis., tag "err" atau awalan "Gagal" di log).
Penamaan yang Salah
Untuk nilai kesalahan yang disimpan sebagai variabel global, gunakan awalan Err
atau err
tergantung apakah mereka diekspor. Silakan lihat panduan. Untuk konstanta dan variabel tingkat atas yang tidak diekspor, gunakan garis bawah (_) sebagai awalan.
var (
// Ekspor dua kesalahan berikut agar pengguna paket ini dapat mencocokkannya dengan errors.Is.
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// Kesalahan ini tidak diekspor karena kami tidak ingin itu menjadi bagian dari API publik kami. Kami masih dapat menggunakannya dalam paket dengan errors.
errNotFound = errors.New("not found")
)
Untuk jenis kesalahan kustom, gunakan akhiran Error
.
// Demikian pula, kesalahan ini diekspor agar pengguna paket ini dapat mencocokkannya dengan errors.As.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// Kesalahan ini tidak diekspor karena kami tidak ingin itu menjadi bagian dari API publik. Kami masih bisa menggunakannya dalam paket dengan errors.As.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
Penanganan Kesalahan
Ketika pemanggil menerima kesalahan dari pemanggil, dapat menangani kesalahan dengan berbagai cara berdasarkan pemahaman atas kesalahan tersebut.
Ini termasuk tetapi tidak terbatas pada:
- Memadankan kesalahan dengan
errors.Is
atauerrors.As
jika pemanggil telah menyetujui definisi kesalahan tertentu, dan menangani percabangan dengan cara yang berbeda - Mencatat kesalahan dan merosot dengan indah jika kesalahan dapat dipulihkan
- Mengembalikan kesalahan yang telah ditentukan dengan baik jika itu mewakili kondisi kegagalan spesifik domain
- Mengembalikan kesalahan, baik itu dibungkus atau apa adanya
Terlepas dari bagaimana pemanggil menangani kesalahan, biasanya seharusnya hanya menangani setiap kesalahan sekali. Misalnya, pemanggil seharusnya tidak mencatat kesalahan dan kemudian mengembalikannya, karena pemanggilnya juga mungkin menangani kesalahan tersebut.
Sebagai contoh, pertimbangkan skenario berikut:
Buruk: Mencatat kesalahan dan mengembalikannya
Pemanggil lain lebih tinggi di tumpukan mungkin mengambil tindakan serupa terhadap kesalahan ini. Ini akan menciptakan banyak kebisingan dalam log aplikasi dengan sedikit manfaat.
u, err := getUser(id)
if err != nil {
// BURUK: Lihat deskripsi
log.Printf("Tidak dapat mendapatkan pengguna %q: %v", id, err)
return err
}
Baik: Membungkus kesalahan dan mengembalikkannya
Kesalahan lebih tinggi di tumpukan akan menangani kesalahan ini. Menggunakan %w
memastikan bahwa mereka dapat memadankan kesalahan dengan errors.Is
atau errors.As
jika relevan.
u, err := getUser(id)
if err != nil {
return fmt.Errorf("mendapatkan pengguna %q: %w", id, err)
}
Baik: Mencatat kesalahan dan merosot dengan indah
Jika operasi tidak mutlak diperlukan, kita dapat memberikan keruntuhan yang mulus dengan pulih darinya tanpa mengganggu pengalaman.
if err := emitMetrics(); err != nil {
// Kegagalan menulis metrik seharusnya tidak
// memutuskan aplikasi.
log.Printf("Tidak dapat mengeluarkan metrik: %v", err)
}
Baik: Memadankan kesalahan dan merosot dengan indah sesuai
Jika pemanggil telah menentukan kesalahan spesifik dalam persetujuannya dan kegagalan dapat dipulihkan, cocokkan kasus kesalahan tersebut dan merosot dengan indah. Untuk semua kasus lain, bungkus kesalahan dan kembalikan. Kesalahan lebih tinggi di tumpukan akan menangani kesalahan lain.
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// Pengguna tidak ada. Gunakan UTC.
tz = time.UTC
} else {
return fmt.Errorf("mendapatkan pengguna %q: %w", id, err)
}
}
Penanganan Gagal Asersi
Asumsi tipe akan memunculkan panic dengan satu nilai pengembalian jika deteksi tipe tidak benar. Oleh karena itu, selalu gunakan idiom "koma, OK".
Tidak Direkomendasikan:
t := i.(string)
Direkomendasikan:
t, ok := i.(string)
if !ok {
// Tangani kesalahan dengan indah
}
Hindari menggunakan panic
Kode yang dijalankan di lingkungan produksi harus menghindari panic. Panic adalah sumber utama kegagalan beruntun. Jika terjadi kesalahan, fungsi harus mengembalikan kesalahan dan membiarkan pemanggil untuk menentukan bagaimana cara menanganinya.
Tidak disarankan:
func run(args []string) {
if len(args) == 0 {
panic("diperlukan satu argumen")
}
// ...
}
func main() {
run(os.Args[1:])
}
Disarankan:
func run(args []string) error {
if len(args) == 0 {
return errors.New("diperlukan satu argumen")
}
// ...
return nil
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
panic/recover bukanlah strategi penanganan kesalahan. Panic hanya boleh terjadi ketika suatu kejadian tak dapat dipulihkan (misalnya, referensi nil) terjadi. Satu pengecualian adalah selama inisialisasi program: situasi yang menyebabkan program panic harus ditangani selama startup program.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
Bahkan dalam kode uji coba, disarankan untuk menggunakan t.Fatal
atau t.FailNow
daripada panic untuk memastikan bahwa kegagalan ditandai.
Tidak disarankan:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
panic("gagal menyiapkan uji coba")
}
Disarankan:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal("gagal menyiapkan uji coba")
}