1 điểm bởi GN⁺ 2025-06-04 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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ế checkhandle 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

 
GN⁺ 2025-06-04
Ý 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ết if 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ữa if err != nil dùng thường xuyên và if err == nil ít gặp thì có lẽ sẽ giảm được sai sót

    • Mỗi lần dùng if err == nil mì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ơn
    • Thực ra đây lại là một lập trường phản đối thay đổi cú pháp. Mẫu thường dùng if 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ích
    • Người ta lập luận rằng cùng kiểu nhầm lẫn cũng có thể xảy ra với các mẫu như if 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ác
    • Có thể trong IDE hoặc cấu hình font, if 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òn if err == nil khá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ảo
    • Cũng có đề xuất rằng trình soạn thảo có thể cải thiện khả năng đọc bằng cách rút gọn cách hiển thị các mẫu như if 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ểu Result<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 1

    • Theo kinh nghiệm của mình, chính caller phải quyết định chính sách xử lý lỗi; xử lý ở tầng stack thấp hơn là điều không mong muốn. Cuối cùng rất dễ biến thành công việc lặp đi lặp lại là chỉ bọc lỗi rồi chuyển lên tầng trên
    • “Xử lý lỗi của Go” thật ra đã có ở đa số ngôn ngữ như ngôn ngữ hàm, Rust, Java chứ không chỉ JavaScript hay Python. Rốt cuộc chỉ cần có generic là có thể triển khai kiểu xử lý lỗi như Go ở bất kỳ ngôn ngữ nào. Nếu đối tượng so sánh chỉ dừng ở JS hay Python thì đây cũng chỉ là một mẫu rất phổ biến
    • Người ta chỉ ra rằng chính chỗ “nếu hàm thất bại thì bắt buộc phải xử lý” mới là điểm thất bại của Go. Trong Go, trên thực tế có thể bỏ qua lỗi hoàn toàn, nên nếu muốn xây dựng phần mềm thực sự robust thì cách làm của Go lại có thể là điểm yếu
    • Có ý kiến chua chát rằng Go2 rốt cuộc sẽ chỉ mãi là một “phòng thí nghiệm không bao giờ phát hành”
  • Ban đầ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

    • Có phản biện rằng kiểu lập luận này rốt cuộc vẫn không thể bảo vệ được khả năng xử lý lỗi nghèo nàn của Go, và dù có cải thiện thì các ưu điểm cũng chẳng biến mất
    • Có nhắc tới việc PHP cũng cho phép xử lý lỗi theo mức độ hoặc dùng toán tử @ để chặn lỗi ở call site, còn bash cũng có kỹ thuật quản lý lỗi như -e
    • Lần đầu thấy luồng try/catch/finally trong 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ơn
    • Cũng có nhắc rằng kiểu lỗi dựa trên sum type của Rust cũng thuộc cùng một hệ tư tưởng ‘errors are values’
  • Khi 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 nghi ngờ liệu hàng chục dòng stack trace có thực sự là thông tin rõ ràng hay không. Cá nhân mình thấy một dòng wrap error còn hiệu quả hơn nhiều và cũng giúp dọn log tốt hơn. Dùng Go hơn 10 năm rồi mà mình chưa bao giờ cần đến stack info dài dòng có cả các hàm runtime bên trong
  • 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.Is khớ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.

    • 90% lập trình viên Go chuyên nghiệp đều phải viết test case cho từng nhánh trả lỗi để đạt coverage, trong khi ở các ngôn ngữ dùng exception thì đó là việc không cần thiết
    • Mình nghĩ nhận định rằng bài này coi It’s too verbose là vấn đề chính là không đúng với thực tế. Dù đổi cú pháp thì mức cải thiện về bản chất cũng không lớn
    • Cũng có góc nhìn cho rằng tốc độ thay đổi rất chậm của Go (generic cũng mất rất lâu) lại là một ưu điểm
    • Với tư cách là một Googler, mình lại một lần nữa thất vọng về quyết định của nhóm Go
  • Trong Elixir (và Erlang), hàm thường trả về tuple {:ok, result} hoặc {:error, description}. Nhờ cú pháp with củ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ể

    • Vì vấn đề đồng thuận cộng đồng nên Go đưa vào rất chậm ngay cả những tính năng có giá trị như sum type cơ bản nhất, xử lý lỗi, hay quản lý package. Generic mất 13 năm, xử lý lỗi 16 năm, quản lý package 9 năm, mức thay đổi quá chậm. Thận trọng là quan trọng, nhưng cảm giác như vì cứ theo đuổi sự hoàn hảo nên quyết định luôn bị trì hoãn
    • Mẫu trả về nhiều giá trị của Go đôi khi bị xem là bất thường tùy theo góc nhìn. Chỉ trích ở đây là với một hàm trả về nhiều kiểu, thứ duy nhất có thể làm lại chỉ là gán cho biến
  • 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

    • Có ý kiến cho rằng lý do Go không thể đưa vào Result là vì không có sum type và vì có thiết kế đặc thù là mọi kiểu đều cần zero value
    • Với lập luận rằng các tính năng tiện lợi như “toán tử ?” 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 wrapping
    • Người ta giải thích rằng nhược điểm của các tính năng đề cao tính tiện lợi (kiểu Rust) là luồng phân nhánh bị giấu trong một dòng, khó đặt breakpoint khi debug, và quá thiên về bubbling thay vì enrich/handling; đây cũng là kiểu cú pháp Go đã loại bỏ (ví dụ toán tử ba ngôi)
    • Ngay cả nếu so sánh và áp dụng nguyên phong cách Rust, vẫn có câu hỏi kỹ thuật rằng trong Go thì cái gì mới thực sự là equivalent chưa hề rõ ràng
    • Cũng có phản hồi hỏi rằng sau khi có generic thì cụ thể đã triển khai kiểu Rust bằng cách nào, mong được xem ví dụ code
  • Mì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ữ

    • Rust bị gắn với hình ảnh là đi theo kiểu design by committee nên cú pháp khó đọc và thiếu nhất quán
    • Có ý kiến rằng không tồn tại cái gọi là “giải pháp hoàn hảo”
    • Kết quả khảo sát cho thấy chỉ 13% trả lời rằng xử lý lỗi là vấn đề nghiêm trọng duy nhất của Go, và cũng có không ít người dùng thích trạng thái hiện tại. Xem kết quả khảo sát