Rust untuk Developer Go: Panduan Praktis
Kalau kamu menulis Go, kamu akan menemukan lebih banyak hal yang terasa akrab di Rust daripada yang mungkin kamu kira. Kedua bahasa di-compile menjadi satu binary yang cepat. Keduanya punya struct dengan method alih-alih class, tidak punya inheritance, mengembalikan error sebagai nilai alih-alih exception, punya formatter standar yang dipakai semua orang, dan satu tool command-line yang mem-build, mengetes, dan mengambil package. Sebuah struct Go dengan beberapa method bisa diterjemahkan ke Rust hampir baris demi baris.
Yang baru sebagian besar menyangkut memori dan sikap eksplisit. Rust tidak punya garbage collector: setiap nilai punya satu pemilik, dan compiler mengecek ownership (kepemilikan) dan borrowing (peminjaman) sebelum programmu berjalan. Tidak ada nil (kamu memakai Option), dan if err != nil berubah menjadi Result dan operator ?. Interface tetap ada (namanya trait), tapi sebuah tipe harus menyatakan secara terang-terangan bahwa ia mengimplementasikannya. Siap-siap menghabiskan sebagian besar waktu belajarmu untuk ownership. Sisanya akan terasa seperti Go yang lebih ketat dan lebih rinci.
Kenapa developer Go belajar Rust? Biasanya karena salah satu alasan ini: layanan yang mulai terganggu oleh jeda garbage collection atau pemakaian memori, pekerjaan berat di CPU, kode yang berjalan tanpa sistem operasi (embedded) atau di browser (WebAssembly), atau sekadar ingin compiler menangkap data race dan crash ala nil sebelum masuk production. Untuk perbandingan yang lebih umum antara Rust dengan Go, C++, dan Python, lihat Rust vs C++, Go, dan Python. Halaman ini membahas lebih dalam dan ditulis khusus untukmu, developer Go.
Konsep bahasa: dari Go ke Rust
Section titled “Konsep bahasa: dari Go ke Rust”Pakai tabel ini seperti kamus percakapan. Kolom terakhir berisi link ke pelajaran di situs ini yang menjelaskan sisi Rust-nya.
| Go | Rust | Pelajari lebih lanjut |
|---|---|---|
| Garbage collector | Ownership dan borrowing, dicek saat compile; memori dibebaskan saat pemiliknya keluar dari scope | Ownership, Borrowing |
struct + method |
struct untuk datanya + blok impl untuk method-nya |
Struct |
Value receiver func (a Account) |
&self (membaca, tanpa salinan) atau self (mengambil ownership) |
Struct |
Pointer receiver func (a *Account) |
&mut self (boleh mengubah nilainya) |
Struct |
Fungsi constructor NewAccount(...) |
Associated function, menurut kebiasaan Account::new(...) |
Struct |
| Huruf kapital = diekspor | Keyword pub |
Modul |
Zero value (var c Config) |
Tidak ada zero value implisit; setiap field harus diisi. #[derive(Default)] + Config::default() |
Struct, Trait yang sering dipakai |
| Struct embedding | Tidak ada embedding. Taruh struct-nya di sebuah field dan tulis method penerus kecil, atau bagi perilaku lewat trait | Trait, Design pattern |
interface (dipenuhi secara implisit) |
trait (diimplementasikan secara eksplisit dengan impl Trait for Type) |
Trait |
[]Shape berisi tipe yang berbeda-beda |
Vec<Box<dyn Shape>> (sebuah trait object) |
Trait object |
any / interface{} + type switch |
enum dengan satu varian per kasus, lalu match |
Enum, Match |
Generics func Max[T cmp.Ordered](a, b T) T |
Generics dengan trait bound: fn max<T: PartialOrd>(a: T, b: T) -> T |
Generik, Generik lanjutan |
Pointer nil, map nil, interface nil |
Option<T>: bisa Some(value) atau None |
Option |
v, ok := m[k] |
m.get(&k) mengembalikan Option |
HashMap |
(T, error) + if err != nil { return err } |
Result<T, E> + operator ? |
Result |
errors.New, sentinel error, errors.Is / errors.As |
Sebuah enum error yang kamu match (crate thiserror membantu) |
Error kustom, anyhow dan thiserror |
fmt.Errorf("...: %w", err) |
anyhow dengan .context("..."), atau map_err |
anyhow dan thiserror |
panic / recover |
panic! untuk bug sungguhan; memulihkannya jarang dilakukan (std::panic::catch_unwind) |
Panic |
defer |
Drop, yang berjalan saat sebuah nilai keluar dari scope (RAII) |
Scope, Smart pointer |
go f() |
std::thread::spawn(...), atau tokio::spawn(...) untuk task async |
Konkurensi, Async/Await |
chan T, make(chan T, n) |
std::sync::mpsc::channel() (tanpa batas) atau sync_channel(n); tokio::sync::mpsc di kode async |
Channel |
select |
tokio::select! di kode async, atau select! dari crossbeam-channel |
Async lebih dalam |
sync.Mutex di samping datanya |
Mutex<T> membungkus datanya; ia terbuka sendiri saat guard-nya di-drop |
Shared state |
sync.WaitGroup |
JoinHandle::join(), atau std::thread::scope |
Konkurensi |
sync.Once |
std::sync::OnceLock / LazyLock |
Trik pustaka standar |
context.Context |
Tidak ada satu padanan: drop sebuah future untuk membatalkannya, tokio::time::timeout, atau CancellationToken dari tokio-util |
Async lebih dalam |
Nilai func dan closure |
Closure, yang dijelaskan oleh trait Fn, FnMut, FnOnce |
Closure, Fn, FnMut, dan FnOnce |
for i, v := range s |
for (i, v) in s.iter().enumerate() |
Perulangan for, Iterator |
Satu-satunya perulangan for |
loop, while, dan for |
Loop |
Iterator range-over-func (iter.Seq) |
Trait Iterator, dengan adapter seperti filter, map, sum, collect |
Iterator, Iterator kustom |
switch |
match (compiler mengecek bahwa semua kasus sudah kamu tangani) |
Match, Pattern matching |
const + iota |
Sebuah enum (boleh dengan angka eksplisit) |
Enum |
:= |
let (immutable secara default; let mut supaya bisa diubah) |
Variabel |
string (byte, biasanya UTF-8) |
String (dimiliki) dan &str (dipinjam), selalu UTF-8 yang valid |
String |
rune, for _, r := range s |
char, for c in s.chars() |
String |
[]byte |
Vec<u8> atau &[u8] |
Slice |
fmt.Println, %v, %+v |
println!, {} (Display), {:?} dan {:#?} (Debug, dengan #[derive(Debug)]) |
Output |
String() string (interface Stringer) |
Implementasikan Display (lalu .to_string() langsung tersedia gratis) |
Trait yang sering dipakai |
Struct tag `json:"user_name"` |
Attribute: #[serde(rename = "user_name")] |
JSON dengan Serde |
Konversi int(x), float64(n) |
as untuk angka, From/Into, dan TryFrom kalau konversinya bisa gagal |
Konversi tipe, Konversi |
| Integer overflow berputar diam-diam | Panic di build debug, berputar di build release; checked_add, wrapping_add, saturating_add menyatakan apa yang kamu mau |
Integer overflow |
*T dan &x |
Referensi &T dan &mut T (tidak pernah null, tidak bisa hidup lebih lama dari nilainya); Box<T> untuk nilai di heap yang dimiliki |
Borrowing, Smart pointer |
| Package | Modul (mod) di dalam crate; use alih-alih import |
Modul |
go generate |
Makro dan build script build.rs |
Makro, Makro prosedural |
Package unsafe |
Blok unsafe dan raw pointer |
Unsafe Rust |
| cgo | extern "C" dan FFI (bindgen membuat binding dari header C) |
FFI: memanggil C |
Build tag //go:build linux |
#[cfg(target_os = "linux")] dan fitur Cargo |
Fitur Cargo dan cfg |
Koleksi
Section titled “Koleksi”Go punya slice dan map yang sudah tertanam di bahasanya. Rust menyediakannya di standard library, plus beberapa lagi.
| Go | Rust | Pelajari lebih lanjut |
|---|---|---|
[]T yang kamu miliki dan kamu tambahi dengan append |
Vec<T> |
Vektor |
[]T yang diberikan ke fungsi yang hanya membacanya |
&[T] (slice yang dipinjam dari Vec atau array) |
Slice |
[N]T |
[T; N] |
Array |
append(s, x) / append(s, other...) |
v.push(x) / v.extend(other) |
Vektor |
s[a:b] |
&v[a..b] |
Slice |
len(s), cap(s), make([]T, 0, n) |
v.len(), v.capacity(), Vec::with_capacity(n) |
Vektor |
map[K]V |
HashMap<K, V> |
HashMap |
map[K]struct{} atau map[K]bool sebagai set |
HashSet<T> |
HashSet |
| Mengumpulkan dan mengurutkan key map sebelum mencetak | BTreeMap<K, V> (key-nya tetap terurut) |
HashMap |
container/list |
VecDeque<T> (push_back / pop_front) |
VecDeque dan BinaryHeap |
container/heap (implementasikan 5 method) |
BinaryHeap<T>: cukup push dan pop; yang terbesar duluan, bungkus dengan Reverse agar yang terkecil duluan |
VecDeque dan BinaryHeap |
slices.Sort, sort.Slice |
v.sort(), v.sort_by(...), v.sort_by_key(...) |
Mengurutkan dan mencari |
slices.Contains, slices.Index |
v.contains(&x), v.iter().position(...) |
Vektor |
strings.Builder |
String dengan push_str, atau write! |
String |
sync.Map |
Mutex<HashMap<K, V>>, RwLock, atau crate dashmap |
Shared state |
Ada dua hal yang perlu kamu tahu. Pertama, seperti map di Go, HashMap di Rust tidak punya urutan tetap, jadi urutkan dulu sebelum mencetak atau pakai BTreeMap. Kedua, kejutan terkenal dari slice Go (dua slice berbagi satu backing array, dan sebuah append mengubah slice yang lain) tidak mungkin terjadi di Rust: selama satu bagian kodemu memegang borrow &mut atas sebuah Vec, tidak ada bagian lain yang boleh melihatnya.
Tooling: dari go ke cargo
Section titled “Tooling: dari go ke cargo”Kabar baik: kamu sudah terbiasa dengan satu tool yang mengerjakan semuanya, dan Rust juga punya satu. Cargo mencakup go build, go run, go test, go mod, dan go install.
| Go | Rust | Catatan |
|---|---|---|
Perintah go |
cargo |
Mem-build, menjalankan, mengetes, menambah package, mem-publish. Lihat Cargo dan crate |
go mod init |
cargo new my_app (cargo new --lib untuk library) |
Membuat folder, Cargo.toml, src/main.rs, dan repo git |
go.mod |
Cargo.toml |
|
go.sum |
Cargo.lock |
Dibuat dan diperbarui otomatis untukmu |
go get github.com/gin-gonic/gin |
cargo add axum |
|
go get -u |
cargo update |
Memperbarui dalam rentang versi yang ada di Cargo.toml |
| pkg.go.dev | crates.io untuk package-nya, docs.rs untuk dokumentasi API-nya | |
go build / go run / go test |
cargo build / cargo run / cargo test |
Tambahkan --release untuk build yang dioptimalkan; build debug jauh lebih lambat saat dijalankan |
go vet, staticcheck, golangci-lint |
cargo clippy |
Resmi, dengan ratusan lint yang membantu |
gofmt / goimports |
cargo fmt (rustfmt) |
Satu gaya standar, persis seperti gofmt |
go doc |
cargo doc --open |
Contoh kode di doc comment dijalankan sebagai test. Lihat Dokumentasi |
go test -bench |
criterion (dijalankan dengan cargo bench) |
Lihat Performa |
go test -cover |
cargo llvm-cov atau cargo tarpaulin |
|
go test -race |
Tidak perlu untuk kode yang aman: data race adalah error compile. Miri mengecek kode unsafe |
|
go test -fuzz |
cargo fuzz |
Instal dengan cargo install cargo-fuzz |
| testify | assert! dan assert_eq! bawaan; pretty_assertions untuk diff yang lebih enak dibaca |
Lihat Testing dan Pengujian lanjutan |
| gomock / mockery | mockall, atau fake buatan sendiri yang mengimplementasikan trait-mu |
|
go install pkg@latest |
cargo install name |
|
go work (workspace) |
Cargo workspace | Satu Cargo.toml di level teratas mendaftar crate-crate anggotanya |
Baris toolchain di go.mod |
rustup dan file rust-toolchain.toml |
Lihat Tools dan setup Rust |
GOOS=linux GOARCH=arm64 go build |
rustup target add aarch64-unknown-linux-gnu + cargo build --target ... |
Sering butuh linker untuk target-nya; cross atau cargo-zigbuild mempermudahnya |
| govulncheck | cargo audit (advisory RustSec), cargo deny |
|
| gopls | rust-analyzer | Bisa dipakai di VS Code, Neovim, Helix, Zed, dan lainnya |
| GoLand | JetBrains RustRover | |
| Delve | rust-gdb / rust-lldb, atau CodeLLDB di VS Code |
|
| pprof | cargo flamegraph, perf, atau profiler native apa saja |
|
| air (live reload) | bacon atau cargo watch |
Cargo.toml kecil di samping go.mod yang sudah kamu kenal:
module example.com/orders-api
go 1.23
require github.com/gin-gonic/gin v1.10.0[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]axum = "0.8"serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }Satu perbedaan yang langsung terasa: standard library Go sudah mencakup server HTTP, JSON, template, dan kriptografi. Standard library Rust sengaja dibuat lebih kecil, jadi hal-hal seperti JSON, HTTP, async, dan angka acak datang dari crate. Menambahkan satu crate cukup dengan satu cargo add, dan segelintir crate (serde, tokio, clap, regex, rand) dipakai hampir semua orang.
Library dan framework: “Kalau begitu, pakai apa sebagai gantinya…?”
Section titled “Library dan framework: “Kalau begitu, pakai apa sebagai gantinya…?””Inilah pertanyaan praktis pertama. “Kami pakai Gin dan GORM. Di Rust ada apa?” Berikut padanan yang paling dekat. Seperti di Go, Rust biasanya punya dua atau tiga pilihan komunitas yang bagus, bukan satu jawaban resmi.
Web dan networking
Section titled “Web dan networking”| Go | Rust | Catatan |
|---|---|---|
| Gin, Echo, Chi, Fiber | axum (kita membuatnya di Proyek: Web API dengan Axum), actix-web, Rocket | axum dan actix-web banyak dipakai di production |
Server net/http |
hyper (level rendah), biasanya dipakai lewat axum |
Tidak ada server HTTP di standard library |
Middleware http.Handler |
Layer tower dan tower-http (CORS, kompresi, tracing, timeout) |
axum memakai tower secara langsung |
Client net/http |
reqwest |
Async secara default, dengan client blocking opsional. Lihat Jaringan di Rust |
| grpc-go | tonic (dengan prost untuk protobuf) |
|
| gorilla/websocket | tokio-tungstenite, atau fitur ws milik axum |
|
html/template, text/template |
askama (dicek saat compile), tera, minijinja |
|
net (TCP/UDP) |
std::net, atau tokio::net untuk async |
Lihat Jaringan di Rust |
| errgroup | tokio::task::JoinSet |
| Go | Rust | Catatan |
|---|---|---|
encoding/json + struct tag |
serde + serde_json |
Juga bisa TOML (crate toml) dan format lain. Lihat JSON dengan Serde |
database/sql + pgx |
sqlx (async, SQL biasa yang dicek terhadap database-mu saat compile), tokio-postgres |
Tidak ada hubungannya dengan jmoiron/sqlx di Go, meskipun namanya sama |
| sqlc | Makro query! dari sqlx |
Keduanya berangkat dari SQL yang kamu tulis dan memberimu hasil yang bertipe |
| GORM | SeaORM (async, gaya entity), Diesel (query builder yang dicek tipenya) | Lihat Database |
| go-sqlite3 | rusqlite |
|
| go-redis | redis |
CLI, konfigurasi, logging, dan tool sehari-hari
Section titled “CLI, konfigurasi, logging, dan tool sehari-hari”| Go | Rust | Catatan |
|---|---|---|
cobra, urfave/cli, flag |
clap (turunkan argumenmu dari sebuah struct) |
Lihat Argumen command line dan Proyek: CLI dengan clap |
| viper | config atau figment |
Gabungkan file dan environment variable berlapis-lapis, lalu deserialize dengan serde |
log/slog, zap, zerolog |
tracing + tracing-subscriber, atau log + env_logger |
Structured logging. Lihat Logging |
regexp |
regex |
Ide yang sama dengan RE2 di Go: pencocokan dalam waktu linear, tanpa backreference. Lihat Regular expression |
time |
std::time untuk durasi; chrono atau jiff untuk tanggal dan zona waktu |
Lihat Tanggal dan waktu |
embed |
include_str! / include_bytes!, atau rust-embed untuk seluruh folder |
|
| bubbletea | ratatui |
UI terminal |
| Prometheus client_golang | prometheus atau metrics |
|
sync/atomic |
std::sync::atomic |
Desktop, game, dan embedded
Section titled “Desktop, game, dan embedded”| Go | Rust | Catatan |
|---|---|---|
| Wails | Tauri (UI web + backend Rust) | Idenya sangat mirip. Lihat Aplikasi GUI |
| Fyne | egui, Iced, Slint |
Tidak ada toolkit GUI resmi |
| Ebitengine | macroquad, Bevy |
Bevy belum 1.0 dan API-nya berubah di setiap rilis. Lihat Pengembangan game |
| TinyGo | embedded-hal, Embassy |
Rust bisa berjalan tanpa OS dan tanpa heap. Lihat Rust embedded |
GOOS=js GOARCH=wasm |
wasm-bindgen, Leptos, Yew, Dioxus |
Binary kecil, tanpa GC yang ikut dikirim. Lihat Rust dan WebAssembly |
Berdampingan: struct dan method (hampir sama)
Section titled “Berdampingan: struct dan method (hampir sama)”Di sinilah developer Go merasa seperti di rumah sendiri. Ini tipe rekening kecil di Go:
type Account struct { Owner string // diekspor: huruf kapital balance int // tidak diekspor}
func NewAccount(owner string, balance int) *Account { return &Account{Owner: owner, balance: balance}}
func (a *Account) Deposit(amount int) { a.balance += amount }func (a *Account) Balance() int { return a.balance }func (a *Account) String() string { return fmt.Sprintf("%s: %d", a.Owner, a.balance) }
func main() { account := NewAccount("Ada", 100) account.Deposit(50) fmt.Println(account) fmt.Println("balance is", account.Balance())}Dan di Rust:
use std::fmt;
// `pub` mengerjakan tugas huruf kapital di Go.pub struct Account { pub owner: String, balance: i64, // private, seperti field berhuruf kecil di Go}
impl Account { // Seperti NewAccount di Go: fungsi biasa. `new` hanya kebiasaan. pub fn new(owner: &str, balance: i64) -> Self { Account { owner: owner.to_string(), balance } }
// Seperti pointer receiver: method ini mengubah rekeningnya. pub fn deposit(&mut self, amount: i64) { self.balance += amount; }
// Hanya membaca. Tidak ada salinan yang dibuat, berbeda dengan value receiver di Go. pub fn balance(&self) -> i64 { self.balance }}
// Method String() di Go (interface Stringer) menjadi Display.impl fmt::Display for Account { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}: {}", self.owner, self.balance) }}
fn main() { let mut account = Account::new("Ada", 100); account.deposit(50); println!("{account}"); println!("saldonya {}", account.balance());}Output:
Ada: 150saldonya 150Bentuknya sama: data di dalam struct, method yang menempel padanya, dan constructor yang hanyalah sebuah fungsi. Perbedaannya kecil tapi penting. Method-method berkumpul di satu blok impl. Receiver-nya menyatakan dengan tepat apa yang dilakukan method itu: &self membaca, &mut self mengubah, dan self biasa mengambil nilainya lalu menghabiskannya. Dan karena deposit mengubah rekening, variabelnya harus dideklarasikan dengan let mut. Di Go, siapa pun yang memegang *Account bisa mengubahnya; di Rust, hak untuk mengubah sesuatu terlihat di kode.
Berdampingan: interface implisit menjadi trait eksplisit
Section titled “Berdampingan: interface implisit menjadi trait eksplisit”Di Go, sebuah tipe memenuhi sebuah interface hanya dengan memiliki method yang tepat:
type Shape interface { Area() float64 Name() string}
type Circle struct{ R float64 }type Rect struct{ W, H float64 }
// Tidak ada "implements" di mana pun: punya method-nya saja sudah cukup.func (c Circle) Area() float64 { return math.Pi * c.R * c.R }func (c Circle) Name() string { return "circle" }func (r Rect) Area() float64 { return r.W * r.H }func (r Rect) Name() string { return "rect" }
func describe[T Shape](s T) { fmt.Printf("%s: %.2f\n", s.Name(), s.Area())}
func main() { describe(Circle{R: 1})
shapes := []Shape{Circle{R: 1}, Rect{W: 2, H: 3}} total := 0.0 for _, s := range shapes { total += s.Area() } fmt.Printf("total area: %.2f\n", total)}Di Rust, kamu menuliskan hubungannya dengan impl Shape for Circle:
use std::f64::consts::PI;
trait Shape { fn area(&self) -> f64; fn name(&self) -> &str;}
struct Circle { r: f64,}
struct Rect { w: f64, h: f64,}
// Baris inilah bagian yang tidak ditulis di Go.impl Shape for Circle { fn area(&self) -> f64 { PI * self.r * self.r } fn name(&self) -> &str { "lingkaran" }}
impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } fn name(&self) -> &str { "persegi panjang" }}
// Generik, seperti describe[T Shape](s T) di Go.fn describe<T: Shape>(s: &T) { println!("{}: {:.2}", s.name(), s.area());}
fn main() { describe(&Circle { r: 1.0 });
// Seperti []Shape di Go: tipe yang berbeda-beda di balik satu trait. let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { r: 1.0 }), Box::new(Rect { w: 2.0, h: 3.0 }), ]; let total: f64 = shapes.iter().map(|s| s.area()).sum(); println!("luas total: {total:.2}");}Output:
lingkaran: 3.14luas total: 9.14Bersikap eksplisit memang memakan satu baris per tipe, tapi memberimu beberapa hal yang tidak bisa dilakukan Go. Trait bisa punya method default, jadi tipe-tipe mendapat perilaku bersama secara gratis. Kamu bisa mengimplementasikan trait buatanmu untuk tipe yang bukan buatanmu, bahkan i32 atau String (Go hanya membolehkanmu menambahkan method ke tipe di package-mu sendiri). Dan describe yang generik di-compile sekali untuk setiap tipe konkret, jadi pemanggilannya tidak butuh pencarian saat runtime. Box<dyn Shape> adalah gaya interface Go: satu nilai yang menunjuk ke “suatu tipe yang mengimplementasikan Shape”, dengan method yang dicari saat runtime. Lihat Trait dan Trait object.
Berdampingan: nil menjadi Option
Section titled “Berdampingan: nil menjadi Option”Di Go, pointer yang mungkin tidak ada bernilai nil, dan kamu harus ingat untuk mengeceknya:
type User struct { Name string Manager *User // nil artinya "tidak punya manajer"}
func findUser(users []*User, name string) *User { for _, u := range users { if u.Name == name { return u } } return nil}
func main() { ada := &User{Name: "Ada"} users := []*User{ada, {Name: "Bob", Manager: ada}}
for _, name := range []string{"Bob", "Ada", "Eve"} { u := findUser(users, name) if u == nil { fmt.Println(name + ": no such user") continue } boss := "nobody" if u.Manager != nil { boss = u.Manager.Name } fmt.Println(name, "reports to", boss) }}Di Rust, “mungkin tidak ada” adalah bagian dari tipenya. Option<&User> bisa berisi Some(user) atau None, dan compiler tidak akan membiarkanmu memakai user-nya sebelum kamu menangani None:
struct User { name: String, manager: Option<String>, // Go: Manager *User, yang bisa nil}
fn find_user<'a>(users: &'a [User], name: &str) -> Option<&'a User> { users.iter().find(|u| u.name == name)}
fn main() { let users = vec![ User { name: "Ada".to_string(), manager: None }, User { name: "Bob".to_string(), manager: Some("Ada".to_string()) }, ];
for name in ["Bob", "Ada", "Eve"] { match find_user(&users, name) { Some(user) => { let boss = user.manager.as_deref().unwrap_or("tidak ada"); println!("atasan {name}: {boss}"); } None => println!("{name}: user tidak ditemukan"), } }}Output:
atasan Bob: Adaatasan Ada: tidak adaEve: user tidak ditemukanIde yang sama menggantikan pencarian comma-ok di Go. if email, ok := emails[name]; ok { ... } menjadi if let Some(email) = emails.get(name) { ... }. Satu hal yang perlu diperhatikan: di Go, emails["missing"] diam-diam memberimu zero value "". Di Rust, emails["missing"] akan panic, jadi pakai .get() setiap kali key-nya mungkin tidak ada. 'a di find_user adalah sebuah lifetime: ia memberi tahu compiler bahwa &User yang dikembalikan menunjuk ke dalam users. Kamu akan segera berkenalan dengannya.
Berdampingan: if err != nil menjadi Result dan ?
Section titled “Berdampingan: if err != nil menjadi Result dan ?”Penanganan error di Go terkenal dengan pola tiga barisnya:
func parsePort(s string) (int, error) { n, err := strconv.Atoi(s) if err != nil { return 0, fmt.Errorf("invalid port %q: %w", s, err) } if n < 1 || n > 65535 { return 0, fmt.Errorf("port %d out of range", n) } return n, nil}
func address(host, port string) (string, error) { p, err := parsePort(port) if err != nil { return "", err } return fmt.Sprintf("%s:%d", host, p), nil}
func main() { for _, port := range []string{"8080", "abc", "70000", "0"} { addr, err := address("localhost", port) if err != nil { fmt.Println("error:", err) continue } fmt.Println("listening on", addr) }}Rust mempertahankan idenya (error adalah nilai yang kamu kembalikan) tapi melipat polanya menjadi satu karakter, ?:
fn parse_port(s: &str) -> Result<u16, String> { // u16 tidak bisa menampung 70000, jadi parse() sudah menangkap "di luar rentang" untuk kita. let port: u16 = s.parse().map_err(|e| format!("port tidak valid {s:?}: {e}"))?; if port == 0 { return Err("port 0 sudah dicadangkan".to_string()); } Ok(port)}
fn address(host: &str, port: &str) -> Result<String, String> { // `?` artinya: kalau ini error, kembalikan sekarang juga. let port = parse_port(port)?; Ok(format!("{host}:{port}"))}
fn main() { for port in ["8080", "abc", "70000", "0"] { match address("localhost", port) { Ok(addr) => println!("mendengarkan di {addr}"), Err(e) => println!("error: {e}"), } }}Output:
mendengarkan di localhost:8080error: port tidak valid "abc": invalid digit found in stringerror: port tidak valid "70000": number too large to fit in target typeerror: port 0 sudah dicadangkanPesan invalid digit found in string dan number too large to fit in target type berasal dari standard library Rust, jadi tetap dalam bahasa Inggris. Artinya: ada karakter yang bukan angka, dan angkanya terlalu besar untuk tipe tujuannya.
Ada perbedaan yang lebih dalam daripada sekadar menghemat baris. Di Go, n, err := strconv.Atoi(s) memberimu kedua nilai, dan tidak ada yang mencegahmu memakai n saat err bukan nil. Result berisi nilainya atau error-nya, jadi kamu tidak bisa menyentuh nilainya tanpa menangani error-nya dulu. Dan kalau kamu mengabaikan sebuah Result sepenuhnya, compiler akan memberimu warning.
Di proyek sungguhan, error berupa String diganti dengan sesuatu yang lebih kaya. Sebuah enum error (sering ditulis dengan crate thiserror) berperan sebagai sentinel error dan errors.Is: pemanggil melakukan match pada varian-nya. Crate anyhow memberimu .context("loading config")?, yang merupakan versi Rust dari fmt.Errorf("loading config: %w", err). Lihat Error kustom dan anyhow dan thiserror.
Berdampingan: goroutine dan channel menjadi thread, mpsc, dan task
Section titled “Berdampingan: goroutine dan channel menjadi thread, mpsc, dan task”Ini fan-in klasik di Go: jalankan beberapa goroutine, kirim hasilnya lewat channel, lalu tutup channel saat semuanya selesai:
func main() { results := make(chan int) var wg sync.WaitGroup
for _, n := range []int{1, 2, 3} { wg.Add(1) go func(n int) { defer wg.Done() results <- n * n }(n) }
go func() { wg.Wait() close(results) }()
total := 0 for r := range results { total += r } fmt.Println("total:", total)}Versi Rust-nya, dengan thread dan channel dari standard library, sedikit lebih pendek:
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
for n in [1, 2, 3] { let tx = tx.clone(); // setiap thread mendapat sender-nya sendiri thread::spawn(move || { tx.send(n * n).unwrap(); }); }
// Drop sender milik kita sendiri. Saat semua sender sudah tidak ada, channel-nya // tertutup dan perulangan di bawah berakhir: tidak perlu WaitGroup. drop(tx);
let total: i32 = rx.iter().sum(); println!("total: {total}");}Output:
total: 14Beberapa hal dipetakan secara berbeda. mpsc artinya “multi-producer, single-consumer”: kamu bisa meng-clone sender-nya, tapi tidak receiver-nya (untuk channel ala Go dengan banyak pembaca, pakai crossbeam-channel). mpsc::channel() tidak punya batas, sesuatu yang tidak ada di Go; mpsc::sync_channel(n) adalah make(chan T, n) di Go, dan sync_channel(0) adalah channel tanpa buffer. Tidak ada yang memanggil close: channel tertutup saat sender terakhir di-drop. move memberi setiap thread salinan n dan tx miliknya sendiri, dan begitulah cara Rust memastikan thread tidak pernah berbagi data secara tidak aman. Lihat Channel dan Konkurensi.
thread::spawn menjalankan thread OS sungguhan, yang lebih berat daripada goroutine. Itu tidak masalah untuk segelintir worker. Untuk ribuan koneksi bersamaan, Rust memakai task async di atas runtime seperti tokio, yang jauh lebih mirip goroutine:
#[tokio::main]async fn main() { let mut handles = Vec::new();
for n in [1, 2, 3] { // Seperti `go func() { ... }()`, tapi kamu mendapat handle sebagai gantinya. handles.push(tokio::spawn(async move { n * n })); }
let mut total = 0; for handle in handles { total += handle.await.unwrap(); } println!("total: {total}");}Output:
total: 14Sejujurnya: di bagian ini Go memang lebih sederhana. Di Go, setiap fungsi boleh blocking dan runtime yang menanganinya. Di Rust, kamu menandai fungsi dengan async, memanggilnya dengan .await, dan memilih sebuah runtime. Imbalannya, compiler menolak mem-build kode di mana dua thread bisa mengubah data yang sama secara bersamaan, jadi go test -race tidak punya apa pun untuk ditemukan. Lihat Async/Await, Async lebih dalam, dan Shared state untuk Arc<Mutex<T>>, versi Rust dari struct dengan sync.Mutex di samping datanya.
Berdampingan: defer menjadi Drop
Section titled “Berdampingan: defer menjadi Drop”Di Go, kamu menjadwalkan pembersihan dengan defer, dan pemanggilan yang di-defer berjalan dalam urutan terbalik saat fungsinya selesai:
func process() { fmt.Println("open file") defer fmt.Println("close file")
fmt.Println("lock mutex") defer fmt.Println("unlock mutex")
fmt.Println("working")}
func main() { process() fmt.Println("done")}Di Rust, pembersihan adalah milik nilainya sendiri. Saat sebuah nilai keluar dari scope (cakupan), implementasi Drop-nya berjalan, dan nilai-nilai di-drop dalam urutan terbalik, persis seperti defer-mu:
struct Guard(&'static str);
impl Drop for Guard { fn drop(&mut self) { println!("{}", self.0); }}
fn process() { println!("buka file"); let _file = Guard("tutup file");
println!("kunci mutex"); let _lock = Guard("buka kunci mutex");
println!("bekerja");} // _lock di-drop di sini, lalu _file
fn main() { process(); println!("selesai");}Output:
buka filekunci mutexbekerjabuka kunci mutextutup fileselesaiDi kode sungguhan, kamu jarang menulis Drop sendiri: File menutup dirinya sendiri, MutexGuard membuka kuncinya sendiri, TcpStream memutuskan koneksinya sendiri. Tidak ada yang bisa terlupa. Ada dua perbedaan dari defer yang perlu kamu tahu. Drop berjalan di akhir blok, bukan di akhir fungsi, jadi jebakan Go berupa defer yang menumpuk di dalam perulangan tidak ada: setiap putaran perulangan membersihkan dirinya sendiri. Dan hati-hati dengan let _ = Guard(...): _ saja sama sekali tidak menyimpan nilainya, jadi nilai itu langsung di-drop. Pakai nama seperti _lock.
Hal-hal yang akan membuatmu tersandung sebagai developer Go
Section titled “Hal-hal yang akan membuatmu tersandung sebagai developer Go”Semua orang yang datang dari Go menabrak polisi tidur yang sama. Mengetahuinya lebih awal akan membantu.
-
Memberikan nilai bisa memindahkannya. Di Go, memberikan slice ke sebuah fungsi menyalin header kecil, dan kedua pihak tetap bisa memakainya. Di Rust, memberikan
Vecmemindahkan (move) nilai itu ke dalam fungsi, dan kamu tidak bisa memakainya lagi setelahnya:fn total(prices: Vec<u32>) -> u32 {prices.iter().sum()}fn main() {let prices = vec![3, 4, 5];let sum = total(prices);println!("{sum} from {} items", prices.len());}error[E0382]: borrow of moved value: `prices`--> src/main.rs:8:37|6 | let prices = vec![3, 4, 5];| ------ move occurs because `prices` has type `Vec<u32>`, which does not implement the `Copy` trait7 | let sum = total(prices);| ------ value moved here8 | println!("{sum} from {} items", prices.len());| ^^^^^^ value borrowed here after moveCompiler memberitahumu bahwa
pricessudah dipindahkan ketotaldi baris 7 (“value moved here”), lalu kamu mencoba memakainya lagi di baris 8. Solusinya adalah kebiasaan Go dalam bentuk Rust: buat fungsinya meminjam slice,fn total(prices: &[u32]), dan panggil dengantotal(&prices). Baca Ownership dulu; halaman itu menjelaskan aturan ini. E0382 membahas error ini secara rinci. -
Borrow checker. Pada satu waktu kamu boleh punya banyak pembaca (
&T) atau satu penulis (&mut T), tidak pernah keduanya. Kode Go di mana banyak struct memegang pointer ke satu struct bersama lalu mengubahnya butuh desain yang berbeda di Rust, atau smart pointer sepertiRc<RefCell<T>>atauArc<Mutex<T>>. Bersiaplah untuk beberapa minggu yang bikin frustrasi. Setelahnya jauh lebih mudah. -
Tidak ada zero value.
let x: i32;yang diikutiprintln!("{x}")adalah error compile, dan setiap field struct harus diisi saat kamu membuat struct. Kalau kamu ingin nilai default ala Go, deriveDefaultlalu tulisConfig { port: 8080, ..Default::default() }. -
Mengimplementasikan trait itu eksplisit. Punya method
areatidak otomatis menjadikan tipemu sebuahShape. Kamu butuhimpl Shape for Circle. Sisi baiknya: kamu bisa melihat setiap implementasi dengan mencarinya. -
Stringvs&str. Go punya satu tipestring. Rust punya dua:Stringmemiliki teksnya,&strmeminjamnya. Fungsi biasanya menerima&str, struct biasanya menyimpanString. Dan kamu tidak bisa mengakses string dengan indekss[0]seperti di Go; pakais.chars()ataus.as_bytes()[0]. Lihat String. -
Hal yang tidak terpakai hanya warning, bukan error. Go menolak meng-compile kalau ada import atau variabel yang tidak terpakai. Rust hanya memberi warning, dan kamu bisa memberi awalan
_pada sebuah nama untuk berkata “memang sengaja tidak dipakai”. -
Integer overflow. Go berputar diam-diam. Rust panic di build debug dan berputar di build release, jadi pilih
checked_add,wrapping_add, atausaturating_addkalau overflow mungkin terjadi. Literal integer secara default bertipei32, dan indeks bertipeusize. Lihat Integer overflow. -
Standard library yang lebih kecil. Tidak ada server HTTP, tidak ada JSON, tidak ada runtime async di
std. Kamu akan menambahkannya dengancargo add. Awalnya terasa aneh, tapi crate-crate populer itu stabil dan terdokumentasi dengan baik. -
Async butuh runtime dan menyebar. Sebuah
async fntidak melakukan apa-apa sampai di-await di dalam runtime seperti tokio, dan hanya kode async yang bisa melakukan.await. Go tidak punya pemisahan seperti itu. Mulailah dengan thread biasa; pakai async saat kamu butuh banyak koneksi bersamaan. -
Waktu compile. Go dibuat untuk di-compile dengan sangat cepat. Rust lebih lambat, terutama clean build dengan banyak crate. Pakai
cargo checkselagi kamu mengedit; ia melewati tahap pembuatan kode dan jauh lebih cepat. -
Cross-compile butuh sedikit persiapan.
GOOS=linux go buildlangsung jalan. Di Rust, kamu menambahkan target-nya dengan rustup dan sering juga butuh linker untuk target itu. Tool seperticrossmengurus semuanya untukmu.
Go atau Rust? Kapan memilih masing-masing
Section titled “Go atau Rust? Kapan memilih masing-masing”Keduanya pilihan yang bagus, dan banyak tim memakai keduanya. Ini patokan yang adil.
Pilih Go kalau kamu ingin cepat merilis network service dan tool internal, timmu menghargai kode yang bisa dibaca siapa saja setelah seminggu, build yang cepat sangat penting, atau kamu bekerja di dunia cloud-native (Kubernetes, Docker, Terraform) di mana Go adalah bahasa bersama. Standard library Go dan konkurensinya yang sederhana membuat banyak program server mudah ditulis.
Pilih Rust kalau kamu butuh latensi yang bisa diprediksi tanpa jeda garbage collection, pemakaian memori yang rendah, atau setiap tetes tenaga CPU (parser, database, game engine, video, kompresi). Rust juga lebih cocok untuk kode tanpa sistem operasi, untuk WebAssembly, dan untuk library yang ingin kamu panggil dari bahasa lain. Dan Rust sepadan saat crash akibat data race atau pengecekan nil yang terlupa harganya mahal. Discord menulis artikel terkenal tentang memindahkan satu layanan dari Go ke Rust (bahasa Inggris) karena lonjakan latensi akibat garbage collection.
Harga dari Rust adalah waktu belajar dan build yang lebih lambat. Harga dari Go adalah garbage collector dan bug yang muncul saat runtime, bukan saat compile. Harga mana yang lebih menyakitkan tergantung pada proyekmu.
Jalur belajar untuk developer Go
Section titled “Jalur belajar untuk developer Go”Kamu tidak perlu membaca situs ini berurutan. Sebagai developer Go, rute ini melewati apa yang sudah kamu tahu dan memberi waktu lebih untuk bagian Rust yang berbeda:
- Mulai dengan Rust dan Tools dan setup Rust: instal Rust, siapkan rust-analyzer, dan jalankan
cargo new. - Variabel dan Tipe data: baca sekilas saja, terutama untuk mengenal
let mutdan tipe-tipe angka. - Ownership, lalu Borrowing, lalu Slice: halaman paling penting untukmu. Jangan terburu-buru.
- String:
Stringvs&str. - Struct dan Enum, lalu Match dan Pattern matching. Enum Rust yang membawa data menggantikan banyak type switch di Go.
- Option dan Result, lalu Error kustom dan anyhow dan thiserror.
- Trait, Generik, Trait yang sering dipakai, dan Trait object.
- Closure dan Iterator: Rust jauh lebih sering memakai rantai iterator daripada Go.
- Vektor, HashMap, dan HashSet.
- Modul, Cargo dan crate, dan Testing.
- Lifetime dan Smart pointer.
- Konkurensi, Channel, Shared state, lalu Async/Await.
- JSON dengan Serde dan Logging, lalu bangun proyek Web API.
Latihan
Section titled “Latihan”Kedua latihan memberimu potongan kode Go. Porting kode itu ke Rust.
1. Porting penghitung kata
Section titled “1. Porting penghitung kata”Program Go ini menghitung kata-kata lalu mencetaknya menurut abjad. Porting kode ini ke Rust. Petunjuk: pakai HashMap<&str, u32> dan *counts.entry(word).or_insert(0) += 1 untuk counts[w]++ di Go. text.split_whitespace() sama dengan strings.Fields.
func main() { text := "the cat and the dog and the bird"
counts := map[string]int{} for _, w := range strings.Fields(text) { counts[w]++ }
keys := make([]string, 0, len(counts)) for k := range counts { keys = append(keys, k) } sort.Strings(keys)
for _, k := range keys { fmt.Printf("%s: %d\n", k, counts[k]) }}Lihat solusi
use std::collections::HashMap;
fn main() { let text = "the cat and the dog and the bird";
let mut counts: HashMap<&str, u32> = HashMap::new(); for word in text.split_whitespace() { *counts.entry(word).or_insert(0) += 1; }
let mut keys: Vec<&str> = counts.keys().copied().collect(); keys.sort();
for key in keys { println!("{key}: {}", counts[key]); }}Output:
and: 2bird: 1cat: 1dog: 1the: 3Tidak ada zero value yang bisa diandalkan di sini, jadi entry(word).or_insert(0) berkata “mulai dari 0 kalau katanya masih baru”. Mengakses dengan counts[key] aman karena setiap key berasal dari map itu sendiri. Lebih pendek lagi: pakai BTreeMap alih-alih HashMap, maka key-nya keluar sudah terurut, dan kamu bisa membuang vektor keys. Kalimatnya sengaja dibiarkan dalam bahasa Inggris, karena urutan abjad kata-katanya ikut menentukan output.
2. Porting sentinel error
Section titled “2. Porting sentinel error”Kode Go ini mengembalikan sentinel error dan mengeceknya dengan errors.Is. Porting kode ini supaya withdraw mengembalikan Result<(), WithdrawError>, di mana WithdrawError adalah enum dengan dua varian. Petunjuk: implementasikan fmt::Display untuk enum-nya supaya bisa dicetak, dan lakukan match pada varian-nya di tempat Go memakai errors.Is.
var ErrInsufficientFunds = errors.New("insufficient funds")
type Account struct{ Balance int }
func (a *Account) Withdraw(n int) error { if n <= 0 { return fmt.Errorf("invalid amount %d", n) } if n > a.Balance { return ErrInsufficientFunds } a.Balance -= n return nil}
func main() { acc := &Account{Balance: 100} for _, n := range []int{30, 0, 500} { if err := acc.Withdraw(n); err != nil { if errors.Is(err, ErrInsufficientFunds) { fmt.Println("declined:", err) } else { fmt.Println("error:", err) } continue } fmt.Println("ok, balance", acc.Balance) }}Lihat solusi
use std::fmt;
enum WithdrawError { InvalidAmount(i64), InsufficientFunds,}
impl fmt::Display for WithdrawError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { WithdrawError::InvalidAmount(n) => write!(f, "jumlah tidak valid {n}"), WithdrawError::InsufficientFunds => write!(f, "saldo tidak cukup"), } }}
struct Account { balance: i64,}
impl Account { fn withdraw(&mut self, n: i64) -> Result<(), WithdrawError> { if n <= 0 { return Err(WithdrawError::InvalidAmount(n)); } if n > self.balance { return Err(WithdrawError::InsufficientFunds); } self.balance -= n; Ok(()) }}
fn main() { let mut acc = Account { balance: 100 }; for n in [30, 0, 500] { match acc.withdraw(n) { Ok(()) => println!("ok, saldo {}", acc.balance), Err(e @ WithdrawError::InsufficientFunds) => println!("ditolak: {e}"), Err(e) => println!("error: {e}"), } }}Output:
ok, saldo 70error: jumlah tidak valid 0ditolak: saldo tidak cukupSentinel error dan error dari fmt.Errorf menjadi dua varian dari satu enum, dan errors.Is menjadi satu cabang match. e @ WithdrawError::InsufficientFunds mencocokkan varian itu sekaligus memberimu seluruh nilainya sebagai e untuk dicetak. Bonus yang menyenangkan: kalau nanti kamu menambahkan varian ketiga, compiler akan menunjukkan setiap match yang perlu diperbarui. Crate thiserror bisa menuliskan implementasi Display untukmu.
Ringkasan
Section titled “Ringkasan”- Banyak hal dari Go tetap terpakai: struct dengan method, tidak ada inheritance, error sebagai nilai, satu formatter standar, dan satu tool untuk mem-build dan mengetes.
- Perubahan besarnya: ownership menggantikan garbage collector,
Optionmenggantikannil, danResult+?menggantikanif err != nil. - Interface menjadi trait, dan mengimplementasikannya harus eksplisit (
impl Trait for Type).defermenjadiDropyang otomatis di akhir scope. - Goroutine dan channel menjadi
thread::spawn+mpscuntuk beberapa worker, atau task tokio untuk banyak task; compiler mencegah data race. - Cargo menggantikan
go build,go test, dango mod; clippy dan rustfmt menggantikango vetdangofmt. Sebagai pengganti Gin, lihat axum atau actix-web; untuk GORM, lihat SeaORM atau Diesel; untuk pgx dan sqlc, lihat sqlx; dan untuk cobra, lihat clap.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Di Go, sebuah tipe memenuhi interface hanya dengan memiliki method yang tepat. Bagaimana tipe Rust mengimplementasikan sebuah trait?
2. Apa yang dilakukan ? setelah pemanggilan yang mengembalikan Result?
3. Dengan std::sync::mpsc, kapan for msg in rx (atau rx.iter()) berhenti?
4. Tim Go-mu membangun API HTTP dengan Gin. Crate Rust mana yang paling mirip?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- The Rust Programming Language (bahasa Inggris): buku resmi Rust. Bab 4 (ownership), 9 (error), 10 (generics dan trait), dan 16 (konkurensi) yang paling penting untuk developer Go.
- Tokio tutorial (bahasa Inggris): runtime async yang dipakai sebagian besar server Rust, hal yang paling mirip dengan scheduler goroutine di Go.
- Dokumentasi axum (bahasa Inggris): framework web yang sebaiknya kamu lihat pertama kali kalau kamu datang dari Gin, Echo, atau Chi.
- Why Discord is switching from Go to Rust (bahasa Inggris): kisah nyata sebuah tim yang memindahkan layanan yang sensitif terhadap latensi dari Go ke Rust.
- Are We Web Yet? (bahasa Inggris): ringkasan dari komunitas tentang library dan framework web di Rust.