Lewati ke konten

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.

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

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.

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.

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
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: 150
saldonya 150
▶ Coba sendiri

Bentuknya 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.14
luas total: 9.14
▶ Coba sendiri

Bersikap 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.

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: Ada
atasan Ada: tidak ada
Eve: user tidak ditemukan
▶ Coba sendiri

Ide 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:8080
error: port tidak valid "abc": invalid digit found in string
error: port tidak valid "70000": number too large to fit in target type
error: port 0 sudah dicadangkan
▶ Coba sendiri

Pesan 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: 14
▶ Coba sendiri

Beberapa 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: 14
▶ Coba sendiri

Sejujurnya: 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.

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 file
kunci mutex
bekerja
buka kunci mutex
tutup file
selesai
▶ Coba sendiri

Di 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 Vec memindahkan (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` trait
    7 | let sum = total(prices);
    | ------ value moved here
    8 | println!("{sum} from {} items", prices.len());
    | ^^^^^^ value borrowed here after move

    Compiler memberitahumu bahwa prices sudah dipindahkan ke total di 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 dengan total(&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 seperti Rc<RefCell<T>> atau Arc<Mutex<T>>. Bersiaplah untuk beberapa minggu yang bikin frustrasi. Setelahnya jauh lebih mudah.

  • Tidak ada zero value. let x: i32; yang diikuti println!("{x}") adalah error compile, dan setiap field struct harus diisi saat kamu membuat struct. Kalau kamu ingin nilai default ala Go, derive Default lalu tulis Config { port: 8080, ..Default::default() }.

  • Mengimplementasikan trait itu eksplisit. Punya method area tidak otomatis menjadikan tipemu sebuah Shape. Kamu butuh impl Shape for Circle. Sisi baiknya: kamu bisa melihat setiap implementasi dengan mencarinya.

  • String vs &str. Go punya satu tipe string. Rust punya dua: String memiliki teksnya, &str meminjamnya. Fungsi biasanya menerima &str, struct biasanya menyimpan String. Dan kamu tidak bisa mengakses string dengan indeks s[0] seperti di Go; pakai s.chars() atau s.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, atau saturating_add kalau overflow mungkin terjadi. Literal integer secara default bertipe i32, dan indeks bertipe usize. Lihat Integer overflow.

  • Standard library yang lebih kecil. Tidak ada server HTTP, tidak ada JSON, tidak ada runtime async di std. Kamu akan menambahkannya dengan cargo add. Awalnya terasa aneh, tapi crate-crate populer itu stabil dan terdokumentasi dengan baik.

  • Async butuh runtime dan menyebar. Sebuah async fn tidak 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 check selagi kamu mengedit; ia melewati tahap pembuatan kode dan jauh lebih cepat.

  • Cross-compile butuh sedikit persiapan. GOOS=linux go build langsung jalan. Di Rust, kamu menambahkan target-nya dengan rustup dan sering juga butuh linker untuk target itu. Tool seperti cross mengurus semuanya untukmu.

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.

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:

  1. Mulai dengan Rust dan Tools dan setup Rust: instal Rust, siapkan rust-analyzer, dan jalankan cargo new.
  2. Variabel dan Tipe data: baca sekilas saja, terutama untuk mengenal let mut dan tipe-tipe angka.
  3. Ownership, lalu Borrowing, lalu Slice: halaman paling penting untukmu. Jangan terburu-buru.
  4. String: String vs &str.
  5. Struct dan Enum, lalu Match dan Pattern matching. Enum Rust yang membawa data menggantikan banyak type switch di Go.
  6. Option dan Result, lalu Error kustom dan anyhow dan thiserror.
  7. Trait, Generik, Trait yang sering dipakai, dan Trait object.
  8. Closure dan Iterator: Rust jauh lebih sering memakai rantai iterator daripada Go.
  9. Vektor, HashMap, dan HashSet.
  10. Modul, Cargo dan crate, dan Testing.
  11. Lifetime dan Smart pointer.
  12. Konkurensi, Channel, Shared state, lalu Async/Await.
  13. JSON dengan Serde dan Logging, lalu bangun proyek Web API.

Kedua latihan memberimu potongan kode Go. Porting kode itu ke Rust.

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: 2
bird: 1
cat: 1
dog: 1
the: 3
▶ Coba sendiri

Tidak 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.

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 70
error: jumlah tidak valid 0
ditolak: saldo tidak cukup
▶ Coba sendiri

Sentinel 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.

  • 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, Option menggantikan nil, dan Result + ? menggantikan if err != nil.
  • Interface menjadi trait, dan mengimplementasikannya harus eksplisit (impl Trait for Type). defer menjadi Drop yang otomatis di akhir scope.
  • Goroutine dan channel menjadi thread::spawn + mpsc untuk beberapa worker, atau task tokio untuk banyak task; compiler mencegah data race.
  • Cargo menggantikan go build, go test, dan go mod; clippy dan rustfmt menggantikan go vet dan gofmt. 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

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

  • 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.