Những lời nói dối chúng ta tự nhủ để tiếp tục dùng Golang (2022)
(fasterthanli.me)- Những phản biện lặp đi lặp lại trước các chỉ trích về Go như “các công ty lớn cũng dùng”, “nó cũng có điểm tốt”, “tính đúng đắn có chi phí cao” có thể che mờ vấn đề rằng khiếm khuyết trong thiết kế ngôn ngữ sẽ tích lũy thành chi phí dài hạn
- Go mạnh về runtime bất đồng bộ, GC, công cụ quản lý package, refactoring và cross-compile, nhưng bạn cũng phải chấp nhận cả custom toolchain, hệ thống build, quy ước gọi hàm, một GC duy nhất và hệ sinh thái của nó
- Do thiếu sum type, thiếu kiểm soát mutability, zero value, cùng hành vi khác nhau của
nilslice, map và channel, compiler không thể bảo vệ nhiều điều kiện bất biến - Nếu không có cgo thì khó tích hợp với các ngôn ngữ và công cụ khác; còn nếu dùng cgo thì phát sinh gánh nặng về GC và quy tắc pointer, việc nhúng runtime, các static library lớn, khiến ranh giới mạng trở thành điểm tích hợp thực tế nhất
- Kỳ vọng “chỉ làm prototype bằng Go rồi sau này viết lại” trên thực tế có thể làm tăng chi phí vận hành do né tránh việc viết lại, sự kết dính với mã Go hiện có, cũng như gánh nặng từ linter, tài liệu và kiểm chứng thủ công
Các lập luận phòng thủ lặp lại trước chỉ trích về Go
- Sau bài viết năm 2020 I want off Mr Golang’s Wild Ride, các phản ứng quanh việc chỉ trích Go cứ tiếp tục lặp lại
- Phản ứng rằng ví dụ về Windows không phải là lĩnh vực Go làm tốt
- Phản ứng rằng bài viết không nói đến điểm tốt của Go
- Phản ứng rằng người viết không hiểu những đánh đổi mà Go đã chọn
- Phản ứng rằng các công ty lớn dùng Go nên nó không thể tệ đến vậy
- Phản ứng rằng chi phí mô hình hóa vấn đề “đúng cách” là lớn, nên bàn về tính đúng đắn là vô nghĩa
- Phản ứng rằng Rust cũng có nhược điểm, nên chỉ trích Go là không còn giá trị
- Điểm cốt lõi không phải là Go có ưu điểm hay không, mà là các lập luận phòng thủ được lặp lại để tiếp tục dùng Go có đang che khuất chi phí thực hay không
Cách tấn công tư cách của người chỉ trích trước
- Khi nhận được phản hồi không muốn nghe, nếu cho rằng người nói kém năng lực hoặc có động cơ ẩn, ta sẽ không cần xem xét nội dung nữa
- Việc phát hiện vấn đề không nhất thiết đòi hỏi trình độ rất cao; các developer senior thậm chí có thể bỏ qua những bất tiện vì đã quen với chúng
- Developer junior, vì chưa quen với những điểm kỳ lạ, có thể cảm thấy bất tiện và đặt câu hỏi nhanh hơn
- Giá trị của phản hồi cần được xem xét dựa trên việc có thực sự có vấn đề hay không, hơn là dựa trên ai là người nói
Giới hạn của luận cứ “công ty khác cũng dùng”
- Trong lựa chọn công nghệ, việc xem công ty khác dùng gì hữu ích cho khám phá, nhưng không đủ làm căn cứ để áp dụng
- Blog kỹ thuật của công ty thường khó bộc lộ đầy đủ chi phí áp dụng
- Nhiều khi được viết sau khi quyết định đã được đưa ra
- Có mục đích làm hình ảnh công ty tốt hơn và hỗ trợ tuyển dụng
- Các phê phán kỹ thuật mạnh thường đến từ cá nhân hơn là từ công ty
- Việc Tailscale dùng Go gần với một trường hợp mà đội ngũ chuyên gia Go biết và có thể gánh chi phí
- netaddr.IP: a new IP address type for Go cho thấy các giới hạn của Go trong quá trình biểu diễn địa chỉ IPv4 hoặc IPv6
- Hey linker, can you spare a meg? xử lý một vấn đề đòi hỏi kiến thức sâu về linker của Go
- Nguyên nhân gây đau đớn thể hiện trong
netaddr.IPgắn với nhiều hạn chế của Go- Vì Go không có sum type, việc biểu diễn kiểu “IPv4 hoặc IPv6” trở nên gượng ép
- Slice được dùng như một cấu trúc dữ liệu tổng quát có chi phí 24 byte trên máy 64-bit
- Không có operator overloading, nên nảy sinh phân biệt như
a == bvàa.equals(b) - Không hỗ trợ dữ liệu bất biến, nên phải truyền bản sao và cẩn thận với thay đổi bên trong
- Khó tạo
newtypeopaque, nên cần đi đường vòng qua package riêng và interface
- Những đội như Tailscale có thể đào sâu vào hoạt động nội bộ và các edge case của Go, nhưng không phải tổ chức nào cũng có chuyên gia Go và ngân sách kỹ thuật như Google hay Tailscale
Những phần Go thực sự làm tốt
- Go cung cấp một runtime bất đồng bộ khá tốt, các mặc định có quan điểm rõ ràng, GC hiện đại với hai núm điều chỉnh, và toolchain tốt
- Vì runtime bất đồng bộ nằm ở trung tâm ngôn ngữ, Go có những công cụ mà developer Rust có thể ghen tị
- Có thể dễ dàng dump backtrace của tất cả goroutine đang chạy
- Có thể phát hiện deadlock
- Có profiler riêng
- Giúp bớt phải để ý đến function coloring
- Các công cụ quản lý package, refactoring và cross-compile ban đầu có thể cho cảm giác là cải tiến so với
pkg-config, autotools, CMake trong hệ sinh thái C - Tuy nhiên, không phải bạn chỉ chọn dùng phần runtime tiện lợi của Go
- Custom toolchain
- Hệ thống build
- Quy ước gọi hàm
- Một GC duy nhất
- Các thành phần chuẩn được bao gồm và hệ sinh thái
- Cả những thiết kế ngôn ngữ hình thành một cách ngẫu nhiên cũng được áp dụng cùng lúc
Khoảng trống trong type system và thiết kế ngôn ngữ
- Go đặt runtime cho TCP, HTTP, TLS, HTTP/2, DNS và triển khai web service ở trung tâm, đồng thời có hình thức quen thuộc với các developer Google đã học Java/C/C++/Python
- Giống C, Go không xử lý bản thân việc error handling một cách sâu sắc; developer phải tự quản lý nhiều trạng thái có thể thay đổi bằng
ifvàelse - Giống Java, Go làm mờ ranh giới giữa value và reference, khiến chỉ nhìn vào điểm gọi khó biết giá trị có bị thay đổi hay không
- Tùy method receiver là
Ahay*A, biến cục bộatrongmaincó thể bị thay đổi hoặc không
- Tùy method receiver là
- Khó giới hạn rõ ràng mutability và immutability bằng type, nên khi truyền reference có nguy cơ giá trị bên trong bị thay đổi hoặc bị giữ quá lâu và không được giải phóng
- Go không ngăn chặn đủ tốt các nhóm lỗi sau
- Vô tình copy mutex khiến nó mất tác dụng
- Không khởi tạo field của struct khiến zero value được đưa vào
nil, zero, empty string sai lan sâu vào hệ thống
Giới hạn của zero value và “chỉ cần cẩn thận”
- Ngay cả khi thêm field mới vào struct, về thực chất làm thay đổi dạng input của hàm, nếu bỏ sót field mới trong composite literal thì compiler cũng không chặn
- Trong ví dụ,
b int32được thêm vàoParams, nhưng nếu phía gọi chỉ truyềna: 47thì chương trình chạy vớib=0 - Ngược lại, nếu trực tiếp thêm tham số hàm, sẽ có lỗi compile do thiếu số lượng tham số
- Trong ví dụ,
- Một trong các nguyên tắc của Go là zero value hữu ích, nhưng ý nghĩa thực tế lại khác nhau theo từng type
- Gọi
lentrênnilslice trả về 0 nên nó hoạt động như slice rỗng - Gán giá trị vào
nilmap gây rapanic: assignment to entry in nil map
- Gọi
- Channel cũng có các quy tắc riêng
- Send vào
nilchannel sẽ block mãi mãi - Receive từ
nilchannel sẽ block mãi mãi - Send vào channel đã đóng sẽ gây panic
- Receive từ channel đã đóng sẽ trả về zero value ngay lập tức
- Send vào
- Vì Go không có sum type, khó biểu diễn bằng type một input “chỉ cho phép một trong các option đã biết”, và nó gần với cách của C là “cứ cẩn thận”
- Thái độ cho rằng vì không thể ngăn mọi vấn đề nên cũng không cần ngăn một phần vấn đề là một lưỡng phân sai lầm, đẩy cả những vấn đề mà type system có thể hỗ trợ sang sự cẩn trọng thủ công
So sánh với Rust không phải là tranh luận về “phương án thay thế hoàn hảo”
- Đây không phải lập trường rằng Rust hoàn hảo; Go và Rust có thể được xem là các câu chuyện thành công theo những cách khác nhau
- Thành công của Go phụ thuộc nhiều vào các thành phần được bao gồm và các mặc định có quan điểm rõ
- Thành công của Rust phụ thuộc nhiều vào khả năng áp dụng từng bước và dễ tích hợp với ngôn ngữ khác
- Các trường hợp áp dụng Rust thực tế gần với di chuyển dần dần hơn là thay thế toàn bộ
- Firefox bao gồm nhiều component Rust cốt lõi trong một codebase C++ lớn
- Android triển khai lại Bluetooth stack bằng Rust
- Mã mã hóa Rust đã đi vào Python, và mã HTTP Rust trở thành một trong nhiều backend của curl
- Patch Rust cho Linux kernel đang liên tục được cải thiện
- Người dùng Rust cũng tích cực nói về thời gian build, thiếu hụt tính năng ngôn ngữ và các nhược điểm khác
- Luận điểm không phải là bắt buộc chọn Go hoặc Rust, mà là các công cụ có thể biểu diễn mutability, lifetime và abstraction bằng type giúp giảm những vấn đề nào
- Ngay cả trong Rust, không phải lúc nào cũng mô hình hóa mọi trường hợp một cách nghiêm ngặt
Go gần như một hòn đảo tách khỏi các hệ sinh thái khác
- Nếu không dùng cgo, Go xa rời assembly, linker, debugger, memory checker và calling convention thông thường
- Go gần với ngôn ngữ closed-world hơn C/C++, và kém thân thiện với FFI hơn Node.js, Python, Ruby
- Khác biệt này cũng có ưu điểm
- Có thể quan sát bên trong TLS và HTTP stack bằng profiler theo cùng cách với business logic
- Tương phản với việc stack trace trong ngôn ngữ động dừng lại ở OpenSSL
- Runtime có thể xử lý non-blocking I/O và scheduling
- Nhưng khó dùng nguyên trạng nhiều công cụ hiện có và tri thức tổ chức, và việc tích hợp Go với ngôn ngữ khác cũng rắc rối
- Mỗi cách tích hợp Go đều có chi phí riêng
- Khi gọi C từ Go, ngoài chi phí ranh giới FFI còn cần theo dõi descriptor thủ công để không phá GC
- Khi gọi Go từ ngôn ngữ khác, phải đưa vào toàn bộ Go runtime, bao gồm GC, tạo ra static library lớn và gánh nặng vận hành
- Ranh giới tốt nhất khi tiếp xúc với Go gần với ranh giới mạng
- RPC over TCP như REST-ish HTTP/1, JSON-RPC, gRPC tương đối ít đau đớn hơn nếu có thể chấp nhận chi phí độ trễ
- Dù vậy, cả hai phía vẫn phải duy trì các điều kiện bất biến, và ở phía Go nếu không dùng đều đặn các package validation thì compiler sẽ không đảm bảo giúp
Cạm bẫy trong kỳ vọng dùng Go cho prototype
- Vì Go dễ học và dễ tuyển dụng, có suy nghĩ rằng hãy làm prototype trước, rồi khi khó hơn thì viết lại hoặc đưa chuyên gia vào
- Nhưng “mã sẽ vứt đi” hầu như không tồn tại trong thực tế
- Viết lại mất thời gian
- Khó điều phối chuyển đổi trơn tru
- Các chi tiết có thể biến mất
- Phát triển tính năng mới bị dừng lại
- Cần đào tạo lại nhân sự theo công nghệ mới
- Càng có nhiều component Go, càng có nhiều lý do để tiếp tục chọn Go nhằm tương tác với mã Go hiện có
- Linter có giúp một phần, nhưng không thể làm được như compiler của một ngôn ngữ đã nghiêm túc xử lý vấn đề ngay từ đầu, và cũng làm chậm tốc độ phát triển
- Độ phức tạp không có trong ngôn ngữ sẽ chuyển sang codebase
- Các điều kiện bất biến không biểu diễn bằng type phải được viết trực tiếp bằng code
- Chỉ nhìn vào function signature khó biết dữ liệu có bị thay đổi hay không, có bị lưu giữ hay không, zero value có được phép hay không, goroutine có được khởi động hay không, có khả năng là
nilchannel hay không, và thực sự có thể truyền type nào vàointerface{} - Trở nên phụ thuộc vào tài liệu, mà tài liệu thì có cả chi phí cập nhật lẫn chi phí không cập nhật
Những “lời nói dối” được tóm lại cuối cùng
- Những điều ta tự nhủ để tiếp tục dùng Go có thể được tóm tắt như sau
- Người khác dùng nên nó cũng sẽ tốt cho chúng ta
- Người nêu lo ngại là kẻ tinh hoa chủ nghĩa hoặc thô lỗ
- Runtime bất đồng bộ và GC hấp dẫn bù đắp cho mọi nhược điểm khác
- Từng khiếm khuyết thiết kế ngôn ngữ, dù riêng lẻ hay cộng lại, đều không sao
- Có thể vượt qua bằng “cẩn thận”, linter và nhiều reviewer hơn
- Vì dễ viết nên phát triển phần mềm production cũng dễ
- Vì ngôn ngữ đơn giản nên phần còn lại cũng đơn giản
- Chỉ cần dùng một chút hoặc chỉ lúc đầu, rồi có thể thoát ra dễ dàng
- Sau này lúc nào cũng có thể viết lại
- Nếu công cụ được chọn không loại bỏ được độ phức tạp, độ phức tạp đó sẽ chuyển sang các developer, operator và khách hàng khác
- Tình huống giá trị
nilđi vào nơi không được phép và đánh thức bạn vào ban đêm khi vận hành sẽ dẫn tới sự lặp lại của Billion Dollar Mistake
2 bình luận
Tôi băn khoăn không biết một người nghiệp dư mới gắn bó khá tập trung với ngôn ngữ Go trong một khoảng thời gian rất ngắn như chớp mắt có nên viết vài dòng hay không... Go là một ngôn ngữ có ưu điểm và nhược điểm rất rõ ràng, nên có vẻ cả những người chọn dùng lẫn những người né tránh nó đều có lý do rất cụ thể. Cá nhân tôi nghĩ không nên so sánh với Rust, mà hợp lý hơn là so với Kotlin(Java).
Goroutine của Go thực sự rất xuất sắc, nhưng không phải phép màu. Đặc biệt ở backend, với những dự án nhỏ chỉ dùng một MySQL, việc quản lý tính đồng thời này thật sự rất khó. Những vấn đề như cạn kiệt tài nguyên MySQL hay quản lý pool — vốn trong runtime JS/TS không cần bận tâm quá nhiều — lại khó hơn tưởng tượng. Cuối cùng trong tình huống này, DB trở thành nút thắt cổ chai nên ưu điểm về tính đồng thời của Go phần nào bị giảm đi. (I/O bất đồng bộ hay event loop của runtime JS/TS thậm chí có thể phù hợp hơn) Chỉ cần thử ném
-c 100bằng công cụ như hey là sẽ thấy ngay.Ngoài ra, dù GC rất tốt, cũng không có nghĩa là có thể tùy tiện chỉ truyền con trỏ của object rồi mặc kệ khâu dọn dẹp phía sau. Mọi thứ đều là trade-off, nhưng với Go, nếu có thể thì các object nhỏ vẫn nên được truyền bằng copy giá trị để khi hàm kết thúc là được xử lý ngay sẽ tốt hơn. Có thể tôi đang bị mắc kẹt trong lối nghĩ cũ, nhưng không thể tiếp cận con trỏ một cách dễ dãi từ góc độ hiệu suất như với C/C++.
Việc gần như lúc nào cũng phải trả về
errorkhi function return, rồi lần nào cũng phải kiểm tra bằngif err != nil {}đúng là rất phiền, nhưng đây lại là một ưu điểm. Vì chi phí của nó rẻ hơntry catch. Và từ khóadefer, thứ đảm nhiệm vai trò giống nhưfinally {}, cũng rất tuyệt vời. Không cần phải bận tâm về thời điểm giải phóng tài nguyên nên rất tiện. Việc chỉ với standard library đã có thể nhanh chóng dựng được một backend server khá tốt cũng là điểm hay (1.23 trở lên). Trên hết, điều tôi thích nhất là chỉ cần build theo target OS thì không cần runtime khác hay cài đặt sẵn gì thêm.Tôi chưa dùng Go quá lâu mà lại viết dài toàn ý kiến quá cá nhân, nên xin dừng ở đây. haha Tôi thích cả Go lẫn những ngôn ngữ khác!
Ý kiến trên Hacker News
Bài viết này chỉ ra khá đúng nhiều nhược điểm của Go, nhưng tôi không nghĩ xử lý lỗi tường minh là một trong số đó
Tôi luôn không thích xử lý ngoại lệ vì nó thêm một tầng “ma thuật” rất dễ bị phá vỡ; dù đây không hẳn là lập luận hoàn hảo về mặt kỹ thuật, sau vài chục năm kinh nghiệm tôi vẫn thích cách lỗi được xử lý ngay tại điểm nó phát sinh hơn
Có thể nói Rust làm việc này thanh lịch hơn, và với dự án cá nhân tôi thích Rust hơn, nhưng trong các dự án lớn có lập trình viên ở nhiều mức kinh nghiệm ra vào, tôi cho rằng triết lý của Go là khá hợp lý trong các cách xử lý lỗi hiện đại
Ở khu vực của tôi, nhờ sự đơn giản, Go đang được áp dụng nhiều hơn hẳn các ngôn ngữ “mới” khác. Nó không phải ngôn ngữ tốt nhất, nhưng có nhiều phán đoán tích hợp sẵn giúp bạn không tự làm mình bị thương, nên với vai trò ngôn ngữ đa dụng, nó thường là lựa chọn tốt nhất
Không có stack trace và phải bọc lỗi, nên ngoài thông báo lỗi riêng, còn phải tạo cả thông báo bọc riêng ở từng điểm gọi; như vậy mới có thể
grepthông báo để suy đoán thứ gì đó giống stack trace“Tuple trả về” là một cơ chế kỳ lạ chỉ tồn tại vì lỗi, trong khi ở những chỗ khác của ngôn ngữ thậm chí cũng không dùng được tuple. Do các quy tắc khởi tạo biến gượng gạo, sớm muộn bạn cũng sẽ dùng nhầm biến
errGo lẽ ra nên có enum, hoặc đưa vào cú pháp đường tốt hơn như
?của Rust thay vì “tuple trả về” kỳ quặc chỉ dành cho lỗi. Cách hiện tại nhàm chán và dễ gây lỗiGo có sự cố chấp, nhưng quá thường xuyên trông như sự cố chấp nhằm bảo vệ ngôn ngữ khỏi bị phê bình. Đáng tiếc là để một ngôn ngữ lập trình trở nên phổ biến, marketing — tức cả những lời nói dối — cũng quan trọng, và điều này có hiệu quả
Ngoại lệ là một
gotokinh khủng mà bất kỳ thư viện nào cũng có thể ném vào mã của tôi, và hầu như không bao giờ được xử lý đúng. Ngay cả sau khi ngoại lệ được “xử lý”, ứng dụng thường rơi vào trạng thái không xác định và không thể suy luận đượcLỗi trong Go nhìn chung ngay cả kỹ sư junior cũng debug khá dễ, còn nguyên nhân thật sự của ngoại lệ Java thì ngay cả principal dày dạn cũng khó xác định
Ngược lại, tôi vẫn chưa thấy ai quen với kiểu tổng như Maybe, Option, Result mà lại cho rằng cách của Go dù chỉ hơi ổn
Khi đọc logic của một phương thức cụ thể mà 75% là nhiễu lỗi, chỉ 25% số dòng mới cần để hiểu hành vi thực sự, thì rất khó đọc
Khi lần đầu nhìn vào codebase, tôi muốn biết nó đang cố đạt được điều gì trước các trường hợp biên về lỗi, rồi sau đó mới xem cách bảo đảm điều đó là đúng
Ở điểm này, xử lý lỗi của Go đã thất bại nặng nề. Vấn đề không phải là xử lý lỗi tường minh là xấu, mà là sự khăng khăng phải trộn đường đi thành công và xử lý lỗi theo từng dòng. Ta vẫn có thể xử lý tường minh trong một khu vực chuyên trách cho xử lý lỗi
Điểm tôi đồng ý là nó buộc bạn phải nghĩ đến mọi khả năng mà mã có thể tạo ra. Tuy nhiên đây là câu hỏi gần với C hơn là các ngôn ngữ khác
Khi trừu tượng hóa mã, có những lúc bạn không cần xử lý lỗi ngay lập tức, hoặc muốn xử lý ở phía dưới call stack
Tức là phải hy sinh rất nhiều khả năng đọc để ngôn ngữ ép buộc một cách làm cụ thể
Rust và Go rất khác nhau, và có vẻ mọi người muốn một điểm trung gian hiện chưa tồn tại
Ý là một ngôn ngữ có garbage collection, tương đối đơn giản và biên dịch thành binary liên kết tĩnh, nhưng có hệ thống kiểu giống Rust và những thứ như kiểu sum type
Về mặt cú pháp thì Gleam và Kotlin phần nào gần với điều đó, nhưng chưa hoàn toàn. Tôi thích Rust, nhưng với nhiều người có khả năng tạo ra thứ gì đó, dù không học chuyên ngành khoa học máy tính và cũng không làm lập trình viên, Rust quá phức tạp
Nếu chỉ thỉnh thoảng mới dùng ngôn ngữ đó, họ sẽ không nhớ vtable là gì, giá trị được giải phóng khi nào và như thế nào. Không có “ngôn ngữ hoàn hảo”, nhưng cả Go lẫn Rust đều đã mang lại những điều tuyệt vời, và tôi mong ai đó lấy cảm hứng từ cả hai để tạo ra một ngôn ngữ đơn giản có thể được dùng rộng rãi
Kotlin thực tế rất phức tạp, và bạn sẽ mất thời gian cố làm cho khôn khéo. Có tới 1000 cách để làm một việc, lại có operator overloading, proxy, property, companion object, exception và result type
Nếu giả định dùng Gradle, hệ thống build gần như là cực hình với người đã quen
go buildAPI coroutine đồng thời cho cảm giác vừa phức tạp hơn vừa hạn chế hơn Goroutine. Nó có cấu trúc hơn, nhưng kém linh hoạt hơn và tốn nhiều công hơn để dùng
Kiểm soát truy cập cũng gượng gạo. Không có cách nào để biến một kiểu thành chỉ dùng trong package; hoặc là chỉ trong file, hoặc là public cho toàn module. Vì vậy bề mặt API dễ phình to ngoài ý muốn, hoặc bạn không thể chia nhỏ độ phức tạp ra nhiều file
Kotlin/Native và Kotlin/JVM về cơ bản là hai thứ khác nhau. Kotlin/JVM đã trưởng thành, nhưng vì chạy trên JVM nên một phần các trường hợp dùng đáng lẽ cần Rust sẽ biến mất
Hệ sinh thái Kotlin/Native rất yếu và tài liệu cũng thiếu. Trong issue tracker còn có những bug đáng sợ
Kotlin/Native thậm chí không thể phân phối thư viện chỉ có source, nên cần CI phức tạp để build binary cho mọi hệ điều hành và kiến trúc. Nếu không thì cuối cùng người ta chẳng phân phối thư viện nữa, và điều này dường như càng làm hệ sinh thái vốn yếu lại yếu hơn
Giống như đã thêm generic, chỉ cần thêm đúng ba thứ: Result, một container generic cho lỗi; Optional để xử lý nil tốt hơn; và một chút cú pháp ngọt như toán tử Elvis
Lợi thế lớn gần như một mình giải thích thành công của Go là nó được một công ty khổng lồ tạo ra và trực tiếp sử dụng, nên có nguồn lực để làm công cụ xuất sắc và thư viện mã nguồn mở. Việc đã có người dùng và có thư viện dường như là yếu tố quyết định lớn nhất cho thành công của một ngôn ngữ
Cú pháp cơ bản rất trực tiếp, mức độ được chấp nhận đã rất lớn, hệ sinh thái rộng, runtime tiếp tục tốt lên, và cũng có thể biên dịch sang WASM
Sau đó loại bỏ những phần kỳ quặc, chẳng hạn quá nhiều linh hoạt trong việc định nghĩa lại, lạm dụng phạm vi toàn cục, những điểm lạ liên quan đến số, rồi thêm kiểu, sum type/product type, Result, Maybe, decimal, v.v.
Rồi thêm phong cách mọi thứ đều là biểu thức, pattern matching, các tính năng hàm như currying và một thư viện chuẩn toàn diện, thì có thể sẽ có một ngôn ngữ gần nhất với điều nhiều người mong muốn
Một bước tiến lớn khác tôi muốn thấy trong công cụ phát triển là cải tổ HTML để tích hợp sẵn các control cao cấp đúng nghĩa, và cho phép style dễ dàng bằng inline CSS. Như vậy lượng JS cần ở client sẽ giảm
Tôi nghĩ hai điều này sẽ cải thiện mạnh năng suất lập trình trong phát triển liên quan đến web
Có thể biên dịch thành binary native tĩnh bằng
dotnet publish -p:PublishAot=true. Trong F# phải dùng các phương thứcConsole.*thay vìprint*, vìprintcó reflection không bị ràng buộc bên trong do output có cấu trúc của format specifier%A, ảnh hưởng xấu đến kích thước binary và khiến compiler phàn nànĐặc biệt F# đáng được đề xuất như một “phương án thay thế Rust dễ hơn và hướng nghiệp vụ hơn”. Nó hướng biểu thức, có discriminated union, suy luận kiểu HM đầy đủ, và áp dụng kiểu dần dần rất hợp lý. Phân tích dữ liệu và mô hình hóa domain cũng rất dễ chịu
Với lập trình hệ thống, C# là một lựa chọn. Nó cung cấp các primitive concurrency xuất sắc, tương tác native nhanh và đôi khi gần như không tốn chi phí, binary native nhỏ hơn Go, cùng nhiều API cấp thấp bao gồm SIMD khả chuyển
Go thường không phù hợp với các việc như vậy, hoặc trong nhiều trường hợp không thể làm được nếu không dùng đường vòng. .NET đang được cộng đồng ngoài game ưa chuộng hơn trong lĩnh vực này, và cũng ngày càng có nhiều thư viện hiệu năng cao. Ngoài ra, khác với các ngôn ngữ ngách được đề xuất trong comment anh em, nó hưởng lợi từ một hệ sinh thái hiện hữu khổng lồ, nên bạn không phải tự làm mọi phần nền tảng
Go: “Tôi là một ngôn ngữ đơn giản!”
Người dùng dùng Go một thời gian
Người dùng: “Tôi ghét cậu, cậu là một ngôn ngữ đơn giản mà!”
Có lẽ vì tôi đã ngoài 50 nên tôi thích ngôn ngữ đơn giản
Tôi thấy lời “phê bình” này không cân bằng cho lắm, mà trong công nghệ mọi thứ đều là đánh đổi; những nhận định thiếu cân bằng thì yếu
Tất nhiên kết luận của tôi thì khác: https://www.inkmi.com/blog/why-we-chose-go-over-rust-for-our...
Bài viết có nhiều ví dụ chỉ ra những điểm thiếu nhất quán gây khó chịu của ngôn ngữ, và những thứ đó là đối lập với sự đơn giản
Tôi thích bài thuyết trình về Go của Rob Pike, một số phần còn mở mang tầm mắt cho tôi. Chỉ là tôi mong Go mà tôi thấy trong thực tế gần với Go mà các fan Go mô tả một cách trừu tượng hơn nhiều
Trái với vẻ bề ngoài, khi bắt đầu dùng Go thì sẽ sớm lộ ra rằng nó không đơn giản. Có rất nhiều độ phức tạp ẩn và cạm bẫy. Ví dụ: https://archive.ph/WcyF4
Dù vậy nó vẫn là một ngôn ngữ hữu ích và được dùng khá nhiều, nhưng không “đơn giản”
Theo tôi, một ngôn ngữ đơn giản phải có tính nhất quán cao. Quy tắc thì ít, nhưng phải phổ quát và nhất quán ở mọi nơi. Nhưng trong Go có đầy rẫy sự kỳ quặc, thiếu nhất quán và các cách đi vòng
Một ví dụ hay là lược bỏ kiểu. Nó có thể dùng trong khai báo array, slice, map, nhưng không dùng được với struct. Nếu cũng cho phép với struct thì sẽ có nhiều lợi ích về khả năng đọc, đồng thời có thể tạo named parameters thông qua đối số anonymous struct, giúp thiết kế tốt hơn nhiều so với cách đi vòng xấu xí hiện nay
Tất nhiên, phần lớn các lựa chọn thay thế hợp lý cho Go cũng tệ hơn ở góc nhìn này. Như Python, Ruby, JS
Tôi tò mò động cơ nào khiến người ta hạ thấp bất kỳ ngôn ngữ nào dài đến vậy. Ngay cả vài điểm hợp lý trong bài cũng không được viết bằng giọng văn mang tính xây dựng, nên tôi sẽ gọi đó là chỉ trích
Có ngôn ngữ nào không thể bị phê bình sao?
Đây có phải là bài viết đổ lỗi cho ngôn ngữ vì thất bại trong dự án đó để cảm thấy khá hơn không? Hay là không hiểu rằng không phải ai cũng nghĩ và làm việc theo cùng một cách, rằng thứ khiến người này khó chịu không chịu nổi lại gần như không đáng chú ý với người khác?
Những ngôn ngữ không đáp ứng nhu cầu thực tế thường tự biến mất, hiếm khi cần ai giúp
Go, dù khác với các ngôn ngữ “tinh tế hơn”, vẫn hoạt động rất tốt trong các dự án tôi từng gặp. Hy vọng tác giả không bị ép phải làm việc với Go. Phần còn lại của cộng đồng sẽ tiếp tục xây dựng dịch vụ, gửi phản hồi cho đội ngũ Go, và theo dõi ngôn ngữ tiến hóa chậm rãi mà không phá vỡ production
Độ phức tạp bản chất không biến mất chỉ vì bạn nhắm mắt lại
Nếu bạn chọn không quan tâm đến độ phức tạp, thì bạn chỉ đang đẩy nó sang các developer khác trong tổ chức, người vận hành, khách hàng, hay một ai đó. Giờ họ phải đi vòng qua các giả định của bạn để mọi thứ chạy trơn tru
Dạo này tôi thường là “ai đó” đó, và tôi đã mệt rồi
Việc Go hoạt động tốt trong các dự án bạn gặp là điều hiển nhiên. C, C++, PHP, JavaScript cũng hoạt động. Trong thế giới của chúng ta, rất nhiều thứ “hoạt động”. Thêm một làn đường nữa cũng hoạt động, nhà nước cảnh sát cũng hoạt động
Nhưng chẳng phải có thể có thứ khác hoạt động tốt hơn rất nhiều sao?
Lý do viết có lẽ là vì tác giả bài gốc cảm thấy suy nghĩ của mình chưa được đại diện đủ “trên đời”. Tôi cũng không phải fan Go, nên vào năm 2022 tôi thấy sự phổ biến của Go khá khó hiểu. Vì những nhược điểm khá rõ ràng dường như bị phớt lờ
Giờ người ta không còn viết nhiều bài blog dài nói PHP là ngôn ngữ tệ nữa, vì chủ đề đó đã được nói quá nhiều rồi
Việc bài blog này không được viết theo cách bạn thích không có nghĩa là nó không có giá trị với người khác
Bất cứ thứ gì Go làm, dù chỉ là cách triển khai cơ bản nhất hay là thứ ngôn ngữ khác đã làm tốt hơn từ trước, đều được đóng gói như thiết yếu, tốt nhất, và là cách duy nhất để giải quyết vấn đề đó
Những gì Go không làm thì đều bị xem là không cần thiết, như âm mưu của Big Complexity nhằm làm chúng tôi, những người dùng không phải Go chưa giác ngộ, nghiện syntax sugar. Những tính năng như xử lý null tử tế, xử lý lỗi, ít boilerplate, generics, hoặc tạo ra một giải pháp nhất quán thay vì biến mọi thứ thành trường hợp đặc biệt đều bị đối xử như vậy
Ngay cả trong thread này, việc dựng người rơm vẫn tiếp diễn. Khi nói đến xử lý lỗi, thay vì thừa nhận sự thật hiển nhiên rằng xử lý lỗi của Go có thể tốt hơn nhiều, họ lại lôi những thứ như JavaScript ra. Cứ như người phê bình Go nghĩ JavaScript là đỉnh cao của xử lý lỗi vậy
Mỗi lần làm việc với ngôn ngữ khác, dù Go không phải ngôn ngữ hoàn hảo, tôi vẫn luôn muốn quay lại với nó
Tôi thích việc nó đúng nghĩa là cứ thế mà chạy. Cài Go, tải mã nguồn xuống, viết code là xong
Không cần phải tìm hiểu nên dùng phiên bản nào, runtime nào, cấu hình nào, công cụ build nào, hay trình quản lý package nào. Chỉ cần cài và dùng Go
Ngôn ngữ khác có trải nghiệm tương tự có lẽ chỉ có Rust
Có thể đó là lời tự dối mình, nhưng mỗi lần dùng Python, TypeScript, Java, tôi chỉ muốn viết code mà lại thấy sợ lập trình vì phải debug hoặc tìm hiểu các vấn đề về cấu hình, trình quản lý package, công cụ build, phiên bản
Mỗi lần đọc chỉ trích về Go, tôi lại nghĩ như vậy. Dù sao thì tôi vẫn sẽ tiếp tục dùng Go
Tôi hiểu rằng có nhiều vấn đề về lý thuyết có thể dễ dàng được ghi tài liệu, và thực tế mọi người cũng thường xuyên gặp phải, nhưng trong công việc thực tế tôi vẫn cảm thấy đây là một trong những ngôn ngữ lập trình tốt hơn
Trong những thứ thường được liệt kê là nhược điểm, có vài điều tôi không đồng ý; chẳng hạn tôi thích cách xử lý lỗi tường minh ở khắp nơi. Những phần khác thì tôi đồng ý, nhưng chúng không gây khó chịu hơn nhiều so với nhược điểm của các ngôn ngữ lập trình khác
Tôi cũng thấy hơi tiếc cho những người đặc biệt nhạy cảm với các lĩnh vực mà Go không thành công, vì có vẻ họ sẽ phàn nàn về nó cả đời
Nhưng khi chọn ngôn ngữ cho một dự án, tôi không đưa ra hàng loạt lập luận lý tính kiểu “ngôn ngữ tốt nhất là gì”. Tôi chọn ngôn ngữ hợp với mình, cho tôi cảm giác có thể tiếp tục viết phần mềm tốt, và dễ làm việc
Là người coi trọng tính đúng đắn, tôi vẫn mong ngôn ngữ đó gần với Rust hơn, nhưng hiện tại thì không phải vậy. Tôi cũng không ghét Rust, nhưng đó không phải ngôn ngữ tôi với tay tới khi đang vội
Đã có quá nhiều bài viết về việc Go tệ hại đến mức nào. Ít nhất giờ tôi cũng hiểu cảm giác làm fan PHP vài năm trước là thế nào. Nói vậy có phóng đại, nhưng theo tôi thì không phóng đại quá nhiều
Bạn sẽ dùng Go
Và bạn sẽ hối hận
Nhưng lúc đó thì đã quá muộn, và bạn đã bị trói vào nó
Tác giả đưa ra lập luận khá thuyết phục, và cũng rất khó phản bác. Trong nhiều trường hợp được liệt kê rõ ràng, cũng như trong nhiều tình huống nghề nghiệp nơi các công ty không đủ năng lực kỹ thuật dùng công nghệ không phù hợp, Go là lựa chọn tệ và bạn sẽ hối hận vì đã đưa nó vào production
Công ty nên bám vào công cụ nhàm chán. Còn dự án cá nhân thì tất nhiên muốn làm gì cũng được
Ngoài ra, tôi xem sự đơn giản là điểm mạnh. Cảm giác dùng tốt, dễ học, và nhìn chung hiệu năng cũng khá ổn. Tôi sẽ không chọn nó cho mọi tình huống, nhưng có nhiều tình huống nó là một công cụ tốt
Tương tự, tôi cũng khá thích Java và Python, và dùng cả hai đủ tốt. Những ngôn ngữ này là công cụ tốt cho những mục đích mà chúng được thiết kế
Tôi không hiểu vì sao mọi người lại bám chấp đầy nhiệt huyết vào ngôn ngữ lập trình đến vậy. Ngôn ngữ là công cụ. Bạn có thể thích công cụ này hơn công cụ kia, nhưng điều đó không làm công cụ bạn không thích trở nên vô giá trị
if err != niltát vào mặtĐã có nhiều đề xuất tốt để sửa vấn đề này, nhưng một bộ phận cộng đồng cực kỳ bảo thủ và ồn ào đã giết chết mọi đề xuất bằng phản hồi gần như cay độc
Giờ đây đội Go về mặt tâm lý đã sợ những người đó và từ bỏ cuộc chiến cải thiện xử lý lỗi
Trong mọi khảo sát nhà phát triển Go, cộng đồng đều bỏ phiếu áp đảo cho việc cải thiện xử lý lỗi, nhưng một thiểu số cực đoan đã làm trật hướng mọi tiến bộ
Go có thái độ ngạo mạn rằng nó sẽ tự quyết định bạn cần tính năng nào, và chỉ cung cấp rất ít thứ để bạn dùng trong công việc
Thảo luận thời điểm đó:
(130 điểm, 148 bình luận) https://news.ycombinator.com/item?id=34188528
(748 điểm, 544 bình luận) https://news.ycombinator.com/item?id=31205072
Có đoạn nói rằng “tôi bắt đầu quan tâm đến ngữ nghĩa nhiều hơn hẳn cú pháp, nên cũng không xem Zig, Nim, Odin, v.v. nữa. Tôi không còn quan tâm đến ‘một C tốt hơn’”
Tôi thấy bài viết khá dài dòng. Phần nói Go là “ngẫu nhiên” là nhảm nhí, vì Rust cũng “ngẫu nhiên” không kém xét về nguồn gốc và thiết kế
Một điều nổi bật là Nim hoàn toàn không phải “một C tốt hơn”. Nó có garbage collection, và cũng có thể dùng reference counting. Nếu muốn, bạn có thể dùng nó như một C tốt hơn
Hệ thống kiểu của Nim tránh được nhiều vấn đề mà Go gặp phải, nhưng không nghiêm ngặt như Rust
Rốt cuộc, rất nhiều phần mềm đã được viết và triển khai bằng Go, chạy nhanh, ít downtime, nhìn chung hiệu quả, và có vẻ cũng ít vấn đề bảo mật. Tôi cho rằng số dự án phần mềm được triển khai bằng Rust ít hơn nhiều, và Firefox vẫn còn hơn 95% là C++
Tôi không có lợi ích liên quan trong cuộc tranh luận này, và ở chỗ làm tôi đang dùng Python và C++ một cách hài lòng. Dù phần mềm viết bằng Rust ít hơn nhiều, vẫn có thể nói rất nhiều phần mềm đã được viết và triển khai bằng Rust. Cũng không thể bỏ qua các khoản đầu tư của những tập đoàn lớn
0: https://x.com/m_ou_se/status/1599173341117435905
Không hiểu vì sao Go lúc nào cũng bị đem so với Rust. Đối tượng so sánh phù hợp nhất là Java
Lý do việc so sánh Go với Rust phổ biến hơn nhưng kém hữu ích hơn có lẽ là vì cả hai đều thuộc làn sóng ngôn ngữ mới, và về cơ bản cả hai đều tạo binary liên kết tĩnh
Trong những tình huống Java là lựa chọn tốt, Go thực sự là lựa chọn rất tệ. Không chỉ ngôn ngữ bị hạn chế, mà hệ sinh thái so với Java cũng rất hạn chế
Tôi đang giữ Go, C#, TypeScript làm các ngôn ngữ chính, và tổ hợp này bao phủ phạm vi rất tốt. Nếu có 6 tháng để chấp nhận giảm năng suất, hoặc có một dự án với yêu cầu chất lượng thấp, tôi sẽ thêm Rust
Học ngôn ngữ thì 1–2 tuần là đủ, nhưng học cách làm engineering trong hệ sinh thái đó mới tốn thời gian
Hầu hết các công ty không quá lo về hiệu năng đều không dùng C++ cho dịch vụ mạng. Nếu Rust khi đó đã trưởng thành thì hẳn đã là lựa chọn tốt, nhưng thời gian compile, một trong những vấn đề cốt lõi ban đầu, có thể còn tệ hơn trong codebase Rust
Thực tế tôi cũng từng thấy các trường hợp dùng Go thay vì Rust, và lý do lớn nhất là Go compile nhanh hơn rất nhiều
Nếu hỏi liệu Rust và Go có đủ khác nhau để nên được chọn cho những trường hợp khác nhau không, thì có. Chính vì vậy người ta cân nhắc cả hai cùng nhau rồi chọn một
Có một thời tôi từng tìm một Python “tốt hơn”. Tôi muốn một ngôn ngữ đơn giản và an toàn hơn C/C++, nhưng nhanh hơn Python, và quan trọng nhất là có thể tạo một binary duy nhất
Vì vậy tôi đã xem từ Rust cho tới những ngôn ngữ ít được biết đến hơn như Hare
Go đáng lẽ phải là lựa chọn hiển nhiên, nhưng không hiểu vì sao tôi lại ghét cú pháp của nó. Rust thì tôi hiểu được. Nó dùng nhiều ký tự đặc biệt khó nhớ và khó gõ trên bàn phím không phải QWERTY, cùng các điểm gây đau đầu khác. Nhiều Lisp thì vấn đề là dấu ngoặc và ký pháp Ba Lan ngược. Nhưng với Go, tôi không thể hợp lý hóa được vì sao mình ghét nó
Cuối cùng tôi quyết định compile code Python bằng Nuitka. Không biết tốc độ có cải thiện không, nhưng giờ tôi có thể cung cấp binary
Tôi cũng ngày càng xem xét C#, vì AOT compilation đang tiến bộ. Tuy nhiên tôi không thích sự dài dòng ở chế độ mặc định và cảm giác nó bị gắn quá chặt với Windows
Nim và Crystal thì tôi khá thích, nhưng cộng đồng nhỏ là rào cản. Dù vậy tôi thực sự ấn tượng với những gì Nim làm được với một cộng đồng nhỏ như vậy, và dần nghĩ đó là một ngôn ngữ tuyệt vời
Tôi sẽ thử tự tạo động lực để học một trong các ngôn ngữ đã nhắc đến
Với WPF cũng có các lựa chọn thay thế chạy được ở mọi nơi
Dạo này thực sự dùng khá tốt
Cũng có cách tiếp cận khác là từ bỏ C-API và tạo cầu nối sang các ngôn ngữ như Go hoặc Rust
Tôi đã làm py2many trong vài năm. Tôi muốn nhận phản hồi về cách tiếp cận này