10 điểm bởi GN⁺ 2025-08-28 | 3 bình luận | Chia sẻ qua WhatsApp
  • Rust mang lại các ضمانات an toàn mạnh mẽ, cho phép refactor một cách tự tin ngay cả với codebase lớn, từ đó nâng cao năng suất và khả năng bảo trì
  • Trình biên dịch có thể phát hiện trước các lỗi liên quan đến lập lịch bất đồng bộ, tăng cường độ ổn định bằng cách ngăn hành vi không xác định
  • Các ngôn ngữ như TypeScript thường xuyên để lọt lỗi bất đồng bộ ra môi trường production do hệ thống kiểu lỏng lẻo
  • Hệ thống kiểu của Rust cho biết rõ tác động của thay đổi mã nguồn, giúp tăng độ tin cậy và tinh thần thử nghiệm trong các dự án phức tạp
  • Không giống Rust, Zig có kiểm tra lỏng hơn trong xử lý lỗi nên có thể bỏ sót bug do lỗi gõ nhầm, làm giảm độ tin cậy

Tóm tắt và bối cảnh

  • Backend của Lubeno được viết 100% bằng Rust, và codebase đã phát triển đến mức khó có thể nắm toàn bộ trong đầu
    • Với các dự án lớn, thông thường rất khó xác định tác dụng phụ của thay đổi, dẫn đến suy giảm năng suất
  • Các đảm bảo an toàn của Rust cho biết rõ ảnh hưởng khi thay đổi mã, giúp giảm nỗi sợ khi refactor
    • Điều này góp phần cải thiện khả năng bảo trì và năng suất về lâu dài
  • Bài viết bắt đầu từ một trường hợp trình biên dịch Rust phát hiện lỗi bất đồng bộ, rồi khám phá các lợi thế năng suất của Rust

Ví dụ về các đảm bảo an toàn của Rust

  • Tình huống vấn đề: bọc một struct bằng mutex để truy cập đồng thời, rồi thực hiện tác vụ bất đồng bộ sau khi lấy lock
    let lock = mutex.lock();  
    db.insert_commit(commit).await;  
    
  • Phát hiện vấn đề: rust-analyzer không hiển thị lỗi, nhưng lỗi biên dịch xuất hiện trong file định nghĩa router
    .route("/api/git/post-receive", post(git::post_receive))  
                                         ^^^^^^^^^^^^^^^^^  
    error: future cannot be sent between threads safely  
    
    Quảng cáo
  • Phân tích nguyên nhân:
    • Framework web tạo một tác vụ bất đồng bộ cho mỗi kết nối HTTP, và bộ lập lịch tác vụ sẽ di chuyển tác vụ giữa các thread
    • Mutex yêu cầu phải được mở khóa trên cùng thread, nên nếu thread bị đổi tại điểm .await thì có thể phát sinh hành vi không xác định
    • Trình biên dịch Rust theo dõi vòng đời của lock và phát hiện khả năng nó bị giải phóng ở thread khác
  • Cách khắc phục: giải phóng lock trước .await
  • Ý nghĩa: Rust ngăn chặn ngay từ lúc biên dịch các lỗi bất đồng bộ khó tái hiện trong môi trường phát triển

Trường hợp so sánh với TypeScript

  • Tình huống vấn đề: phát sinh lỗi redirect bất đồng bộ trong mã TypeScript
    if (redirect) {  
        window.location.href = redirect;  
    }  
    let content = await response.json();  
    if (content.onboardingDone) {  
        window.location.href = "/dashboard";  
    } else {  
        window.location.href = "/onboarding";  
    }  
    
  • Nguyên nhân vấn đề:
    • window.location.href không chuyển hướng ngay lập tức mà chỉ lên lịch, nên mã vẫn tiếp tục chạy
    • Do race condition, quá trình chuyển hướng ngoài ý muốn có thể xảy ra
  • Cách khắc phục: thêm return vào khối if
    if (redirect) {  
        window.location.href = redirect;  
        return;  
    }  
    
  • Giới hạn: TypeScript không có theo dõi vòng đời hay quy tắc borrow nên không thể phát hiện kiểu lỗi này ở thời điểm biên dịch
    • Lỗi chỉ được phát hiện ở môi trường production và tốn nhiều thời gian để debug
    Quảng cáo

Lợi thế refactor của Rust

  • Trong phát triển web, Python, Ruby, JavaScript/Node.js có năng suất ban đầu cao, nhưng khi codebase mở rộng thì sự liên kết lỏng lẻo khiến việc thay đổi trở nên khó khăn
    • Sau khi thay đổi, các lỗi bất ngờ có thể xuất hiện và làm giảm ý chí chỉnh sửa mã
  • Rust dùng hệ thống kiểu để cho biết rõ tác động của thay đổi, nhờ đó giảm nỗi sợ refactor
    • Ví dụ: cảnh báo kiểu “thay đổi này có thể ảnh hưởng đến phần khác”, giúp ngăn vấn đề từ sớm
  • Ngay cả khi codebase tăng trưởng, năng suất vẫn tăng, có thể tái sử dụng mã hiện có và giữ được độ an toàn khi thay đổi

So sánh với test

  • Test hữu ích để ngăn hồi quy khi refactor, nhưng vì không bị trình biên dịch bắt buộc nên có thể bị lược bỏ
    • Việc viết test đòi hỏi phải quyết định mức độ trừu tượng, kiểm tra hành vi hay chi tiết triển khai, và khả năng ngăn lỗi, nên tạo ra gánh nặng tinh thần lớn
    Quảng cáo
  • Rust cho phép trình biên dịch chặn trước các lỗi phổ biến, từ đó giảm gánh nặng ra quyết định mà test thường đòi hỏi
    • Các thuộc tính không thể xác minh bằng hệ thống kiểu thì được bổ sung bằng test

So sánh với Zig

  • Zig là một ngôn ngữ lập trình hệ thống tương tự Rust, nhưng lỏng hơn trong xử lý lỗi
    • Ví dụ: mã xử lý lỗi
      const FileError = error{ AccessDenied };  
      fn doSomethingThatFails() FileError!void {  
          return FileError.AccessDenied;  
      }  
      pub fn main() !void {  
          doSomethingThatFails() catch |err| {  
              if (err == error.AccessDenid) {  
                  std.debug.print("Access was denied!\n", .{});  
              }  
          };  
      }  
      
    • Lỗi gõ nhầm AccessDenid gây ra bug, nhưng trình biên dịch Zig lại xử lý nó như một con số nên vẫn biên dịch thành công
  • Khi dùng câu lệnh switch, lỗi gõ nhầm có thể bị phát hiện, nhưng trong câu lệnh if thì lại bị bỏ qua, tạo ra vấn đề về độ tin cậy
  • Rust tránh được những lỗ hổng thiết kế kiểu này và kiểm tra nghiêm ngặt các lỗi gõ nhầm hay lỗi logic

Hàm ý

  • Rust cải thiện năng suất và độ ổn định của các dự án lớn nhờ đảm bảo an toàn và hệ thống kiểu nghiêm ngặt
  • Ngay cả những vấn đề phức tạp như lỗi bất đồng bộ cũng có thể được phát hiện ở thời điểm biên dịch, giúp giảm chi phí bảo trì
  • Các ví dụ từ TypeScript và Zig cho thấy rủi ro do kiểm tra lỏng lẻo, đồng thời nhấn mạnh giá trị của trình biên dịch nghiêm ngặt của Rust
  • Rust đang khẳng định vị thế là một công cụ mạnh mẽ cho phát triển web không chỉ ở năng suất ban đầu mà cả trong quản lý codebase dài hạn

3 bình luận

 
taptaps 2025-08-30

Mỗi lần thấy người ta nói đây là cái tốt nhất, đây là ngôn ngữ mạnh mẽ nhất!!
thì tôi lại có cảm giác là có lẽ số dev Rust thực ra không nhiều như mình tưởng, nên họ mới đi dụ người khác dùng Rust chăng??

 
colus001 2025-08-29

Có phải chỉ mình tôi thấy các bài viết được gợi ý về Rust giống kiểu câu cửa miệng của thực khách sành ăn là "Thử đi! Thử đi!" không nhỉ?

 
GN⁺ 2025-08-28
Ý kiến trên Hacker News
  • Năm ngoái tôi đã port driver mạng virtio-host được viết bằng Rust. Tôi đã thay backend, chuyển cơ chế interrupt, và đổi từ thư viện sang tiến trình độc lập. Đó là một chương trình phức tạp, xử lý từ memory mapping, VM interrupt, network socket cho tới multithreading. Dù gần như không có kinh nghiệm với Rust và cũng ít kinh nghiệm với virtio, đến lúc dự án compile được thì nó chạy hoàn hảo. Ngoại trừ một bug liên quan đến Drop thì sửa cũng dễ. Tôi nghĩ mình đã được giúp rất nhiều nhờ các thư viện Rust được thiết kế theo cách khó có thể dùng sai

    • Tôi đã phát triển bằng Rust khá lâu, và phần lớn là chỉ cần compile được thì code sẽ chạy tốt. Thỉnh thoảng vẫn có deadlock hoặc bug liên quan đến thứ tự, nhưng về cơ bản compile thành công đồng nghĩa với việc một phần đáng kể của dự án đã hoạt động đúng
  • Tôi nghĩ Rust rất tuyệt. Nhưng tôi không đồng ý với ý kiến cho rằng bug gán href là lỗi của TypeScript. Cốt lõi vấn đề là dù có gán href thì việc chuyển trang không diễn ra ngay mà được xử lý sau đó. Trong Rust cũng có thể gặp đúng vấn đề này. Nếu Rust có hàm set_href và hành vi này được xử lý về sau, thì đoạn code như dưới đây vẫn có thể xảy ra:

    set_href('/foo')

    if (some_condition) { set_href('/bar') }

    Tôi nghĩ Rust sẽ không thiết kế như vậy. Việc hành động xảy ra trong setter không phải là thiết kế thư viện tốt, và việc không chuyển trang ngay khi gán href là điều kỳ lạ. Nếu là standard library của Rust thì sẽ không có kiểu triển khai ngớ ngẩn như vậy. Đây không phải vấn đề Rust vs TypeScript mà là khác biệt giữa standard library của Rust và Web Platform API. Tôi đồng ý rằng Rust sẽ không mang đến trải nghiệm người dùng như thế

    • Nói chính thức thì thiết kế để hành động xảy ra ngay trong setter là không đáng mong muốn. Tên gọi cũng nên đổi thành kiểu navigate_to(href). Trong môi trường trình duyệt, toàn bộ mã JS đều chạy dưới dạng callback và bị điều khiển bởi event loop, nên việc không thực thi ngay cũng là tình huống tự nhiên

    • Ví dụ Rust thì thú vị, nhưng chỉ với ví dụ TypeScript thì không thể kết luận TS có phù hợp cho dự án quy mô lớn hay không. Tôi thấy bất an vì trong Ruby thường xuyên phải bắt bug ở runtime, nhưng cuối cùng nếu trước khi commit mọi thứ chạy ổn và việc đọc, sửa code dễ dàng thì vẫn thấy hài lòng. Vấn đề chuyển hướng là vấn đề của JavaScript và là thứ TS kế thừa. Nó xảy ra vì JS cho phép tùy ý sửa thuộc tính. Tuy vậy, vì trang cũng không biến mất ngay, nên khi đã biết thì hành vi này cũng hợp lý

    • Về mặt kỹ thuật, trong Rust thì tùy vào việc set_href trả về () hay ! mà ý nghĩa có thể được gợi ý rõ ràng hơn. Nhưng trong trường hợp redirect có điều kiện thì vẫn khó ngăn được cách dùng sai

    • Ý tôi là với mô hình ownership của Rust, API có thể được thiết kế sao cho khi gọi window.set_href('/foo') thì ownership của window bị lấy đi, khiến việc gọi lần hai là bất khả thi. TypeScript không hề có khái niệm theo dõi lifetime nên không thể làm như vậy. Mà vì JS API đã tồn tại sẵn, phía TypeScript cũng không có cách đưa vào hệ thống ownership. Tôi muốn nêu đây như một ví dụ cho việc nhiều tính năng của Rust kết hợp lại để mang đến các bảo đảm mạnh hơn

    • Cơ sở cho lập luận của bạn rằng Rust tốt hơn nghe rốt cuộc giống như “vì lập trình viên Rust giỏi hơn”. Tôi nghĩ lập trình viên Rust sẽ không đưa ra kiểu lập luận vòng vo như vậy

  • Code sau phép gán vẫn tiếp tục chạy trừ khi bạn explicit return sớm. Thật lòng mà nói, tôi không hiểu vì sao lại nghĩ rằng việc gán một giá trị sẽ dừng việc thực thi script. Ví dụ TS có thể thiếu ngữ cảnh, nhưng đem nó ra như một ví dụ về "data race" thì khá lạ

    • Gán giá trị cho window.location.href có tác dụng phụ là trình duyệt sẽ điều hướng tới liên kết đó. Hành vi này khá bất ngờ, và vì việc gán đơn thuần lại dẫn tới tải một trang mới, nó có cảm giác hơi giống execve, nên nghĩ rằng việc thực thi JS sẽ dừng ngay cũng không phải là điều quá lạ. Khi lập trình thì không nên dựa vào giả định như thế, nhưng vì bản thân hành vi này thực sự kỳ quặc nên tôi nghĩ việc bị nhầm cũng dễ hiểu

    • Dù có nghĩ như vậy hay không, loại bug này khi ai đó chỉ ra rồi thì cách sửa khá rõ ràng. Luận điểm chính mà tác giả muốn nói là những bug kiểu này, thứ TS không bắt được, trong thực tế có thể rất khó tìm và tốn nhiều thời gian

    • exit(), execve() v.v. thực sự dừng việc thực thi ngay lập tức, nên người ta có thể nghĩ redirect cũng hoạt động như vậy

    • Chỉ vì ai đó chia sẻ trải nghiệm của mình mà coi đó là vấn đề thì khá kỳ lạ

    • Phép gán này có tác dụng phụ rất lớn là khiến bạn rời khỏi trang. Việc coi nó như một async action sẽ chạy ngay lập tức cũng không phải là suy nghĩ vô lý. Tôi cũng từng giả định như vậy

  • Đây là câu chuyện về việc một nhà phát triển nhận ra static type system hữu ích thế nào. Mỗi lần thấy những bài như vậy tôi đều thấy thú vị

    • Điều tôi muốn thể hiện trên blog là tracking lifetime và trait system của Rust có thể bắt cả những vấn đề phức tạp hơn nhiều so với chỉ bắt lỗi không khớp kiểu. TypeScript cũng là ngôn ngữ static type, nhưng không thể bảo đảm mạnh như Rust
  • Có phải phần lớn ưu điểm rốt cuộc đều đến từ việc dùng static type, tức ngôn ngữ biên dịch, đúng không? Java, Go, C++ cũng vậy. TypeScript có chút đánh đố vì nó compile sang JS và thừa hưởng các vấn đề của JS, nhưng dù vậy vẫn dùng được. Rust có type system nghiêm ngặt hơn nên nhận được thêm các kiểm tra ở compile time, nhưng đổi lại tôi thấy nó khó học hơn và cũng khó đọc hơn

    • Tôi đồng ý ở một mức độ nào đó, nhưng trong Rust thì type system còn có nhiều chiều hơn như ownership, shared/exclusive access, thread safety, sum type, v.v. Nhờ hệ thống ownership/borrowing mà việc truyền tham số là một view tạm thời hay là chuyển giao hoàn toàn được thể hiện rõ ràng. Điều này rất có ích trong các chương trình lớn hoặc khi dùng thư viện bên ngoài. Ví dụ, kiểu slice của Go không cho thấy rõ ở runtime phép toán nào được phép, và cũng mơ hồ trong cách borrow read-only. Rust có thể bảo đảm thread safety ở cấp độ type system, nên cả data race vốn khó tìm ở runtime trong ngôn ngữ khác cũng có thể bị chặn ngay từ compile time

    • Việc gom tất cả ngôn ngữ static type lại thành một nhóm là vì bạn vẫn chưa thực sự cảm nhận được sức mạnh của union(sum) type và pattern matching. Một khi đã quen với union type, bạn sẽ không còn thấy thỏa mãn với các ngôn ngữ static type truyền thống khác nữa

    • Một ưu điểm lớn là traits/impl traits. Rust cho phép bổ sung trait cho bất kỳ kiểu nào về sau, tương tự Extension Method của C#. Trong đa số ngôn ngữ, kiểu dữ liệu sẽ được cố định ngay khi được định nghĩa trong thư viện, còn Rust cho phép tiếp tục tích lũy dần chức năng lên các kiểu đơn giản. Tính chất late-bound này là yếu tố bơm thêm tính động vào type system. Nói hơi cực đoan thì siêu năng lực thực sự của Rust không phải borrow checker mà là độ mở và linh hoạt của type system. Không cần phải thiết kế mọi thứ hoàn chỉnh ngay từ đầu, chỉ cần mở rộng dần dần là được

    • Không phải mọi ngôn ngữ static type đều mang lại cùng một hiệu quả. Java rốt cuộc vẫn phụ thuộc vào Object và runtime casting. Go không có enum. C++ có thêm khái niệm variant, nhưng để dùng an toàn thì phải xử lý thủ công kiểu try/except nên về cấu trúc khá bất tiện

    • Bạn nói Rust khó học, nhưng thật ra nếu đã học cho chắc thì nó không khó. Giai đoạn đầu học code, việc viết bừa một chút để làm cho cái gì đó chạy được thường khá quan trọng, còn Rust thì không thân thiện với cách làm đó. Tôi không khuyên dùng nó làm ngôn ngữ nhập môn, nhưng bảo nó khó đọc thì không hẳn

  • Nhờ tính an toàn mạnh của Rust, tôi tự tin hơn nhiều khi đụng vào codebase. Chính sự tự tin này khiến việc refactor các phần cốt lõi không còn đáng sợ, và kết quả là năng suất lẫn khả năng bảo trì tăng lên đáng kể. Nhưng vì những hiệu quả như vậy nên ta mới viết test. Nếu không có test thì compiler nghiêm ngặt sẽ giúp được rất nhiều, nhưng nếu viết test tốt thì với ngôn ngữ nào bạn cũng có thể refactor một cách tự tin

    • Tốt hơn là để compiler chứng minh tĩnh những gì có thể chứng minh. Test là thứ nên dùng chỉ trong các tình huống mà bảo đảm tĩnh khó thực hiện. Đỉnh cao lý tưởng là formal verification, nhưng trong thực tế rất khó nên không thể nói chung cho mọi trường hợp, dù về nguyên tắc thì đúng là như vậy

    • Cả test tốt lẫn type system được tận dụng đúng cách đều hiệu quả trong việc bắt bug. Nhưng viết test đôi khi khiến tôi nhớ tới truyện tranh xkcd "Standards". Kiểu như sửa tiêu chuẩn bằng cách tạo thêm tiêu chuẩn, ở đây là bắt bug bằng cách viết thêm code. Dù sao thì việc bảo trì type system là do người thiết kế ngôn ngữ lo, không phải từng dự án tự gánh

    • Mỗi lần refactor code thì cũng phải refactor cả test, thành ra khối lượng công việc tăng gấp đôi

  • Tôi nghĩ type system của Rust hay F# tỏa sáng nhất khi refactor code. Cụm từ refactor không sợ hãi thực sự rất đúng

    • Nhược điểm là Rust không chấp nhận code chưa hoàn thiện, nên trong lúc refactor bạn không thể có trạng thái 'code chạy được một phần'. Hoặc hoàn thành hẳn, hoặc đừng làm gì cả, nên sẽ hơi bất tiện nếu muốn viết code mang tính thử nghiệm. Nhưng sự nghiêm ngặt này cuối cùng lại là một yếu tố dẫn tới code tốt
  • Ví dụ về Zig thật đáng sốc. Nó trông quá bất ổn, đến mức tôi không hiểu sao có thể cho rằng kiểu thiết kế đó là tốt được

    • Tôi nghĩ đây có lẽ là bug. Nhưng với ngôn ngữ thiên về người sáng tạo như Zig, để bug được sửa thì điều quan trọng là chính người sáng tạo cũng phải thừa nhận đó là bug. Nếu họ cho rằng đó là chủ ý, có thể nó sẽ tiếp tục đi theo thiết kế đó

    • Ngôn ngữ nào cũng có một ít thiết kế thiếu an toàn. Ví dụ, Go hay Zig đều yêu cầu phải luôn explicit mutex.unlock(), và khi ra khỏi scope thì không tự động giải phóng. Ngược lại, với toán tử as của Rust thì việc chuyển đổi giữa các kiểu số quá dễ, và tôi từng mất cả ngày lần bug vì chuyện đó

    • Ban đầu tôi đã không để ý lỗi đó, cho đến khi đọc bình luận này mới nhận ra

    • Tôi tự hỏi liệu linter có thể cảnh báo kiểu như bắt tham chiếu tới lỗi không tồn tại trong hệ thống, rồi khuyến nghị dùng switch hay không

    • Tôi cứ nghĩ error set được tạo ra dựa trên function signature. Khá độc đáo

  • Tôi thích việc một static type system mạnh và sound có thể cung cấp nhiều tính năng như vậy. Tôi cũng từng có trải nghiệm refactor quy mô lớn rất dễ dàng trong một codebase Haskell (1 triệu SLOC). Ngay cả không cần các tính năng quá cao siêu, chỉ riêng type system thôi cũng đã đủ để làm được điều đó

  • Rust đã phát hiện đúng việc giữ lock qua ranh giới await, nhưng việc giải phóng lock trước await có thực sự an toàn hay không thì còn cần thêm ngữ cảnh. Tôi nghĩ lock phải được giữ cho tới khi transaction commit được tạo ra; nếu thả trước await thì có thể phát sinh vấn đề đồng thời. Tôi không rành Rust async, nhưng sau commit chẳng phải nên chặn bằng join hay select sao?

    • Nếu cần giữ lock qua await, thì có thể dùng mutex có nhận thức về async. Các crate như futures hay tokio có triển khai loại lock này. Chúng chủ yếu được dùng khi cần giữ lock lâu hoặc giữ lock xuyên qua các lần await. Chi phí của chúng cao hơn lock thông thường

    • Nếu thực sự cần giữ lock cả qua ranh giới await, bạn có thể dùng async-aware mutex của Tokio. Xem tài liệu tokio/sync/struct.Mutex