Halo, para pembaca Habrahabr. Saat membahas kemungkinan desain baru untuk penanganan kesalahan dan berdebat tentang manfaat penanganan kesalahan eksplisit, saya mengusulkan untuk mempertimbangkan beberapa fitur kesalahan, panik dan pemulihannya dalam Go yang akan berguna dalam praktiknya.

kesalahan
kesalahan adalah antarmuka. Dan seperti kebanyakan antarmuka di Go, definisi kesalahannya pendek dan sederhana:
type error interface { Error() string }
Ternyata jenis apa pun yang memiliki metode Kesalahan dapat digunakan sebagai kesalahan. Seperti yang diajarkan oleh Rob Pike, Kesalahan adalah nilai , dan nilai dapat digunakan untuk memanipulasi dan memprogram berbagai logika.
Ada dua fungsi di pustaka standar Go yang nyaman digunakan untuk membuat kesalahan. Kesalahan. Fungsi baru sangat cocok untuk membuat kesalahan sederhana. Fungsi fmt.Errorf memungkinkan penggunaan pemformatan standar.
err := errors.New("emit macho dwarf: elf header corrupted") const name, id = "bimmler", 17 err := fmt.Errorf("user %q (id %d) not found", name, id)
Biasanya, jenis kesalahan sudah cukup untuk menangani kesalahan. Tetapi kadang-kadang mungkin perlu untuk mengirimkan informasi tambahan dengan kesalahan, dalam kasus seperti itu, Anda dapat menambahkan jenis kesalahan Anda sendiri.
Contoh yang bagus adalah jenis PathError dari paket os
Nilai kesalahan semacam itu akan mengandung operasi, jalur, dan kesalahan.
Mereka diinisialisasi dengan cara ini:
... return nil, &PathError{"open", name, syscall.ENOENT} ... return nil, &PathError{"close", file.name, e}
Pemrosesan dapat memiliki bentuk standar:
_, err := os.Open("---") if err != nil{ fmt.Println(err) }
Tetapi jika ada kebutuhan untuk mendapatkan informasi tambahan, maka Anda dapat membongkar kesalahan ke * os.PathError :
_, err := os.Open("---") if pe, ok := err.(*os.PathError);ok{ fmt.Printf("Err: %s\n", pe.Err) fmt.Printf("Op: %s\n", pe.Op) fmt.Printf("Path: %s\n", pe.Path) }
Pendekatan yang sama dapat diterapkan jika fungsi dapat mengembalikan beberapa jenis kesalahan.
mainkan
Deklarasi beberapa jenis kesalahan, masing-masing memiliki data sendiri:
kode type ErrTimeout struct { Time time.Duration Err error } func (e *ErrTimeout) Error() string { return e.Time.String() + ": " + e.Err.Error() } type ErrPermission struct { Status string Err error } func (e *ErrPermission) Error() string { return e.Status + ": " + e.Err.Error() }
Fungsi yang dapat mengembalikan kesalahan ini:
kode func proc(n int) error { if n <= 10 { return &ErrTimeout{Time: time.Second * 10, Err: errors.New("timeout error")} } else if n >= 10 { return &ErrPermission{Status: "access_denied", Err: errors.New("permission denied")} } return nil }
Kesalahan penanganan melalui gips:
kode func main(){ err := proc(11) if err != nil { switch e := err.(type) { case *ErrTimeout: fmt.Printf("Timeout: %s\n", e.Time.String()) fmt.Printf("Error: %s\n", e.Err) case *ErrPermission: fmt.Printf("Status: %s\n", e.Status) fmt.Printf("Error: %s\n", e.Err) default: fmt.Println("hm?") os.Exit(1) } } }
Jika kesalahan tidak memerlukan properti khusus, itu adalah praktik yang baik di Go untuk membuat variabel untuk menyimpan kesalahan di tingkat paket. Contohnya adalah kesalahan seperti io.EOF, io.ErrNoProgress, dan sebagainya.
Dalam contoh di bawah ini, kami menyela membaca dan terus menjalankan aplikasi ketika kesalahannya adalah io.EOF atau kami menutup aplikasi untuk kesalahan lainnya.
func main(){ reader := strings.NewReader("hello world") p := make([]byte, 2) for { _, err := reader.Read(p) if err != nil{ if err == io.EOF { break } log.Fatal(err) } } }
Ini efektif karena kesalahan hanya dihasilkan sekali dan digunakan kembali.
tumpukan jejak
Daftar fungsi yang dipanggil pada saat penangkapan stack. Penumpukan tumpukan membantu Anda mendapatkan ide yang lebih baik tentang apa yang terjadi dalam sistem. Menyimpan jejak dalam log dapat sangat membantu saat debugging.
Go sering kekurangan informasi ini karena kesalahan, tetapi untungnya mendapatkan dump di Go tidak sulit.
Anda dapat menggunakan debug.PrintStack () untuk menampilkan jejak ke output standar:
func main(){ foo() } func foo(){ bar() } func bar(){ debug.PrintStack() }
Akibatnya, informasi berikut akan ditulis ke Stderr:
tumpukan goroutine 1 [running]: runtime/debug.Stack(0x1, 0x7, 0xc04207ff78) .../Go/src/runtime/debug/stack.go:24 +0xae runtime/debug.PrintStack() .../Go/src/runtime/debug/stack.go:16 +0x29 main.bar() .../main.go:13 +0x27 main.foo() .../main.go:10 +0x27 main.main() .../main.go:6 +0x27
debug.Stack () mengembalikan sepotong byte dengan tumpukan dump, yang nantinya bisa dicatat atau di tempat lain.
b := debug.Stack() fmt.Printf("Trace:\n %s\n", b)
Ada hal lain jika kita suka ini:
go bar()
maka kita mendapatkan informasi berikut di output:
main.bar() .../main.go:19 +0x2d created by main.foo .../main.go:14 +0x3c
Setiap goroutine memiliki tumpukan terpisah, masing-masing, kami hanya mendapatkan dumpnya. By the way, goroutine memiliki tumpukan mereka sendiri, pulih masih terhubung dengan ini, tetapi lebih lanjut tentang itu nanti.
Jadi, untuk melihat informasi tentang semua goroutine, Anda dapat menggunakan runtime.Stack () dan meneruskan argumen kedua benar.
func bar(){ buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } fmt.Printf("Trace:\n %s\n", buf) }
tumpukan Trace: goroutine 5 [running]: main.bar() .../main.go:21 +0xbc created by main.foo .../main.go:14 +0x3c goroutine 1 [sleep]: time.Sleep(0x77359400) .../Go/src/runtime/time.go:102 +0x17b main.foo() .../main.go:16 +0x49 main.main() .../main.go:10 +0x27
Tambahkan informasi ini ke kesalahan dan dengan demikian sangat meningkatkan konten informasinya.
Misalnya, seperti ini:
type ErrStack struct { StackTrace []byte Err error } func (e *ErrStack) Error() string { var buf bytes.Buffer fmt.Fprintf(&buf, "Error:\n %s\n", e.Err) fmt.Fprintf(&buf, "Trace:\n %s\n", e.StackTrace) return buf.String() }
Anda dapat menambahkan fungsi untuk membuat kesalahan ini:
func NewErrStack(msg string) *ErrStack { buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } return &ErrStack{StackTrace: buf, Err: errors.New(msg)} }
Maka Anda sudah bisa bekerja dengan ini:
func main() { err := foo() if err != nil { fmt.Println(err) } } func foo() error{ return bar() } func bar() error{ err := NewErrStack("error") return err }
tumpukan Error: error Trace: goroutine 1 [running]: main.NewErrStack(0x4c021f, 0x5, 0x4a92e0) .../main.go:41 +0xae main.bar(0xc04207ff38, 0xc04207ff78) .../main.go:24 +0x3d main.foo(0x0, 0x48ebff) .../main.go:21 +0x29 main.main() .../main.go:11 +0x29
Dengan demikian, kesalahan dan jejaknya dapat dibagi:
func main(){ err := foo() if st, ok := err.(*ErrStack);ok{ fmt.Printf("Error:\n %s\n", st.Err) fmt.Printf("Trace:\n %s\n", st.StackTrace) } }
Dan tentu saja sudah ada solusi yang sudah jadi. Salah satunya adalah paket https://github.com/pkg/errors . Ini memungkinkan Anda untuk membuat kesalahan baru, yang sudah akan berisi jejak jejak, dan Anda dapat menambahkan jejak dan / atau pesan tambahan untuk kesalahan yang ada. Ditambah pemformatan output yang nyaman.
import ( "fmt" "github.com/pkg/errors" ) func main(){ err := foo() if err != nil { fmt.Printf("%+v", err) } } func foo() error{ err := bar() return errors.Wrap(err, "error2") } func bar() error{ return errors.New("error") }
tumpukan error main.bar .../main.go:20 main.foo .../main.go:16 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361 error2 main.foo .../main.go:17 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361
% v hanya akan menampilkan pesan
error2: error
panik / pulih
Panik (alias kecelakaan, alias panik), sebagai suatu peraturan, memberi sinyal adanya kerusakan, karena sistem (atau subsistem tertentu) tidak dapat terus berfungsi. Jika panik disebut, Go runtime melihat ke tumpukan, mencoba mencari penangan untuknya.
Kepanikan yang belum diproses menghentikan aplikasi. Ini secara mendasar membedakan mereka dari kesalahan yang memungkinkan Anda untuk tidak memproses sendiri.
Anda bisa meneruskan argumen apa pun ke panggilan fungsi panik.
panic(v interface{})
Sangat mudah dalam panik untuk melewati kesalahan jenis yang menyederhanakan pemulihan dan membantu debugging.
panic(errors.New("error"))
Pemulihan bencana di Go didasarkan pada panggilan fungsi yang ditangguhkan, alias ditangguhkan. Fungsi seperti itu dijamin akan dieksekusi setelah kembali dari fungsi induk. Apa pun alasannya - pernyataan kembali, akhir fungsi, atau panik.
Dan sekarang fungsi pemulihan memungkinkan untuk mendapatkan informasi tentang kecelakaan dan menghentikan pemutusan tumpukan panggilan.
Panggilan panik dan penangan yang khas:
func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() foo() } func foo(){ panic(errors.New("error")) }
pulihkan pengembalian antarmuka {} (yang kami anggap panik) atau nihil jika tidak ada panggilan untuk panik.
Pertimbangkan contoh lain penanganan darurat. Kami memiliki beberapa fungsi yang kami transfer misalnya sumber daya dan yang, secara teori, dapat menyebabkan kepanikan.
func bar(f *os.File) { panic(errors.New("error")) }
Pertama, Anda mungkin harus selalu melakukan beberapa tindakan di akhir, misalnya, membersihkan sumber daya, dalam kasus kami, menutup file.
Kedua, eksekusi yang salah dari fungsi tersebut seharusnya tidak mengarah pada akhir seluruh program.
Masalah ini dapat diselesaikan dengan menunda, memulihkan, dan menutup:
func foo()(err error) { file, _ := os.Open("file") defer func() { if r := recover(); r != nil { err = r.(error)
Penutup memungkinkan kita untuk beralih ke variabel yang dideklarasikan di atas, berkat ini kami menjamin untuk menutup file dan jika terjadi kecelakaan, ekstrak kesalahan dari itu dan meneruskannya ke mekanisme penanganan kesalahan yang biasa.
Ada situasi terbalik ketika fungsi dengan argumen tertentu harus selalu berhasil dengan benar, dan jika ini tidak terjadi, maka itu benar-benar buruk.
Dalam kasus seperti itu, tambahkan fungsi pembungkus di mana fungsi target dipanggil, dan jika terjadi kesalahan, panik disebut.
Go biasanya memiliki awalan Harus :
Perlu diingat satu hal lagi yang berkaitan dengan kepanikan dan goroutin.
Bagian dari tesis dari apa yang dibahas di atas:
- Tumpukan terpisah dialokasikan untuk setiap goroutine.
- Saat memanggil panic, recover dicari di stack.
- Dalam kasus ketika pemulihan tidak ditemukan, seluruh aplikasi berakhir.
Pawang di main tidak akan memotong panik dari foo dan program akan crash:
func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() go foo() time.Sleep(time.Minute) } func foo(){ panic(errors.New("error")) }
Ini akan menjadi masalah jika, misalnya, seorang pawang dipanggil untuk terhubung ke server. Dalam hal panik di salah satu penangan, seluruh server akan menyelesaikan eksekusi. Dan Anda tidak dapat mengontrol penanganan kecelakaan dalam fungsi-fungsi ini, untuk beberapa alasan.
Dalam kasus sederhana, solusinya mungkin terlihat seperti ini:
type f func() func Def(fn f) { go func() { defer func() { if err := recover(); err != nil { log.Println("panic") } }() fn() }() } func main() { Def(foo) time.Sleep(time.Minute) } func foo() { panic(errors.New("error")) }
menangani / memeriksa
Mungkin di masa depan kita akan melihat perubahan dalam penanganan kesalahan. Anda dapat berkenalan dengan mereka di tautan:
go2draft
Penanganan Kesalahan saat Go 2
Itu saja untuk hari ini. Terima kasih