Menangani bug di Go 1.13


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 { // something went wrong } 

Terkadang kami membandingkan kesalahan untuk mengetahui nilai kontrol dan melihat apakah ada kesalahan tertentu.

 var ErrNotFound = errors.New("not found") if err == ErrNotFound { // something wasn't found } 

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 { // e.Name wasn't found } 

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 { // query failed because of a permission problem } 

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.

 // Similar to: // if err == ErrNotFound { โ€ฆ } if errors.Is(err, ErrNotFound) { // something wasn't found } 

Fungsi As memeriksa apakah kesalahan berasal dari tipe tertentu.

 // Similar to: // if e, ok := err.(*QueryError); ok { โ€ฆ } var e *QueryError if errors.As(err, &e) { // err is a *QueryError, and e is set to the error's value } 

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 { // query failed because of a permission problem } 

Menggunakan fungsi errors.Is . Apakah errors.Is dapat menulis ini:

 if errors.Is(err, ErrPermission) { // err, or some error that it wraps, is a permission problem } 

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 { // Return an error which unwraps to err. return fmt.Errorf("decompress %v: %w", name, err) } 

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"}) { // err's User field is "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") // FetchItem returns the named item. // // If no item with the name exists, FetchItem returns an error // wrapping ErrNotFound. func FetchItem(name string) (*Item, error) { if itemNotFound(name) { return nil, fmt.Errorf("%q: %w", name, ErrNotFound) } // ... } 

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 { // The *os.PathError returned by os.Open is an internal detail. // To avoid exposing it to the caller, repackage it as a new // error with the same text. We use the %v formatting verb, since // %w would permit the caller to unwrap the original *os.PathError. return fmt.Errorf("%v", err) } 

Jika suatu fungsi mengembalikan kesalahan dengan nilai atau tipe sinyal yang dikemas, maka jangan langsung mengembalikan kesalahan semula.

 var ErrPermission = errors.New("permission denied") // DoSomething returns an error wrapping ErrPermission if the user // does not have permission to do something. func DoSomething() { if !userHasPermission() { // If we return ErrPermission directly, callers might come // to depend on the exact error value, writing code like this: // // if err := pkg.DoSomething(); err == pkg.ErrPermission { โ€ฆ } // // This will cause problems if we want to add additional // context to the error in the future. To avoid this, we // return an error wrapping the sentinel so that users must // always unwrap it: // // if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... } return fmt.Errorf("%w", ErrPermission) } // ... } 

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.

Source: https://habr.com/ru/post/id473658/


All Articles