よく言われているGoとRustの特徴を比較してみた(2024年版)

何番煎じぐらいの話ですが、自分用の整理も兼ねてGoとRustの特徴を比較しつつまとめてみました。

Goの特徴

Goはシンプル、セキュア、スケーラブルなシステムを構築するための言語です。

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}
  • Googleがサポートするオープンソースプログラミング言語
  • 学びやすく、チーム開発に最適
  • ビルトインの同時実行性と堅牢な標準ライブラリ
  • パートナー、コミュニティ、ツールなどの大規模なエコシステム

Rustの特徴

Rustは信頼性が高く、効率的なソフトウェアを構築できるようにする言語です。

fn main() {
    println!("Hello, World!");
}
  • パフォーマンス: ランタイム環境やガーベジコレクタを使用せず非常に高速でメモリ効率が高い
    • 実行可能バイナリにコンパイルするのでランタイム環境が必要ない
    • ガーベジコレクタを使用せずコンパイル時にメモリの使い方を決める
  • 信頼性:豊富な型システムと所有権モデルでメモリ安全性とスレッド安全性を保証し信頼性を高めている
  • 生産性:パッケージマネージャ、フォーマッター、ビルドツールなどの周辺開発ツールが備わっている

言語のシンプルさ

Goはシンプルな言語で、コードに一定の統一性をもたらします。どのGoプログラマも比較的早く新しいコードベースを理解できます。また、比較的経験の浅いプログラマーもキャッチアップを素早く行うことができまるので、採用や育成などチームも比較的拡大しやすいです。

Rustは機能が豊富な言語で、同じ解決方法を複数の方法で実現できます。また、所有権システム(値は原則1つの変数のみが保持できるルール)により、メモリ安全性やスレッドセーフを保証しています。しかし、コンパイルエラーに苦しめれがちです。

メモリ管理

メモリ管理方法には、ガベージコレクタつきの言語、明示的にメモリ管理する言語、所有権モデルの言語などあります。

  • ガベージコレクション付きの言語:ガベージコレクタがメモリ管理をしてくれるので、プログラマは簡単にメモリ安全なコーディングをすることができます。しかし、デメリットとしてパフォーマンスのオーバーヘッドやストップザワールド(プログラムの実行を一時的に停止させる状態)が発生してしまいます。
  • 明示的にメモリ管理する言語:プログラマが明示的にメモリの確保と解放のコードを呼び出します。ガベージコレクションのオーバーヘッドはなくなりますが、メモリ割り当てと解放の実装をミスすると、バグやメモリリークを起こしかねません。
  • 所有権モデルの言語:所有している変数がスコープを抜けたら、メモリは自動的に返還されます。Rustでは変数がスコープを抜けるときに特別な関数であるdrop関数が自動的に呼ばれます。

Goはガベージコレクションを使う言語です。ガベージコレクションにより、メモリ安全なプログラムを簡単に書くことができます。しかし、トレードオフとして一定のパフォーマンス上のオーバーヘッドは発生してしまいます。

Rustは所有権モデルの言語です。所有権モデルを使いコンパイル時にメモリの使い方を決めます。また、ゼロコスト抽象化を追求していおり、型や関数でコードを抽象化してもコンパイル時に解決して実行時に追加のコストがかからないようにしています。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // 所有権の移動する。これ以降はs1にはアクセスできない

    println!("{}, world!", s2);
}

同時実行性

Goはゴルーチン(スレッドの軽量版)やチャネル(ゴルーチン間でデータを安全に送受信できる方法)が言語にビルトインされており、同時実行性が高めつつも、データ競合を発生しづらくしています。

package main

import (
    "fmt"
    "time"
)

func main() {
    say := func(message string) {
        fmt.Println(message)
    }

    go say("Hello")
    go say("World")

    // メインプロセスが終了しないように、少し待機
    time.Sleep(1 * time.Second)
}

Rustでも、スレッド、チャネル、asinc/awaitなどの機能があります。また、所有権と型チェックを活用することで、多くの並列処理のエラーを、実行時ではなくコンパイラ時にチェックできるようにしています。

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    // moveを使うことで、クロージャに使用されている
    // 値vの所有権を強制的に奪わせている
    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    drop(v);

    handle.join().unwrap();
}
// Here's a vector: [1, 2, 3]

結論:どちらも良い言語で適材適所

GoとRustはどちらも良い言語です。

どちらも実行可能ファイルを生成し高速に動作します。また、関数型やオブジェクト指向に関連する機能を備えておりモダンなソフトウェアを開発に使用できます。また、ビルド、テスト、フォーマッタや依存関係ツールなど、エンジニアの生産性を高めるためのツールも公式で備えています。

GoはWebサーバーやマイクロサービスなどの同時実行性が高いアプリケーションに向いています。また、シンプルな言語なので採用や育成なども含めてスケーラブルなチームの構築に向いています。

一方、Rustは、CLIツール、ネットワーク、組み込みシステム、Webブラウザコンポーネントなど実行速度が他の考慮事項よりも優先される分野に適しています。また、所有権モデルや型システムによりデータ競合やメモリ安全性が求められるシステムにも適しています。

もちろん、言語選定は適材適所です。どんな組織で、どんなプロセスで、どんな要件の、どんなシステムを作るかいった状況によって適している言語は異なってきます。

参考サイト