Bukan tanpa panik di Go

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.
gambar


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


 // PathError records an error and the operation and file path that caused it. type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() } 

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) } // open ---: The system cannot find the file specified. 

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) } // Err: The system cannot find the file specified. // Op: open // 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) //   ,   ,     // err := errors.New("trapped panic: %s (%T)", r, r) //     } file.Close() //   }() bar(file) return err } 

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 :


 // MustCompile is like Compile but panics if the expression cannot be parsed. // It simplifies safe initialization of global variables holding compiled regular // expressions. func MustCompile(str string) *Regexp { regexp, error := Compile(str) if error != nil { panic(`regexp: Compile(` + quote(str) + `): ` + error.Error()) } return regexp } 

 // Must is a helper that wraps a call to a function returning (*Template, error) // and panics if the error is non-nil. It is intended for use in variable initializations // such as // var t = template.Must(template.New("name").Parse("html")) func Must(t *Template, err error) *Template { if err != nil { panic(err) } return t } 

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

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


All Articles