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

 
GN⁺ 2025-06-04
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

    • Bản nháp thiết kế làm nền cho phản hồi có nhắc đến C++, Rust, Swift, nhưng trong tài liệu phản hồi đồ sộ được liên kết, tôi không thấy các cách tiếp cận như do notation, for-comprehension, monadic-let được dùng trong Haskell/Scala/OCaml
      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
    • Thật lạ là dù những người rất thông minh và nhiều kinh nghiệm đã viết trang đó và thảo luận suốt nhiều năm, nhưng giải pháp kiểu Haskell là monad Maybe/Either cùng do notation dùng toán tử bind lại không xuất hiện ở đâu cả
      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ế
    • Chắc hẳn đâu đó đã có câu trả lời, nhưng tôi tò mò vì sao đây lại là vấn đề đặc biệt khó trong Go
      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
    • Một mô-típ thường thấy trong các phê phán Go là những người tương đối nghiệp dư mặc định rằng những người tạo ra Go hiểu về ngôn ngữ lập trình kém hơn họ
      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
    • Giờ muốn sửa thì phải được phê duyệt, vậy mà vẫn gọi là Wiki thì thật kỳ lạ
  • 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, /await hay .await!() sẽ lại biến mất
    Rust 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

    • Không hề có thứ gọi là “nhiều đề xuất hoàn hảo đã hoàn thiện”
    • Đó chính là cách Rust mang tiếng là một ngôn ngữ xấu khi đọc và cú pháp thiếu nhất quán vì thiết kế kiểu ủy ban
    • Ngôn ngữ lập trình là một hệ thống được thiết kế, nên tổng thể phải hợp lý
      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
    • Muốn đặt nhắc nhở sau 25 năm
      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?
    • Nói “Go không giải quyết được một vấn đề đơn lẻ mà mọi người đều lập tức gặp phải” thì kỳ lạ
      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ùng if 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 để debug
    Vì 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 đó ở đây
    Vì 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 != nil cực kỳ phổ biến và if err == nil hiếm gặp được làm nổi bật hơn thì thực sự đã có ích

    • Mỗi khi dùng if err == nil, tôi thêm chú thích // inverted để làm nó nổi bật
      Sẽ 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
    • Dĩ nhiên if fruit != "Apple" { ... } cũng có thể tạo ra tình huống y hệt
      Tô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
    • Điều này lại là một lập luận phản đối thay đổi cú pháp
      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 code
      Giả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
    • Nếu đóng vai người biện hộ cho quỷ dữ, IDE và font chữ có thể, chỉ trong chế độ cú pháp Go, làm nổi bật hoặc render mờ nền if err != nil như một ligature đơn nhất, nhỏ gọn
      Như vậy những dạng khác đúng chuỗi đó, chẳng hạn if err == nil, sẽ nổi bật hơn
    • Ý hay. Có vẻ cũng có thể giải quyết bằng ký hiệu gấp trong editor, kiểu như if 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ánh
    Ví 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 error
    Tuy 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

    • Theo kinh nghiệm của tôi, chính sách xử lý lỗi nên được giao cho caller
      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
    • “Nếu hàm thất bại thì phải xử lý thất bại đó” chính là điểm Go thất bại
      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
    • Giá mà cú pháp borgo[1] trở thành nguyên dạng ngôn ngữ Go 2 thì tốt. Ta vẫn có thể mơ
      [1]: https://borgo-lang.github.io/ | https://github.com/borgo-lang/borgo
    • Với tốc độ này, Go2 trông giống một phòng thí nghiệm ý tưởng sẽ không bao giờ được phát hành
    • Tôi muốn hỏi là tốt so với cái gì
      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

    • Nghe đáng tiếc. Cả hai lý do đưa ra đều không liên quan đến việc xử lý lỗi của Go tệ đến mức nào, và việc làm cho xử lý lỗi tốt hơn cũng chẳng khiến chúng tệ đi
      Ngược lại còn có khả năng được cải thiện
    • Ngay cả PHP cũng có xử lý lỗi tốt hơn nhờ các mức lỗi và toán tử @ để chặn lỗi tại điểm gọi
      bash cũng có -e
    • Tôi cũng thích cách của Go. Nếu có thể biết chắc hơn chuyện gì đang diễn ra, tôi sẵn sàng chấp nhận nhiều dòng code hơn
      Hồ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
    • Lỗi dạng sum type kiểu Rust cũng là giá trị
  • 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ểu
    Switch 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?

    • 90% thời gian làm việc chuyên nghiệp với Go là cố tạo test case để phủ từng nhánh trả về lỗi bằng statement coverage
      Nếu là ngôn ngữ có exception thì chẳng ai làm vậy cả
    • Tôi không thấy ở đâu trong bài này có lập luận rằng “vấn đề chính là cú pháp quá dài dòng”
      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
    • Cần nhớ rằng Go cũng đã mất cực kỳ lâu mới hỗ trợ một dạng generic nào đó
      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
    • Đồng ý 100%. Cả hai đều là Googler, nên thật đáng tiếc khi lại phải thất vọng về đội ngũ Go
    • Tôi đồng ý với điểm 1, nhưng có thể giảm nhẹ phần nào bằng các công cụ phát triển như errcheck: https://github.com/kisielk/errcheck?tab=readme-ov-file
  • 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++

    • Thật thú vị khi mọi người nhìn stack trace đầy màn hình rồi nói nó rõ ràng và hữu ích
      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 with của Elixir, có thể gom phần xử lý lỗi xuống phía dưới nên dễ đọc hơn nhiều
    Go 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ưới

    • Nhìn vào những bằng chứng hiện có thì có vẻ Go hoàn toàn không có khả năng chấp nhận câu lệnh with
      Đ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
    • Bản thân đa giá trị trả về của Go, theo góc nhìn của tôi, đã kỳ lạ rồ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ì
    • Người dùng Haskell và fan Rust xem sum type như của riêng họ, và mọi người sau khi đọc rồi tin các bình luận và bài viết của họ lại ngại sum type vì không muốn rơi vào cái hang thỏ Hindley-Milner đáng sợ
      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áo declared and not used: err, nhưng sau lần chuyển đổi thứ hai, dù không kiểm tra err, vì giá trị mặc định của y là 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ì đó sai
    Nế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ả panic sao?

    • Go không có sum type, nên không thể có Result
      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
    • Tôi hiểu ý là nếu ? dễ dùng thì sẽ không còn ai wrap lỗi nữa
      Mộ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à được
    • Vì khi đưa kiểu Rust vào Go, không rõ hình thức tương đương chính xác là gì
      Ví dụ, thứ tương ứng với From củ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 if nơ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
    • Tôi cứ nghĩ := 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ải err được khai báo lại và err mới che khuất err cũ sao?
      Nếu vậy, vì biến err mới không được dùng nên có vẻ nó phải thất bại với declared and not used: err
      Hay 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