コンテンツにスキップ

Go開発者のためのRust

Goを書いているなら、Rustには思った以上になじみのあるものが見つかるはずです。どちらの言語も、速いバイナリひとつにコンパイルされます。どちらにも、クラスの代わりにメソッド付きの構造体があり、継承はなく、エラーは例外ではなく値として返し、みんなが使う標準のフォーマッタがあり、ビルド、テスト、パッケージの取得をこなすコマンドラインツールがひとつあります。メソッドをいくつか持つGoの struct は、ほとんど1行ずつRustに置き換えられます。

新しく覚えることは、ほとんどがメモリと、物事を明示的に書くことに関係しています。Rustにはガベージコレクタがありません。その代わりに、すべての値には所有者がひとりだけいて、コンパイラがプログラムの実行前に所有権(ownership)と借用(borrowing)をチェックします。nil もありません(Optionを使います)。そして if err != nil は、Resultと ? 演算子になります。インターフェースはあります(トレイトと呼ばれます)が、型は自分がそれを実装していることをはっきり宣言しなければなりません。学習時間のほとんどは所有権に使うことになるでしょう。それ以外は、より厳しく、より細かいGoのように感じられるはずです。

Go開発者がRustを学ぶのはなぜでしょうか。たいていは、次のどれかが理由です。ガベージコレクションによる停止やメモリ使用量が問題になり始めたサービス、CPU中心の処理、OSなしで動くコード(組み込み)やブラウザで動くコード(WebAssembly)、あるいは単純に、データ競合やnilによるクラッシュを本番環境の前にコンパイラに捕まえてほしいから。RustとGo、C++、Pythonを全般的に比べたい場合は、Rust vs C++・Go・Pythonを見てください。このページは、もっと深く、Go開発者であるあなたに向けて書いています。

この表は、旅行の会話帳のように使ってください。いちばん右の列は、Rust側を説明しているこのサイトのページへのリンクです。

Go Rust 詳しくは
ガベージコレクタ 所有権と借用。コンパイル時にチェックされる。所有者がスコープを抜けるとメモリが解放される 所有権、借用
struct + メソッド データ用の struct + メソッド用の impl ブロック 構造体
値レシーバ func (a Account) &self(読む。コピーはしない)または self(所有権を受け取る) 構造体
ポインタレシーバ func (a *Account) &mut self(値を変更してよい) 構造体
コンストラクタ関数 NewAccount(...) 関連関数。慣習として Account::new(...) 構造体
大文字で始まる名前 = エクスポート pub キーワード モジュール
ゼロ値(var c Config) 暗黙のゼロ値はない。すべてのフィールドを設定する必要がある。#[derive(Default)] + Config::default() 構造体、よく使うトレイト
構造体の埋め込み 埋め込みはない。構造体をフィールドに入れて小さな転送用メソッドを書くか、トレイトで振る舞いを共有する トレイト、デザインパターン
interface(暗黙的に満たされる) trait(impl Trait for Type で明示的に実装する) トレイト
異なる型を入れた []Shape Vec<Box<dyn Shape>>(トレイトオブジェクト) トレイトオブジェクト
any / interface{} + 型スイッチ ケースごとにバリアントを持つ enum と match 列挙型、match
ジェネリクス func Max[T cmp.Ordered](a, b T) T トレイト境界付きのジェネリクス:fn max<T: PartialOrd>(a: T, b: T) -> T ジェネリクス、高度なジェネリクス
nil ポインタ、nil マップ、nil インターフェース Option<T>:Some(value) か None のどちらか Option
v, ok := m[k] m.get(&k) は Option を返す HashMap
(T, error) + if err != nil { return err } Result<T, E> + ? 演算子 Result
errors.New、センチネルエラー、errors.Is / errors.As match で調べられるエラー用の enum(thiserror クレートが便利) 独自のエラー型、anyhowとthiserror
fmt.Errorf("...: %w", err) anyhow の .context("...")、または map_err anyhowとthiserror
panic / recover 本物のバグには panic!。回復させることはまれ(std::panic::catch_unwind) panic
defer Drop。値がスコープを抜けると実行される(RAII) スコープ、スマートポインタ
go f() std::thread::spawn(...)、またはasyncのタスクなら tokio::spawn(...) 並行処理、async/await
chan T、make(chan T, n) std::sync::mpsc::channel()(上限なし)または sync_channel(n)。asyncのコードでは tokio::sync::mpsc チャネル
select asyncのコードなら tokio::select!、または crossbeam-channel の select! asyncをもっと深く
データの隣に置く sync.Mutex Mutex<T> がデータを包み込む。ガードがドロップされると自動でロックが外れる 共有状態
sync.WaitGroup JoinHandle::join()、または std::thread::scope 並行処理
sync.Once std::sync::OnceLock / LazyLock 標準ライブラリの便利技
context.Context ひとつで対応するものはない。Futureをドロップしてキャンセルする、tokio::time::timeout、tokio-util の CancellationToken asyncをもっと深く
func 値とクロージャ クロージャ。Fn、FnMut、FnOnce というトレイトで表される クロージャ、Fn・FnMut・FnOnce
for i, v := range s for (i, v) in s.iter().enumerate() forループ、イテレータ
ただひとつの for ループ loop、while、for ループ
関数を使ったrangeのイテレータ(iter.Seq) Iterator トレイトと、filter、map、sum、collect などのアダプタ イテレータ、独自のイテレータ
switch match(すべてのケースを網羅したかコンパイラがチェックする) match、パターンマッチング
const + iota enum(必要なら数値を明示できる) 列挙型
:= let(デフォルトで不変。変更したいときは let mut) 変数
string(バイト列。たいていはUTF-8) String(所有する)と &str(借用する)。いつでも正しいUTF-8 文字列
rune、for _, r := range s char、for c in s.chars() 文字列
[]byte Vec<u8> または &[u8] スライス
fmt.Println、%v、%+v println!、{}(Display)、{:?} と {:#?}(Debug。#[derive(Debug)] を付ける) 出力
String() string(Stringer インターフェース) Display を実装する(すると .to_string() もタダで使える) よく使うトレイト
構造体タグ `json:"user_name"` 属性:#[serde(rename = "user_name")] RustでJSONを扱う
int(x)、float64(n) の変換 数値には as、それに From/Into。失敗する可能性があるなら TryFrom 型変換、型変換(From・Into)
整数オーバーフローは黙って折り返す デバッグビルドではパニック、リリースビルドでは折り返す。checked_add、wrapping_add、saturating_add で意図を示す 整数オーバーフロー
*T と &x &T と &mut T の参照(nullにならず、値より長く生き残れない)。ヒープ上の値を所有するなら Box<T> 借用、スマートポインタ
パッケージ クレートの中のモジュール(mod)。import の代わりに use モジュール
go generate マクロと build.rs ビルドスクリプト マクロ、手続き的マクロ
unsafe パッケージ unsafe ブロックと生ポインタ unsafe Rust
cgo extern "C" とFFI(bindgen がCのヘッダーからバインディングを生成する) FFI
ビルドタグ //go:build linux #[cfg(target_os = "linux")] とCargoのフィーチャー Cargoのフィーチャーとcfg

Goでは、スライスとマップが言語に組み込まれています。Rustでは、それらは標準ライブラリにあり、ほかにもいくつかのコレクションがあります。

Go Rust 詳しくは
所有していて、appendする []T Vec<T> ベクタ
読むだけの関数に渡す []T &[T](Vec や配列から借用したスライス) スライス
[N]T [T; N] 配列
append(s, x) / append(s, other...) v.push(x) / v.extend(other) ベクタ
s[a:b] &v[a..b] スライス
len(s)、cap(s)、make([]T, 0, n) v.len()、v.capacity()、Vec::with_capacity(n) ベクタ
map[K]V HashMap<K, V> HashMap
セットとして使う map[K]struct{} や map[K]bool HashSet<T> HashSetとBTreeMap
表示する前にマップのキーを集めて並べ替える BTreeMap<K, V>(キーが並んだまま保たれる) HashMap
container/list VecDeque<T>(push_back / pop_front) VecDequeとBinaryHeap
container/heap(5つのメソッドを実装する) BinaryHeap<T>:push と pop だけ。大きいものから出る。小さいものから出したいなら Reverse で包む VecDequeとBinaryHeap
slices.Sort、sort.Slice v.sort()、v.sort_by(...)、v.sort_by_key(...) 並べ替えと検索
slices.Contains、slices.Index v.contains(&x)、v.iter().position(...) ベクタ
strings.Builder push_str を使う String、または write! 文字列
sync.Map Mutex<HashMap<K, V>>、RwLock、または dashmap クレート 共有状態

知っておきたいことが2つあります。1つ目は、Goのマップと同じく、Rustの HashMap にも決まった順序がないことです。表示する前に並べ替えるか、BTreeMap を使いましょう。2つ目は、有名なGoのスライスの落とし穴(2つのスライスがひとつの配列を共有していて、append がもう一方を変えてしまう)は、Rustでは起きようがないことです。コードのどこかが Vec を &mut で借用している間は、ほかの誰もそれを見ることができません。

うれしいお知らせです。あなたは何でもこなすツールひとつに慣れていますが、Rustにもそういうツールがあります。Cargoは、go build、go run、go test、go mod、go install の仕事をこなします。

Go Rust メモ
go コマンド cargo ビルド、実行、テスト、パッケージの追加、公開。Cargoとクレートを参照
go mod init cargo new my_app(ライブラリなら cargo new --lib) フォルダ、Cargo.toml、src/main.rs、gitリポジトリを作る
go.mod Cargo.toml
go.sum Cargo.lock 自動で作られ、更新される
go get github.com/gin-gonic/gin cargo add axum
go get -u cargo update Cargo.toml のバージョン範囲の中で更新する
pkg.go.dev パッケージはcrates.io、APIドキュメントは docs.rs
go build / go run / go test cargo build / cargo run / cargo test 最適化したビルドには --release を付ける。デバッグビルドは実行がかなり遅い
go vet、staticcheck、golangci-lint cargo clippy 役に立つリントが何百もある公式ツール
gofmt / goimports cargo fmt(rustfmt) gofmtと同じく、標準スタイルはひとつ
go doc cargo doc --open ドキュメントコメント内のコード例はテストとして実行される。Rustのドキュメントを参照
go test -bench criterion(cargo bench で実行) パフォーマンスを参照
go test -cover cargo llvm-cov または cargo tarpaulin
go test -race 安全なコードには不要。データ競合はコンパイルエラーになる。unsafe なコードはMiriがチェックする
go test -fuzz cargo fuzz cargo install cargo-fuzz でインストールする
testify 組み込みの assert! と assert_eq!。差分を見やすくするなら pretty_assertions Rustのテストとテストをもっと活用するを参照
gomock / mockery mockall、またはトレイトを実装した手書きのフェイク
go install pkg@latest cargo install name
go work(ワークスペース) Cargoワークスペース 最上位の Cargo.toml ひとつにメンバーのクレートを並べる
go.mod の toolchain 行 rustup と rust-toolchain.toml ファイル Rustのツールと環境構築を参照
GOOS=linux GOARCH=arm64 go build rustup target add aarch64-unknown-linux-gnu + cargo build --target ... ターゲット用のリンカが必要なことが多い。cross や cargo-zigbuild を使うと楽になる
govulncheck cargo audit(RustSecのアドバイザリ)、cargo deny
gopls rust-analyzer VS Code、Neovim、Helix、Zedなどで使える
GoLand JetBrains RustRover
Delve rust-gdb / rust-lldb、またはVS CodeのCodeLLDB
pprof cargo flamegraph、perf、または任意のネイティブプロファイラ
air(ライブリロード) bacon または cargo watch

おなじみの go.mod と、小さな Cargo.toml を並べてみます。

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"] }

すぐに気づく違いがひとつあります。Goの標準ライブラリには、HTTPサーバー、JSON、テンプレート、暗号処理が含まれています。Rustの標準ライブラリは意図的に小さくしてあるので、JSON、HTTP、async、乱数などはクレートから取ってきます。追加は cargo add 一発で済みますし、いくつかのクレート(serde、tokio、clap、regex、rand)はほとんど誰もが使っています。

ライブラリとフレームワーク:「代わりに何を使えばいいの?」

Section titled “ライブラリとフレームワーク:「代わりに何を使えばいいの?」”

これが最初に出てくる実際的な質問です。「うちはGinとGORMを使っているけど、Rust版は何?」というものですね。ここでは、いちばん近いものを紹介します。Goと同じく、Rustにも公式の答えがひとつあるわけではなく、良いコミュニティ製の選択肢が2〜3個あるのがふつうです。

Go Rust メモ
Gin、Echo、Chi、Fiber axum(RustとAxumで作るWeb APIで実際に作ります)、actix-web、Rocket axumとactix-webは本番環境で広く使われている
net/http のサーバー hyper(低レベル)。ふつうはaxumを通して使う 標準ライブラリにHTTPサーバーはない
http.Handler のミドルウェア tower のレイヤーと tower-http(CORS、圧縮、トレース、タイムアウト) axumはtowerを直接使っている
net/http のクライアント reqwest 標準で非同期。ブロッキング版のクライアントもオプションである。Rustのネットワークを参照
grpc-go tonic(protobufには prost)
gorilla/websocket tokio-tungstenite、またはaxumの ws フィーチャー
html/template、text/template askama(コンパイル時にチェックされる)、tera、minijinja
net(TCP/UDP) std::net、asyncなら tokio::net Rustのネットワークを参照
errgroup tokio::task::JoinSet
Go Rust メモ
encoding/json + 構造体タグ serde + serde_json TOML(toml クレート)などの形式にも対応。RustでJSONを扱うを参照
database/sql + pgx sqlx(非同期。普通のSQLをコンパイル時にデータベースと照らし合わせてチェックする)、tokio-postgres 名前は同じでも、Goのjmoiron/sqlxとは関係ない
sqlc sqlxの query! マクロ どちらも自分で書いたSQLから出発して、型の付いた結果を返す
GORM SeaORM(非同期、エンティティ中心)、Diesel(型チェックされるクエリビルダー) Rustのデータベースを参照
go-sqlite3 rusqlite
go-redis redis

CLI、設定、ログ、日常的なツール

Section titled “CLI、設定、ログ、日常的なツール”
Go Rust メモ
cobra、urfave/cli、flag clap(引数を構造体からderiveする) Rustのコマンドライン引数とRustとclapで作るコマンドラインツールを参照
viper config または figment ファイルと環境変数を重ねて、serdeでデシリアライズする
log/slog、zap、zerolog tracing + tracing-subscriber、または log + env_logger 構造化ログ。ログ出力を参照
regexp regex GoのRE2と同じ考え方で、線形時間でマッチし、後方参照はない。Rustの正規表現を参照
time 時間の長さには std::time、日付とタイムゾーンには chrono か jiff Rustの日付と時刻を参照
embed include_str! / include_bytes!、フォルダごとなら rust-embed
bubbletea ratatui ターミナルUI
Prometheusのclient_golang prometheus または metrics
sync/atomic std::sync::atomic

デスクトップ、ゲーム、組み込み

Section titled “デスクトップ、ゲーム、組み込み”
Go Rust メモ
Wails Tauri(Web UI + Rustのバックエンド) 考え方はとても近い。RustのGUIアプリ開発を参照
Fyne egui、Iced、Slint 公式のGUIツールキットはない
Ebitengine macroquad、Bevy Bevyは1.0前で、リリースのたびにAPIが変わる。Rustのゲーム開発を参照
TinyGo embedded-hal、Embassy RustはOSなし、ヒープなしでも動く。組み込みRustを参照
GOOS=js GOARCH=wasm wasm-bindgen、Leptos、Yew、Dioxus バイナリが小さく、GCを同梱する必要もない。RustとWebAssemblyを参照

並べて比較:構造体とメソッド(ほとんど同じ)

Section titled “並べて比較:構造体とメソッド(ほとんど同じ)”

ここは、Go開発者がくつろげる部分です。Goで書いた小さな口座の型です。

type Account struct {
Owner string // エクスポートされる:大文字で始まる
balance int // エクスポートされない
}
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())
}

Rustではこうなります。

use std::fmt;
// `pub` が、Goの大文字の役割をします。
pub struct Account {
pub owner: String,
balance: i64, // 非公開。Goの小文字のフィールドと同じ
}
impl Account {
// Goの NewAccount と同じく、普通の関数です。`new` はただの慣習です。
pub fn new(owner: &str, balance: i64) -> Self {
Account { owner: owner.to_string(), balance }
}
// ポインタレシーバと同じく、このメソッドは口座を変更します。
pub fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
// 読むだけです。Goの値レシーバと違って、コピーは作られません。
pub fn balance(&self) -> i64 {
self.balance
}
}
// Goの String() メソッド(Stringer インターフェース)は 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!("残高は{}です", account.balance());
}

実行結果:

Ada: 150
残高は150です
▶ 実際に試してみよう

形は同じです。データは構造体に入り、メソッドはそれにくっついていて、コンストラクタはただの関数です。違いは小さいですが、大事なものです。メソッドは impl ブロックにまとめて置きます。レシーバは、メソッドが何をするのかを正確に表します。&self は読むだけ、&mut self は変更する、ただの self は値を受け取って使い切ります。そして deposit は口座を変更するので、変数は let mut で宣言しなければなりません。Goでは *Account を持っている人なら誰でも変更できますが、Rustでは、何かを変更する権利がコードの上で目に見えるのです。

並べて比較:暗黙のインターフェースは明示的なトレイトになる

Section titled “並べて比較:暗黙のインターフェースは明示的なトレイトになる”

Goでは、正しいメソッドを持っているだけで、型はインターフェースを満たします。

type Shape interface {
Area() float64
Name() string
}
type Circle struct{ R float64 }
type Rect struct{ W, H float64 }
// どこにも "implements" はない:メソッドを持っていれば十分
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)
}

Rustでは、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,
}
// この行が、Goでは省略される部分です。
impl Shape for Circle {
fn area(&self) -> f64 {
PI * self.r * self.r
}
fn name(&self) -> &str {
"円"
}
}
impl Shape for Rect {
fn area(&self) -> f64 {
self.w * self.h
}
fn name(&self) -> &str {
"長方形"
}
}
// Goの describe[T Shape](s T) と同じく、ジェネリックです。
fn describe<T: Shape>(s: &T) {
println!("{}: {:.2}", s.name(), s.area());
}
fn main() {
describe(&Circle { r: 1.0 });
// Goの []Shape と同じく、ひとつのトレイトの裏に異なる型があります。
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!("面積の合計: {total:.2}");
}

実行結果:

円: 3.14
面積の合計: 9.14
▶ 実際に試してみよう

明示的に書くコストは型ごとに1行で、その代わりにGoではできないことがいくつか手に入ります。トレイトはデフォルトメソッドを持てるので、型は共通の振る舞いをタダで手に入れられます。自分で定義したトレイトを、自分が書いていない型、たとえ i32 や String にでも実装できます(Goでは、メソッドを追加できるのは自分のパッケージの型だけです)。そして、ジェネリックな describe は具体的な型ごとにコンパイルされるので、呼び出しに実行時の探索はありません。Box<dyn Shape> はGoのインターフェースのスタイルです。「Shape を実装した何らかの型」を指すひとつの値で、メソッドは実行時に探されます。トレイトとトレイトオブジェクトを見てください。

並べて比較:nil は Option になる

Section titled “並べて比較:nil は Option になる”

Goでは、ないかもしれないポインタは nil で、忘れずにチェックする必要があります。

type User struct {
Name string
Manager *User // nil は「上司なし」の意味
}
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)
}
}

Rustでは、「ないかもしれない」ことが型の一部になります。Option<&User> は Some(user) か None のどちらかで、None を処理するまで、コンパイラはユーザーを使わせてくれません。

struct User {
name: String,
manager: Option<String>, // Go:nil になりうる Manager *User
}
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("なし");
println!("{name}の上司: {boss}");
}
None => println!("{name}: そのようなユーザーはいません"),
}
}
}

実行結果:

Bobの上司: Ada
Adaの上司: なし
Eve: そのようなユーザーはいません
▶ 実際に試してみよう

同じ考え方が、Goのcomma-okを使った検索にも当てはまります。if email, ok := emails[name]; ok { ... } は if let Some(email) = emails.get(name) { ... } になります。ひとつ注意があります。Goでは emails["missing"] は黙ってゼロ値の "" を返しますが、Rustの emails["missing"] はパニックします。キーがないかもしれないときは、いつも .get() を使いましょう。find_user の 'a はライフタイムです。返される &User が users の中を指していることを、コンパイラに伝えています。これにはすぐに出会うことになります。

並べて比較:if err != nil は Result と ? になる

Section titled “並べて比較:if err != nil は Result と ? になる”

Goのエラー処理は、3行のパターンで有名です。

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は考え方(エラーは返す値である)をそのまま残し、パターンを ? という1文字にまとめます。

fn parse_port(s: &str) -> Result<u16, String> {
// u16 には70000が入らないので、「範囲外」は parse() が捕まえてくれます。
let port: u16 = s.parse().map_err(|e| format!("不正なポート {s:?}: {e}"))?;
if port == 0 {
return Err("ポート0は予約されています".to_string());
}
Ok(port)
}
fn address(host: &str, port: &str) -> Result<String, String> {
// `?` の意味:エラーなら、今すぐそれを返す。
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!("{addr}で待ち受け中"),
Err(e) => println!("エラー: {e}"),
}
}
}

実行結果:

localhost:8080で待ち受け中
エラー: 不正なポート "abc": invalid digit found in string
エラー: 不正なポート "70000": number too large to fit in target type
エラー: ポート0は予約されています
▶ 実際に試してみよう

invalid digit found in string と number too large to fit in target type は標準ライブラリが返す英語のメッセージで、それぞれ「文字列に数字として不正な文字が含まれています」「対象の型に収まらないほど大きな数値です」という意味です。

行数が減ること以上に、深い違いがあります。Goの n, err := strconv.Atoi(s) は両方の値を返すので、err が nil でないときに n を使ってしまうのを止めるものは何もありません。Result は値とエラーのどちらか一方なので、先にエラーを処理しないと値にはたどり着けません。そして、Result を完全に無視すると、コンパイラが警告してくれます。

実際のプロジェクトでは、String のエラーはもっと情報の多いものに置き換えます。エラー用の enum(thiserror クレートで書くことが多い)が、センチネルエラーと errors.Is の役割をします。呼び出し側はバリアントに対して match します。anyhow クレートを使うと .context("loading config")? と書けます。これがRust版の fmt.Errorf("loading config: %w", err) です。独自のエラー型とanyhowとthiserrorを見てください。

並べて比較:goroutineとチャネルは、スレッド、mpsc、タスクになる

Section titled “並べて比較:goroutineとチャネルは、スレッド、mpsc、タスクになる”

Goの典型的なファンインです。goroutineをいくつか起動し、結果をチャネルに送り、全部終わったらチャネルを閉じます。

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)
}

スレッドと標準ライブラリのチャネルを使ったRust版は、少しだけ短くなります。

use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
for n in [1, 2, 3] {
let tx = tx.clone(); // スレッドごとに専用の送信側を持たせる
thread::spawn(move || {
tx.send(n * n).unwrap();
});
}
// 自分の送信側をドロップします。送信側がすべてなくなるとチャネルが
// 閉じられ、下のループが終わります。WaitGroup は必要ありません。
drop(tx);
let total: i32 = rx.iter().sum();
println!("合計: {total}");
}

実行結果:

合計: 14
▶ 実際に試してみよう

いくつか、対応の仕方が違うものがあります。mpsc は「multi-producer, single-consumer(送信側は複数、受信側はひとつ)」の意味で、送信側はクローンできますが、受信側はできません(Goのように複数の読み手がいるチャネルなら crossbeam-channel を使います)。mpsc::channel() には上限がなく、これはGoにはないものです。mpsc::sync_channel(n) がGoの make(chan T, n) にあたり、sync_channel(0) はバッファなしのチャネルです。誰も close を呼びません。最後の送信側がドロップされたときに、チャネルは閉じられます。move はそれぞれのスレッドに専用の n のコピーと専用の tx を渡します。こうやって、Rustはスレッドが危険な形でデータを共有しないことを保証しています。チャネルと並行処理を見てください。

thread::spawn は本物のOSスレッドを起動するので、goroutineより重くなります。ワーカーが数個なら問題ありません。何千もの同時接続を扱うなら、Rustではtokioのようなランタイムの上でasyncのタスクを使います。こちらのほうがずっとgoroutineに近いものです。

#[tokio::main]
async fn main() {
let mut handles = Vec::new();
for n in [1, 2, 3] {
// `go func() { ... }()` と同じですが、ハンドルが返ってきます。
handles.push(tokio::spawn(async move { n * n }));
}
let mut total = 0;
for handle in handles {
total += handle.await.unwrap();
}
println!("合計: {total}");
}

実行結果:

合計: 14
▶ 実際に試してみよう

正直に言うと、ここはGoのほうがシンプルな部分です。Goでは、どの関数もブロックしてかまわず、ランタイムがうまく処理してくれます。Rustでは、関数に async を付け、.await で呼び出し、ランタイムを選びます。その見返りとして、2つのスレッドが同じデータを同時に変更できてしまうコードは、コンパイラがビルドを拒否してくれます。だから go test -race で見つけるものが何もないのです。async/await、asyncをもっと深く、そして Arc<Mutex<T>>(データの隣に sync.Mutex を置いた構造体のRust版)については共有状態を見てください。

並べて比較:defer は Drop になる

Section titled “並べて比較:defer は Drop になる”

Goでは defer で後片付けを予約し、予約した呼び出しは関数が戻るときに逆順で実行されます。

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")
}

Rustでは、後片付けは値そのものに属しています。値がスコープを抜けると、その Drop 実装が実行されます。そして値は、deferと同じく逆順にドロップされます。

struct Guard(&'static str);
impl Drop for Guard {
fn drop(&mut self) {
println!("{}", self.0);
}
}
fn process() {
println!("ファイルを開く");
let _file = Guard("ファイルを閉じる");
println!("ミューテックスをロックする");
let _lock = Guard("ミューテックスのロックを外す");
println!("作業中");
} // ここで _lock がドロップされ、次に _file がドロップされます
fn main() {
process();
println!("完了");
}

実行結果:

ファイルを開く
ミューテックスをロックする
作業中
ミューテックスのロックを外す
ファイルを閉じる
完了
▶ 実際に試してみよう

実際のコードで Drop を自分で書くことはめったにありません。File は自分で閉じますし、MutexGuard は自分でロックを外しますし、TcpStream は自分で接続を切ります。忘れるものが何もないのです。defer との違いで知っておきたいことが2つあります。Drop は関数ではなくブロックの終わりで実行されるので、ループの中で defer がたまっていくというGoの落とし穴は存在しません。ループの1回ごとに後片付けが行われます。そして、let _ = Guard(...) には注意してください。ただの _ は値をまったく保持しないので、値はすぐにドロップされてしまいます。代わりに _lock のような名前を付けましょう。

Go開発者がつまずきやすいところ

Section titled “Go開発者がつまずきやすいところ”

Goから来た人は、みんな同じところでつまずきます。前もって知っておくだけで、ずいぶん楽になります。

  • 値を渡すと、ムーブされることがあります。 Goでは、スライスを関数に渡すと小さなヘッダーがコピーされ、どちらの側もそのまま使い続けられます。Rustでは、Vec を渡すと関数にムーブされ、そのあとは使えなくなります。

    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

    このエラーは「ムーブ済みの値 prices を借用しようとしている」という意味です。直し方は、Goの習慣をRustの形にしたものです。関数がスライスを借用するように fn total(prices: &[u32]) として、total(&prices) と呼びます。まず所有権を読んでください。このルールを説明しています。このエラーそのものはE0382で詳しく解説しています。

  • 借用チェッカー。 同時に持てるのは、読み取り用の参照(&T)いくつか、または書き込み用の参照(&mut T)ひとつだけで、両方を同時に持つことはできません。たくさんの構造体がひとつの共有の構造体へのポインタを持って変更する、といったGoのコードには、Rustでは別の設計か、Rc<RefCell<T>> や Arc<Mutex<T>> のようなスマートポインタが必要になります。最初の数週間はイライラするかもしれませんが、そのあとはずっと楽になります。

  • ゼロ値がありません。 let x: i32; のあとに println!("{x}") と書くとコンパイルエラーになりますし、構造体を作るときはすべてのフィールドを設定しなければなりません。Goのようなデフォルト値がほしいなら、Default をderiveして Config { port: 8080, ..Default::default() } と書きます。

  • トレイトの実装は明示的です。 area メソッドを持っているだけでは、型は Shape になりません。impl Shape for Circle が必要です。その代わり、検索すればすべての実装を見つけられます。

  • String と &str。 Goの文字列型は string ひとつですが、Rustには2つあります。String はテキストを所有し、&str はテキストを借用します。関数はたいてい &str を受け取り、構造体はたいてい String を持ちます。また、Goのように s[0] で文字列にインデックスでアクセスすることはできません。s.chars() や s.as_bytes()[0] を使いましょう。Rustの文字列を見てください。

  • 使っていないものは、エラーではなく警告になります。 Goは使っていないインポートや変数があるとコンパイルを拒否します。Rustは警告するだけで、名前の先頭に _ を付ければ「わざと使っていない」と示せます。

  • 整数オーバーフロー。 Goは黙って折り返します。Rustはデバッグビルドではパニックし、リリースビルドでは折り返すので、オーバーフローの可能性があるときは checked_add、wrapping_add、saturating_add を選びましょう。整数リテラルのデフォルトは i32 で、インデックスは usize です。整数オーバーフローを見てください。

  • 標準ライブラリが小さめです。 std には、HTTPサーバーもJSONも非同期ランタイムもありません。それらは cargo add で追加します。最初は不思議に感じますが、人気のクレートは安定していて、ドキュメントもしっかりしています。

  • asyncにはランタイムが必要で、しかも広がっていきます。 async fn はtokioのようなランタイムの中でawaitされるまで何もせず、.await できるのはasyncのコードだけです。Goにはこうした区別がありません。まずは普通のスレッドから始めて、たくさんの同時接続が必要になったらasyncを使いましょう。

  • コンパイル時間。 Goはとても速くコンパイルできるように作られています。Rustのコンパイルはそれより遅く、多くのクレートを使うクリーンビルドでは特にそうです。編集中は cargo check を使いましょう。コード生成を省略するので、ずっと速く済みます。

  • クロスコンパイルには少し準備が必要です。 GOOS=linux go build ならそのまま動きます。Rustでは、rustupでターゲットを追加し、そのためのリンカも必要になることがよくあります。cross のようなツールが、それを代わりにやってくれます。

どちらも良い選択肢で、両方を使っているチームもたくさんあります。公平な目安はこんなところです。

Goを選ぶのは、 ネットワークサービスや社内ツールを素早く出したいとき、1週間で誰でも読めるようになるコードをチームが大事にしているとき、ビルドの速さがとても大切なとき、あるいはGoが共通語になっているクラウドネイティブの世界(Kubernetes、Docker、Terraform)で仕事をしているときです。Goの標準ライブラリとシンプルな並行処理のおかげで、多くのサーバープログラムが簡単に書けます。

Rustを選ぶのは、 ガベージコレクションによる停止のない予測可能なレイテンシ、少ないメモリ使用量、あるいはCPUの性能を最後まで引き出すこと(パーサー、データベース、ゲームエンジン、動画、圧縮)が必要なときです。OSなしで動くコード、WebAssembly、ほかの言語から呼び出したいライブラリにも、Rustのほうが向いています。そして、データ競合やnilチェック漏れによるクラッシュのコストが高い場合にも、Rustを選ぶ価値があります。Discordは、ガベージコレクションが原因のレイテンシの急上昇のためにあるサービスをGoからRustに移した(英語)ことについて、よく知られた記事を書いています。

Rustのコストは、学習時間と遅いビルドです。Goのコストは、ガベージコレクタと、コンパイル時ではなく実行時に表れるバグです。どちらのコストがより痛いかは、プロジェクトによって変わります。

このサイトを順番どおりに読む必要はありません。Go開発者なら、次のルートで進むと、すでに知っていることは飛ばして、Rustならではの部分に時間をかけられます。

  1. Rustを始めるとRustのツールと環境構築:Rustをインストールし、rust-analyzerを設定して、cargo new を実行します。
  2. Rustの変数とRustのデータ型:ざっと目を通しましょう。主に let mut と数値の型を覚えるためです。
  3. 所有権、次に借用、そしてスライス:あなたにとっていちばん大事なページです。じっくり時間をかけてください。
  4. Rustの文字列:String と &str の違い。
  5. 構造体と列挙型、次にmatchとパターンマッチング。データを持つRustのenumが、Goの型スイッチの多くを置き換えます。
  6. OptionとResult、次に独自のエラー型とanyhowとthiserror。
  7. トレイト、ジェネリクス、よく使うトレイト、トレイトオブジェクト。
  8. クロージャとイテレータ:Rustは、Goよりずっとイテレータのチェーンをよく使います。
  9. ベクタ、HashMap、HashSetとBTreeMap。
  10. モジュール、Cargoとクレート、テスト。
  11. ライフタイムとスマートポインタ。
  12. 並行処理、チャネル、共有状態、そしてasync/await。
  13. RustでJSONを扱うとログ出力。そのあとWeb APIのプロジェクトを作ってみましょう。

どちらの問題も、Goのコード片をRustに移植(ポーティング)します。

このGoのプログラムは単語を数え、アルファベット順に表示します。これをRustに移植してください。ヒント: Goの counts[w]++ には、HashMap<&str, u32> と *counts.entry(word).or_insert(0) += 1 を使います。text.split_whitespace() が 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])
}
}
解答を見る
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]);
}
}

実行結果:

and: 2
bird: 1
cat: 1
dog: 1
the: 3
▶ 実際に試してみよう

ここでは頼れるゼロ値がないので、entry(word).or_insert(0) で「新しい単語なら0から始める」と書きます。counts[key] でのインデックスアクセスが安全なのは、どのキーもマップから取り出したものだからです。もっと短くすることもできます。HashMap の代わりに BTreeMap を使えばキーが並んだ順に出てくるので、keys のベクタはいらなくなります。この例では、アルファベット順に並べるので、文章は英語のままにしています。

2. センチネルエラーを移植する

Section titled “2. センチネルエラーを移植する”

このGoのコードはセンチネルエラーを返し、errors.Is でそれを調べます。withdraw が Result<(), WithdrawError> を返すように移植してください。WithdrawError は、2つのバリアントを持つ enum です。ヒント: 表示できるように、enumに fmt::Display を実装しましょう。そして、Goで errors.Is を使っているところでは、バリアントに対して match します。

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)
}
}
解答を見る
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, "不正な金額です: {n}"),
WithdrawError::InsufficientFunds => write!(f, "残高が足りません"),
}
}
}
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、残高は{}です", acc.balance),
Err(e @ WithdrawError::InsufficientFunds) => println!("拒否されました: {e}"),
Err(e) => println!("エラー: {e}"),
}
}
}

実行結果:

OK、残高は70です
エラー: 不正な金額です: 0
拒否されました: 残高が足りません
▶ 実際に試してみよう

センチネルエラーと fmt.Errorf のエラーは、ひとつのenumの2つのバリアントになり、errors.Is は match のアームになります。e @ WithdrawError::InsufficientFunds はバリアントにマッチし、さらに値全体を e として受け取るので、それを表示できます。うれしいおまけもあります。あとで3つ目のバリアントを追加すると、更新が必要な match をコンパイラがすべて教えてくれるのです。Display の実装は、thiserror クレートに書いてもらうこともできます。

  • Goの多くはそのまま活かせます。メソッド付きの構造体、継承がないこと、値としてのエラー、ひとつの標準フォーマッタ、ビルドとテストのためのひとつのツールです。
  • 大きな違いは、ガベージコレクタの代わりに所有権、nil の代わりに Option、if err != nil の代わりに Result + ? を使うことです。
  • インターフェースはトレイトになり、その実装は明示的です(impl Trait for Type)。defer は、スコープの終わりで自動的に動く Drop になります。
  • goroutineとチャネルは、ワーカーが少しなら thread::spawn + mpsc に、たくさんならtokioのタスクになります。データ競合はコンパイラが防いでくれます。
  • Cargoが go build、go test、go mod の代わりになり、clippyとrustfmtが go vet と gofmt の代わりになります。Ginならaxumかactix-web、GORMならSeaORMかDiesel、pgxやsqlcならsqlx、cobraならclapを見てみましょう。

理解度チェック

1. Goでは、正しいメソッドを持っているだけで型はインターフェースを満たします。Rustの型は、どうやってトレイトを実装しますか?

2. Result を返す呼び出しのあとに付けた ? は、何をしますか?

3. std::sync::mpsc を使うとき、for msg in rx(または rx.iter())はいつ止まりますか?

4. あなたのGoチームは、GinでHTTP APIを作っています。いちばん近いRustのクレートはどれですか?

スコア: 0 / 4

  • The Rust Programming Language 日本語版:公式の本の日本語訳です。Go開発者にとって特に大事なのは、4章(所有権)、9章(エラー処理)、10章(ジェネリクスとトレイト)、16章(並行処理)です。
  • Tokio tutorial(英語):ほとんどのRustのサーバーが使っている非同期ランタイムのチュートリアルです。Goのgoroutineスケジューラにいちばん近いものです。
  • axumのドキュメント(英語):Gin、Echo、Chiから来たなら、最初に見てほしいWebフレームワークです。
  • Why Discord is switching from Go to Rust(英語):レイテンシに敏感なサービスをGoからRustに移した、実際のチームの話です。
  • Are We Web Yet?(英語):RustのWebライブラリとフレームワークをまとめたコミュニティのサイトです。