Selama dekade terakhir, kami telah berhasil mengeksploitasi fakta bahwa Go menangani
kesalahan sebagai nilai . Meskipun pustaka standar memiliki dukungan minimal untuk kesalahan: hanya kesalahan.
errors.New
dan
fmt.Errorf
yang menghasilkan kesalahan yang hanya mengandung pesan - antarmuka
fmt.Errorf
memungkinkan programmer untuk menambahkan informasi apa pun. Yang Anda butuhkan adalah tipe yang mengimplementasikan metode
Error
:
type QueryError struct { Query string Err error } func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
Jenis kesalahan ini ditemukan dalam semua bahasa dan menyimpan berbagai informasi, dari cap waktu hingga nama file dan alamat server. Kesalahan tingkat rendah yang menyediakan konteks tambahan sering disebutkan.
Pola, ketika satu kesalahan mengandung yang lain, sangat sering ditemui di Go sehingga setelah
diskusi yang hangat di Go 1.13 dukungan eksplisitnya ditambahkan. Pada artikel ini, kita akan melihat penambahan ke pustaka standar yang menyediakan dukungan yang disebutkan: tiga fungsi baru dalam paket kesalahan dan perintah pemformatan baru untuk
fmt.Errorf
.
Sebelum membahas perubahan secara rinci, mari kita bicara tentang bagaimana kesalahan diselidiki dan dibuat dalam versi bahasa sebelumnya.
Kesalahan sebelum Go 1.13
Penelitian galat
Kesalahan dalam Go adalah artinya. Program membuat keputusan berdasarkan nilai-nilai ini dengan cara yang berbeda. Paling sering, kesalahan dibandingkan dengan nol untuk melihat apakah operasi gagal.
if err != nil {
Terkadang kami membandingkan kesalahan untuk mengetahui nilai
kontrol dan melihat apakah ada kesalahan tertentu.
var ErrNotFound = errors.New("not found") if err == ErrNotFound {
Nilai kesalahan bisa dari jenis apa pun yang memenuhi antarmuka kesalahan yang ditentukan dalam bahasa. Suatu program dapat menggunakan pernyataan tipe atau saklar tipe untuk melihat nilai kesalahan dari tipe yang lebih spesifik.
type NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + ": not found" } if e, ok := err.(*NotFoundError); ok {
Menambahkan Informasi
Seringkali suatu fungsi melewatkan kesalahan di tumpukan panggilan, menambahkan informasi ke dalamnya, misalnya, deskripsi singkat tentang apa yang terjadi ketika kesalahan terjadi. Ini mudah dilakukan, cukup buat kesalahan baru yang menyertakan teks dari kesalahan sebelumnya:
if err != nil { return fmt.Errorf("decompress %v: %v", name, err) }
Saat membuat kesalahan baru menggunakan
fmt.Errorf
kami membuang semuanya kecuali teks dari kesalahan asli. Seperti yang kita lihat dalam contoh
QueryError
, kadang-kadang Anda perlu mendefinisikan jenis kesalahan baru yang berisi kesalahan asli untuk menyimpannya untuk analisis menggunakan kode:
type QueryError struct { Query string Err error }
Program dapat melihat di dalam nilai
*QueryError
dan membuat keputusan berdasarkan kesalahan asli. Ini kadang-kadang disebut pembatalan kesalahan.
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
Jenis
os.PathError
dari pustaka standar adalah contoh lain tentang bagaimana satu kesalahan berisi yang lain.
Kesalahan dalam Go 1.13
Membuka Metode
Dalam Go 1.13, paket standar pustaka
errors
dan
fmt
menyederhanakan
fmt
kesalahan yang berisi kesalahan lain. Yang paling penting adalah konvensi, bukan perubahan: kesalahan yang mengandung kesalahan lain dapat menerapkan metode
Unwrap
, yang mengembalikan kesalahan asli. Jika
e1.Unwrap()
mengembalikan
e2
, maka kita mengatakan bahwa
paket e1
e2
dan Anda dapat
membongkar paket e1
untuk mendapatkan
e2
.
Menurut konvensi ini, Anda bisa memberikan tipe
QueryError
dijelaskan di atas ke metode
QueryError
, yang mengembalikan kesalahan yang terkandung di dalamnya:
func (e *QueryError) Unwrap() error { return e.Err }
Hasil membongkar kesalahan juga dapat berisi metode
Unwrap
. Urutan kesalahan yang diperoleh melalui pembongkaran berulang, kami sebut
rantai kesalahan .
Investigasi kesalahan dengan Is dan As
Di Go 1.13, paket
errors
berisi dua fungsi baru untuk menyelidiki kesalahan:
Is
dan
As
.
Kesalahan. Fungsi membandingkan kesalahan dengan nilai.
Fungsi
As
memeriksa apakah kesalahan berasal dari tipe tertentu.
Dalam kasus paling sederhana, fungsi
errors.Is
berperilaku seperti perbandingan dengan kesalahan kontrol, dan fungsi
errors.As
berfungsi seperti pernyataan tipe. Namun, ketika bekerja dengan kesalahan yang dikemas, fungsi-fungsi ini mengevaluasi semua kesalahan dalam rantai. Mari kita lihat contoh
QueryError
atas untuk memeriksa kesalahan asli:
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
Menggunakan fungsi
errors.Is
. Apakah
errors.Is
dapat menulis ini:
if errors.Is(err, ErrPermission) {
Paket
errors
juga berisi fungsi
Unwrap
baru yang mengembalikan hasil memanggil metode
Unwrap
kesalahan, atau mengembalikan nol jika kesalahan tidak memiliki metode
Unwrap
. Biasanya lebih baik menggunakan
errors.Is
atau salah. Karena, karena mereka memungkinkan Anda untuk memeriksa seluruh rantai dalam satu panggilan.
Kemasan galat dengan% w
Seperti yang saya sebutkan, itu adalah praktik normal untuk menggunakan fungsi
fmt.Errorf
untuk menambahkan informasi tambahan ke kesalahan.
if err != nil { return fmt.Errorf("decompress %v: %v", name, err) }
Di Go 1.13, fungsi
fmt.Errorf
mendukung perintah
%w
baru. Jika ya, maka kesalahan yang dikembalikan oleh
fmt.Errorf
akan berisi metode
Unwrap
yang mengembalikan argumen
%w
, yang seharusnya merupakan kesalahan. Dalam semua kasus lain,
%w
identik dengan
%v
.
if err != nil {
Packing kesalahan dengan
%w
membuatnya tersedia untuk
errors.Is
dan
errors.As
:
err := fmt.Errorf("access denied: %w", ErrPermission) ... if errors.Is(err, ErrPermission) ...
Kapan harus berkemas?
Ketika Anda menambahkan konteks tambahan untuk kesalahan menggunakan
fmt.Errorf
atau implementasi jenis kustom, Anda perlu memutuskan apakah kesalahan baru akan berisi yang asli. Tidak ada jawaban tunggal untuk ini, semuanya tergantung pada konteks di mana kesalahan baru dibuat. Paket untuk menunjukkan peneleponnya. Jangan mengemas kesalahan jika ini menyebabkan pengungkapan rincian implementasi.
Misalnya, bayangkan fungsi
Parse
yang membaca struktur data yang kompleks dari
io.Reader
. Jika terjadi kesalahan, kami ingin mengetahui jumlah baris dan kolom tempat terjadinya. Jika terjadi kesalahan saat membaca dari
io.Reader
, kita harus mengemasnya untuk mengetahui alasannya. Karena penelepon disediakan dengan fungsi
io.Reader
, masuk akal untuk menunjukkan kesalahan yang dihasilkannya.
Kasus lain: fungsi yang membuat beberapa panggilan basis data mungkin tidak boleh mengembalikan kesalahan di mana hasil dari salah satu panggilan ini dikemas. Jika database yang digunakan oleh fungsi ini adalah bagian dari implementasi, maka mengungkapkan kesalahan ini akan melanggar abstraksi. Misalnya, jika fungsi
LookupUser
dari paket
pkg
menggunakan paket Go
database/sql
, maka itu mungkin mengalami kesalahan
sql.ErrNoRows
. Jika Anda mengembalikan kesalahan menggunakan
fmt.Errorf("accessing DB: %v", err)
, maka pemanggil tidak dapat melihat ke dalam dan menemukan
sql.ErrNoRows
. Tetapi jika fungsi mengembalikan
fmt.Errorf("accessing DB: %w", err)
, maka pemanggil dapat menulis:
err := pkg.LookupUser(...) if errors.Is(err, sql.ErrNoRows) โฆ
Dalam hal ini, fungsi tersebut harus selalu mengembalikan
sql.ErrNoRows
jika Anda tidak ingin memecah klien, bahkan ketika beralih ke paket dengan database yang berbeda. Dengan kata lain, pengemasan membuat kesalahan bagian dari API Anda. Jika Anda tidak ingin melakukan dukungan untuk kesalahan ini di masa mendatang sebagai bagian dari API, jangan mengemasnya.
Penting untuk diingat bahwa terlepas dari apakah Anda mengemasnya atau tidak, kesalahannya akan tetap tidak berubah.
Seseorang yang akan memahaminya akan memiliki informasi yang sama. Membuat keputusan tentang pengemasan tergantung pada apakah informasi tambahan diperlukan untuk
program sehingga mereka dapat membuat keputusan yang lebih tepat; atau jika Anda ingin menyembunyikan informasi ini untuk mempertahankan tingkat abstraksi.
Menyiapkan Pengujian Kesalahan Menggunakan Metode Is dan As
Kesalahan. Fungsi memeriksa setiap kesalahan dalam rantai terhadap nilai target. Secara default, kesalahan cocok dengan nilai ini jika mereka setara. Selain itu, kesalahan dalam rantai dapat menyatakan kepatuhannya dengan nilai target menggunakan penerapan
metode Is
.
Pertimbangkan kesalahan yang disebabkan
oleh paket Upspin , yang membandingkan kesalahan dengan templat dan hanya mengevaluasi bidang yang bukan nol:
type Error struct { Path string User string } func (e *Error) Is(target error) bool { t, ok := target.(*Error) if !ok { return false } return (e.Path == t.Path || t.Path == "") && (e.User == t.User || t.User == "") } if errors.Is(err, &Error{User: "someuser"}) {
Fungsi
errors.As
As
juga menyarankan metode
As
, jika ada.
API Kesalahan dan Paket
Paket yang mengembalikan kesalahan (dan sebagian besar paket melakukan ini) harus menjelaskan sifat-sifat kesalahan ini yang dapat diandalkan oleh seorang programmer. Paket yang dirancang dengan baik juga akan menghindari pengembalian kesalahan dengan properti yang tidak dapat diandalkan.
Yang paling sederhana adalah mengatakan apakah operasi itu berhasil, mengembalikan, masing-masing, nilai nihil atau non-nihil. Dalam banyak kasus, tidak ada informasi lain yang diperlukan.
Jika Anda membutuhkan fungsi untuk mengembalikan status kesalahan yang dapat diidentifikasi, misalnya, "elemen tidak ditemukan", maka Anda dapat mengembalikan kesalahan di mana nilai sinyal dikemas.
var ErrNotFound = errors.New("not found")
Ada pola-pola lain untuk menyediakan kesalahan yang bisa diperiksa pemanggil secara semantik. Misalnya, secara langsung mengembalikan nilai kontrol, tipe tertentu, atau nilai yang dapat dianalisis menggunakan fungsi predikatif.
Dalam hal apa pun, jangan mengungkapkan detail internal kepada pengguna. Seperti yang disebutkan dalam bab "Kapan kemasannya layak?", Jika Anda mengembalikan kesalahan dari paket lain, maka konversikannya agar tidak mengungkapkan kesalahan asli, kecuali jika Anda bermaksud berkomitmen untuk mengembalikan kesalahan spesifik ini di masa mendatang.
f, err := os.Open(filename) if err != nil {
Jika suatu fungsi mengembalikan kesalahan dengan nilai atau tipe sinyal yang dikemas, maka jangan langsung mengembalikan kesalahan semula.
var ErrPermission = errors.New("permission denied")
Kesimpulan
Meskipun kami hanya membahas tiga fungsi dan perintah pemformatan, kami berharap bahwa mereka akan sangat membantu meningkatkan penanganan kesalahan dalam program Go. Kami berharap bahwa pengemasan demi menyediakan konteks tambahan akan menjadi praktik yang normal, membantu programmer membuat keputusan yang lebih baik dan menemukan bug lebih cepat.
Seperti yang dikatakan Russ Cox dalam
pidatonya di GopherCon 2019 , dalam perjalanan ke Go 2 kami bereksperimen, menyederhanakan, dan mengirim. Dan sekarang, setelah mengirimkan perubahan ini, kami membuat percobaan baru.