- Việc lặp lại
if err != nil trong Go nhiều năm qua luôn là một trong những phàn nàn lớn nhất trong các khảo sát người dùng, nhưng nhóm Go đã quyết định tạm thời không thúc đẩy thay đổi cú pháp xử lý lỗi
- Các đề xuất
check/handle năm 2018, try năm 2019 và ? năm 2024 đều không đạt được đồng thuận đủ lớn; đặc biệt try vấp phải phản đối mạnh vì luồng điều khiển bị che giấu
- Theo quy trình đề xuất của Go, nếu không có đồng thuận chung thì đề xuất thường sẽ bị từ chối, và ngay cả giữa các thành viên kỳ cựu của nhóm Go tại Google hiện cũng chưa có sự nhất trí tuyệt đối về hướng đi tốt nhất
- Phía ủng hộ giữ nguyên hiện trạng cho rằng Go đã có cách xử lý lỗi đang hoạt động tốt, còn cú pháp mới sẽ tạo ra chi phí lớn cho phong cách mã, gỡ lỗi, tài liệu, công cụ và mã hiện có
- Nhóm Go sẽ đóng các đề xuất đang mở và đề xuất mới mà mục tiêu chính là cú pháp xử lý lỗi mà không điều tra thêm, và sẽ tập trung vào các cơ hội cải tiến khác cho tới khi có nhận thức rõ ràng hơn về vấn đề
Phàn nàn kéo dài do if err != nil
- Một trong những phàn nàn dai dẳng nhất về Go là tính dài dòng của mã xử lý lỗi
- Mẫu tiêu biểu có dạng như sau
x, err := call()
if err != nil {
// handle err
}
- Trong các chương trình có nhiều lời gọi API và chỉ đơn giản trả lỗi về,
if err != nil có thể lấn át phần còn lại của mã
- Trong hàm ví dụ
printSum, trên 10 dòng thân hàm thì chỉ có 4 dòng trông như công việc thực sự, gồm gọi hàm, in ra và trả về; 6 dòng còn lại trông giống nhiễu
- Trong khảo sát người dùng hằng năm của Go, xử lý lỗi nhiều năm liền là phàn nàn hàng đầu; có thời gian việc thiếu generics đứng trên, nhưng sau khi Go hỗ trợ generics thì xử lý lỗi lại quay về vị trí phàn nàn số một
Ba đề xuất cú pháp lớn
- Nỗ lực rõ ràng đầu tiên của nhóm Go bắt đầu năm 2018 như một phần của Go 2, khi Russ Cox tổng kết chính thức vấn đề
- Bản thiết kế nháp của Marcel van Lohuizen dựa trên cơ chế
check và handle, đồng thời cũng phân tích cách tiếp cận của các ngôn ngữ khác và các phương án thay thế
func printSum(a, b string) error {
handle err { return err }
x := check strconv.Atoi(a)
y := check strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
- Cách tiếp cận
check/handle bị đánh giá là quá phức tạp, nên đến năm 2019 xuất hiện đề xuất try đơn giản hơn
- Từ khóa kiểu
check được chuyển thành hàm dựng sẵn try
- Phần
handle bị loại bỏ
- Công cụ tryhard được tạo ra để chuyển mã xử lý lỗi hiện có sang kiểu
try
- Issue GitHub liên quan được tranh luận rất gay gắt với gần 900 bình luận
func printSum(a, b string) error {
// use a defer statement to augment errors before returning
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x + y)
return nil
}
try ảnh hưởng đến luồng điều khiển bằng cách trả về từ hàm bao quanh khi có lỗi, và việc có thể xảy ra trả về ngay cả trong biểu thức lồng sâu khiến nhiều người dùng khó chấp nhận
- Khi đó, có lẽ việc đưa vào một từ khóa mới sẽ tốt hơn; hiện nay ngôn ngữ có thể kiểm soát phiên bản chi tiết hơn bằng tệp
go.mod và chỉ thị theo từng tệp
- Đề xuất gần đây của Jimmy Frasche đi theo hướng quay lại thiết kế
check/handle ban đầu đồng thời xử lý một số nhược điểm
Nhìn lại quy trình sau try và đề xuất ?
- Sau đề xuất
try, Russ Cox đã nhìn lại quy trình đề xuất qua loạt bài “Thinking about the Go Proposal Process”
- “Go Proposal Process: Large Changes” đánh giá rằng
try lẽ ra nên là bản thiết kế nháp thứ hai chứ không phải một đề xuất gắn với lịch triển khai
- Trong những năm sau đó, nhóm Go không thúc đẩy thay đổi cú pháp xử lý lỗi, trong khi cộng đồng vẫn tiếp tục đưa ra các đề xuất tương tự, thú vị, khó hiểu hoặc không khả thi
- Ian Lance Taylor đã tạo umbrella issue để tổng hợp tình hình các đề xuất cải thiện xử lý lỗi, và cũng có Go Wiki để gom phản hồi và thảo luận liên quan
- “go error handling proposals” của Sean K. H. Liao theo dõi nhiều đề xuất xử lý lỗi trong nhiều năm
- Khi phàn nàn vẫn tiếp diễn, Ian Lance Taylor đã công bố năm 2024 đề xuất dùng
? để giảm boilerplate xử lý lỗi
- Đây là ký pháp mượn từ toán tử
? của Rust
- Trong một nghiên cứu người dùng không chính thức quy mô nhỏ, phần lớn người tham gia đoán đúng ý nghĩa của mã Go dùng
?
- Công cụ chuyển đổi mã Go thông thường sang cú pháp mới và nguyên mẫu trình biên dịch cũng đã được tạo ra
func printSum(a, b string) error {
x := strconv.Atoi(a) ?
y := strconv.Atoi(b) ?
fmt.Println("result:", x + y)
return nil
}
- Đề xuất này cũng nhanh chóng bị lấp đầy bởi rất nhiều bình luận và các đề xuất chỉnh sửa chi tiết dựa trên sở thích; Ian đã đóng đề xuất rồi chuyển nội dung sang discussion
- Một phiên bản chỉnh sửa nhẹ nhận được phản hồi tích cực hơn đôi chút, nhưng vẫn không giành được ủng hộ rộng rãi
Vì sao hiện tại muốn dừng lại
- Nhóm Go cho rằng trong tương lai có thể dự đoán được, họ nên ngừng cố gắng giải quyết vấn đề cú pháp của xử lý lỗi
- Quy trình đề xuất ủng hộ quyết định này
- Mục tiêu của quy trình đề xuất là đạt được đồng thuận chung về kết quả trong thời gian hợp lý
- Nếu không tìm được đồng thuận chung trong thảo luận trên issue tracker thì đề xuất thường sẽ bị từ chối
- Nếu không tìm được cả đồng thuận lẫn bước tiếp theo, các Go architects sẽ xem xét thảo luận và cố gắng đạt đồng thuận nội bộ
- Không có đề xuất xử lý lỗi nào nhận được mức ủng hộ gần với đồng thuận, và tất cả đều bị từ chối
- Ngay cả các thành viên kỳ cựu của nhóm Go tại Google cũng chưa nhất trí tuyệt đối về hướng đi tốt nhất hiện nay, và nếu không có đồng thuận mạnh thì rất khó tiến lên một cách hợp lý
Lập luận cho hiện trạng và thay đổi
- Phía ủng hộ giữ nguyên hiện trạng có những lý do thực tế về độ trưởng thành của Go và chi phí hệ sinh thái
- Nếu Go đưa vào cú pháp đường tắt chuyên cho xử lý lỗi từ rất sớm thì có lẽ hiện nay tranh cãi sẽ ít hơn, nhưng Go đã 15 năm tuổi và đã có một cách xử lý lỗi đang vận hành
- Ngay cả khi tìm ra lời giải hoàn hảo bây giờ, tình huống có thể chỉ chuyển từ việc người ủng hộ thay đổi không hài lòng sang người thích giữ nguyên hiện trạng không hài lòng
- Generics là thứ người dùng không nhất thiết phải tự viết trực tiếp, nhưng cú pháp xử lý lỗi mới nếu không dùng thì mã có thể trông không còn đúng phong cách, nên trên thực tế gần như ai cũng sẽ phải dùng
- Việc bổ sung cú pháp mới cũng có thể xung đột với nguyên tắc thiết kế của Go là không cung cấp nhiều cách để làm cùng một việc
- Tính năng khai báo lại của khai báo biến ngắn
:= được đưa vào để giải quyết vấn đề phát sinh từ xử lý lỗi
- Nếu không có khai báo lại, mỗi lần kiểm tra lỗi liên tiếp sẽ cần các tên
err khác nhau hoặc khai báo biến riêng
- Nếu khi đó đã có hỗ trợ cú pháp tốt hơn cho xử lý lỗi, có lẽ quy tắc khai báo lại và độ phức tạp liên quan cũng không cần thiết
- Khi lỗi được bổ sung ngữ cảnh và xử lý đúng cách, tỷ trọng của phần lặp lại đơn thuần sẽ giảm xuống
- Trong khảo sát người dùng thường xuyên có ý kiến lặp lại rằng lỗi không có stack trace
- Có thể dùng hàm trợ giúp để tạo và trả về lỗi đã được bổ sung ngữ cảnh
- Khi gắn thêm thông tin đầu vào như
fmt.Errorf("invalid integer: %q", a), tỷ lệ boilerplate so với phần hữu ích sẽ nhỏ đi
- Tính năng thư viện chuẩn cũng có thể giảm boilerplate xử lý lỗi
- Đây là hướng giống bài “Errors are values” của Rob Pike
- Trong một số trường hợp có thể dùng
cmp.Or để xử lý nhiều lỗi cùng lúc
- Viết, đọc và gỡ lỗi là những hoạt động khác nhau
- Việc viết đi viết lại các kiểm tra lỗi thì nhàm chán, nhưng IDE và tính năng hoàn thành mã có hỗ trợ LLM có thể dễ dàng sinh ra phần kiểm tra lỗi cơ bản
- Khi đọc mã, sự dài dòng nổi bật hơn, và IDE có thể cung cấp nút bật/tắt để ẩn mã xử lý lỗi
- Khi gỡ lỗi, nếu đã có câu lệnh
if riêng thì việc thêm println hoặc đặt breakpoint sẽ dễ dàng
- Nếu xử lý lỗi bị ẩn sau
check, try hoặc ?, thông thường có thể phải chuyển ngược về câu lệnh if; quá trình đó có thể làm việc gỡ lỗi phức tạp hơn hoặc tạo ra lỗi tinh vi
- Thay đổi ngôn ngữ không chỉ tốn chi phí thiết kế và triển khai mà còn kéo theo chi phí chỉnh sửa mã hiện có, cập nhật tài liệu và điều chỉnh công cụ
- Nhóm Go tương đối nhỏ, và còn nhiều ưu tiên khác cần xử lý
- Ưu tiên và quy mô nhóm có thể thay đổi
- Tại Google Cloud Next 2025, một số người dùng Go mà nhóm Go gặp đã nói rất mạnh rằng không nên thay đổi ngôn ngữ chỉ để có xử lý lỗi tốt hơn
- Họ cho biết khi vừa chuyển sang từ ngôn ngữ khác, việc Go không có cú pháp chuyên biệt cho xử lý lỗi là điều dễ nhận thấy nhất, nhưng khi đã viết Go theo phong cách quen thuộc hơn thì điều đó bớt quan trọng đi
- Mẫu này không đủ lớn để có tính đại diện, nhưng có thể là một nhóm khác với những người thường thấy trên GitHub
- Các lập luận ủng hộ thay đổi vẫn còn giá trị
- Việc thiếu hỗ trợ xử lý lỗi tốt hơn vẫn là phàn nàn hàng đầu trong khảo sát người dùng
- Cách tiếp cận chỉ tập trung giảm số ký tự có thể là sai hướng
- Nếu có thể loại bỏ boilerplate
err != nil đồng thời làm cho xử lý lỗi cơ bản nổi bật bằng từ khóa, thì trong review mã sẽ dễ kiểm tra hơn việc có xử lý lỗi hay chưa
- Vẫn chưa hiểu đủ rõ cốt lõi vấn đề là sự dài dòng đơn thuần của cú pháp, hay là sự dài dòng của việc xử lý lỗi tốt bằng các API và lỗi có ý nghĩa cho lập trình viên lẫn người dùng cuối
Quyết định của nhóm Go
- Cho tới nay, mọi nỗ lực xử lý lỗi đều chưa giành được đủ động lực
- Nhóm Go cho rằng còn thiếu một cách hiểu chung về vấn đề, và thậm chí mọi người cũng chưa đồng ý liệu có thực sự tồn tại vấn đề hay không
- Trong tương lai có thể dự đoán được, họ sẽ không thúc đẩy thay đổi ngôn ngữ ở cấp cú pháp cho xử lý lỗi
- Các đề xuất đang mở và các đề xuất sẽ được gửi tới trong tương lai mà mục tiêu chính là cú pháp xử lý lỗi sẽ bị đóng mà không điều tra thêm
- Việc cộng đồng khám phá và thảo luận tuy không dẫn tới thay đổi cú pháp xử lý lỗi, nhưng đã dẫn tới nhiều cải tiến khác cho ngôn ngữ Go và quy trình của nó
1 bình luận
Các ý kiến trên Hacker News
Nếu muốn nhẹ nhàng ném ra một đề xuất kiểu “lẽ ra nhóm Go chỉ cần làm thế này là được”, trước hết nên xem trang wiki được liên kết trong bài https://go.dev/wiki/Go2ErrorHandlingFeedback và tìm kiếm issue trên GitHub https://github.com/golang/go/issues?q=+is%3Aissue+label%3Aerror-handling
Điều bạn định đề xuất gần như chắc chắn không phải là ý tưởng đầu tiên, và phần lớn có khả năng đã được xem xét kỹ
Tôi đánh giá cao cách tiếp cận thẳng thắn này của nhóm Go, và vẫn vui vẻ dùng Go hằng ngày trong công việc
Tôi cũng không thấy thứ gì tương tự trong vài trang GitHub issue có nhiều bình luận nhất, nên sẽ là quá mức nếu cho rằng nhóm Go là những phù thủy thiết kế ngôn ngữ và đương nhiên đã xem xét các giải pháp mà mọi người ở đây tiện miệng đưa ra
Nhóm Go đã mắc sai lầm giống Java, tức là có kiểu tĩnh nhưng không có đa hình tham số, và gốc rễ của vấn đề xử lý lỗi này cũng nằm ở đó, nhưng có vẻ họ đang giơ tay đầu hàng và không sửa
Nghe có thể to tát và đáng sợ, nhưng đó là một cách thanh nhã và thuần hàm để truyền lỗi đến nơi có thể xử lý mà vẫn không để lỗi bị quên mất
Với những người viết mã Haskell, đây là cách làm đã ăn sâu đến mức khó hiểu vì sao trong cộng đồng Go lại không có ai biết và thích nó
Cảm ơn vì trang đó và các liên kết, nhưng thật khó hiểu khi những người quan tâm đến ngôn ngữ của mình như vậy lại bỏ qua một giải pháp đã được thiết lập tốt đến thế
Gần như mọi ngôn ngữ đều có cách tốt hơn của riêng mình; tôi muốn biết liệu chỉ là vì không quyết được hoặc không thể làm hài lòng tất cả, hay có lý do cụ thể nào trong bản thân Go khiến các giải pháp của ngôn ngữ khác không phù hợp
Thực tế thì gần như trong mọi trường hợp, họ biết nhiều hơn rất nhiều
Người nghiệp dư ngây thơ nghĩ rằng ngôn ngữ nhồi nhét nhiều tính năng nhất là tốt nhất, đặc biệt nếu có các tính năng hợp gu mình
Nó giống như một người mới nhập môn làm dao nhìn một con dao bếp kiểu Nhật rồi thấy nó thiếu thốn, sau đó nghĩ rằng sẽ tốt hơn nếu gắn thêm tay cầm in 3D có rãnh đặt ngón tay, ngăn bí mật, bật lửa và loa Bluetooth
Chỉ cần lập một danh sách checkbox, thảo luận từng mục để điền vào, rồi đừng bỏ mục nào ra nữa trừ khi phát hiện lỗi ngữ nghĩa nghiêm trọng hoặc lỗ hổng soundness
Khi đã điền đủ thì triển khai, và những người từng ồn ào xem nên viết
.await,/awaithay.await!()sẽ lại biến mấtRust vận hành theo kiểu này; có issue bị trì hoãn hơn 10 năm, nhưng cuối cùng các mục cũng được điền và được ổn định hóa trên nightly mới nhất
Nếu Go không giải quyết được một vấn đề đơn lẻ mà mọi người đều lập tức va phải, dù có nhiều đề xuất hoàn thiện, chỉ vì không chọn được một cái trong số đó và cứ chờ bikeshedding dừng lại, thì quy trình đó đúng là hài kịch
Nó không phải là một tập hợp tính năng cứ thỏa mãn yêu cầu checkbox là thêm vào
Rust liệu có trở nên hỗn độn như C++ không? Go liệu có còn là một ngôn ngữ vượt thời gian như khi ra mắt không?
Trong khảo sát, tỷ lệ nhắc đến xử lý lỗi là 13%, và cũng có những người thích nguyên cách làm hiện tại
https://go.dev/blog/survey2024-h1-results
Trước đây tôi từng viết một hàm Go khá kỳ lạ, trong đó kỳ vọng hàm bên trong sẽ trả về lỗi
Vì vậy nếu hàm bên trong không trả về lỗi thì hàm bên ngoài phải trả về lỗi và thực hiện xử lý khác, còn nếu hàm bên trong trả về lỗi thì phải trả về nil
Tóm lại, không phải
if err != nil { ... }mà phải dùngif err == nil { // return an error }, nhưng do thói quen tôi đã viết kiểu trước, và mất khá lâu để debugVì tôi đã quá trơ với
if err != nil, đến mức não hoàn toàn không xét đến khả năng không được có cú pháp đó ở đâyVì vậy tôi cho rằng các biểu thức phổ biến cần syntactic sugar. Nếu sự khác biệt giữa
if err != nilcực kỳ phổ biến vàif err == nilhiếm gặp được làm nổi bật hơn thì thực sự đã có íchif err == nil, tôi thêm chú thích// invertedđể làm nó nổi bậtSẽ tốt nếu được xử lý ở cấp ngôn ngữ, nhưng ít nhất tôi chia sẻ như một cách để khiến nó dễ thấy hơn
if fruit != "Apple" { ... }cũng có thể tạo ra tình huống y hệtTôi tò mò liệu có giải pháp tổng quát nào để cải thiện việc này không, và xem đây chỉ là vấn đề riêng của xử lý lỗi thì có vẻ hơi lệch hướng
Lỗi không có gì đặc biệt hay độc nhất; nó chỉ là một trạng thái như những thứ khác
Vì nếu xuất hiện mẫu
if err == nil { return ... }phổ biến, thì lần này nó sẽ nằm rải rác khắp codeGiải pháp hiện tại ổn, và có vẻ chủ yếu những người mới bắt đầu hoặc ở trình độ sơ cấp với Go là không thích
Quanh tôi, mọi người thích cách xử lý lỗi “dài dòng” vì nó tường minh, rõ ràng và dễ đọc
if err != nilnhư một ligature đơn nhất, nhỏ gọnNhư vậy những dạng khác đúng chuỗi đó, chẳng hạn
if err == nil, sẽ nổi bật hơnif err … {Tôi thích cách xử lý lỗi tường minh của Go
Một hàm hoặc luôn thành công, hoặc có thể thành công hoặc thất bại. Hàm luôn thành công thì đơn giản; còn hàm có thể thất bại, khi thất bại thì code bên ngoài không thể tiếp tục ở trạng thái thất bại nên phải xử lý
Đây là chỗ các ngôn ngữ rẽ nhánh. Nhiều ngôn ngữ ném exception, đẩy lên cho đến khi ai đó bắt một cách tường minh và cung cấp một dạng stack trace
Trong Go, tôi thích việc khi viết code luôn có các lựa chọn phải đưa ra: bỏ qua lỗi và tiếp tục (
foo, _ := doSomething()), return sớm mà không có thông tin có ý nghĩa (return nil, err), return sớm kèm ngữ cảnh hữu ích, hoặc diễn giải lỗi nhận được để rẽ nhánhVí dụ nếu không tìm thấy hàng cần sửa trong database, tầng service có thể trả về lỗi not found và API sẽ thành 404, hoặc một hàm xóa idempotent có thể diễn giải not found là thành công
Nếu là Go 2 hay một ngôn ngữ khác, tôi muốn có Result type kiểu Rust/Swift thay vì tuple có thể nil, và các kiểu lỗi được định kiểu tốt hơn, có thể liệt kê, thay vì luôn dùng trực tiếp
errorTuy nhiên nếu thêm Result lên trên kiểu trả về tuple quen thuộc của Go 1, sẽ có nhiều cách làm cùng một việc, gây rối và chia rẽ, nên nó hợp với Go 2 hoặc một ngôn ngữ mới hơn
Các tầng thấp trong stack thường không biết phải làm gì, nên không nên xử lý lỗi
Chính sách “xử lý lỗi” rốt cuộc dễ trở thành chính sách bọc lỗi rồi trả ngược lên stack, và đó là khá nhiều việc lặt vặt
Go cho phép bỏ qua lỗi hoàn toàn, và kết quả có thể dẫn đến crash
Tôi hơi khó hiểu khi đã chỉ đúng yêu cầu để tạo phần mềm vững chắc mà vẫn thích cách xử lý lỗi của Go
[1]: https://borgo-lang.github.io/ | https://github.com/borgo-lang/borgo
Tất cả các ngôn ngữ hàm, nhiều ngôn ngữ hiện đại như Rust, thậm chí Java với checked exception cũng cung cấp điều này
Nếu là ngôn ngữ có generics, hầu như có thể tái tạo kiểu “xử lý lỗi” của Go, và có lẽ code còn tốt hơn
Nếu câu trả lời là JavaScript hay Python thì đó đúng là một kiểu so sánh thường gặp
Với Go thì đây là quyết định đúng. Khi mới tiếp xúc với Go, tôi ghét xử lý lỗi, nhưng giờ thì thật sự thích nó
Bước ngoặt đến từ hai điều. Tôi đọc bài https://go.dev/blog/errors-are-values và thực sự tiếp nhận góc nhìn “lỗi là giá trị”, rồi trên nền tảng đó cũng tạo một package tương đối phổ biến https://github.com/stytchauth/sqx
Ngoài ra tôi dần quen với việc dùng
panic(err)một chút cho những trạng thái sai thật sự vô lýKhông có lý do gì bắt code cha xử lý cưỡng ép mọi trạng thái kỳ quặc mà nó cũng chẳng có cảm nhận gì để xử lý; một hai panic đặt đúng chỗ có thể loại bỏ hàng trăm lần kiểm tra lỗi trong codebase
Ví dụ có thể nghĩ đến vấn đề như liệu trong ctx có logger mặc định hay không
Ngược lại còn có khả năng được cải thiện
bash cũng có
-eHồi mới dùng C# ngày xưa, tôi từng nghĩ việc hiểu luồng try/catch/finally, using, lồng nhau, chuyện gì xảy ra nếu lỗi trong catch, nếu lỗi trong finally, là thông minh
Giờ thì tôi thấy không phải nghĩ về những thứ đó sẽ tốt hơn
Tôi không thích cách bài viết này nói rằng vấn đề chính của xử lý lỗi trong Go là cú pháp quá dài dòng. Tôi không bận tâm lắm về chuyện đó
Điều quan trọng hơn là lỗi có thể bị âm thầm bỏ qua hoặc vô tình bị lờ đi, kết quả gọi hàm không phải là một giá trị nên không thể dễ dàng lưu trữ hay truyền đi, cần đến
errors.Is, và toàn bộ lỗi “lồng nhau” là một cơ chế runtime kỳ lạ không khớp lắm với hệ thống kiểuSwitch trên lỗi cũng khó, thư viện chuẩn dùng các giá trị sentinel, và tương tác với generic không tốt nên cần các package như errgroup
Còn điều gì tôi bỏ sót không?
Nếu là ngôn ngữ có exception thì chẳng ai làm vậy cả
Họ đã quyết định trong tương lai có thể dự đoán được sẽ không tiếp tục thử thay đổi cú pháp xử lý lỗi nữa, và nhờ vậy có được sự chú ý để xem xét các vấn đề khác, dù là lỗi hay chủ đề khác
Sự tiến hóa của Go chậm như băng hà, và với nhiều người đó không phải là lỗi mà là tính năng
Cách giải thích kiểu “ý kiến khảo sát rằng lỗi không có stack trace có thể được giải quyết bằng cách để hàm trợ giúp tạo và trả về lỗi đã được bổ sung thông tin” thật buồn cười
Việc gọi cung cấp stack trace thủ công như
if err != nil { return fmt.Errorf("invalid integer: %q", a) }là “xử lý lỗi” thật thú vịTheo định nghĩa của đội ngũ Go thì exception hóa ra là thứ tự động xử lý lỗi giúp bạn. Tất nhiên là trong các ngôn ngữ ngoài C++
Có thể là vậy, nhưng bạn có thực sự cần tất cả những thứ đó không? Còn chi phí log thì sao?
Tôi nghĩ một lỗi được wrap thành một dòng, cắt bỏ nhiễu từ framework và runtime, tốt hơn nhiều
Nếu wrap tốt thì cũng rất dễ tìm kiếm, và thường có thể truy vết hiệu quả hơn stack trace
Dùng Go toàn thời gian hơn 10 năm, tôi chưa từng cần đến các hàm runtime hay đống nhiễu dài dòng của call stack
Từ góc nhìn của một lập trình viên Elixir, chuyện này trông thật điên rồ
Trong Erlang/Elixir, việc này thường được giải quyết bằng cách hàm trả về tuple
{:ok, result}hoặc{:error, description_or_struct}Kết hợp với câu lệnh
withcủa Elixir, có thể gom phần xử lý lỗi xuống phía dưới nên dễ đọc hơn nhiềuGo cũng có thể thêm một thứ tương đương mệnh đề
with, tiếp tục chạy các hàm khi lỗi còn là nil, rồi đặt mệnh đề xử lý lỗi ở phía dướiwithĐiều thú vị là Go trì hoãn rất lâu các cấu trúc cơ bản và rõ ràng có giá trị như generic, xử lý lỗi, quản lý package vì thiếu đồng thuận trong cộng đồng
Generic mất 13 năm sau khi công bố mã nguồn mở, đến nay đã 16 năm vẫn chưa có xử lý lỗi, còn quản lý package mất khoảng 9 năm
Sự cân nhắc kỹ có giá trị, nhưng việc phát hành cũng có giá trị. Những người viết 900 bình luận trên GitHub rốt cuộc vẫn sẽ tiếp tục dùng Go, và có lẽ việc đưa thứ gì đó vào ngôn ngữ vẫn tốt hơn là cứ trì hoãn mãi
Hàm có nhiều kiểu trả về thì ngoài việc gán vào biến ra chẳng làm được gì
Nhưng trong Erlang và Elixir, đó là cách hoàn toàn idiomatic mà không kèm gánh nặng nào
Thực ra nó còn mạnh hơn nhiều so với họ ML, vì sum type ở phía đó là mở
Tôi không theo dõi kỹ cuộc thảo luận này, nhưng không hiểu vì sao họ không đơn giản áp dụng cách kiểu Rust
Sau khi Go có generics, đó cũng là cách tôi lập tức thêm vào
Trong bài được liên kết, tôi chỉ thấy giải thích rằng “Rust không có thứ tương đương với handle, và sự tiện lợi của toán tử
?có khả năng khiến người ta bỏ qua việc xử lý phù hợp”Nhưng tôi không hiểu vì sao tiện lợi lại có nghĩa là phớt lờ lỗi
Một nửa vấn đề của cách Go là nó không ép buộc gì với kết quả, và cũng chỉ ép kiểm tra lỗi ở mức tối thiểu
x, err := strconv.Atoi("123"); fmt.Println("result:", x)sẽ báodeclared and not used: err, nhưng sau lần chuyển đổi thứ hai, dù không kiểm traerr, vì giá trị mặc định củaylà 0 nên chương trình vẫn có thể chạy tốt mà không biết có vấn đềĐể trống như
if err != nil { }cũng biên dịch và chạy được, và không thể biết rằng có gì đó saiNếu biến giá trị trả về thành Result, nó buộc bạn phải đưa ra quyết định. Dù ai đó lạm dụng
!hoặc tiện tay đẩy lỗi lên bằng?mà không xử lý trường hợp lỗi, vậy chẳng lẽ cũng cấm cảpanicsao?Và vì nỗi ám ảnh kỳ lạ rằng mọi kiểu đều phải có zero value được chỉ định, họ cũng không thể thêm sum type
?dễ dùng thì sẽ không còn ai wrap lỗi nữaMột lập luận rất đáng ngờ
Ngay từ đầu chỉ cần thiết kế
?sao cho khuyến khích wrap lỗi là đượcVí dụ, thứ tương ứng với
Fromcủa Rust trong Go nên trông như thế nào??có độ hiển thị thấp, và giấu nhánh luồng điều khiển bên trong một câu lệnh hoặc biểu thứcĐó cũng là một trong những lý do Go bỏ toán tử ba ngôi và chọn câu lệnh
ifnơi mỗi nhánh nằm trên một dòng riêngĐặt breakpoint cũng không dễ, và nó khiến người ta thiên về việc đẩy nguyên lỗi lên trên thay vì bổ sung thông tin hoặc xử lý lỗi
:=là khai báo và gán trong một câu lệnh duy nhất, nhưng ở dòng thứ 5 của ví dụ, chẳng phảierrđược khai báo lại vàerrmới che khuấterrcũ sao?Nếu vậy, vì biến
errmới không được dùng nên có vẻ nó phải thất bại vớideclared and not used: errHay nếu biến đã tồn tại thì
:=chỉ hoạt động như phép gán thông thường?Nói rằng “việc lỗi không có stack trace có thể được giải quyết bằng cách để hàm trợ giúp tạo và trả về lỗi đã được bổ sung thông tin” là quá lạc quan so với thực tế
Những ngôn ngữ có stack trace cho sẵn điều này miễn phí, nhưng trong Go thì lần nào cũng phải tự triển khai
Bản thân bạn có thể là một lập trình viên kỷ luật, luôn gắn thêm chi tiết, nhưng không phải mọi thành viên trong nhóm đều có cùng kỷ luật đó
Điểm hay nhất của stack trace là nó cho biết đường đi lời gọi dẫn tới lỗi
Khi lỗi xảy ra trong một phương thức được gọi từ nhiều nơi, stack trace cho biết ngay nó đã đi theo đường nào
Tôi đã làm những việc gần giống sysadmin/SRE trong nhiều năm và giải quyết nhiều sự cố; khi có stack trace, những vấn đề dễ thường có nguyên nhân hiển nhiên nên chỉ 1–2 phút là xong
Trong Go, nếu ai đó không bổ sung thông tin cho lỗi hoặc tái sử dụng cùng một thông báo lỗi, ngay cả vấn đề dễ cũng trở thành việc suy luận và mất nhiều thời gian hơn