- Tính dài dòng của xử lý lỗi trong ngôn ngữ Go từ lâu đã luôn nằm trong nhóm phàn nàn hàng đầu của người dùng
- Nhiều đề xuất cải tiến cú pháp (ví dụ: check/handle, try, toán tử ? v.v.) đã được thảo luận và thử nghiệm, nhưng đều bị bác bỏ do không đạt được đồng thuận đầy đủ từ cộng đồng
- Ảnh hưởng rộng khắp đến mã nguồn, công cụ, tài liệu v.v. mà thay đổi ngôn ngữ có thể gây ra, cùng với nguyên tắc giữ sự đơn giản đặc trưng của Go, là những yếu tố được cân nhắc chính
- Do tính rõ ràng, thuận tiện cho gỡ lỗi của cách làm hiện tại cùng với sự ưa chuộng của một bộ phận người dùng, cơ sở để buộc phải đưa thay đổi cú pháp vào là không mạnh
- Trong tương lai gần không có kế hoạch thay đổi cú pháp xử lý lỗi, và mọi đề xuất liên quan sẽ được đóng lại mà không tiếp tục nghiên cứu thêm
Nêu vấn đề về tính dài dòng của xử lý lỗi trong Go
- Một trong những phàn nàn lâu năm về Go là cú pháp xử lý lỗi quá dài dòng
- Điển hình là các mẫu như
if err != nil lặp đi lặp lại trong mã
- Chương trình càng cần nhiều lời gọi API thì kiểu cú pháp này càng nổi bật, dẫn đến hiện tượng mã xử lý lỗi còn nhiều hơn cả logic thực tế
- Trong khảo sát người dùng hằng năm, phàn nàn này liên tục được nhắc đến ở nhóm đầu
Trao đổi với cộng đồng và các đề xuất ban đầu
- Đội ngũ Go luôn coi trọng phản hồi từ cộng đồng và đã liên tục nghiên cứu các phương án cải thiện xử lý lỗi
- Trong thảo luận về dự án Go 2 năm 2018, Russ Cox đã chính thức tổng kết cốt lõi của vấn đề xử lý lỗi
- Xuất hiện phương án cơ chế
check và handle do Marcel van Lohuizen đề xuất
- Bao gồm phân tích so sánh với các ngôn ngữ tương tự và xem xét nhiều phương án thay thế
- Cách tiếp cận này thực sự giúp mã ngắn gọn hơn, nhưng không được chấp nhận do làm tăng độ phức tạp
Đề xuất try và những gì diễn ra sau đó
- Năm 2019, đã có đề xuất về hàm dựng sẵn
try đơn giản hơn nhiều
- Chỉ đưa chức năng
check vào mã, bỏ handle
- Đề xuất này bị chỉ trích vì che giấu luồng điều khiển, và cuối cùng bị loại bỏ trong làn sóng phản đối từ cộng đồng
- Từ trải nghiệm này, nhóm phát triển nhận ra rủi ro của việc đưa ra đề xuất đã hoàn thiện khi chưa có đủ phản hồi
- Điều đó xác nhận rằng với các thay đổi quy mô lớn, việc thu thập ý kiến rộng rãi ngay từ giai đoạn thiết kế ban đầu là rất quan trọng
Các nỗ lực bổ sung và nhiều đề xuất khác nhau
- Cộng đồng liên tục đưa ra vô số biến thể và cách xử lý lỗi thay thế
- Tình hình được tổng hợp qua umbrella issue của Ian Lance Taylor, đồng thời các ví dụ tiếp tục được thu thập trên Go Wiki và blog
- Năm 2024, xuất hiện đề xuất áp dụng toán tử
? vay mượn từ Rust
- Trong các thử nghiệm khả dụng quy mô nhỏ, có phản hồi rằng nó trực quan, nhưng rốt cuộc vẫn không đạt được đồng thuận giữa nhiều ý kiến khác nhau
Bế tắc của thảo luận và kết luận
- Dù có hơn 3 đề xuất chính thức hoặc bán chính thức, và hàng trăm đề xuất từ cộng đồng, tất cả đều bị bác bỏ vì thiếu sự đồng cảm/đồng thuận đủ lớn
- Ngay cả nhóm kiến trúc sư nội bộ của Go cũng không thống nhất được về định hướng
- Cho đến khi tình hình thay đổi hoặc xuất hiện một sự đồng thuận đặc biệt, nhóm quyết định dừng hẳn mọi nỗ lực thay đổi cú pháp xử lý lỗi
Những lập luận chính ủng hộ việc giữ nguyên cách hiện tại
- Nếu ngay từ giai đoạn thiết kế ban đầu ngôn ngữ đã có thêm đường cú pháp tắt thì có lẽ đã không gây tranh cãi, nhưng hiện nay hệ sinh thái đã quen với cách làm được dùng suốt 15 năm
- Nếu đưa thêm cú pháp mới, gần như chắc chắn sẽ phát sinh lo ngại về khoảng cách phong cách mã giữa người dùng cũ và mới, cũng như sự đổ vỡ tính nhất quán
- Điều này cũng phù hợp với triết lý thiết kế của Go (không làm cùng một việc theo nhiều cách) và nguyên tắc đề cao tính ngắn gọn/tính nhất quán
- Việc cho phép tái khai báo trong khai báo biến ngắn (
:=) cũng là một thay đổi thứ cấp phát sinh từ xử lý lỗi
- Cú pháp xử lý lỗi rõ ràng (thông qua
if) có ưu điểm trực quan trong việc đọc mã, gỡ lỗi và đặt breakpoint
- Thay đổi ngôn ngữ cũng là gánh nặng lớn xét trên phạm vi thay đổi thực tế (mã nguồn, tài liệu, công cụ v.v.) và chi phí
Các cải tiến thay thế và hướng đi tương lai
- Có thể giảm một phần mã lặp bằng việc tăng cường chức năng của thư viện chuẩn (ví dụ: bổ sung
cmp.Or)
- Trong thực tế, có thể phần nào khắc phục sự dài dòng bằng gập mã, tự động hoàn thành, tận dụng LLM trong IDE và công cụ phát triển
- Trong các nhóm người dùng Go chủ chốt (ví dụ: người tham dự sự kiện Google Cloud Next), quan điểm phản đối sự cần thiết của thay đổi ngôn ngữ chiếm ưu thế
- Càng sử dụng Go nhiều, vấn đề dài dòng trên thực tế càng ít được cảm nhận rõ
Những lập luận ủng hộ nhu cầu cải tiến cú pháp
- Dựa trên phản hồi người dùng, nhu cầu cải thiện cú pháp xử lý lỗi vẫn còn tồn tại
- Một cú pháp xử lý lỗi không chỉ giảm số ký tự mà còn tăng độ rõ ràng có thể góp phần nâng cao chất lượng/độ an toàn của mã
- Cần nghiên cứu kỹ hơn về xử lý lỗi thực sự có vai trò, chứ không chỉ là kiểm tra lỗi đơn thuần
Kết luận cuối cùng và chính sách sắp tới
- Thừa nhận rằng cho đến nay vẫn chưa có đồng thuận hay thay đổi thực chất nào, nhóm tuyên bố trong tương lai gần sẽ dừng mọi thảo luận và đề xuất về thay đổi ngôn ngữ ở cấp cú pháp cho xử lý lỗi
- Các cuộc thảo luận và quá trình nghiên cứu trước đây vẫn gián tiếp đóng góp cho việc cải thiện hệ sinh thái và quy trình của Go
- Nếu trong tương lai xuất hiện định nghĩa vấn đề rõ ràng hơn và có đồng thuận, thảo luận có thể được nối lại
- Trong thời gian tới, định hướng là ưu tiên duy trì sự vững chắc và đơn giản của chính Go hơn là thử nghiệm mới
1 bình luận
Ý kiến trên Hacker News
Nếu muốn dễ dàng đề xuất rằng đội ngũ Go lẽ ra có thể chọn phương án khác, mình thật lòng mong mọi người trước hết hãy xem wiki Go2ErrorHandlingFeedback hoặc tìm kiếm issue trên GitHub. Gần như mọi ý tưởng được đề xuất đều đã được thảo luận nghiêm túc, và với tư cách là một người dùng biết ơn cách tiếp cận minh bạch của nhóm Go, mình rất thích thú khi dùng Go mỗi ngày
Tài liệu thiết kế bản nháp có nhắc đến C++, Rust và Swift, nhưng rất khó tìm thấy những thứ như do-notation/for-comprehensions/monadic-let của các ngôn ngữ hàm như Haskell/Scala/OCaml mà mình đang tìm. Nhóm Go trông như bậc thầy về thiết kế ngôn ngữ, nhưng rồi lại vấp phải giới hạn của kiểu tĩnh không có đa hình tham số như Java và không tìm ra lời giải cho bài toán xử lý lỗi. Mình nghĩ đây là vấn đề bắt nguồn từ thiết kế nền tảng của ngôn ngữ
Dù đây là tài liệu do những người thông minh và dày dạn kinh nghiệm viết ra, việc không hề nhắc tới các giải pháp như monad Maybe/Either của Haskell và toán tử bind (do-notation) vẫn rất kỳ lạ. Thực tế chúng không hề khó hay hàn lâm, mà là cách rất thanh lịch và đã được kiểm chứng để truyền lỗi an toàn. Mình không hiểu vì sao cộng đồng Go lại không áp dụng hướng này. Mình biết ơn vì trang này tồn tại, nhưng thật khó hiểu khi một giải pháp nổi tiếng như vậy lại bị bỏ qua
Gần như mọi ngôn ngữ đều cung cấp nhiều cách tiếp cận tốt hơn khác nhau, vậy tại sao chỉ ở Go vấn đề này lại bị phóng đại đến thế? Mình tò mò không biết đây đơn thuần là do không đạt được đồng thuận, hay vì Go có đặc điểm riêng nào đó khiến các giải pháp từ ngôn ngữ khác không phù hợp
Một hiện tượng hay thấy trong các chỉ trích về Go là những người tương đối thiếu chuyên môn thường mặc định rằng các nhà phát triển Go hiểu về ngôn ngữ còn kém hơn họ. Thực tế thì trong đa số trường hợp, các nhà phát triển Go có kinh nghiệm hơn rất nhiều và biết nhiều hơn rất nhiều. Người không chuyên thường nghĩ rằng ngôn ngữ có nhiều tính năng hơn thì đương nhiên tốt hơn, nhưng lại bỏ qua chuyện quan trọng là phải giữ được sự cân bằng tổng thể
Mình nghĩ người dùng được hưởng lợi từ tính bảo thủ của Go trong việc thận trọng thêm tính năng ngôn ngữ mới. Với Swift, thay đổi tính năng quá nhiều nên học cũng khó, và ngay cả trên máy Mac mới cũng vẫn thường gặp cảnh không build nổi chỉ một dự án đơn giản. Vì từ khóa cứ liên tục tăng thêm và thay đổi nên Swift kém bền vững khi dùng lâu dài, còn Go thì điểm mạnh là sự ổn định nhất quán
Có lần mình gặp một tình huống bất thường trong đó hàm Go bên ngoài lại kỳ vọng hàm bên trong phải phát sinh lỗi, và nếu hàm bên trong không báo lỗi thì chính điều đó mới khiến hàm ngoài phải coi là lỗi. Trong cấu trúc hiếm gặp như vậy mình phải rẽ nhánh bằng
if err == nil, nhưng do thói quen lại viếtif err != nil, và vì quá quen với mẫu thường dùng nên mình mất rất lâu mới tìm ra lỗi. Mình từng nghĩ rằng nếu ngôn ngữ hỗ trợ phân biệt cú pháp giữaif err != nildùng thường xuyên vàif err == nilít gặp thì có lẽ sẽ giảm được sai sótif err == nilmình đều thêm chú thích// invertedđể nhấn mạnh mẫu này. Sẽ tốt hơn nếu ngôn ngữ tự xử lý được, nhưng hiện tại ít nhất cách này cũng giúp phân biệt rõ hơnif err == nil { return ... }có thể nhìn còn gượng hơn trong mã. Có ý kiến rằng cách xử lý lỗi hiện tại của Go rõ ràng, dễ đọc nên nhiều người thíchif fruit != "Apple", vì thế về bản chất đây không chỉ là vấn đề của xử lý lỗi mà là vấn đề phân nhánh trạng thái nói chung. Lỗi rốt cuộc cũng chỉ được xử lý như một giá trị trạng thái khácif err != nilđược render như một ký hiệu đặc biệt để hòa vào nền một cách tự nhiên hơn, cònif err == nilkhác biệt thì được làm nổi bật, từ đó giúp tránh lỗi ở cấp trình soạn thảoif err … {Mình thích cách Go xử lý lỗi một cách tường minh. Mình hiểu đơn giản rằng hàm hoặc luôn thành công (minimal error), hoặc có thể thất bại. Với những hàm có khả năng thất bại thì bắt buộc phải xử lý xong mới có thể sang bước tiếp theo. Mình không thích ở nhiều ngôn ngữ là lỗi được ném dọc theo stack qua cơ chế exception cho đến khi có chỗ
catch, nên cuối cùng chỉ biết lỗi phát sinh ở đâu chứ không có nhiều gợi ý thực chất. Trong Go, bạn có thể có rõ ràng các lựa chọn sau: 1) bỏ qua lỗi 2) trả về ngay khi có lỗi 3) bọc lỗi để thêm thông tin hữu ích 4) diễn giải lỗi cụ thể để rẽ nhánh xử lý (ví dụ chuyển thành 404). Ở Go2 mình muốn thử thêm kiểuResult<Value, Failure>hoặc các kiểu lỗi cụ thể hơn, có thể liệt kê được. Mình nghĩ việc đưa vào Go 2 sẽ phù hợp hơn để giữ tương thích với Go 1Ban đầu mình không thích cách Go xử lý lỗi, nhưng sau khi đọc bài blog errors-are-values và bắt đầu dùng
panic(err)đúng chỗ, mình lại thấy rất hài lòng. Với những trạng thái bất thường mà code cha không nên tự xử lý trực tiếp, dùng panic đã giúp mình giảm mạnh số nhánh lỗi lặt vặt trong mã. Cách quản lý lỗi này thực sự hữu ích trong công việc-etry/catch/finallytrong C# mình cũng thấy mới lạ, nhưng giờ lại thích logic đơn giản kiểu Go hơn. Số dòng code lớn hơn cũng có ưu điểm là luồng mã rõ ràng hơnKhi người ta nói rằng một khi thực sự xử lý lỗi thì sự dài dòng sẽ sớm bị che khuất, mình lại tự hỏi việc tự tạo manual stack trace có thật sự là “xử lý” hay không. Theo định nghĩa của Go thì exception chẳng phải cũng được tính là xử lý sao? Đây là một phản biện khá vui
Mình không thích việc bài này xem vấn đề xử lý lỗi của Go đơn giản chỉ là “cú pháp dài dòng”. Theo mình, các vấn đề thực sự là 1) lỗi rất dễ bị bỏ sót trong im lặng hoặc vô tình bị lờ đi 2) không thể dễ dàng truyền hoặc lưu kết quả hàm như một giá trị 3) lỗi lồng nhau kiểu
errors.Iskhớp rất gượng với hệ thống kiểu 4) khó switch theo lỗi 5) việc dùng sentinel value rất nhiều trong thư viện chuẩn 6) không hợp với generic nên phát sinh nhu cầu dùng package, v.v.Trong Elixir (và Erlang), hàm thường trả về tuple
{:ok, result}hoặc{:error, description}. Nhờ cú phápwithcủa Elixir, phần xử lý lỗi có thể được gom ở cuối khối nên khả năng đọc tốt hơn nhiều. Nếu Go cũng đưa vào thứ gì đó tương tựwith, chỉ chạy tiếp khi lỗi là nil và đặt khối xử lý lỗi ở cuối cùng, thì có thể cải thiện độ dễ đọc đáng kểMình không hiểu vì sao họ không đi theo thẳng phong cách Rust. Nhất là giờ đã có generic thì hoàn toàn có thể sớm làm ra thứ tương tự. Mình không đồng cảm với lập luận rằng toán tử
?của Rust tuy tiện nhưng lại khuyến khích việc lờ đi lỗi. Thực tế trong Go có vô số trường hợp bạn bỏ qua giá trị trả lỗi mà vẫn không bị lỗi biên dịch. Chỉ khi bắt buộc trả về kiểu Result theo kiểu Rust thì mới ngăn được sai sót. Nếu phản đối vì lý do tiện lợi thì lẽ ra cũng phải cấm luôn panic mới đúng, đó là một lập luận khá mạnh?” sẽ khiến “người ta không còn dùng wrapped error nữa”, có phản biện rằng ngược lại hoàn toàn có thể thiết kế các tính năng đó theo hướng khuyến khích wrappingMình nghĩ ngôn ngữ không nên được bàn theo kiểu tick checkbox rồi nhận tính năng như Rust, mà phải được thiết kế trong sự nhất quán tổng thể. Không phải cứ đánh dấu đủ danh sách tính năng là sẽ phù hợp với bản chất của ngôn ngữ