Go или Rust: подробное сравнение и какой язык выбрать

Чем Go и Rust отличаются на практике: четырнадцать критериев, шесть различий, которые чувствуются в работе, какой язык под какую задачу, одни и те же задачи на обоих и частые ошибки.

Стек и технологии Обновлено

Коротко

Go и Rust — оба компилируемые, быстрые и безопасные по памяти, но сделаны для разного. Go ставит на простоту: маленький язык, сборщик мусора, горутины, компиляция за секунды и команда, которая становится продуктивной за недели, — выбор по умолчанию для веб-сервисов, API и облачных инструментов. Rust ставит на контроль: без сборщика мусора, владение проверяет компилятор, в безопасном коде нет гонок данных, задержки предсказуемы — выбор для системного ПО, высокопроизводительных движков, WebAssembly и мест, где на счету каждая миллисекунда и мегабайт. Для типичного бэкенда Go быстрее доведёт до продакшена; Rust окупается там, где паузы или память Go становятся проблемой.

Коротко: что выбрать

Если вы строите API, веб-сервис, бэкенд бота, обработчик очередей или облачный инструмент — берите Go. Язык осваивается быстро, код читается одинаково в любой команде, компиляция занимает секунды, а паузы сборщика мусора достаточно коротки почти для любой веб-нагрузки.

Если вы строите движок базы данных, прокси, который никогда не должен останавливаться, библиотеку для WebAssembly, прошивку или код, который выполнится миллиарды раз, — берите Rust. Платите крутым порогом входа и медленной компиляцией, а получаете максимум скорости, минимум памяти и ошибки, пойманные до запуска.

  • Веб-сервисы и API — Go
  • Системы и каждая миллисекунда — Rust
  • Оба безопасные и компилируемые

Go и Rust: подробное сравнение

Четырнадцать критериев рядом. Скорость зависит от кода, поэтому в таблице — взаимное положение, а не бенчмарки.

КритерийGoRust
Происхождение Google, 2009; версия 1.0 — в 2012-м Mozilla, 2010; версия 1.0 — в 2015-м; теперь Rust Foundation
Философия простота и один очевидный способ контроль и абстракции без накладных расходов
Память сборщик мусора с короткими паузами владение, без сборщика
Гонки данных возможны; детектор гонок находит их в тестах невозможны в безопасном коде
Параллельность горутины и каналы, встроены потоки и async с Tokio
Ошибки значения и if err != nil Result и оператор ?
Скорость высокая наивысшая, близко к C++
Задержки стабильные, с редкими короткими паузами полностью предсказуемые
Расход памяти умеренный минимальный
Компиляция секунды заметно медленнее
Порог входа низкий: недели высокий: месяцы
Экосистема облако и веб: Docker, Kubernetes, Terraform системы, WebAssembly, инструменты: ripgrep, Tauri, Rolldown
Найм проще сложнее, специалистов меньше
Лучше всего для API, веб-сервисов, обработчиков, облачных инструментов движков, прокси, WebAssembly, встраиваемых систем

6 различий, которые чувствуются в работе

  1. Первый месяц

    В Go новичок выпускает функции в первые недели; в Rust первые недели уходят на споры с проверкой заимствований.

  2. Скорость итераций

    Go компилирует сервис за секунды; большой проект на Rust после правки может собираться минуты.

  3. Где ловятся ошибки

    Rust ловит больше при компиляции — гонки, пустые значения, необработанные ошибки; Go часть оставляет тестам.

  4. Чтение чужого кода

    Код на Go везде выглядит одинаково; код на Rust зависит от того, как автор использует трейты и макросы.

  5. Под экстремальной нагрузкой

    Когда паузы сборщика мусора начинают проявляться в самых медленных запросах, Rust убирает их полностью.

  6. Выкладка

    Оба дают один бинарник без зависимостей; Go собирает под любую платформу одной переменной.

Какой язык брать под задачу

Двенадцать типичных проектов с рекомендацией и причиной.

ЗадачаБратьПочему
REST API или веб-сервис Go быстрая разработка, сильная стандартная библиотека
Бэкенд бота или обработчик очередей Go горутины и простой код
Микросервисы в команде Go легко нанимать и читать код друг друга
Облачные и DevOps-инструменты Go родина Docker и Kubernetes
Утилита командной строки любой на Go быстрее написать, на Rust — быстрее работает
Нагруженный прокси или шлюз Rust без пауз и с минимумом памяти
Движок базы или поиска Rust контроль над каждым байтом
WebAssembly в браузере Rust маленькие и быстрые модули без среды выполнения
Встраиваемые устройства Rust работает без операционной системы
Настольное приложение с веб-интерфейсом Rust Tauri: лёгкая альтернатива Electron
MVP стартапа Go скорость разработки важнее последнего процента скорости
Код со строгими требованиями к надёжности Rust компилятор доказывает больше

Одни задачи на Go и Rust: 6 примеров

Параллельная работа, ошибки и полиморфизм на обоих языках. Go 1.26 проверен gofmt и go vet, Rust 1.97 собран с предупреждениями как ошибками; вывод — в комментариях.

Параллельная работа: Go

По горутине на текст и WaitGroup, которая ждёт их все.

main.go
package main

import (
	"fmt"
	"strings"
	"sync"
)

// считаем слова в нескольких текстах сразу: по горутине на текст
func main() {
	texts := []string{"дубовый стол и дубовый стул", "сосновая полка", "ореховый стол для домашнего офиса"}
	counts := make([]int, len(texts))

	var wg sync.WaitGroup
	for i, t := range texts {
		wg.Go(func() { // Go 1.25+: WaitGroup.Go запускает горутину и учитывает её
			counts[i] = len(strings.Fields(t))
		})
	}
	wg.Wait()
	fmt.Println(counts) // [5 2 5]
}

Параллельная работа: Rust

Потоки в области видимости: каждый получает свою ячейку, и компилятор доказывает, что гонки нет.

main.rs
use std::thread;

// то же на Rust: потоки в области видимости; компилятор доказывает, что гонок данных нет
fn main() {
    let texts = ["дубовый стол и дубовый стул", "сосновая полка", "ореховый стол для домашнего офиса"];
    let mut counts = [0; 3];

    thread::scope(|s| {
        for (count, text) in counts.iter_mut().zip(texts) {
            s.spawn(move || *count = text.split_whitespace().count());
        }
    });
    println!("{counts:?}"); // [5, 2, 5]
}

Ошибки: Go

Ошибка — значение: её оборачивают контекстом и проверяют через errors.Is.

qty.go
package main

import (
	"errors"
	"fmt"
	"strconv"
)

var ErrTooBig = errors.New("слишком много товаров")

// ошибка — обычное значение: её возвращают и проверяют явно
func parseQty(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("количество %q: %w", s, err)
	}
	if n > 100 {
		return 0, ErrTooBig
	}
	return n, nil
}

func main() {
	for _, in := range []string{"3", "abc", "500"} {
		n, err := parseQty(in)
		if errors.Is(err, ErrTooBig) {
			fmt.Println("превышен лимит")
			continue
		}
		fmt.Println(n, err)
	}
	// 3 <nil>
	// 0 количество "abc": strconv.Atoi: parsing "abc": invalid syntax
	// превышен лимит
}

Ошибки: Rust

Ошибка — часть типа, ? передаёт её выше, а match обязан обработать каждый вариант.

qty.rs
use std::num::ParseIntError;

enum QtyError {
    NotANumber(ParseIntError),
    TooBig,
}

// Result: ошибка — часть типа, и компилятор не даст её проигнорировать
fn parse_qty(s: &str) -> Result<u32, QtyError> {
    let n: u32 = s.parse().map_err(QtyError::NotANumber)?;
    if n > 100 {
        return Err(QtyError::TooBig);
    }
    Ok(n)
}

fn main() {
    for input in ["3", "abc", "500"] {
        match parse_qty(input) {
            Ok(n) => println!("{n}"),
            Err(QtyError::TooBig) => println!("превышен лимит"),
            Err(QtyError::NotANumber(e)) => println!("не число: {e}"),
        }
    }
    // 3
    // не число: invalid digit found in string
    // превышен лимит
}

Полиморфизм: интерфейсы Go

Тип выполняет интерфейс, просто имея метод, — без объявлений.

shipping.go
package main

import "fmt"

// интерфейс выполняется неявно: достаточно иметь нужный метод
type Shipping interface {
	Price(kg float64) float64
}

type Courier struct{ Base float64 }
type Pickup struct{}

func (c Courier) Price(kg float64) float64 { return c.Base + 50*kg }
func (Pickup) Price(float64) float64       { return 0 }

func main() {
	options := []Shipping{Courier{Base: 300}, Pickup{}}
	for _, o := range options {
		fmt.Printf("%T: %.0f\n", o, o.Price(2))
	}
	// main.Courier: 400
	// main.Pickup: 0
}

Полиморфизм: трейты Rust

Трейт реализуется явно, а динамический вызов виден в типе — Box<dyn Shipping>.

shipping.rs
// трейт реализуется явно, и компилятор проверяет каждую реализацию
trait Shipping {
    fn price(&self, kg: f64) -> f64;
}

struct Courier {
    base: f64,
}
struct Pickup;

impl Shipping for Courier {
    fn price(&self, kg: f64) -> f64 {
        self.base + 50.0 * kg
    }
}
impl Shipping for Pickup {
    fn price(&self, _kg: f64) -> f64 {
        0.0
    }
}

fn main() {
    let options: Vec<(&str, Box<dyn Shipping>)> =
        vec![("Курьер", Box::new(Courier { base: 300.0 })), ("Самовывоз", Box::new(Pickup))];
    for (name, o) in &options {
        println!("{name}: {:.0}", o.price(2.0));
    }
    // Курьер: 400
    // Самовывоз: 0
}

Частые ошибки при выборе

  1. Rust для обычного API

    Месяцы обучения и медленные сборки ради выигрыша, которого пользователи не заметят.

  2. Go там, где паузы недопустимы

    Для жёсткого реального времени и огромных объёмов памяти сборщик становится узким местом.

  3. Выбирать по бенчмаркам

    В веб-сервисе время обычно уходит на базу и сеть, а не на язык.

  4. Забыть про команду

    Язык, который в команде некому поддерживать, — риск больше любой разницы в скорости.

  5. Переписать всё сразу

    Переносите на Rust только медленную часть; остальное может остаться на Go.

Вопросы о Go и Rust

Что быстрее: Go или Rust?

Rust обычно быстрее и расходует меньше памяти, но для веб-сервиса разница часто незаметна: время уходит на базу и сеть.

Что проще выучить?

Go — с большим отрывом: маленький язык, на который уходят недели. Rust требует месяцев из-за владения и времён жизни.

На чём писать бэкенд?

Для большинства API и сервисов — Go. Rust — для частей, где важна каждая миллисекунда и мегабайт.

Rust безопаснее Go?

Оба безопасны по памяти. Rust дополнительно исключает гонки данных при компиляции; в Go их находит детектор гонок в тестах.

Можно ли использовать Go и Rust вместе?

Да, отдельными сервисами, которые общаются по сети, — самый частый и простой способ.

Кому больше платят?

Специалисты по обоим хорошо оплачиваются; разработчиков на Rust меньше, поэтому их сложнее найти.

Есть ли в Go дженерики?

Да, с Go 1.18 (2022). В Rust дженерики и трейты были с самого начала.

Форма

Сервисы
на Go

Пишу сервисы на Go там, где важны скорость отклика и нагрузка. Если задаче действительно нужен Rust — скажу честно и помогу найти правильное решение. Расскажите о проекте — отвечу в течение рабочего дня.

Или пишите на [email protected]