30 điểm bởi GN⁺ 2025-12-06 | 1 bình luận | Chia sẻ qua WhatsApp
  • Tập trung vào sự khác biệt về triết lý và hệ giá trị của ba ngôn ngữ, bài viết so sánh mỗi ngôn ngữ đang cố giải quyết vấn đề gì
  • Go được mô tả là ngôn ngữ đề cao sự đơn giản và ổn định, tối giản tính năng để giúp cộng tác và bảo trì dễ dàng hơn
  • Rust theo đuổi đồng thời tính an toàn và hiệu năng, bảo đảm an toàn bộ nhớ bằng hệ thống kiểu phức tạp và cấu trúc trait
  • Zig được mô tả là ngôn ngữ thử nghiệm trao cho lập trình viên toàn quyền kiểm soát thông qua quản lý bộ nhớ thủ công và thiết kế hướng dữ liệu
  • Cách tiếp cận đối lập của ba ngôn ngữ cho thấy hệ giá trị mà ngôn ngữ lập trình hiện thực hóa, và tiêu chí lựa chọn nằm ở việc nhà phát triển đồng cảm với triết lý nào

Góc nhìn để so sánh ngôn ngữ

  • Tác giả muốn hiểu hệ giá trị của từng ngôn ngữ thông qua việc thử nghiệm các ngôn ngữ mới, chứ không phải những ngôn ngữ đang dùng ở nơi làm việc
  • Tác giả nhấn mạnh rằng điều quan trọng không phải là chỉ so sánh danh sách tính năng, mà là ngôn ngữ đã chọn những đánh đổi nào
  • Go, Rust và Zig có nhiều phần giao thoa về mặt chức năng, nhưng những giá trị mà nhà thiết kế coi trọng lại khác nhau
  • Nắm được triết lý của từng ngôn ngữ giúp đánh giá chúng phù hợp với môi trường và mục đích nào

Go — ngôn ngữ của sự đơn giản và cộng tác

  • Go được phân biệt bởi chủ nghĩa tối giản, với đặc điểm “có thể chứa toàn bộ ngôn ngữ trong đầu”
    • Generics chỉ được thêm vào sau 12 năm, và các tính năng như tagged union hay cú pháp đường tắt cho xử lý lỗi vẫn chưa có
  • Go rất thận trọng khi thêm tính năng, nên có nhiều mã boilerplate, nhưng đổi lại độ ổn định và khả năng đọc của ngôn ngữ cao
  • Slice của Go bao quát chức năng của Vec<T> trong Rust hay ArrayList trong Zig, và runtime tự động quản lý vị trí bộ nhớ
  • Bắt nguồn từ sự bất mãn với độ phức tạp và độ trễ biên dịch của C++, ngôn ngữ này được thiết kế với mục tiêu đơn giản và biên dịch nhanh
  • Go đề cao hiệu quả cộng tác trong môi trường doanh nghiệp, ưu tiên mã rõ ràng và tính nhất quán hơn các tính năng phức tạp

Rust — phức tạp nhưng mạnh mẽ về an toàn và hiệu năng

  • Rust đề cao “zero-cost abstraction” và là một ngôn ngữ theo hướng tối đa hóa tính năng với nhiều khái niệm kết hợp
  • Lý do Rust khó học là vì mật độ khái niệm cao, với hệ thống kiểu phức tạp và cấu trúc trait hiện diện khắp nơi
  • Mục tiêu cốt lõi của Rust là kết hợp hiệu năng với an toàn bộ nhớ
    • Để ngăn UB(Undefined Behavior), ngôn ngữ thực hiện kiểm chứng ở thời điểm biên dịch
    • Điều này chặn hành vi không thể dự đoán do tham chiếu con trỏ sai hoặc giải phóng kép gây ra
  • Để compiler có thể hiểu hành vi runtime của mã, lập trình viên phải định nghĩa tường minh kiểu và trait
  • Nhờ cấu trúc đó, độ tin cậy đối với mã của người khác cao hơn và hệ sinh thái thư viện được duy trì sôi động

Zig — toàn quyền kiểm soát và thiết kế hướng dữ liệu

  • Zig là ngôn ngữ mới nhất trong ba ngôn ngữ, hiện ở giai đoạn phiên bản 0.14 và hầu như chưa có tài liệu hóa đầy đủ cho thư viện chuẩn
  • Ngôn ngữ này áp dụng quản lý bộ nhớ thủ công, nên lập trình viên phải tự gọi alloc() và chọn allocator
  • Khác với Rust hay Go, Zig cho phép tạo biến toàn cục một cách đơn giản và phát hiện “illegal behavior” ở runtime để dừng chương trình
    • Có thể điều chỉnh cân bằng giữa hiệu năng và độ an toàn bằng 4 chế độ phát hành có thể chọn khi build
  • Zig cố ý loại bỏ các tính năng của lập trình hướng đối tượng (OOP)
    • Không có trường private hay dynamic dispatch, và ngay cả std.mem.Allocator cũng không được triển khai như một interface
    • Thay vào đó, ngôn ngữ hướng đến thiết kế hướng dữ liệu (data-oriented design)
  • Về quản lý bộ nhớ, Zig cũng khuyến nghị cấu trúc cấp phát và giải phóng những khối bộ nhớ lớn theo chu kỳ, thay vì quản lý tinh vi theo từng đối tượng kiểu RAII
  • Zig được mô tả là ngôn ngữ có khuynh hướng tự do và phản hệ thống, loại bỏ tư duy OOP và tối đa hóa quyền kiểm soát do lập trình viên nắm giữ
  • Hiện tại đội ngũ đang tập trung vào viết lại toàn bộ các dependency, và phiên bản ổn định (1.0) vẫn chưa được xác định

Kết luận — sự khác biệt về giá trị mà ngôn ngữ bộc lộ

  • Go lấy cộng tác và sự đơn giản, Rust lấy an toàn và hiệu năng, còn Zig lấy tự do và quyền kiểm soát làm giá trị cốt lõi
  • Sự khác biệt giữa ba ngôn ngữ không chỉ là so sánh tính năng đơn thuần, mà phản ánh lựa chọn mang tính triết học về phát triển phần mềm
  • Nhà phát triển sẽ chọn ngôn ngữ tùy theo mình đồng cảm với giá trị nào

1 bình luận

 
GN⁺ 2025-12-06
Ý kiến Hacker News
  • Trong Rust, việc tạo biến toàn cục có thể thay đổi không hề khó
    Chỉ là phải dùng unsafe hoặc smart pointer có cung cấp cơ chế đồng bộ hóa
    Rust về mặc định là re-entrant và bảo đảm an toàn luồng tại thời điểm biên dịch
    Nếu không quan tâm đến an toàn luồng tĩnh thì cũng có thể làm dễ như Zig hay C
    Khác biệt là Rust cung cấp nhiều công cụ bảo chứng hơn cho hành vi runtime của mã

    • Với tư cách là người đã dùng Rust nhiều năm, tôi nghĩ biến toàn cục có thể thay đổi là ví dụ điển hình của kiểu “làm được không có nghĩa là nên làm”
      Khi quay lại các ngôn ngữ khác và thấy người ta dùng thứ này như không có gì, tôi cảm giác đó là chuyện điên rồ về mặt an toàn
    • Kiểu diễn đạt “nó rất trivial, chỉ cần ~ thôi” là điều tôi cũng từng nghe ở C++, Perl và Haskell
      Nhưng khi những việc “đơn giản” kiểu này tích tụ lại thì rốt cuộc chẳng còn đơn giản nữa
      Rust đã vượt qua ranh giới đó rồi, và giờ thì hoàn toàn không còn trivial nữa
    • Tôi tò mò liệu trình biên dịch Rust có bắt được race condition giữa các luồng ngay từ lúc biên dịch không
      Nếu vậy thì có vẻ hấp dẫn hơn C
      Tôi cũng muốn biết khi có hai biến luôn phải được khóa cùng nhau thì xử lý thế nào
    • Nếu tôi làm ra một ngôn ngữ, tôi sẽ cấm hẳn biến toàn cục có thể thay đổi
      Cứ debug một hồi là cuối cùng vấn đề cũng luôn quay về đó
  • Về bài viết chỉ ra mật độ khái niệm của Rust, tôi nghĩ trên thực tế chỉ cần biết 5% trong số đó là đã có thể dùng một cách năng suất
    Tôi đã dùng Rust hơn 12 năm nhưng chưa từng có dịp dùng thứ như #[fundamental]
    Rust cũng làm được arena allocation, và cũng có khái niệm allocator
    Chỉ là có allocator mặc định, và thường dùng cấp phát heap tường minh như Box::new
    Biến toàn cục có thể thay đổi có thể viết kiểu static FOO: Mutex<T> = Mutex::new(...), và cần mutex để đảm bảo an toàn bộ nhớ
    Hệ thống kiểu của Rust được thiết kế không chỉ để đảm bảo an toàn bộ nhớ mà còn cả tính an toàn ngữ nghĩa của mã

    • Nhưng vì các lập trình viên khác có thể dùng 5~10% khái niệm khác, nên khi cộng tác cuối cùng vẫn phải học thêm nhiều khái niệm hơn
      Trong C thì độ phức tạp kiểu này ít hơn
      Độ phức tạp rốt cuộc vẫn là một vấn đề quan trọng
    • Nói “Rust cũng làm được arena allocation” thì đúng, nhưng phần lớn mã Rust/Go lấy nhiều lần cấp phát nhỏ làm đường đi mặc định
      Đây không chỉ là chuyện có làm được hay không, mà là khác biệt về phong cách lập trình cơ bản
    • Nếu allocator trong Rust là một kiểu, tôi cũng tò mò liệu trong mô hình luồng m:n có thể cấp arena riêng cho từng request hay không
    • Cũng có câu hỏi allocator của Rust có phải là toàn cục (global) không, và liệu mọi cấp phát heap có dùng cùng một allocator hay không
    • Có nhắc đến video về batch allocation của Casey Muratori, và chỉ ra rằng một số lập trình viên hiểu sai điều này rồi phê phán RAII của Rust
      Zig Software Foundation cũng từng trích sai phát biểu về Rust của Asahi Lina
      Tôi không thích thái độ marketing kiểu hạ thấp ngôn ngữ khác của Zig
  • Lý do tôi thích Zig là vì đây là ngôn ngữ có thể xử lý cạn kiệt bộ nhớ một cách thanh lịch
    Mọi cấp phát đều được giả định là có thể thất bại (fallible), và phải xử lý một cách tường minh
    Không gian stack cũng không bị đối xử như phép màu; trình biên dịch sẽ phân tích đồ thị gọi để suy ra kích thước tối đa
    Trong môi trường nhúng, kiểu thiết kế lấy tài nguyên làm trung tâm này là bắt buộc

    • Nhưng trên các OS dùng overcommit như Linux thì trên thực tế lỗi cấp phát không xảy ra
      Xử lý ở cấp độ ngôn ngữ cũng không giải quyết được chuyện đó
    • Với câu hỏi lý do gì Zig nên tồn tại khi đã có Rust, tôi thà hỏi “đã có C thì cần gì Zig?”
      Rốt cuộc vẫn mang cùng bài toán quản lý bộ nhớ thủ công
      Nếu vậy tôi nghĩ dùng ngôn ngữ có GC còn tốt hơn
    • Tôi tò mò việc suy luận kích thước stack của Zig hoạt động ra sao khi có đệ quy hoặc lời gọi qua function pointer
    • Zig không phải là đầu tiên, và cần nhìn lại lịch sử các ngôn ngữ hệ thống từ JOVIAL năm 1958 trở đi
    • Rust cũng xử lý pre-allocation khá tốt
      Chỉ là thư viện chuẩn của Rust dùng panic khi OOM, nên có hẳn một hệ sinh thái riêng hỗ trợ phát triển nhúng trong môi trường no-std
  • slice của Go khác với Vec<T> của Rust
    append() trả về một slice mới, có thể chia sẻ vùng nhớ cũ hoặc không
    Không có cách để giảm bộ nhớ, và nếu chỉ viết append(s, ...) thì sẽ bỏ qua slice mới
    Go có thái độ “cứ làm đúng như tôi nói”, còn Rust là “hãy kiểm chứng xem bạn có làm đúng như tôi nói không”
    Tức là Go chấp nhận sai sót để đổi lấy sự đơn giản, còn Rust chọn hướng giảm lỗi dù có phức tạp hơn

    • Trên thực tế có thể giảm bộ nhớ bằng slices.Clip
      Ngoài ra, nếu chỉ viết append(s, ...) thì sẽ bị lỗi biên dịch, nên nguyên văn đó là một nhận định hơi không chính xác
      Go là ngôn ngữ rất thận trọng về việc độ phức tạp tăng lên khi thêm tính năng
    • Tôi nghĩ người mới không thể mắc lỗi đó vì “append(s, …)” thậm chí còn không biên dịch được
    • Thú vị là dù Go đã có generics nhưng những kiểu như List[T] vẫn không được dùng rộng rãi
      Có lẽ vì không mấy khi cần tự truyền quanh một growable list
    • Trong đặc tả và tài liệu của Go, phần lớn các foot gun đều được ghi rất rõ
      Nhiều trường hợp chỉ là không đọc tài liệu rồi ngạc nhiên
  • Tôi nghĩ việc bắt UB (Undefined Behavior) của C/C++ bằng kiểm tra runtime là điều khó khả thi trong thực tế
    Android cũng đã áp dụng sanitizer cho mọi commit, nhưng chỉ sau khi chuyển sang Rust thì các vụ exploit mới giảm xuống

    • Có người yêu cầu nguồn dẫn cho nhận định về sanitizer của Android
  • Tôi thích bài so sánh ngôn ngữ này vì nó bàn khá thẳng thắn về điểm mạnh và điểm yếu của từng ngôn ngữ
    Chỉ tiếc là không nhắc đến Raku
    Theo tôi, nếu C–Zig–C++–Rust–Go là một dải liên tục của ngôn ngữ mức thấp, thì phía mức cao sẽ là Julia–R–Python–Lua–JS–PHP–Raku–WL

    • Có người hỏi WL là gì
    • Raku là ngôn ngữ đa dụng giàu khả năng biểu đạt, tích hợp sẵn multiple dispatch, roles, gradual typing, lazy evaluation và hệ thống regex mạnh
      Nó hỗ trợ định nghĩa cú pháp ở cấp độ ngôn ngữ nên rất thuận tiện cho DSL hay phân tích log
      Vì chạy trên VM nên hiệu năng thấp hơn, nhưng phù hợp để biểu đạt trực tiếp cấu trúc của bài toán
      Với tư cách hậu duệ của Perl, nó hướng đến một ngôn ngữ linh hoạt nhưng nhất quán
  • Việc nghĩ rằng trong Rust, hàm trả về con trỏ thì sẽ tự động cấp phát heap là một sự hiểu nhầm
    Biến cục bộ nằm trên stack và biến mất khi trả về, nên con trỏ sẽ bị vô hiệu
    Trong chế độ an toàn, Rust không cho phép dereference con trỏ, còn trong chế độ unsafe thì nhà phát triển phải tự chịu trách nhiệm bảo đảm tính hợp lệ
    Có lẽ người ta đã nhầm Box::new với “cấp phát ngầm”

    • Rất khó hiểu khi lại nhầm lẫn giữa escape analysis của Go và cấp phát heap tường minh của Rust
      Điều đó hoặc là do hiểu sai khái niệm, hoặc có vẻ như đang cố tình đánh lạc hướng
  • Điểm mạnh lớn nhất của Go là mô hình đồng thời đơn giản
    Nhờ goroutine mà có thể dễ dàng viết mã song song

    • Một ưu điểm khác của Go là nhờ tính nhất quán của mã nguồn nên rất dễ lần theo các codebase lớn
      Việc tìm triển khai interface thì khó, nhưng độ dễ đọc cao nên rất có lợi cho cộng tác nhóm
    • Bài nói “Concurrency is not Parallelism” của Rob Pike giải thích rất hay triết lý đồng thời của Go
      Không có colored function, và giao tiếp dựa trên channel đơn giản nên có thể nhanh chóng viết được mã đồng thời đúng đắn
    • Nhưng tôi nghĩ Structured Concurrency còn là mô hình dễ hơn
      Bài liên quan: Structured Concurrency or Go Statement Considered Harmful
    • Giao diện std.Io mới của Zig khá giống mô hình đồng thời của Go
      Từ khóa go tương ứng với std.Io.async, channel tương ứng với std.Io.Queue, còn select tương ứng với std.Io.select
    • Nếu là lập trình viên Erlang thì có lẽ sẽ không đồng ý rằng mô hình đồng thời của Go là dễ nhất
  • Điều tôi muốn là một ngôn ngữ kết hợp sự đơn giản của Go với xử lý result/error/enum của Rust và generics tốt hơn

    • Tôi cũng đồng ý. Có nhu cầu thị trường lớn cho một ngôn ngữ native có GC nhưng sở hữu hệ thống kiểu mạnh hơn
      Tôi đã nhìn qua OCaml, D, Swift, Nim, Crystal..., nhưng vẫn chưa có ngôn ngữ nào thực sự thống trị thị trường
    • Tôi nghe nói OCaml hiện đại có mô hình đồng thời rất mạnh, và hiệu năng cũng có thể cạnh tranh với Go
    • Ngôn ngữ Borgo từng thử đi theo hướng đó nhưng đã dừng lại
      Thay vào đó có thể xem Gleam
    • error proposal của Go khá thú vị
      Hy vọng sẽ có cải tiến giải quyết được kiểu vấn đề lặp đi lặp lại này
      Còn generics có lẽ vẫn sẽ là một bài toán khó
    • Phương án gần nhất là C#, nhưng đây vẫn là ngôn ngữ thiên về OOP
  • Tôi thích tông chung của bài viết vì thể hiện được sự nhiệt tình và tò mò của một lập trình viên mới
    Việc Go thiếu generics không chỉ là tối giản cực đoan, mà là kết quả của quá trình cân nhắc trade-off
    lifetime của Rust là trở ngại lớn nhất với rất nhiều người, còn tính đổi mới của ngôn ngữ nằm ở sự kết hợp các khái niệm sẵn có
    Việc Zig quản lý bộ nhớ thủ công không hẳn là để loại bỏ OOP, mà dựa trên triết lý Data-Oriented Design (DOD)
    Bài nói liên quan: Andrew trình bày về DOD

    • Như vấn đề Russ Cox từng nêu trong bài “The Generic Dilemma” năm 2009,
      cốt lõi là “chọn lập trình viên chậm, trình biên dịch chậm hay tốc độ thực thi chậm”
      Cuối cùng có vẻ nhóm Go đã tìm ra một phương án thỏa hiệp đủ ổn thỏa