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()
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 {
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)
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
, : 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:
, , . 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)
, " ", 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
, .