Proposal: coba - fungsi pengecekan kesalahan bawaan

Ringkasan


try konstruk baru diusulkan yang secara khusus dirancang untuk menghilangkan if -ekspresi umumnya terkait dengan penanganan kesalahan di Go. Ini adalah satu-satunya perubahan bahasa. Penulis mendukung penggunaan fungsi defer dan perpustakaan standar untuk memperkaya atau membungkus kesalahan. Ekstensi kecil ini cocok untuk sebagian besar skenario, praktis tanpa menyulitkan bahasa.


Konstruk try mudah dijelaskan, mudah diimplementasikan, fungsi ini ortogonal untuk konstruk bahasa lain dan sepenuhnya kompatibel ke belakang. Itu juga bisa diperluas jika kita menginginkannya di masa depan.


Sisa dokumen ini disusun sebagai berikut: setelah pengantar singkat, kami memberikan definisi fungsi bawaan dan menjelaskan penggunaannya dalam praktik. Bagian diskusi mengulas saran alternatif dan desain saat ini. Pada akhirnya, kesimpulan dan rencana implementasi dengan contoh dan bagian dari pertanyaan dan jawaban akan diberikan.


Pendahuluan


Pada konferensi Gophercon terakhir di Denver, anggota tim Go (Russ Cox, Marcel van Lohuizen) mempresentasikan beberapa ide baru tentang cara mengurangi kebosanan penanganan kesalahan manual di Go ( rancangan desain ). Sejak itu kami telah menerima sejumlah besar umpan balik.


Sebagaimana dijelaskan oleh Russ Cox dalam ulasannya tentang masalah tersebut , tujuan kami adalah membuat penanganan kesalahan menjadi lebih ringan dengan mengurangi jumlah kode yang ditujukan khusus untuk pengecekan kesalahan. Kami juga ingin membuat kode penanganan kesalahan penulisan lebih nyaman, meningkatkan kemungkinan bahwa pengembang masih akan mencurahkan waktu untuk memperbaiki penanganan kesalahan. Pada saat yang sama, kami ingin membiarkan kode penanganan kesalahan terlihat jelas dalam kode program.


Ide-ide yang dibahas dalam draft konsep terkonsentrasi di sekitar pernyataan check unary baru, yang menyederhanakan verifikasi eksplisit dari nilai kesalahan yang diperoleh dari beberapa ekspresi (biasanya panggilan fungsi), serta deklarasi penangan kesalahan ( handle ) dan seperangkat aturan yang menghubungkan dua konstruksi bahasa baru ini.


Sebagian besar umpan balik yang kami terima berfokus pada detail dan kompleksitas desain handle , dan gagasan operator check ternyata lebih menarik. Bahkan, beberapa anggota komunitas mengambil ide operator check dan memperluasnya. Berikut adalah beberapa posting yang paling mirip dengan penawaran kami:



Proposal saat ini, meskipun berbeda dalam rinciannya, didasarkan pada ketiganya dan, secara umum, pada umpan balik yang diterima pada rancangan desain yang diusulkan tahun lalu.


Untuk melengkapi gambar, kami ingin mencatat bahwa lebih banyak saran penanganan kesalahan dapat ditemukan di halaman wiki ini . Perlu juga dicatat bahwa Liam Breck datang dengan serangkaian persyaratan untuk mekanisme penanganan kesalahan.


Akhirnya, setelah publikasi proposal ini, kami mengetahui bahwa Ryan Hileman menerapkan try lima tahun lalu menggunakan alat penulis ulang dan berhasil menggunakannya dalam proyek nyata. Lihat ( https://news.ycombinator.com/item?id=20101417 ).


Fungsi coba bawaan


Tawarkan


Kami menyarankan untuk menambahkan elemen bahasa seperti fungsi yang disebut try dan dipanggil dengan tanda tangan


 func try(expr) (T1, T2, ... Tn) 

di mana expr berarti ekspresi dari parameter input (biasanya panggilan fungsi) yang mengembalikan n + 1 nilai tipe T1, T2, ... Tn dan error untuk nilai terakhir. Jika expr adalah nilai tunggal (n = 0), nilai ini harus bertipe error dan try tidak mengembalikan hasilnya. Memanggil try dengan ekspresi yang tidak mengembalikan nilai error tipe terakhir menghasilkan kesalahan kompilasi.


Konstruk try hanya dapat digunakan dalam fungsi yang mengembalikan setidaknya satu nilai, dan nilai pengembalian terakhirnya adalah tipe error . Memanggil try dalam konteks lain mengarah ke kesalahan kompilasi.


Panggil try dengan f() seperti pada contoh


 x1, x2, … xn = try(f()) 

mengarah ke kode berikut:


 t1, … tn, te := f() // t1, … tn,  ()   if te != nil { err = te //  te    error return //     } x1, … xn = t1, … tn //     //     

Dengan kata lain, jika tipe error terakhir yang dikembalikan oleh expr adalah nil , maka try mengembalikan nilai n pertama, menghapus nil terakhir.


Jika nilai terakhir yang dikembalikan oleh expr tidak nil , maka:


  • Nilai pengembalian error dari fungsi melampirkan (dalam pseudocode di atas bernama err , meskipun ini bisa berupa pengidentifikasi atau nilai pengembalian tidak bernama) menerima nilai kesalahan yang dikembalikan dari expr
  • ada jalan keluar dari fungsi membungkus
  • jika fungsi melampirkan memiliki parameter pengembalian tambahan, parameter ini mempertahankan nilai-nilai yang terkandung di dalamnya sebelum panggilan try .
  • jika fungsi melampirkan memiliki parameter pengembalian tanpa nama tambahan, nilai nol yang sesuai dikembalikan untuk mereka (yang identik dengan menyimpan nilai nol aslinya dengan mana mereka diinisialisasi).

Jika try digunakan dalam banyak penugasan, seperti dalam contoh di atas, dan kesalahan bukan nol (selanjutnya tidak-nol - Per.) Terdeteksi, penugasan (oleh variabel pengguna) tidak dilakukan, dan tidak ada variabel di sisi kiri penugasan yang berubah. Yaitu, try berperilaku seperti panggilan fungsi: hasilnya hanya tersedia jika try mengembalikan kontrol ke penelepon (sebagai lawan kasus dengan pengembalian dari fungsi terlampir). Akibatnya, jika variabel di sisi kiri penugasan adalah parameter kembali, menggunakan try akan menghasilkan perilaku yang berbeda dari kode khas yang ditemui sekarang. Misalnya, jika a,b, err dinamai parameter pengembalian fungsi melampirkan, berikut adalah kode ini:


 a, b, err = f() if err != nil { return } 

akan selalu memberikan nilai ke variabel a, b dan err , terlepas dari apakah panggilan ke f() mengembalikan kesalahan atau tidak. Tantangan yang bertolak belakang


 a, b = try(f()) 

dalam hal terjadi kesalahan, biarkan a dan b tidak berubah. Terlepas dari kenyataan bahwa ini adalah nuansa yang halus, kami percaya bahwa kasus seperti itu cukup langka. Jika perilaku tugas tanpa syarat diperlukan, Anda harus terus menggunakan if ekspresi.


Gunakan


Definisi try secara eksplisit memberi tahu Anda cara menggunakannya: banyak ekspresi if yang memeriksa pengembalian kesalahan dapat diganti dengan try . Sebagai contoh:


 f, err := os.Open(filename) if err != nil { return …, err //       } 

dapat disederhanakan menjadi


 f := try(os.Open(filename)) 

Jika fungsi panggilan tidak mengembalikan kesalahan, try tidak dapat digunakan (lihat bagian Diskusi). Dalam hal ini, kesalahan harus diproses secara lokal (karena tidak ada pengembalian kesalahan), dan dalam kasus ini, if tetap mekanisme yang tepat untuk memeriksa kesalahan.


Secara umum, tujuan kami bukan untuk mengganti semua pemeriksaan kesalahan yang mungkin dengan try . Kode yang memerlukan semantik yang berbeda dapat dan harus terus digunakan if ekspresi dan variabel eksplisit dengan nilai kesalahan.


Menguji dan mencoba


Dalam salah satu upaya kami sebelumnya untuk menulis spesifikasi (lihat bagian iterasi desain di bawah), try dirancang untuk panik ketika kesalahan terjadi ketika digunakan di dalam fungsi tanpa kesalahan kembali. Ini diizinkan menggunakan try dalam unit berdasarkan pada paket testing pustaka standar.


Sebagai salah satu opsi, dimungkinkan untuk menggunakan fungsi pengujian dengan tanda tangan dalam paket testing


 func TestXxx(*testing.T) error func BenchmarkXxx(*testing.B) error 

untuk memungkinkan penggunaan uji coba. Fungsi tes yang mengembalikan kesalahan bukan nol secara implisit akan memanggil t.Fatal(err) atau b.Fatal(err) . Ini adalah perubahan perpustakaan kecil yang menghindari perlunya perilaku yang berbeda (kembali atau panik) untuk try , tergantung pada konteksnya.


Salah satu kelemahan dari pendekatan ini adalah bahwa t.Fatal dan b.Fatal tidak akan dapat mengembalikan nomor baris di mana tes jatuh. Kerugian lainnya adalah kita harus mengubah subtitle juga. Solusi untuk masalah ini adalah pertanyaan terbuka; kami tidak mengusulkan perubahan spesifik pada paket testing dalam dokumen ini.


Lihat juga # 21111 , yang menyarankan agar fungsi contoh mengembalikan kesalahan.


Menangani kesalahan


Draf desain asli sebagian besar berkaitan dengan dukungan bahasa untuk kesalahan pembungkus atau penambahan. Draf mengusulkan handle kata kunci baru dan cara baru untuk menyatakan penangan kesalahan . Konstruksi bahasa baru ini menarik masalah seperti lalat karena semantik non-sepele, terutama ketika mempertimbangkan efeknya pada aliran eksekusi. Secara khusus, fungsionalitas handle disilang dengan fungsi defer , yang membuat fitur bahasa baru menjadi non-ortogonal bagi yang lainnya.


Proposal ini mengurangi esensi rancangan asli. Jika pengayaan atau pembungkus kesalahan diperlukan, ada dua pendekatan: lampirkan if err != nil { return err} , atau "nyatakan" penangan kesalahan di dalam ekspresi defer :


 defer func() { if err != nil { //      -   err = … // /  } }() 

Dalam contoh ini, err adalah nama parameter pengembalian error ketik fungsi melampirkan.


Dalam praktiknya, kami membayangkan fungsi pembantu seperti


 func HandleErrorf(err *error, format string, args ...interface{}) { if *err != nil { *err = fmt.Errorf(format + ": %v", append(args, *err)...) } } 

atau yang serupa. Paket fmt dapat menjadi tempat alami untuk pembantu seperti itu (sudah menyediakan fmt.Errorf ). Menggunakan bantuan, definisi penangan kesalahan dalam banyak kasus akan dikurangi menjadi satu baris. Misalnya, untuk memperkaya kesalahan dari fungsi "salin", Anda dapat menulis


 defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) 

jika fmt.HandleErrorf secara implisit menambahkan informasi kesalahan. Konstruksi semacam itu cukup mudah dibaca dan memiliki kelebihan yang dapat diimplementasikan tanpa menambahkan elemen baru dari sintaks bahasa.


Kerugian utama dari pendekatan ini adalah bahwa parameter kesalahan yang dikembalikan harus dinamai, yang berpotensi mengarah ke API yang kurang akurat (lihat FAQ tentang topik ini). Kami percaya bahwa kami akan terbiasa ketika gaya penulisan kode yang sesuai dibuat.


Penundaan efisiensi


Pertimbangan penting saat menggunakan defer sebagai penangan kesalahan adalah efisiensi. Ekspresi defer dianggap lambat . Kami tidak ingin memilih antara kode efisien dan penanganan kesalahan yang baik. Terlepas dari proposal ini, tim runtime Go dan kompiler membahas metode implementasi alternatif, dan kami percaya bahwa kami dapat membuat cara khas menggunakan defer untuk menangani kesalahan yang sebanding dalam efisiensi dengan kode "manual" yang ada. Kami berharap dapat menambahkan implementasi defer lebih cepat di Go 1.14 (lihat juga tiket CL 171158 , yang merupakan langkah pertama ke arah ini).


Kasus khusus, go try(f), defer try(f)


Konstruk try terlihat seperti fungsi, dan karena ini, diharapkan dapat digunakan di mana saja di mana panggilan fungsi dapat diterima. Namun, jika try call digunakan dalam pernyataan go , segalanya menjadi rumit:


 go try(f()) 

Di sini f() dieksekusi ketika ekspresi go dieksekusi di goroutine saat ini, hasil pemanggilan f dilewatkan sebagai argumen untuk try , yang dimulai pada goroutine baru. Jika f mengembalikan kesalahan bukan nol, try diharapkan untuk kembali dari fungsi melampirkan; Namun, tidak ada fungsi (dan tidak ada parameter pengembalian error tipe), karena kode dieksekusi di goroutine terpisah. Karena itu, kami sarankan untuk menonaktifkan try dalam ekspresi go .


Situasi dengan


 defer try(f()) 

terlihat serupa, tetapi di sini semantik defer berarti bahwa pelaksanaan try akan ditunda sampai kembali dari fungsi melampirkan. Seperti sebelumnya, f() dievaluasi ketika defer , dan hasilnya diteruskan ke try ditunda.


try periksa kesalahan f() dikembalikan hanya pada saat-saat terakhir sebelum kembali dari fungsi melampirkan. Tanpa mengubah perilaku try , kesalahan semacam itu dapat menimpa nilai kesalahan lain yang coba dikembalikan oleh fungsi terlampir. Ini paling membingungkan, paling buruk itu memprovokasi kesalahan. Karena itu, kami mengusulkan agar Anda melarang panggilan try dalam pernyataan defer juga. Kami selalu dapat mempertimbangkan kembali keputusan ini jika ada aplikasi semantik yang masuk akal.


Akhirnya, seperti sisa konstruksi bawaan, try hanya dapat digunakan sebagai panggilan; itu tidak dapat digunakan sebagai fungsi nilai atau dalam ekspresi penugasan variabel seperti dalam f := try (sama seperti f := print dan f := new dilarang).


Diskusi


Iterasi desain


Berikut ini adalah diskusi singkat dari desain sebelumnya yang mengarah ke proposal minimal saat ini. Kami berharap ini akan menjelaskan keputusan desain yang dipilih.


Iterasi pertama kami dari kalimat ini terinspirasi oleh dua gagasan dari artikel "Bagian Kunci Penanganan Kesalahan," yaitu, menggunakan fungsi bawaan sebagai ganti operator dan fungsi Go biasa untuk menangani kesalahan alih-alih konstruksi bahasa baru. Tidak seperti publikasi itu, penangan kesalahan kami memiliki kesalahan func(error) error tanda tangan tetap untuk menyederhanakan masalah. Penangan kesalahan akan dipanggil oleh fungsi try jika ada kesalahan sebelum try akan keluar dari fungsi melampirkan. Berikut ini sebuah contoh:


 handler := func(err error) error { return fmt.Errorf("foo failed: %v", err) //   } f := try(os.Open(filename), handler) //      

Sementara pendekatan ini memungkinkan definisi dari penangan kesalahan yang didefinisikan pengguna yang efektif, pendekatan ini juga menimbulkan banyak pertanyaan yang jelas tidak memiliki jawaban yang benar: Apa yang harus terjadi jika nihil diteruskan ke penangan? Haruskah Anda try panik atau menganggap ini sebagai kekurangan pawang? Bagaimana jika pawang dipanggil dengan kesalahan bukan nol dan kemudian mengembalikan hasil nol? Apakah ini berarti kesalahannya "dibatalkan"? Atau haruskah fungsi melampirkan mengembalikan kesalahan kosong? Ada juga keraguan bahwa transfer opsional penangan kesalahan akan mendorong pengembang untuk mengabaikan kesalahan alih-alih memperbaikinya. Ini juga akan mudah untuk melakukan penanganan kesalahan yang benar di mana-mana, tetapi lewati satu kali try . Dan sejenisnya.


Dalam iterasi berikutnya, kemampuan untuk melewati penangan kesalahan khusus dihapus karena menggunakan defer untuk membungkus kesalahan. Ini tampak seperti pendekatan yang lebih baik karena itu membuat penangan kesalahan jauh lebih terlihat dalam kode sumber. Langkah ini juga menghilangkan semua masalah tentang transfer opsional fungsi handler, tetapi menuntut agar parameter yang dikembalikan dengan jenis error dinamai jika akses diperlukan (kami memutuskan bahwa ini normal). Selain itu, dalam upaya membuat try berguna tidak hanya di dalam fungsi yang mengembalikan kesalahan, perlu untuk membuat perilaku try context-sensitive: jika try digunakan pada tingkat paket, atau jika itu disebut di dalam fungsi yang tidak mengembalikan kesalahan, try otomatis panik ketika kesalahan terdeteksi. (Dan sebagai efek samping, karena properti ini, konstruksi bahasa dipanggil must alih-alih try dalam kalimat itu.) Perilaku sensitif konteks try (atau must ) tampak alami dan juga cukup berguna: itu akan menghilangkan banyak fungsi yang ditentukan pengguna yang digunakan dalam ekspresi menginisialisasi variabel paket. Ini juga membuka kemungkinan menggunakan try dalam unit dengan paket testing .


Namun, perilaku try - try peka konteks penuh dengan kesalahan: misalnya, perilaku fungsi menggunakan try dapat dengan diam-diam berubah (panik atau tidak) ketika menambahkan atau menghapus kesalahan pengembalian ke tanda tangan fungsi. Ini properti yang tampaknya terlalu berbahaya. Solusi yang jelas adalah untuk membagi fungsionalitas try menjadi dua must terpisah dan fungsi try , (sangat mirip dengan cara yang disarankan di # 31442 ). Namun, ini akan membutuhkan dua fungsi bawaan, sementara hanya try secara langsung terkait dengan dukungan yang lebih baik untuk penanganan kesalahan.


Oleh karena itu, dalam iterasi saat ini, alih-alih menyertakan fungsi bawaan kedua, kami memutuskan untuk menghapus semantik ganda try dan, oleh karena itu, memungkinkan penggunaannya hanya dalam fungsi yang mengembalikan kesalahan.


Fitur desain yang diusulkan


Saran ini cukup singkat dan mungkin tampak mundur dibandingkan dengan draft tahun lalu. Kami percaya bahwa solusi yang dipilih dibenarkan:


  • Hal pertama yang pertama, try memiliki semantik yang sama persis dengan pernyataan check yang diusulkan dalam aslinya tanpa handle . Ini menegaskan kesetiaan draft asli di salah satu aspek penting.


  • Memilih fungsi bawaan bukannya operator memiliki beberapa keunggulan. Itu tidak memerlukan kata kunci baru seperti check , yang akan membuat desain tidak kompatibel dengan parser yang ada. Juga tidak perlu memperluas sintaks ekspresi dengan operator baru. Menambahkan fungsi bawaan yang baru relatif sepele dan sepenuhnya ortogonal ke fitur bahasa lainnya.


  • Menggunakan fungsi sebaris alih-alih operator membutuhkan penggunaan tanda kurung. Kita harus menulis try(f()) daripada try f() . Ini adalah harga (kecil) yang harus kami bayar untuk kompatibilitas mundur dengan parser yang ada. Namun, ini juga membuat desain tersebut kompatibel dengan versi-versi mendatang: jika kita memutuskan untuk melewati beberapa bentuk fungsi penanganan kesalahan atau menambahkan parameter tambahan untuk try untuk tujuan ini adalah ide yang bagus, menambahkan argumen tambahan ke panggilan panggilan akan sepele.


  • Ternyata, kebutuhan untuk menulis tanda kurung memiliki kelebihan. Dalam ekspresi yang lebih kompleks dengan beberapa panggilan try , tanda kurung meningkatkan keterbacaan dengan menghilangkan kebutuhan untuk berurusan dengan prioritas operator, seperti dalam contoh berikut:



 info := try(try(os.Open(file)).Stat()) //   try info := try (try os.Open(file)).Stat() //  try   info := try (try (os.Open(file)).Stat()) //  try   

try , : try , .. try (receiver) .Stat ( os.Open ).


try , : os.Open(file) .. try ( , try os , , try try ).


, .. .


  • . , . , , , .


. , . defer , .


Go - , . , Go append . append , . , . , try .


, , Go : panic recover . error try .


, try , , — — , . Go:


  • , try
  • -

, , . if -.


Implementasi


:


  • Go.
  • try . , . .
  • go/types try . .
  • gccgo . ( , ).
  • .

- , . , . .


Robert Griesemer go/types , () cmd/compile . , Go 1.14, 1 2019.


, Ian Lance Taylor gccgo , .


"Go 2, !" , .


1 , , , Go 1.14 .



CopyFile :


 func CopyFile(src, dst string) (err error) { defer func() { if err != nil { err = fmt.Errorf("copy %s %s: %v", src, dst, err) } }() r := try(os.Open(src)) defer r.Close() w := try(os.Create(dst)) defer func() { w.Close() if err != nil { os.Remove(dst) //    “try”    } }() try(io.Copy(w, r)) try(w.Close()) return nil } 

, " ", defer :


 defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) 

( defer -), defer , .


printSum


 func printSum(a, b string) error { x := try(strconv.Atoi(a)) y := try(strconv.Atoi(b)) fmt.Println("result:", x + y) return nil } 

:


 func printSum(a, b string) error { fmt.Println( "result:", try(strconv.Atoi(a)) + try(strconv.Atoi(b)), ) return nil } 

main :


 func localMain() error { hex := try(ioutil.ReadAll(os.Stdin)) data := try(parseHexdump(string(hex))) try(os.Stdout.Write(data)) return nil } func main() { if err := localMain(); err != nil { log.Fatal(err) } } 

- try , :


 n, err := src.Read(buf) if err == io.EOF { break } try(err) 


, .


: ?


: check handle , . , handle defer , handle .


: try ?


: try Go . - , . , . , " ". try , .. .


: try try?


: , check , must do . try , . try check (, ), - . . must ; try — . , Rust Swift try ( ). .


: ? Rust?


: Go ; , Go ( ; - ). , ? , . , , , (package, interface, if, append, recover, ...), , (struct, var, func, int, len, image, ..). Rust ? try — Go, , ( ) . , ? . , , (, ..) . . , .


: ( error) , defer , go doc. ?


: go doc , - ( _ ) , . , func f() (_ A, _ B, err error) go doc func f() (A, B, error) . , , , . , , . , , , -, (deferred) . Jonathan Geddes try() .


: defer ?


: defer . , , defer "" . . CL 171758 , defer 30%.


: ?


: , . , ( , ), . defer , . defer - https://golang.org/issue/29934 ( Go 2), .


: , try, error. , ?


: error ( ) , , nil . try . ( , . - ).


: Go , try ?


: try , try . super return -, try Go . try . .


: try , . ?


: try ; , . try ( ), . , if .


: , . try, defer . ?


: , . .


: try ( catch )?


: try — ("") , , ( ) . try ; . . "" . , . , try — . , , throw try-catch Go. , (, ), ( ) , . "" try-catch , . , , . Go . panic , .

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


All Articles