- Việc chuyển từ Go sang Rust gần với một lựa chọn đưa các vấn đề như
nil, xử lý lỗi, data race và vòng đời tài nguyên sang các bảo đảm tại thời điểm biên dịch hơn là chỉ để tăng tốc độ
- Go có ưu điểm là biên dịch nhanh, goroutine đơn giản và hệ sinh thái backend mạnh, nhưng Rust dùng
Option, Result, Send/Sync để chặn nhiều sai sót hơn ngay trong hệ thống kiểu
- Borrow checker và
async/await của Rust tạo ra chi phí về đường cong học tập và tính tiện dụng, đồng thời thời gian biên dịch cũng cần được xem là một bước lùi rõ ràng so với Go
- Với việc chuyển đổi, chiến lược phù hợp là bắt đầu từ các thành phần có ranh giới rõ ràng như dịch vụ hot path, worker hoặc một số endpoint phía sau gateway thay vì viết lại toàn bộ
- Hiệu quả kỳ vọng có thể tóm gọn là giảm CPU 20~60%, giảm bộ nhớ 30~50%, độ trễ P99 ổn định hơn và giảm lỗi do dereference
nil cùng các sự cố mang tính race
Trọng tâm của việc chuyển đổi
- Việc chuyển từ Go sang Rust gần với câu hỏi về bảo đảm tính đúng đắn, đánh đổi ở runtime và khác biệt trong trải nghiệm lập trình viên hơn là chỉ hỏi “Rust có nhanh hơn không”
- Trọng tâm của so sánh là dịch vụ backend, lấy các thế mạnh của Go như binary tĩnh nhỏ, thư viện chuẩn thiên về networking và hệ sinh thái HTTP server·gRPC·cơ sở dữ liệu làm mốc
- Một phần nội dung cũng có thể áp dụng cho công cụ CLI, firmware nhúng và game engine, nhưng đó không phải đối tượng được tối ưu hóa
- Tài liệu nền liên quan được nêu gồm “Go vs Rust? Choose Go.” năm 2017 và “Rust vs Go: A Hands-On Comparison” của đội ngũ Shuttle
- Go là một ngôn ngữ thành công, nhưng những lựa chọn thiết kế như việc dùng
nil rất rộng rãi, xử lý lỗi dựa vào kỷ luật thay vì kiểu, và việc thiếu generics trong thời gian dài trở thành các điểm tranh luận chính khi so với Rust
- Trong JetBrains Developer Ecosystem Survey, Go được mô tả là ngôn ngữ duy trì tỷ lệ lập trình viên đang sử dụng ở mức 17~19%, còn Rust tăng trưởng đều nhưng vẫn chiếm tỷ trọng nhỏ hơn
Hệ thống công cụ
- Cả Go và Rust đều có hệ thống công cụ đi kèm đầy đủ cung cấp build, test, format, lint và quản lý phụ thuộc qua một giao diện nhất quán
cargo cung cấp rộng hơn các chức năng tương ứng với bộ công cụ Go dưới dạng công cụ hạng nhất
go.mod / go.sum → Cargo.toml / Cargo.lock: cấu hình dự án và manifest phụ thuộc
go get / go mod tidy → cargo add / cargo update: thêm và giải quyết phụ thuộc
go build → cargo build: biên dịch
go run . → cargo run: build rồi chạy
go test ./... → cargo test: kiểm thử
go vet ./... → cargo clippy: linter, và Clippy mang tính áp đặt quan điểm mạnh hơn vet rất nhiều
gofmt / goimports → cargo fmt: formatter tự động không cần cấu hình
golangci-lint run → cargo clippy -- -D warnings: chế độ lint nghiêm ngặt
go doc → cargo doc --open: tạo và mở tài liệu API
pprof → cargo flamegraph / samply: profiling CPU
govulncheck → cargo audit: kiểm tra lỗ hổng dựa trên cơ sở dữ liệu advisory
- Trong Go, người ta thường lấp chỗ trống bằng các công cụ bên thứ ba như
golangci-lint, mockgen, air, goreleaser, nhưng ở Rust, hệ sinh thái hạng nhất bao phủ sẵn nhiều chức năng hơn
- Ngay cả khi cần crate bên ngoài, các công cụ như
cargo watch, cargo nextest cũng có thể được cài chỉ với một lần cargo install cargo-nextest và hoạt động như công cụ native với cách gọi như cargo nextest
gofmt và rustfmt có lợi ích lớn hơn ở chỗ loại bỏ tranh cãi về style trong code review hơn là thỏa mãn sở thích chi tiết về phong cách
- Trích Go Proverbs của Rob Pike: “Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.”
Khác biệt cốt lõi giữa Go và Rust
- Cả hai ngôn ngữ đều là ngôn ngữ biên dịch, kiểu tĩnh, triển khai bằng một binary duy nhất và có mô hình đồng thời mạnh, nhưng khác biệt nằm ở phạm vi mà trình biên dịch bảo đảm và mức độ kiểm soát hành vi runtime
- Các hạng mục so sánh chính như sau
- Phát hành ổn định: Go năm 2012, Rust năm 2015
- Hệ thống kiểu: Go là kiểu tĩnh·cấu trúc và hỗ trợ generics từ 1.18, Rust là kiểu tĩnh·danh nghĩa và hỗ trợ generics·trait·lifetime
- Quản lý bộ nhớ: Go dùng garbage collection đồng thời·độ trễ thấp, Rust dựa trên ownership và borrowing, không có GC
- An toàn null: Go có
nil hiện diện rộng rãi, Rust không có null và dùng Option<T> làm phương án thay thế ở cấp kiểu
- Xử lý lỗi: Go dùng interface
error và if err != nil { ... }, Rust dùng Result<T, E>, toán tử ? và pattern matching đầy đủ
- Đồng thời: Go dựa trên goroutine và CSP qua channel, Rust dùng
async/await, channel và thread trên tokio
- Hủy tác vụ: Go dùng
context.Context theo quy ước, Rust dùng cách truyền rõ ràng và được kiểm tra kiểu như CancellationToken
- Data race: Go phát hiện theo xác suất tại runtime bằng
-race, Rust phát hiện tại thời điểm biên dịch bằng Send/Sync
- Thời gian biên dịch: Go rất nhanh, Rust chậm hơn, đặc biệt là clean build
- Runtime: Go có runtime khoảng 2MB và GC, Rust không có runtime ngoài
libc hoặc có thể build tĩnh hoàn toàn bằng MUSL
- Quy mô hệ sinh thái: Go có khoảng 750 nghìn+ module, Rust có 250 nghìn+ crate
- Những kiểm tra như xử lý
nil, truyền lỗi, data race, vòng đời tài nguyên, hủy tác vụ và generics vốn dựa vào quy ước, công cụ hoặc phát hiện ở runtime trong Go được đưa vào trong hệ thống kiểu ở Rust
Mutex<T> của Rust buộc việc truy cập giá trị bên trong phải thông qua guard lấy được bằng .lock(), từ đó loại bỏ ngay trong kiểu bản thân “đường đi quên khóa”
- Mẫu này cũng lặp lại với
Option, Result, &mut T, Send/Sync và các RAII guard nói chung; khi đã quen, trình biên dịch sẽ thay bạn thực hiện phần kiểm tra vốn phải ghi nhớ trong đầu
Những giới hạn của Go khiến bạn cân nhắc Rust
- Vì Go đủ nhanh cho phần lớn workload backend, lý do chính để cân nhắc Rust gần với sự dài dòng trong xử lý lỗi, rủi ro con trỏ
nil, và việc thiếu các tính năng hệ thống kiểu tinh vi như enum và trait hơn là vấn đề tốc độ
- Interface của Go không phải là vật thay thế đầy đủ cho trait của Rust, và thư viện chuẩn không có kiểu
Set, nên cần các cách vòng như map[T]struct{} theo kiểu thành ngữ
-
Panic nil trong production
- Một service Go có thể chạy ổn nhiều tháng rồi ở một đường đi mã cụ thể lại bỏ sót kiểm tra con trỏ
nil, gây panic goroutine
- Trong ví dụ,
Find trả về (*User, error) và ở trường hợp “not found” thì error là nil, nhưng việc kiểm tra user vẫn để cho phía gọi tự lo
user.Account.Notify() có thể bị crash khi user hoặc Account là nil
- Các linter như
nilaway, staticcheck và kiểm tra của IDE có thể bắt được một phần, nhưng chúng là opt-in, mang tính xác suất, và không ổn định khi vượt qua ranh giới package
Option<T> của Rust loại bỏ nhóm sự cố này bằng cách không cho phép dereference nếu chưa xử lý trường hợp None
-
Data race mà -race không bắt được
go test -race là một công cụ tuyệt vời, nhưng vì là trình phát hiện lúc runtime nên nó chỉ tìm được các race thực sự đã chạy trong quá trình test
- Trong Go, mã mà hai goroutine sửa đổi một map không có khóa vẫn biên dịch được, và có thể phát nổ trong production dưới tải
- Trong Rust, chia sẻ trạng thái có thể thay đổi giữa các thread đòi hỏi kiểu triển khai
Send và Sync, và nếu cố chia sẻ một HashMap bình thường giữa các thread thì sẽ không biên dịch được
- Bạn buộc phải dùng một trong
Arc<Mutex<...>>, Arc<RwLock<...>> hoặc channel, khiến điều kiện race trở thành lỗi kiểu
- Paul Dix trực tiếp nhắc đến việc loại bỏ data race như một động cơ khi viết lại InfluxDB 3.0
- “[The main benefit is] fearless concurrency — eliminating data races essentially, which we had before. Really gnarly bugs in version 1 of Influx due to that.”
- Nguồn: Paul Dix, Founder & CTO, InfluxData, Rust in Production
-
Xử lý lỗi có thể kết hợp
if err != nil { return err } của Go có thể làm loãng logic thực sự của hàm, và việc bọc ngữ cảnh bằng fmt.Errorf("doing X: %w", err) phụ thuộc vào kỷ luật chứ không phải quy tắc do compiler áp đặt
- Trong chuỗi thảo luận Lobste.rs, các lập trình viên Go giàu kinh nghiệm phản biện rằng
errcheck và golangci-lint bắt được phần lớn lỗi bỏ sót xử lý lỗi, và if err != nil tường minh dễ đọc hơn chuỗi ? dày đặc
- Peter Bourgon xem xử lý lỗi tường minh của Go là một giá trị văn hóa có chủ đích
- “I think that error handling should be explicit, this should be a core value of the language.”
- Nguồn: Peter Bourgon, GoTime #91, được trích trong Zen of Go của Dave Cheney
Result<T, E> của Rust là một phần của chính chữ ký kiểu nên không thể bị quên, và thông qua enum được định nghĩa bằng thiserror::Error cùng #[from], bạn có được chuyển đổi lỗi và kiểm tra tính đầy đủ
- Khi thêm một variant lỗi mới, compiler sẽ chỉ ra những vị trí
match cần cập nhật
-
Generic không cần boxing
- Generic của Go 1.18 rất hữu ích, nhưng có các giới hạn như không có method với type parameter, GC shape stenciling, và đôi khi có đặc tính hiệu năng gây bất ngờ
- Generic của Rust được monomorphize, nên mỗi lần khởi tạo tạo ra mã chuyên biệt và không có chi phí runtime
- Khi kết hợp với trait, nó cho phép zero-cost abstraction
- Điều này quan trọng hơn ở hạ tầng dùng chung như middleware, generic repository, decoder, parser hơn là ở mã handler, và trong các mảng này Go thường quay về
interface{}/any và ép kiểu
-
Độ trễ có thể dự đoán được
- GC của Go rất tốt, chạy đồng thời, độ trễ thấp, và được tinh chỉnh tốt cho các workload service thông thường, nhưng “low-pause” không có nghĩa là “no-pause”
- Trong các tình huống có nhiều cấp phát, tail latency P99 có thể kém hơn so với triển khai Rust không cấp phát trên hot path
- Với các hệ thống nhạy cảm với độ trễ như giao dịch, đấu giá thời gian thực, network proxy, hay thu thập lưu lượng cao, việc không có GC pause là một lợi thế thực sự
- Stephen Blum nói rằng Rust là cần thiết để đạt được năng lực hiệu năng trên mỗi đô la ở quy mô của PubNub
- “Go is great at our scale, but we really need something that is going to give us the price-per-dollar performance capacity that we need, and Rust is going to get us there. That’s why basically everything is heading towards Rust these days.”
- Nguồn: Stephen Blum, CTO, PubNub, Rust in Production
Các mẫu Go tương ứng trong Rust
- Cách nhanh nhất để làm quen với Rust là ánh xạ các mẫu Go bạn đã biết sang các mẫu tương ứng trong Rust
- Có một ví dụ dài hơn triển khai cùng một dịch vụ backend bằng cả hai ngôn ngữ tại Shuttle comparison
-
Xử lý lỗi: if err != nil vs Result<T, E>
- Go gọi
os.ReadFile(path) và json.Unmarshal, rồi trả về lỗi đã được bọc ngữ cảnh bằng if err != nil
- Rust được cấu thành từ
fs::read_to_string(path)?, serde_json::from_str(&data)?, Ok(cfg)
- Toán tử
? thay thế mẫu if err != nil { return err }, đồng thời xử lý cả chuyển đổi kiểu nếu From<E1> for E2 đã được triển khai
#[from] của thiserror hỗ trợ cách chuyển đổi này theo lối viết idiomatic
-
Null: nil vs Option<T>
GetUser(id string) *User trong Go trả về nil nếu không tìm thấy người dùng, và nếu bên gọi thực hiện fmt.Println(u.Name) thì sẽ panic khi là nil
get_user(id: &str) -> Option<User> trong Rust trả về Some(User) hoặc None
let user = get_user("123"); println!("{}", user.name); sẽ gây lỗi biên dịch vì user không phải là User mà là Option<User>
- Phải dùng
match get_user("123") để xử lý cả Some(u) lẫn None
- Rust an toàn không có
nil, và tham chiếu không thể là null
-
Interface vs trait
- Interface trong Go có tính cấu trúc, và kiểu dữ liệu mặc nhiên thỏa mãn interface
- Trait trong Rust có tính danh nghĩa, và phải được triển khai một cách tường minh
- Cách của Go phù hợp với duck typing tức thời, còn cách của Rust tốt cho refactoring và discoverability, đồng thời có thể dùng grep để tìm các kiểu triển khai một trait cụ thể
- Generic function có trait bound như
fn handle<R: Reader>(r: R) bao phủ được hầu hết trường hợp, và nhờ monomorphization nên không có dispatch lúc chạy
- Khi cần lưu các implementation dị thể với runtime dispatch, dùng
Box<dyn Trait> hoặc Arc<dyn Trait>
-
Goroutine vs async task
- Mô hình đồng thời của Go rất đơn giản như
go doWork(ctx, input), goroutine có chi phí thấp và runtime sẽ lên lịch chúng trên các OS thread
- Một ưu điểm lớn của Go là không có khác biệt cú pháp giữa mã tuần tự và mã song song
- Rust trong dịch vụ backend gần như luôn dùng
async/await trên executor tokio
- Hàm async trả về
Future, và sẽ không chạy cho đến khi được await hoặc spawn
- Trình biên dịch theo dõi
Send/Sync trước và sau các điểm .await, và sẽ báo lỗi biên dịch nếu giữ một giá trị non-Send vượt qua một điểm await
- Vì không có cơ chế preemption tích hợp kiểu goroutine, nếu chạy tác vụ CPU-bound quá lâu bên trong async task thì executor có thể bị bỏ đói, và cần chuyển sang
tokio::task::spawn_blocking hoặc rayon
-
context.Context vs CancellationToken
- Trong Go,
context.Context được truyền vào mọi blocking call
- Rust không có
context.Context tích hợp sẵn, và đối tượng tương ứng gần nhất cho việc hủy là tokio_util::sync::CancellationToken
- Timeout được bọc future bằng
tokio::time::timeout(dur, fut)
- Deadline và value thường được truyền qua các tham số tường minh hoặc
tracing span hơn là một đối tượng context duy nhất
- Trích dẫn từ The Zen of Go của Dave Cheney:
- “Go doesn’t have a way to tell a goroutine to exit. There is no stop or kill function, for good reason. If we cannot command a goroutine to stop, we must instead ask it, politely.”
- Trong Go, “lời đề nghị lịch sự” này theo thông lệ được truyền qua
context.Context, còn trong Rust là CancellationToken hoặc kênh watch, nhưng trình biên dịch có thể cảnh báo khi bạn bỏ sót
-
Chuỗi: string vs String và &str
string của Go là một UTF-8 byte slice; khi gán thì header được sao chép còn các byte bên dưới được chia sẻ, tạo thành một cấu trúc bất biến
- Rust tách điều này thành hai kiểu
String: sở hữu dữ liệu, được cấp phát trên heap, và có thể tăng kích thước
&str: một borrowed view tới dữ liệu chuỗi khác, và trong đa số trường hợp tương ứng với tham số string trong Go
- Kinh nghiệm thực tế là nhận
&str cho tham số, và trả về String khi tạo dữ liệu mới
- Sự tách biệt giữa
&str và String là một phiên bản thu nhỏ của mô hình “borrow vs own” trong Rust
Đánh giá về generic trong Go
- Go đã giới thiệu generic vào phiên bản 1.18, tháng 3 năm 2022, tức là 13 năm sau khi ngôn ngữ ra mắt
- Generic hữu ích, nhưng được đánh giá là không mang lại đầy đủ các lợi ích mà người ta kỳ vọng ở Rust, Haskell hay C++ hiện đại, đồng thời cũng mang theo khá nhiều nhược điểm của hệ thống kiểu generic
-
Thư viện chuẩn hầu như không dùng
- Ngay cả 3 năm sau khi generic được đưa vào, thư viện chuẩn của Go phần lớn vẫn tránh dùng generic
sort.Slice vẫn nhận closure func(i, j int) bool thay vì constraint cmp.Ordered
sync.Map vẫn được định kiểu là any/any
- Các generic helper hiện có chỉ nằm trong một số ít package như
slices, maps, cmp, và một vài mục dưới sync
- Lời hứa tương thích Go 1 khiến việc cải tạo các API non-generic hiện có trở nên khó khăn, điều này phần nào giải thích được, nhưng Go vẫn không dùng generic như công cụ chủ đạo như Rust
- Ngay từ đầu, Rust đã thấm generic vào
Option<T>, Result<T, E>, Vec<T>, HashMap<K, V>, Iterator, From/Into, cùng mọi collection và smart pointer
-
Không có hệ thống trait, chỉ có constraint mang tính cấu trúc
- Generic trong Rust gắn liền với trait, thứ đảm nhiệm ad-hoc polymorphism, supertrait, associated type, blanket impl và coherence
- Constraint của Go gần với interface có thêm toán tử
~ để biểu diễn type-set membership
- Go không có supertrait hierarchy như
trait Ord: Eq + PartialOrd của Rust, không có associated type như type Item; của Iterator, cũng không có blanket impl như impl<T: Display> ToString for T
- Trong Go, không thể dùng method có type parameter, nên dạng như
func (s Set[T]) Map[U](<https://corrode.dev/learn/migration-guides/go-to-rust/f func(T>) U) Set[U] là không thể
- Khi mức độ trừu tượng vượt khỏi kiểu “một hàm hoạt động trên bất kỳ
T nào có vài phép toán”, Go lại quay về any, type assertion, sinh mã và reflection thời gian chạy
-
Khác biệt về type inference và chiến lược triển khai
- Rust lan truyền thông tin kiểu qua toàn bộ expression, bao gồm closure, iterator chain và toán tử
?
- Suy luận kiểu của Go nông hơn, thường suy luận type parameter từ đối số hàm, nhưng không thể suy luận từ ngữ cảnh ở vị trí return, nên thường đòi hỏi type argument tường minh ở nơi gọi
- Go chọn con đường trung gian là GCShape stenciling and dictionaries để giữ thời gian biên dịch nhanh, nhưng mỗi lần gọi method của type parameter có thể phát sinh một lớp gián tiếp
- Một tài liệu được đưa ra để minh họa điều này là bài viết của PlanetScale
- Rust tạo mã máy được chuyên biệt hóa riêng cho
Vec<i32> và Vec<String>, không có runtime dispatch
- Cái giá của monomorphization là thời gian biên dịch, và hai ngôn ngữ tối ưu cho những mục tiêu khác nhau
-
Không lấp được các lỗ hổng của hệ thống kiểu
- Trong Rust, generic và trait loại bỏ phần lớn các tình huống vốn cần
Box<dyn Any> hay reflection thời gian chạy
- Generic của Go không loại bỏ được
any, reflect, hay các mẫu sinh mã đang thống trị trong ORM, decoder và mock
encoding/json vẫn dùng reflection, database/sql vẫn dùng any, và mockgen vẫn tiếp tục sinh mã
- Generic của Go tạo cảm giác như một công cụ mới hữu ích trong các trường hợp hẹp, còn generic của Rust vận hành như nền móng mà nếu bỏ đi thì cả ngôn ngữ sẽ sụp đổ
Hệ sinh thái backend Rust
- Hệ sinh thái Rust cũng đã phần nào hội tụ về “lựa chọn mặc định” cho các dịch vụ backend phổ thông
- Các cặp tương ứng tiêu biểu:
- HTTP server: Go
net/http, chi, gin, echo, fiber → Rust axum on hyper
- HTTP client: Go
net/http, resty → Rust reqwest
- gRPC: Go
google.golang.org/grpc + protoc-gen-go → Rust tonic + prost
- SQL: Go
database/sql, sqlc, sqlx, gorm → Rust sqlx, sea-orm, diesel
- Migrations: Go
golang-migrate, goose → Rust sqlx migrate, refinery
- JSON: Go
encoding/json, sonic, goccy/go-json → Rust serde + serde_json
- Logging: Go
log/slog, zerolog, zap → Rust tracing + tracing-subscriber
- Metrics: Go
prometheus/client_golang → Rust metrics + metrics-exporter-prometheus
- Config: Go
viper, koanf → Rust config / config-rs, figment
- CLI: Go
cobra, urfave/cli → Rust clap derive
- Errors: Go
errors, pkg/errors → Rust thiserror cho thư viện, anyhow cho binary
- Testing: Go
testing, testify, gomega → Rust built-in #[test], rstest, assert_matches
- Mocking: Go
mockgen, moq → Rust, fake tự viết là cách làm phổ biến, và mockall cũng được dùng
- Background tasks: Go goroutine +
errgroup → Rust tokio::spawn + JoinSet
- Với một dịch vụ backend điển hình, tổ hợp
axum + sqlx + tokio + tracing + serde + clap được cho là bao phủ 90% những gì cần thiết
Trình kiểm tra borrow và đường cong học tập
- Cần xác định trước rằng khi chuyển từ Go sang Rust, bạn sẽ đâm vào tường
- Runtime của Go xử lý bộ nhớ và aliasing thay bạn, còn Rust chuyển các quyết định đó vào hệ thống kiểu, nên trong vài tuần đầu, đoạn mã “rõ ràng là phải chạy được” có thể bị compiler từ chối
- Những mẫu mà lập trình viên Go thường hay vướng:
- Tham chiếu sống lâu: trong Go, giữ
*User lấy ra từ map trong thời gian dài là điều tự nhiên, nhưng trong Rust, việc sửa map sẽ bị chặn chừng nào borrow đó còn tồn tại
- Struct tự tham chiếu: trong Go, có thể đặt dữ liệu và iterator chạy trên dữ liệu đó trong cùng một struct, nhưng ở Rust thì cần
Pin, ouroboros, hoặc phải thiết kế lại
- Chia sẻ trạng thái có thể thay đổi giữa các goroutine: mẫu
mu sync.Mutex; data map[K]V của Go sẽ thành Arc<Mutex<HashMap<K, V>>> trong Rust
- Trả về tham chiếu từ hàm: chú thích lifetime xuất hiện, và đây là khái niệm mới với lập trình viên Go
- Trình kiểm tra borrow không nên bị xem là “người gác cổng” gây cản trở, mà là cơ chế phơi bày các lỗi có thật
- Nó lọc ngay từ lúc biên dịch những trường hợp như dùng lại giá trị sau khi đã move, nhiều luồng cùng chạm vào một dữ liệu đồng thời, dereference con trỏ null hoặc dangling, hay tham chiếu sống lâu hơn giá trị mà nó trỏ tới
- Khi đã thật sự thấm khái niệm borrow, nó sẽ không còn là thứ để chống lại mà trở thành cộng sự; các lập trình viên Rust dày dạn thường nói rằng sau khoảng 4–12 tuần, trình kiểm tra borrow đã trở thành trợ thủ
- PubNub CTO Stephen Blum nói trên Rustacean Station rằng tháng đầu tiên “giống như lúc mới học lập trình”, vì ông buộc phải trực diện xử lý borrow checker và lifetime
- Maintainer của
clap là Ed Page nói trong Rustacean Station: clap with Ed Page rằng trình kiểm tra borrow giúp ông tập trung vào các vấn đề cấp cao hơn và còn bắt được những chỗ mà việc phân tích thủ công không phát hiện ra
Những khó khăn chính khi chuyển sang Rust
-
Thời gian biên dịch
- Thời gian biên dịch của Rust phải được xem là một bước lùi rõ rệt so với Go; bản build release sạch của một dịch vụ cỡ trung có thể mất vài phút, trái ngược với tốc độ biên dịch gần như tức thì của Go
- Build tăng dần và
cargo check là các biện pháp hợp lý, và thời gian biên dịch cũng đã cải thiện qua từng năm, nhưng khác biệt với Go vẫn rất dễ cảm nhận
- Trong vòng lặp chỉnh sửa, hãy dùng
cargo check; khi bắt đầu có lợi ích rõ ràng thì tách theo workspace; các crate có nhiều procedural macro nên được giữ ở crate riêng để chỉ bị biên dịch lại khi chính chúng thay đổi
- Có thể tham khảo thêm các mẹo giảm thời gian biên dịch Rust
-
Vấn đề “tô màu” bất đồng bộ
- Sự tách biệt
async fn / fn trong Rust là một trong những bước thụt lùi lớn nhất về trải nghiệm sử dụng khi chuyển từ Go sang
- async trait đã được ổn định từ Rust 1.75, nhưng vẫn còn những điểm gồ ghề khi trộn với dynamic dispatch
- Trong một số tình huống, người ta sẽ dùng crate
async-trait để che đi những phần đó
-
Hệ sinh thái nhỏ hơn
- Hệ sinh thái crate của Rust đang phát triển và chất lượng thư viện nhìn chung cao, nhưng Go vẫn đi trước ở một số mảng gần với backend
- Những mảng Go đi trước gồm operator Kubernetes, SDK của nhà cung cấp cloud và driver cơ sở dữ liệu cho một số hệ lưu trữ ngách
- Trước khi chốt việc di chuyển, nên dành khoảng một ngày để kiểm tra xem các thư viện phụ thuộc của bạn có lựa chọn thay thế đủ dùng trong Rust hay không
- Một số đội có thể sẽ phải cập nhật các crate kiểm tra tính hợp lệ XML schema đã bị bỏ hoang, hoặc tự viết client cho những giao thức ít phổ biến hơn
Chiến lược tích hợp
- Việc chuyển thành công từ Go sang Rust gần với một lựa chọn chiến thuật hơn là kiểu viết lại toàn bộ trong một lần
- Microsoft Principal Engineer Victor Ciura nói trong Rust in Production rằng đây là “lựa chọn chiến thuật: không phải viết lại mọi thứ sang Rust cho vui, mà là dùng Rust nếu thành phần mới phù hợp với Rust hơn”
-
1. Tách hot path thành dịch vụ riêng
- Nếu một dịch vụ cụ thể liên tục gây vấn đề, cách di chuyển ít rủi ro nhất là chỉ viết lại dịch vụ đó bằng Rust, vẫn giữ nó sau cùng một hợp đồng API
- Mục tiêu có thể là dịch vụ dùng CPU cao, nhạy cảm với độ trễ, hoặc lặp lại vấn đề ổn định
- Các dịch vụ Go khác vẫn tiếp tục giao tiếp qua HTTP/gRPC nên không cần biết ngôn ngữ triển khai bên trong
- Radar CTO Jeff Kao nói trong Rust in Production rằng bài viết về việc Discord chuyển từ Go sang Rust đã khiến Radar nghĩ đến việc thử điều tương tự
-
2. Thay sidecar hoặc worker process
- Worker nền, queue consumer, pipeline thu thập dữ liệu, và các tác vụ batch bị ràng buộc CPU là những mục tiêu đầu tiên rất phù hợp
- Chúng thường có ranh giới đầu vào/đầu ra rõ ràng như queue hoặc topic, và không có trạng thái chia sẻ trong cùng tiến trình với phần còn lại của hệ thống
-
3. cgo khả thi nhưng đau đầu
- Có thể gọi Rust từ Go qua cgo, và cũng có hướng dẫn tốt về việc này
- Nhưng trong dịch vụ backend thì thường không được khuyến nghị
- Độ phức tạp khi build và chi phí FFI thường làm mất đi lợi ích so với cách “dựng một dịch vụ Rust và đặt nó sau một lời gọi mạng”
- Với thư viện và công cụ CLI, cách này có thể thực tế hơn
-
4. Áp dụng Strangler Pattern phía sau gateway
- Nếu có API gateway hoặc reverse proxy, bạn có thể chỉ định tuyến một số endpoint nhất định sang dịch vụ Rust mới, còn phần còn lại vẫn để ở Go
- Cách này đặc biệt phù hợp khi một bounded context như xác thực, tìm kiếm hay thanh toán là đơn vị di chuyển hợp lý
- Mẫu này được gọi là “strangler fig” vì dịch vụ mới sẽ lớn dần quanh dịch vụ cũ và cuối cùng thay thế hoàn toàn nó
Mẹo di chuyển trong thực tế
- Hãy bắt đầu từ dịch vụ có ranh giới rõ ràng, và đừng chọn dịch vụ trung tâm nhất hay được triển khai thường xuyên nhất
- Nên chọn dịch vụ có hợp đồng với phần còn lại của hệ thống đã được định nghĩa tốt và bán kính ảnh hưởng nhỏ
-
Giữ nguyên cùng hợp đồng API
- Nếu dịch vụ Go đang cung cấp REST API, thì dịch vụ Rust cũng nên giữ cùng đường dẫn, cùng dạng JSON, cùng kiểu bọc lỗi
- Việc di chuyển sẽ vô hình với client, và bạn có thể chuyển lưu lượng dần dần bằng gateway
-
Đừng bê nguyên xi các thành ngữ
if err != nil { return err } sẽ thành ?
- Mẫu goroutine cho mỗi request chỉ nên chuyển thành
tokio::spawn khi thực sự cần
axum vốn đã xử lý request đồng thời
- Interface chỉ có một method thường sẽ trở thành trait bound của generic thay vì
Box<dyn Trait>
-
Dùng compiler như một người pair programming
- Thông báo lỗi của compiler Rust nhìn chung rất chất lượng, và nếu đọc chậm rãi thì gần như lúc nào cũng chỉ ra câu trả lời đúng
- Những thành viên vật vã lâu nhất thường là người không xem compiler là cộng sự mà là đối thủ
-
Đầu tư đào tạo từ sớm
- Nếu cố vừa học Rust “bên lề” vừa làm migration thì thường khó có kết quả tốt
- Cần thực sự dành thời gian học tập như workshop, khóa học online, hoặc các buổi pair session trên codebase thật
- Khi cả nhóm đã thành thạo, khoản đầu tư ban đầu này sẽ được hoàn vốn gấp nhiều lần
Những lĩnh vực mà Go vẫn tiếp tục phù hợp
- Không cần chuyển mọi thứ sang Rust, và có những lĩnh vực mà Go đặc biệt phù hợp
-
Công cụ native cho Kubernetes
- Mảng operator, controller và CRD có hệ sinh thái áp đảo theo hướng Go
-
Tiện ích CLI và công cụ phát triển
- Điểm mạnh là biên dịch nhanh, cross-compile dễ và triển khai đơn giản
-
Dịch vụ glue
- Với các lớp API mỏng, proxy và bộ chuyển đổi định dạng, tỷ lệ boilerplate của Rust có thể không đáng với công sức bỏ ra
-
Những nơi tốc độ của nhóm quan trọng hơn việc bảo đảm độ chính xác tuyệt đối
- Trong các mảng cần di chuyển nhanh, Go vẫn có thể tiếp tục là lựa chọn phù hợp
- Jon Seager, VP of Engineering tại Canonical, nói trong Rust in Production rằng Go là một lựa chọn rất tốt cho các dịch vụ mạng, Canonical có rất nhiều Go, và Juju cũng là một codebase Go khổng lồ
- Chiến lược hybrid là điều phổ biến; nhiều nhóm cuối cùng đi đến backend đa ngôn ngữ, dùng Go cho các dịch vụ “nhàm chán” và Rust cho các dịch vụ mà độ ổn định và hiệu năng đủ để bù lại công sức bổ sung
Những cải thiện có thể kỳ vọng
- Các con số khác biệt rất lớn tùy theo workload, vì vậy nên xem đây là hướng dẫn gần đúng chứ không phải cam kết
- Phạm vi cải thiện gần đúng được quan sát khi di chuyển từ Go sang Rust:
- Mức sử dụng CPU: giảm 20~60%
- Vì Go vốn đã hiệu quả, nên không kịch tính như khi chuyển từ Python sang Rust
- Lợi ích đến từ việc không có GC và các vòng lặp chặt hơn
- Bộ nhớ: giảm 30~50%
- Chủ yếu do không có overhead của GC và runtime nhỏ hơn
- Độ trễ P99: nhất quán hơn nhiều
- Các dịch vụ Rust có xu hướng phẳng hơn và giảm jitter do GC gây ra vốn thường thấy ở dịch vụ Go
- Sau khi Go đưa vào GC độ trễ thấp, phía Go cũng đã cải thiện nhiều, nhưng dưới tải cao vẫn còn khác biệt
- Sự cố production: đây là lĩnh vực cải thiện mà các nhóm báo cáo tích cực nhất
- Những loại lỗi như data race, nil dereference và đường đi lỗi bị bỏ sót, vốn có thể vượt qua
go test -race rồi đi vào production, sẽ không biên dịch được trong Rust
- Sau khi migrate sang Rust, lịch trực on-call nhìn chung trở nên cực kỳ yên ắng
- Andrew Lamb, Staff Engineer tại InfluxData, nói trong Rustacean Station: Rebuilding InfluxDB with Rust rằng sau khi viết lại InfluxDB, họ không còn phải lần theo crash, các race condition đa luồng kỳ quặc và những vấn đề trước đây từng ngốn rất nhiều thời gian
- Khi chuyển từ Go sang Rust, khả năng đạt mức cải thiện throughput gấp 10 lần như khi chuyển từ Python sang Rust là thấp
- Lợi ích thực sự là giảm các “lỗi ngớ ngẩn”, làm phẳng phần đuôi độ trễ, và khả năng mở rộng sang các mảng khác như phát triển nhúng hay lập trình hệ thống bằng cùng một ngôn ngữ
Lưu ý bổ sung
- Hệ thống kiểu của Rust không loại bỏ mọi lỗi logic đồng bộ hóa, nhưng các kiểu không thể chia sẻ giữa các thread nếu thiếu đồng bộ hóa sẽ không biên dịch được
- Các vấn đề kiểu như “quên khóa” dẫn đến hỏng dữ liệu âm thầm là loại vấn đề mà hệ thống kiểu của Rust có thể ngăn chặn
- Go
string là một chuỗi byte bất biến và theo quy ước là UTF-8, nhưng điều đó không được bảo đảm ở cấp độ kiểu
- Cặp tương ứng gần nhất là Go
string ↔ Rust &str xét theo góc nhìn chỉ đọc, và Go []byte ↔ Rust Vec<u8> xét theo góc nhìn buffer có thể thay đổi
- Rust
String là phiên bản có quyền sở hữu và có thể mở rộng của &str, kèm thêm bảo đảm rằng nội dung là UTF-8 hợp lệ
- Có thể tham khảo thêm tại Strings, bytes, runes and characters in Go
- Từ Go 1.18, hàm generic và kiểu generic đã khả dụng, nhưng type parameter cho bản thân method vẫn chưa được đưa vào
- Các chuỗi iterator như
(0..100).filter(|n| ...).collect() của Rust có thể xa lạ với lập trình viên Go, nhưng trong Rust vẫn có thể dùng vòng lặp for, và trong mã dùng một lần thì đó thường là lựa chọn đúng
Kết luận
- Việc chuyển từ Go sang Rust khác với việc chuyển từ Python hay TypeScript sang Rust
- Lập trình viên xuất thân từ Go vốn đã hiểu lợi ích của kiểu tĩnh và ngôn ngữ biên dịch, nên đây không phải là quá trình từ bỏ kiểu động hay runtime chậm
- Đánh đổi cốt lõi là bỏ
nil để đổi lấy codebase vững chắc hơn, ít cạm bẫy hơn và một compiler nghiêm ngặt hơn, bắt được nhiều sai sót hơn ngay lúc biên dịch
- Đổi lại, đường cong học tập sẽ dốc hơn
- Với các dịch vụ quan trọng đối với doanh nghiệp, cần uptime cao và là thứ tổ chức phụ thuộc vào như dịch vụ nền tảng, đánh đổi này rõ ràng là xứng đáng
- Với các dịch vụ khác, Go vẫn có thể là câu trả lời phù hợp
- Mục tiêu của migration là đặt mỗi bài toán vào ngôn ngữ giải quyết nó tốt nhất
1 bình luận
Ý kiến trên Hacker News
Việc chuyển từ C/C++ hay Python sang Rust thì có thể hiểu được vì nhiều lý do, nhưng với backend web thì Go có vẻ là lựa chọn rất phù hợp
Tôi gần như chỉ dùng Rust, nhưng lần gần nhất làm phần máy chủ web bằng Rust thì tôi đã cảm thấy lẽ ra nên dùng Go
Bài gốc chỉ ra rằng cú pháp xử lý lỗi của Go khá dài dòng, và đó là nhận xét đúng. Rust cũng từng có vấn đề tương tự rồi sau đó bổ sung cú pháp
?để trả về giá trị lỗi khi có lỗi. Phần lớn xử lý lỗi trong Go về cơ bản là viết dài ra của cách nàyRust không có một kiểu lỗi thống nhất, mà có các hệ thống lỗi phổ biến như
io::Error,thiserror,anyhow, nên khi đẩy lỗi ngược lên theo chuỗi gọi hàm thì khá phiềnCó những thứ nếu bị bỏ qua ở một ngôn ngữ mới thì sau này rất khó vá thêm vào. Ví dụ như kiểu hằng số, kiểu boolean, kiểu lỗi, kiểu mảng đa chiều, các kiểu vector·ma trận kích thước 2/3/4 cùng các phép toán chuẩn. Nếu không chuẩn hóa từ đầu thì sẽ tốn rất nhiều thời gian để dung hòa nhiều cách biểu diễn của cùng một khái niệm
Ngoài xử lý lỗi ra thì điều này ít ảnh hưởng hơn với phát triển web, nhưng trong tính toán số, đồ họa, mô hình hóa thì lại là nỗi đau lớn vì phải áp dụng phép toán chuẩn lên các mảng số
Hai ưu điểm của Go trong dịch vụ web là như sau. Thứ nhất là goroutine như bài gốc nói, và thứ hai là thư viện, thứ mà bài gốc không bàn nhiều. Go có hầu hết các thư viện cần cho dịch vụ web, và nhiều thư viện trong số đó còn được dùng nội bộ tại Google nên đã chịu đựng môi trường rất khắc nghiệt. Trong khi đó các crate của Rust còn kém trưởng thành hơn và nhiều trường hợp không có đảm bảo chất lượng chính thức
Ngoài ra, Rust so với Go vẫn còn phụ thuộc nhiều vào các thư viện C/C++, nên cross-compilation, build tái lập được và tạo binary tĩnh thường dễ thành vấn đề hơn
Nhược điểm của Go là garbage collector quá đơn giản. Nếu xuất hiện các đợt tăng độ trễ đột biến thì ngoài việc viết lại đầy đau đớn ra gần như không có nhiều cách ứng phó
Những thứ được liệt kê chỉ là các cách phổ biến để dùng nó, và chỉ dùng
Boxthôi cũng hoàn toàn ổn. Về cơ bản điều này khá giống với những gìanyhow::ErrorlàmDù vậy, ở phía standard library thì theo tôi Go làm tốt hơn Rust rất nhiều
Tôi thích ngôn ngữ Rust và dùng nó cho firmware nhúng lẫn ứng dụng PC, nhưng với backend web thì tôi vẫn dùng Python. Lý do là Rust không có bộ công cụ cấp Django hay Rails
Có thứ na ná Flask, nhưng không có hệ sinh thái vững mạnh như Flask. Tôi ít kinh nghiệm với Go, nhưng nếu là backend web thì có lẽ tôi sẽ chọn Go thay vì Rust. Lý do là hệ sinh thái thư viện và framework
Ngoài ra, vì những lý do quen thuộc mà mọi người vẫn nhắc, tôi không thích Async Rust lắm. Hệ sinh thái web của Rust gần như ở đâu cũng bắt buộc phải dùng bất đồng bộ
io::Errorchỉ là một trong nhiều kiểu có triển khai nó, không có gì đặc biệt. Các lỗi định nghĩa bằngthiserrorcũng triển khai trait nàyanyhowchỉ đơn giản là cách để nói thoải mái “một kiểu Error nào đó” khi bạn không muốn viết chi tiết kiểu lỗi mà hàm có thể ném ra như một phần của hợp đồng APIRust giúp viết mã deterministic dễ hơn Go, nên rất hữu ích khi cần test mô phỏng deterministic và test dựa trên thuộc tính
Gần đây tôi viết công cụ mirror dữ liệu Postgres-to-Iceberg https://github.com/polynya-dev/pg2iceberg bằng Go, nhưng sau đó port sang Rust vì tôi muốn làm test mô phỏng deterministic mà không phải vật lộn với runtime của Go
Tuy vậy, nếu miền bài toán đó không đủ quan trọng để biện minh cho mức kiểm thử như vậy, thì tôi sẽ luôn chọn Go thay vì Rust
Bài liên quan: https://www.polarsignals.com/blog/posts/2024/05/28/mostly-ds...
Có thể nghe sáo rỗng và lặp lại, nhưng điều tôi khó chịu nhất ở Rust là tình hình quản lý package, và theo tôi đó hoàn toàn là hệ quả của tư duy của các nhà phát triển
Tôi thích tính dễ dùng ở phía Rust. Cách tiếp cận kiểu hàm đối với kiểu dữ liệu rất đẹp. Nhưng hiện giờ tôi đang làm song song một dự án Rust và một dự án Go, và cây phụ thuộc đúng là hai con quái vật hoàn toàn khác nhau
Dự án Go hầu như giải quyết bằng standard library, còn dự án Rust chỉ mới cần
rusqlite(sqlite),clap(CLI),ratatui(TUI),tauri(GUI) mà đã có vẻ vượt 400 dependency. Đặc biệttaurilà thủ phạm áp đảo, nhưng kể cả bỏ nó đi thì vẫn gần 100, cảm giác phát điênSẽ tốt hơn rất nhiều nếu có các crate Rust thay thế được quản lý tốt và xử lý dependency hợp lý, nhưng tôi vẫn chưa tìm thấy. Tôi chỉ không muốn rước shai hulud vào hệ thống, vậy mà ở mảng web Rust người ta có vẻ muốn biến
cargothànhnpmtheo khía cạnh đóVì thế số lượng dependency nhìn có vẻ lớn hơn thực tế. Dù là crate riêng nhưng thường cùng một maintainer quản lý và là một phần của cùng kho Git upstream
Dù vậy tôi vẫn đồng ý với cảm nhận chung. Rust có khá nhiều crate ở phiên bản 0.x bị bỏ mặc nửa vời, và nhiều khi cũng chẳng có phương án thay thế tốt hơn
Rồi sau đó sẽ có
httplib3rồi tiếp theo làhttplib4Nói cách khác, tôi thích cách tiếp cận của Rust hơn nhiều. Với tôi việc phụ thuộc vào standard library hay một dependency khác không khác biệt mấy. Dù sao thì vẫn là dependency
Người ta nghĩ rằng vì là standard library nên chất lượng sẽ tốt hơn hoặc được bảo trì tốt hơn, nhưng đó là hai khái niệm tách biệt
Cuối cùng mọi thứ đều phụ thuộc vào nguồn lực. Tất nhiên standard library có thể nhận được nhiều nguồn lực hơn, nhưng ngược lại nó cũng có thể phình to và trở nên không thể bảo trì
rusqlite,clap,ratatui,taurihay khôngNgoài ra cũng cần nhìn vào việc bản thân Tauri gồm 14 crate, và mỗi crate đó đều xuất hiện trong cây build
https://github.com/tauri-apps/tauri/blob/dev/Cargo.toml
Ratatui cũng là 6 crate
https://github.com/ratatui/ratatui/blob/main/Cargo.toml
Chưa ai “giải quyết” được nó, và tôi cũng nghĩ rất khó có một lời giải duy nhất về sau
Trong Go, bạn phải tin rằng tác giả thư viện sẽ tuân thủ semantic versioning một cách chính xác, và bạn không thể khóa phiên bản. Cá nhân tôi thấy đây cũng là điểm khá khó chịu
Có vài cách lách. Có thể dùng SHA như hash commit Git để tạo ra kiểu version gần đúng, hoặc dùng vendoring là bộ đệm dependency đã biết. Nhưng vendoring lại kéo theo bài toán quản lý cache
Cuối tuần rồi tôi phải dùng virtual environment của Python và kết quả không mấy tốt đẹp, khiến tôi nhớ lại vì sao mình đã rời xa Python
CPAN của Perl, Maven/Gradle của Java, gems của Ruby, dep/glide/vgo/modules của Go, Cargo của Rust, npm/yarn của Node đều gặp các vấn đề tương tự
Hệ điều hành cũng vậy, kiểu yum/rpm của Redhat, apt của Debian, snap của Ubuntu. Đặc biệt là snap, tôi thật sự không hiểu tại sao lại như thế
Với trường hợp sử dụng này, có khi vẫn để frontend bằng Go và chỉ chuyển backend sang Rust lại hợp lý hơn chăng
Tài liệu này tạo cảm giác lạ vì vừa muốn là hướng dẫn migration vừa muốn là tài liệu cổ vũ Rust
Rốt cuộc, nếu đang cân nhắc dùng Rust hay Go thì điểm cốt lõi gần như hoàn toàn quy về câu hỏi “có muốn runtime được quản lý hay không”. Cả một thế hệ lập trình viên Rust đã tự thuyết phục mình rằng runtime được quản lý là điều xấu, và việc không có nó là một tính năng quan trọng
Nhưng điều đó rõ ràng là sai. Có nhiều lĩnh vực lập trình muốn runtime được quản lý hơn là không muốn
Điều đó cũng không có nghĩa trong mọi trường hợp như vậy Go phải là lựa chọn mặc định. Có nhiều lý do chủ quan để thích Rust hơn. Khi dùng Go tôi nhớ
match, nhưng không nhớtokiohay Async RustCả hai đều là lựa chọn hợp lý trong gần như mọi trường hợp mà bạn không cần bẻ cong không gian vấn đề một cách cưỡng ép. Ví dụ, viết module nhân Linux bằng Go sẽ là một lựa chọn kỳ quặc
Cuộc chiến Rust đối đầu Go giống như một góc kỳ quặc và hơi xấu hổ của ngành này. Một phần lớn ngành công nghiệp vẫn xây tốt cả hệ thống bằng Python hay Node và đang cười nhạo những kẻ lập dị cãi nhau xem nên dùng ngôn ngữ biên dịch kiểu tĩnh nào. Câu hỏi thật sự là Python so với Rust/Go, chứ không phải Rust so với Go
Nhưng nhìn chung, phía Rust và Go nên hợp lực chống lại cái ác của dynamic typing. Nếu type hint giờ đã được xem là thực hành tốt nhất, thì chẳng phải đó gần như là một sự thừa nhận rằng trước đây có vấn đề hay sao
Dù type hint tốt đến đâu thì vẫn kém type inference. Type inference cho phép giữ nguyên rất nhiều mã khi thay đổi kiểu, đồng thời vẫn ngăn được các thay đổi kiểu ngoài ý muốn
Tôi chỉ ước TS có thêm chút runtime. Điều duy nhất tôi ghen tị với Python là ở các HTTP endpoint, việc xác thực JSON schema diễn ra rất tự nhiên
Quy trình phải đi qua Zod cứ liên tục là nguồn bực bội, và tôi nghĩ đây là vấn đề do đội TS quá giáo điều
Dấu vết của lối viết LLM ngày càng tinh vi hơn, nhưng vẫn rất dễ nhận ra. Đặc biệt là từ genuine
Kiểu như “This is the area where Go genuinely shines, and it’s worth being precise about why”, “the lack of GC pauses is a genuine selling point”, “Humans are genuinely bad at reasoning about memory”, “There are cases where the borrow checker is genuinely too strict”
Tôi không nghĩ cả bài là do AI tạo, mà có vẻ là được AI hỗ trợ. Nếu vậy thì tác giả đã làm khá tốt theo đúng nghĩa genuine
Việc không thấy người khác nhắc đến điều này cho thấy có lẽ nó không thực sự làm hỏng nội dung, nhưng cảm giác ngày càng phổ biến và ngày càng khó phát hiện như vậy vẫn rất kỳ lạ
Tôi mới đọc đến mức kiểu “Go is clearly working for a lot of people,” là đã bắt đầu nghi ngờ có AI hỗ trợ. Tất nhiên có thể không phải, và tôi cũng không giỏi phân biệt
Trớ trêu là đó giống một cảm giác hơn là một dấu hiệu cụ thể. Khi một bài viết “nghe như” có AI hỗ trợ thì dù bản thân bài có ổn tôi cũng mất hứng ngay
Tôi mong mọi người thấy thoải mái hơn trong việc tự viết suy nghĩ của mình theo đúng cách nó xuất hiện trong đầu
it's worth being precise about ...là kiểu diễn đạt nghe đậm chất AI hơn nhiều so với cách dùng genuineVí dụ nhìn đoạn này thì thấy rõ: “Go got generics in 1.18, and they’re useful, but the implementation has constraints (no methods with type parameters, GC shape stenciling, occasional surprising performance characteristics). Rust generics monomorphize, each instantiation produces specialized code with zero runtime cost. Combined with traits, this gives you real zero-cost abstractions.”
Câu nào cũng đang nói điều gì đó, câu nào cũng quan trọng và làm đúng phần việc của nó. Kiểu văn này đáng lẽ chỉ trông đợi ở sách cực kỳ chuyên môn hay bài báo học thuật hơn là ở một bài blog
Vì thế nghịch lý là nó làm bài viết khó đọc hơn và chán hơn
Tôi không mong văn bản do LLM tạo sẽ không đầy những cách diễn đạt sáo mòn. Chỉ là tôi hy vọng tất cả chúng ta sẽ có gu biên tập tốt hơn để không phải đọc đi đọc lại cùng một giọng văn
Nếu là dự án mới thì viết bằng Rust hoàn toàn được
Nhưng nếu đã có code sẵn, hệ thống đang chạy và đang tạo doanh thu, thì hợp lý hơn là chỉ viết lại những phần thật sự cần viết lại bằng chính ngôn ngữ gốc và tiếp tục đi tiếp
Hãy cải thiện hệ thống theo cách nhỏ, đo được, với ngôn ngữ bạn biết và đội ngũ bạn tin tưởng. Ngoài ra chỉ là tranh cãi tôn giáo lãng phí
Tôi đã thích Rust từ trước cả khi chạy benchmark, nhưng hóa ra chênh lệch hiệu quả giữa việc hầu hết LLM viết Rust và viết Go còn lớn hơn tôi tưởng rất nhiều. Đặc biệt là với các harness dạng agent có thể tự sửa các vấn đề môi trường ban đầu thì điều đó càng rõ
Sau khi thấy vậy tôi gần như trở thành người truyền đạo khá nhiệt thành cho Rust. Tôi đã đạt kết quả tốt khi viết công cụ xử lý batch bằng Rust để gọi từ codebase hiện có, nhưng vẫn chưa thử migration toàn bộ production
Các vấn đề của Go mà bài viết nói tới, đặc biệt là chuyện liên quan
nil, theo tôi đang dần được giải quyết nếu code được review cực kỳ kỹ bằng Codex. Tất nhiên không có lỗi ngay từ đầu thì vẫn tốt hơn, nhưng với những nhà phát triển bỏ công vào review và hiểu hệ thống tương xứng với công sức bỏ vào thiết kế và triển khai, thì các bug bảo mật kiểu này đang dần trở thành chuyện có thể lựa chọn tránh đượcDữ liệu ngôn ngữ ở đây: https://gertlabs.com/rankings?mode=agentic_coding
Rust đặt người dùng rất mạnh vào một quỹ đạo định sẵn. Codex lúc nào cũng có thể tạo ra thứ gì đó biên dịch được
Mặt trái là đôi khi nó đáng ra nên thất bại khi không thể có cách tiếp cận đúng kiểu thông dụng, nhưng thay vào đó lại tạo ra một hiện thực hóa ngớ ngẩn, vẫn biên dịch được và vẫn đáp ứng yêu cầu
LLM viết code nhanh hơn con người, nên thời gian chờ biên dịch trở thành phần tương đối lớn hơn. Với dự án có quy mô nhất định, chẳng hạn trên 100 nghìn dòng, tốc độ biên dịch chậm hơn khoảng 10 lần của Rust bắt đầu trở thành nút thắt cổ chai
Nếu đang viết hạ tầng cốt lõi thì có thể đáng để trả cái giá đó, nhưng nếu đang làm một dịch vụ nội bộ không công khai trên Internet thì tốc độ phát triển có thể là mối quan tâm lớn hơn
Tôi cũng cho rằng biên dịch chậm ảnh hưởng đến cả tốc độ phát triển của con người, nhưng kỳ lạ là rất hiếm khi thấy các lập trình viên cố định lượng hóa điều đó
Nếu sự dài dòng là rào cản chính, thì thứ này dự kiến vào Go 1.28 có thể giảm nó đi đáng kể
https://github.com/golang/go/issues/12854#issue-110104883
Cụm “dịch vụ mà tổ chức phụ thuộc vào, cần uptime cao và có tính sống còn với doanh nghiệp” nghe khá buồn cười
Nhất là khi dịch vụ Rust đó chạy trên Kubernetes
Tôi đã dùng Rust từ trước và không có kinh nghiệm với Go, nên có thể bài này không thực sự dành cho tôi
Nhưng có một điểm khiến tôi băn khoăn. Nói rằng data race trong Rust “được bắt ở compile time” nghe có vẻ ít nhiều là cường điệu
Cách nói đó có thể khiến người ta tưởng rằng Rust còn xử lý được cả các vấn đề như starvation do mutex hay những vấn đề đồng thời khác. Thực tế không phải vậy
Tôi biết data race là một thuật ngữ hình thức với phạm vi hẹp, nhưng dù sao tôi vẫn nghĩ có thể viết cho rõ ràng hơn