Konsep desain penanganan kesalahan baru di Go 2 baru-baru ini telah diterbitkan. Sangat menyenangkan bahwa bahasa tidak berdiri di satu tempat - berkembang dan setiap tahun tumbuh lebih cantik.
Hanya sekarang, sementara Go 2 hanya terlihat di cakrawala, sangat menyakitkan dan sedih untuk menunggu. Karena itu, kami menangani masalah kami sendiri. Sedikit pembuatan kode, sedikit kerja dengan ast, dan dengan sedikit gerakan tangan, panik berubah, panik berubah ... menjadi pengecualian elegan!
Dan segera saya ingin membuat pernyataan yang sangat penting dan sangat serius.
Keputusan ini bersifat menghibur dan pedagogis secara eksklusif .
Maksud saya hanya 4 kesenangan. Ini umumnya merupakan bukti konsep, dalam kebenaran. Saya peringatkan :)
Jadi apa yang terjadi?
Hasilnya adalah generator kode perpustakaan kecil seperti itu. Dan pembuat kode, seperti semua orang tahu, membawa kebaikan dan keanggunan di dalam diri mereka. Sebenarnya tidak, tetapi di dunia Go mereka cukup populer.
Kami mengatur generator kode seperti itu di go-raw. Dia mem-parsingnya untuk bantuan modul standar go/ast
fungsi, melakukan beberapa tidak transformasi licik, hasilnya ditulis di sebelah file, menambahkan akhiran _jex.go
. File yang dihasilkan ingin runtime kecil berfungsi.
Dengan cara sederhana ini, kami menambahkan pengecualian ke Go.
Kami menggunakan
Kami menghubungkan generator ke file, di header (sebelum package
) kami menulis
Jika Anda sekarang menjalankan perintah go generate -tags jex
, utilitas jex
akan dieksekusi. Dia mengambil nama file dari os.Getenv("GOFILE")
, memakannya, mencernanya dan menulis {file}_jex.go
. File baru lahir sudah memiliki //+build !jex
di header (tag dibalik), jadi go build
, dan di kompartemen dengannya, perintah lain, seperti go test
atau go install
, hanya memperhitungkan file baru yang benar. Lepota ...
Sekarang dot-import github.com/anjensan/jex
.
Ya, sementara impor melalui suatu titik adalah wajib. Di masa depan direncanakan untuk pergi sama saja.
import . "github.com/anjensan/jex"
Hebat, sekarang Anda dapat memasukkan panggilan ke fungsi rintisan. TRY
, THROW
, EX
dalam kode. Untuk semua ini, kode tersebut tetap valid secara sintaksis, dan bahkan dikompilasi dalam bentuk yang tidak diproses (tidak berfungsi), jadi pelengkapan otomatis tersedia dan linter tidak benar-benar bersumpah. Editor juga akan menunjukkan dokumentasi untuk fungsi-fungsi ini, jika saja mereka punya satu.
Lempar pengecualian
THROW(errors.New("error name"))
Tangkap pengecualian
if TRY() {
Fungsi anonim dihasilkan di bawah tenda. Dan di dalamnya defer
. Dan itu memiliki satu fungsi lagi. Dan di dalamnya recover
... Yah, masih ada sedikit ast-magic untuk menangani return
dan defer
.
Dan ya, omong-omong, mereka didukung!
Selain itu, ada ERR
variabel makro khusus. Jika Anda menetapkan kesalahan, pengecualian dilemparkan. Lebih mudah memanggil fungsi yang masih mengembalikan error
dengan cara lama
file, ERR := os.Open(filename)
Selain itu, ada beberapa tas utilitas kecil ex
dan must
, tetapi tidak ada banyak yang perlu dibicarakan.
Contohnya
Berikut adalah contoh kode Go yang benar dan idiomatis
func CopyFile(src, dst string) error { r, err := os.Open(src) if err != nil { return fmt.Errorf("copy %s %s: %v", src, dst, err) } defer r.Close() w, err := os.Create(dst) if err != nil { return fmt.Errorf("copy %s %s: %v", src, dst, err) } if _, err := io.Copy(w, r); err != nil { w.Close() os.Remove(dst) return fmt.Errorf("copy %s %s: %v", src, dst, err) } if err := w.Close(); err != nil { os.Remove(dst) return fmt.Errorf("copy %s %s: %v", src, dst, err) } }
Kode ini tidak begitu bagus dan elegan. Ngomong-ngomong, ini bukan hanya pendapatku!
Tapi jex
akan membantu kita meningkatkannya.
func CopyFile_(src, dst string) { defer ex.Logf("copy %s %s", src, dst) r, ERR := os.Open(src) defer r.Close() w, ERR := os.Create(dst) if TRY() { ERR := io.Copy(w, r) ERR := w.Close() } else { w.Close() os.Remove(dst) THROW() } }
Tapi misalnya, program berikut
func main() { hex, err := ioutil.ReadAll(os.Stdin) if err != nil { log.Fatal(err) } data, err := parseHexdump(string(hex)) if err != nil { log.Fatal(err) } os.Stdout.Write(data) }
dapat ditulis ulang sebagai
func main() { if TRY() { hex, ERR := ioutil.ReadAll(os.Stdin) data, ERR := parseHexdump(string(hex)) os.Stdout.Write(data) } else { log.Fatal(EX()) } }
Berikut adalah contoh lain untuk merasakan ide yang diusulkan lebih baik. Kode asli
func printSum(a, b string) error { x, err := strconv.Atoi(a) if err != nil { return err } y, err := strconv.Atoi(b) if err != nil { return err } fmt.Println("result:", x + y) return nil }
dapat ditulis ulang sebagai
func printSum_(a, b string) { x, ERR := strconv.Atoi(a) y, ERR := strconv.Atoi(b) fmt.Println("result:", x + y) }
atau bahkan itu
func printSum_(a, b string) { fmt.Println("result:", must.Int_(strconv.Atoi(a)) + must.Int_(strconv.Atoi(b))) }
Pengecualian
Intinya adalah struktur pembungkus sederhana di atas contoh error
.
type exception struct {
Poin penting adalah bahwa serangan panik biasa tidak dianggap sebagai pengecualian. Jadi, semua kesalahan standar seperti runtime.TypeAssertionError
tidak terkecuali. Ini sejalan dengan praktik terbaik yang diterima di Go - jika kita memiliki, katakanlah, nihil-dereferensi, maka kita membatalkan seluruh proses dengan riang dan riang. Dapat diandalkan dan dapat diprediksi. Meskipun saya tidak yakin, mungkin ada baiknya meninjau momen ini dan menangkap kesalahan seperti itu. Mungkin opsional?
Dan di sini adalah contoh rantai pengecualian
func one_() { THROW(errors.New("one")) } func two_() { THROW(errors.New("two") } func three() { if TRY() { one_() } else { two_() } }
Di sini kita dengan tenang menangani satu pengecualian, karena tiba-tiba bam ... dan two
pengecualian dilemparkan. Jadi, sumber yang one
suppress
melampirkannya di bidang suppress
. Tidak ada yang akan hilang, semuanya akan masuk ke log. Oleh karena itu, tidak ada kebutuhan khusus untuk mendorong seluruh rantai kesalahan langsung ke teks pesan menggunakan pola fmt.Errorf("blabla: %v", err)
sangat populer fmt.Errorf("blabla: %v", err)
. Meskipun tidak seorang pun, tentu saja, tidak melarang penggunaannya di sini, jika Anda benar-benar menginginkannya.
Saat lupa menangkap
Ah, poin lain yang sangat penting. Untuk meningkatkan keterbacaan, ada pemeriksaan tambahan: jika suatu fungsi dapat melempar pengecualian, maka namanya harus diakhiri dengan _
. Nama yang sengaja dibengkokkan yang akan memberi tahu programmer, "Tuan, di sini di program Anda ada yang tidak beres, harap berhati-hati dan rajin!"
Pemeriksaan secara otomatis mulai untuk file yang diubah, ditambah itu juga dapat dimulai secara manual dalam proyek menggunakan perintah jex-check
. Mungkin masuk akal untuk menjalankannya sebagai bagian dari proses pembangunan bersama dengan linter lainnya.
Pengecekan komentar //jex:nocheck
. Omong-omong, ini adalah satu-satunya cara untuk membuang pengecualian dari fungsi anonim.
Tentu saja, ini bukan obat mujarab untuk semua masalah. Pemeriksa akan melewatkan ini
func bad_() { THROW(errors.New("ups")) } func worse() { f := bad_ f() }
Di sisi lain, itu tidak jauh lebih buruk daripada pemeriksaan standar untuk err declared and not used
, yang sangat mudah untuk dihindari.
func worse() { a, err := foo() if err != nil { return err } b, err := bar()
Secara umum, pertanyaan ini agak filosofis, apa yang lebih baik untuk dilakukan ketika Anda lupa memproses kesalahan - diam-diam mengabaikannya, atau membuang kepanikan ... Ngomong-ngomong, hasil terbaik dari tes dapat dicapai dengan menerapkan dukungan pengecualian di kompiler, tetapi ini jauh di luar cakupan artikel ini .
Beberapa orang mungkin mengatakan bahwa, meskipun ini adalah solusi yang luar biasa, itu bukan lagi pengecualian, karena sekarang pengecualian berarti implementasi yang sangat spesifik. Nah, di sana, karena tumpukan jejak tidak melekat pada pengecualian, atau ada linter terpisah untuk memeriksa nama fungsi, atau bahwa fungsi dapat diakhiri dengan _
tetapi tidak membuang pengecualian, atau tidak ada dukungan langsung dalam sintaks, atau bahwa itu benar-benar panik, dan kepanikan bukan pengecualian sama sekali, karena gladiol ... Spora bisa sepanas tidak berharga dan tidak ada gunanya. Oleh karena itu, saya akan meninggalkan mereka di belakang papan artikel, dan saya akan terus memanggil solusi yang dijelaskan secara tidak selektif yang disebut "pengecualian."
Tentang stackraces
Seringkali pengembang, untuk menyederhanakan debugging, menempel jejak stack untuk implementasi error
kustom. Bahkan ada beberapa perpustakaan populer untuk ini. Tetapi, untungnya, dengan pengecualian, ini tidak memerlukan tindakan tambahan karena satu fitur menarik dari Go - selama panik, blok defer
dieksekusi dalam konteks tumpukan kode yang melemparkan panik. Karena itu di sini
func foo_() { THROW(errors.New("ups")) } func bar() { if TRY() { foo_() } else { debug.PrintStack() } }
Jejak tumpukan lengkap akan dicetak, meskipun sedikit verbose (saya memotong nama file)
runtime/debug.Stack runtime/debug.PrintStack main.bar.func2 github.com/anjensan/jex/runtime.TryCatch.func1 panic main.foo_ main.bar.func1 github.com/anjensan/jex/runtime.TryCatch main.bar main.main
Tidak ada salahnya membuat helper Anda sendiri untuk memformat / mencetak jejak stack, dengan mempertimbangkan fungsi pengganti, menyembunyikannya agar mudah dibaca. Saya pikir ide yang bagus, tulis di.
Atau Anda dapat mengambil tumpukan dan melampirkannya ke pengecualian menggunakan ex.Log()
. Kemudian, pengecualian seperti itu diizinkan untuk ditransfer ke horoutin lain - strextraces tidak hilang.
func foobar_() { e := make(chan error, 1) go func() { defer close(e) if TRY() { checkZero_() } else { EX().Log(debug.Stack())
Sayangnya
Eh ... tentu saja, sesuatu seperti itu akan terlihat jauh lebih baik
try { throw io.EOF, "some comment" } catch e { fmt.Printf("exception: %v", e) }
Tapi sayangnya, ah, sintaksis Go tidak dapat diperluas.
[serius] Meskipun, mungkin, ini menjadi lebih baik ...
Bagaimanapun, Anda harus memutarbalikkan. Salah satu ide alternatif adalah membuat
TRY; { THROW(io.EOF, "some comment") }; CATCH; { fmt.Printf("exception: %v", EX) }
Tapi kode seperti itu terlihat agak bodoh setelah go fmt
. Dan kompiler bersumpah ketika melihat return
di kedua cabang. Tidak ada masalah dengan if-TRY
.
Akan lebih keren untuk mengganti ERR
macro dengan fungsi MUST
(lebih baik dari sekadar). Untuk menulis
return MUST(strconv.Atoi(a)) + MUST(strconv.Atoi(b))
Pada prinsipnya, ini masih layak, ketika menganalisis Anda, Anda dapat memperoleh jenis ekspresi, karena semua jenis jenis menghasilkan fungsi pembungkus sederhana, seperti yang dinyatakan dalam paket must
, dan kemudian ganti MUST
dengan nama fungsi pengganti yang sesuai. Ini tidak sepenuhnya sepele, tetapi sepenuhnya mungkin ... Hanya editor / ide yang tidak akan dapat memahami kode tersebut. Bagaimanapun, tanda tangan dari fungsi rintisan MUST
tidak dapat diungkapkan dalam sistem tipe Go. Dan karena itu tidak ada pelengkapan otomatis.
Di bawah tenda
Impor baru ditambahkan ke semua file yang diproses.
import _jex "github.com/anjensan/jex/runtime"
Panggilan panic(_jex.NewException(...))
diganti dengan panic(_jex.NewException(...))
. EX()
juga diganti dengan nama variabel lokal yang berisi pengecualian yang ditangkap.
Tetapi if TRY() {..} else {..}
diproses sedikit lebih rumit. Pertama, pemrosesan khusus terjadi untuk semua return
dan defer
. Kemudian cabang diproses jika ditempatkan dalam fungsi anonim. Dan kemudian fungsi-fungsi ini diteruskan ke _jex.TryCatch(..)
. Ini dia
func test(a int) (int, string) { fmt.Println("before") if TRY() { if a == 0 { THROW(errors.New("a == 0")) } defer fmt.Printf("a = %d\n", a) return a + 1, "ok" } else { fmt.Println("fail") } return 0, "hmm" }
berubah menjadi sesuatu seperti ini (saya menghapus //line
komentar):
func test(a int) (_jex_r0 int, _jex_r1 string) { var _jex_ret bool fmt.Println("before") var _jex_md2502 _jex.MultiDefer defer _jex_md2502.Run() _jex.TryCatch(func() { if a == 0 { panic(_jex.NewException(errors.New("a == 0"))) } { _f, _p0, _p1 := fmt.Printf, "a = %d\n", a _jex_md2502.Defer(func() { _f(_p0, _p1) }) } _jex_ret, _jex_r0, _jex_r1 = true, a+1, "ok" return }, func(_jex_ex _jex.Exception) { defer _jex.Suppress(_jex_ex) fmt.Println("fail") }) if _jex_ret { return } return 0, "hmm" }
Banyak, tidak cantik, tetapi berhasil. Oke, tidak semua dan tidak selalu. Misalnya, Anda tidak dapat melakukan defer-recover
di dalam TRY, karena panggilan fungsi berubah menjadi lambda tambahan.
Juga, ketika menampilkan pohon ast, opsi "simpan komentar" ditunjukkan. Jadi, secara teori, go/printer
harus mencetaknya ... Apa yang dia jujur, kebenarannya sangat, sangat bengkok =) Saya tidak akan memberikan contoh, hanya bengkok. Pada prinsipnya, masalah seperti itu sepenuhnya dapat dipecahkan jika Anda dengan hati-hati menentukan posisi untuk semua ast-node (sekarang mereka kosong), tetapi ini jelas tidak termasuk dalam daftar hal-hal yang diperlukan untuk prototipe.
Coba
Karena penasaran, saya menulis tolok ukur kecil.
Kami memiliki implementasi qsort kayu yang memeriksa duplikat dalam beban. Ditemukan - kesalahan. Satu versi hanya melempar melalui return err
, yang lain mengklarifikasi kesalahan dengan memanggil fmt.Errorf
. Dan satu lagi menggunakan pengecualian. Kami mengurutkan irisan dengan ukuran yang berbeda, baik tanpa duplikat sama sekali (tidak ada kesalahan, irisan disortir sepenuhnya), atau dengan satu pengulangan (pengurutan terputus sekitar setengah, itu dapat dilihat oleh timing).
Hasil ~ > cat /proc/cpuinfo | grep 'model name' | head -1 model name : Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz ~ > go version go version go1.11 linux/amd64 ~ > go test -bench=. github.com/anjensan/jex/demo goos: linux goarch: amd64 pkg: github.com/anjensan/jex/demo BenchmarkNoErrors/_____10/exception-8 10000000 236 ns/op BenchmarkNoErrors/_____10/return_err-8 5000000 255 ns/op BenchmarkNoErrors/_____10/fmt.errorf-8 5000000 287 ns/op BenchmarkNoErrors/____100/exception-8 500000 3119 ns/op BenchmarkNoErrors/____100/return_err-8 500000 3194 ns/op BenchmarkNoErrors/____100/fmt.errorf-8 500000 3533 ns/op BenchmarkNoErrors/___1000/exception-8 30000 42356 ns/op BenchmarkNoErrors/___1000/return_err-8 30000 42204 ns/op BenchmarkNoErrors/___1000/fmt.errorf-8 30000 44465 ns/op BenchmarkNoErrors/__10000/exception-8 3000 525864 ns/op BenchmarkNoErrors/__10000/return_err-8 3000 524781 ns/op BenchmarkNoErrors/__10000/fmt.errorf-8 3000 561256 ns/op BenchmarkNoErrors/_100000/exception-8 200 6309181 ns/op BenchmarkNoErrors/_100000/return_err-8 200 6335135 ns/op BenchmarkNoErrors/_100000/fmt.errorf-8 200 6687197 ns/op BenchmarkNoErrors/1000000/exception-8 20 76274341 ns/op BenchmarkNoErrors/1000000/return_err-8 20 77806506 ns/op BenchmarkNoErrors/1000000/fmt.errorf-8 20 78019041 ns/op BenchmarkOneError/_____10/exception-8 2000000 712 ns/op BenchmarkOneError/_____10/return_err-8 5000000 268 ns/op BenchmarkOneError/_____10/fmt.errorf-8 2000000 799 ns/op BenchmarkOneError/____100/exception-8 500000 2296 ns/op BenchmarkOneError/____100/return_err-8 1000000 1809 ns/op BenchmarkOneError/____100/fmt.errorf-8 500000 3529 ns/op BenchmarkOneError/___1000/exception-8 100000 21168 ns/op BenchmarkOneError/___1000/return_err-8 100000 20747 ns/op BenchmarkOneError/___1000/fmt.errorf-8 50000 24560 ns/op BenchmarkOneError/__10000/exception-8 10000 242077 ns/op BenchmarkOneError/__10000/return_err-8 5000 242376 ns/op BenchmarkOneError/__10000/fmt.errorf-8 5000 251043 ns/op BenchmarkOneError/_100000/exception-8 500 2753692 ns/op BenchmarkOneError/_100000/return_err-8 500 2824116 ns/op BenchmarkOneError/_100000/fmt.errorf-8 500 2845701 ns/op BenchmarkOneError/1000000/exception-8 50 33452819 ns/op BenchmarkOneError/1000000/return_err-8 50 33374000 ns/op BenchmarkOneError/1000000/fmt.errorf-8 50 33705994 ns/op PASS ok github.com/anjensan/jex/demo 64.008s
Jika kesalahan belum dilemparkan (kode stabil dan diperkuat beton), maka jaminan dengan lemparan pengecualian kira-kira sebanding dengan return err
dan fmt.Errorf
. Terkadang sedikit lebih cepat. Tetapi jika kesalahan dilemparkan, maka pengecualian berada di posisi kedua. Tetapi itu semua tergantung pada rasio "pekerjaan bermanfaat / kesalahan" dan kedalaman tumpukan. Untuk irisan kecil, return err
berjalan di depan celah, untuk irisan menengah dan besar, pengecualian sudah sama dengan penerusan manual.
Singkatnya, jika kesalahan sangat jarang terjadi, pengecualian bahkan dapat mempercepat kode sedikit. Jika seperti orang lain, maka itu akan menjadi sesuatu seperti itu. Tetapi jika sangat sering ... maka pengecualian lambat jauh dari masalah yang paling penting, yang patut dikhawatirkan.
Sebagai ujian, saya memigrasikan perpustakaan gosh nyata untuk pengecualian.
Saya sangat menyesal, tidak berhasil menulis ulang 1-in-1Lebih tepatnya, itu akan berubah, tetapi ini pasti terganggu.
Jadi, misalnya, fungsi rpc2XML
tampaknya mengembalikan error
... ya, itu tidak pernah mengembalikannya. Jika Anda mencoba membuat serialisasi tipe data yang tidak didukung - tidak ada kesalahan, cukup kosongkan output. Mungkin ini yang dimaksud? Tidak, hati nurani tidak membiarkannya seperti itu. Ditambahkan oleh
default: THROW(fmt.Errorf("unsupported type %T", value))
Tetapi ternyata fungsi ini digunakan dengan cara khusus
func rpcParams2XML(rpc interface{}) (string, error) { var err error buffer := "<params>" for i := 0; i < reflect.ValueOf(rpc).Elem().NumField(); i++ { var xml string buffer += "<param>" xml, err = rpc2XML(reflect.ValueOf(rpc).Elem().Field(i).Interface()) buffer += xml buffer += "</param>" } buffer += "</params>" return buffer, err }
Di sini kita menjalankan daftar parameter, membuat serial semua, tetapi mengembalikan kesalahan hanya untuk yang terakhir. Kesalahan yang tersisa diabaikan. Perilaku aneh menjadi lebih mudah
func rpcParams2XML_(rpc interface{}) string { buffer := "<params>" for i := 0; i < reflect.ValueOf(rpc).Elem().NumField(); i++ { buffer += "<param>" buffer += rpc2XML_(reflect.ValueOf(rpc).Elem().Field(i).Interface()) buffer += "</param>" } buffer += "</params>" return buffer }
Jika setidaknya satu bidang tidak berhasil membuat cerita bersambung - kesalahan. Ya, itu lebih baik. Tetapi ternyata fungsi ini juga digunakan dengan cara khusus .
xmlstr, _ = rpcResponse2XML(response)
sekali lagi, untuk kode sumber ini tidak begitu penting, karena ada kesalahan yang diabaikan. Saya mulai menebak mengapa beberapa programmer menyukai penanganan kesalahan secara eksplisit if err != nil
... Tapi dengan pengecualian itu masih lebih mudah untuk diteruskan atau diproses daripada mengabaikan
xmlstr = rpcResponse2XML_(response)
Dan saya tidak mulai menghapus "rantai kesalahan." Ini kode aslinya
func DecodeClientResponse(r io.Reader, reply interface{}) error { rawxml, err := ioutil.ReadAll(r) if err != nil { return FaultSystemError } return xml2RPC(string(rawxml), reply) }
di sini adalah yang ditulis ulang
func DecodeClientResponse_(r io.Reader, reply interface{}) { var rawxml []byte if TRY() { rawxml, ERR = ioutil.ReadAll(r) } else { THROW(FaultSystemError) } xml2RPC_(string(rawxml), reply) }
Di sini kesalahan awal (yang mana ioutil.ReadAll
dikembalikan) tidak akan hilang, itu akan dilampirkan ke pengecualian di bidang suppress
. Sekali lagi, ini dapat dilakukan seperti pada yang asli, tetapi harus dibuat bingung ...
Saya menulis ulang tes, menggantikan if err != nil { log.Error(..) }
dengan lemparan pengecualian sederhana. Ada poin negatif - tes jatuh pada kesalahan pertama, tidak terus bekerja "baik, setidaknya entah bagaimana." Menurut pikiran, akan perlu untuk membaginya menjadi sub-tes ... Apa, secara umum, layak dilakukan dalam hal apapun. Tetapi sangat mudah untuk mendapatkan stackrace yang benar
func errorReporter(t testing.TB) func(error) { return func(e error) { t.Log(string(debug.Stack())) t.Fatal(e) } } func TestRPC2XMLConverter_(t *testing.T) { defer ex.Catch(errorReporter(t))
Secara umum, kesalahan sangat mudah untuk diabaikan. Dalam kode asli
func fault2XML(fault Fault) string { buffer := "<methodResponse><fault>" xml, _ := rpc2XML(fault) buffer += xml buffer += "</fault></methodResponse>" return buffer }
di sini kesalahan dari rpc2XML
sekali lagi diabaikan dengan tenang. Sudah menjadi seperti ini
func fault2XML(fault Fault) string { buffer := "<methodResponse><fault>" if TRY() { buffer += rpc2XML_(fault) } else { fmt.Printf("ERR: %v", EX()) buffer += "<nil/>" } buffer += "</fault></methodResponse>" return buffer }
Menurut perasaan pribadi saya, lebih mudah untuk mengembalikan hasil "setengah jadi" dengan kesalahan.
Misalnya, respons setengah dibangun. Pengecualian lebih rumit, karena fungsi mengembalikan hasil yang sukses atau tidak menghasilkan apa-apa sama sekali. Semacam atomicity. Di sisi lain, pengecualian lebih sulit untuk diabaikan atau kehilangan akar penyebab dalam rantai pengecualian. Bagaimanapun, Anda masih harus secara khusus mencoba melakukan ini. Dengan kesalahan, ini terjadi dengan mudah dan alami.
Alih-alih sebuah kesimpulan
Saat menulis artikel ini, tidak ada gopher yang terluka.
Terima kasih atas foto http://migranov.ru dari goffer-alcoholic
Saya tidak dapat memilih antara hub "Pemrograman" dan "Pemrograman abnormal".
Pilihan yang sangat sulit, ditambahkan ke keduanya.