Rust vs Go: Rust или Go?

Перевод | Автор оригинала: Talha Malik

"Мне остаться или идти?" Отличная песня группы The Clash. Я слушаю это прямо сейчас, пока пишу эту статью. Песня дебютировала еще в 1982 году, очень давно. В то время я был всего лишь ребенком, который занимался новым хобби - программированием своего Atari 2600. Первая видеоигра, которую я когда-либо писал, была написана с использованием сборки 6502 для этой консоли. Компилятор для него стоил около 65 долларов, если я припоминаю, что в то время приравнивалось к стрижке около 13 газонов.

Игра была простой: используя джойстик, маневрируйте своим космическим кораблем через случайно сгенерированную прокручивающуюся пещеру. Стены пещеры были синусоидальными, прокручивались по вертикали с левой и с правой стороны экрана, и вы должны были убедиться, что ваш корабль не врезался в них. Знаю, знаю: не так уж и сложно. Но мне тогда было всего десять или одиннадцать лет.

Несмотря на «мощность» процессора, вычисление синусоидальных значений во время выполнения было для него просто невыносимо. Итак, используя свой удобный калькулятор Texas Instruments, я предварительно рассчитал несколько значений синусов, тщательно записал их на бумаге и затем ввел в качестве констант для игры. Это значительно повысило производительность игры и сделало ее пригодной для использования.

Так в чем же я? При чем здесь Rust или Go?

Сегодняшние языки намного более продвинуты, чем 6502 Assembly, что упрощает написание сложных программ. Написание этой игры заняло у меня много времени, и сегодня я мог бы сделать это намного быстрее, с меньшим количеством кода, чем тогда. Но какой язык сегодня обеспечивает это волшебное сочетание простоты и силы?

И Rust, и Go обеспечивают потрясающую производительность. Они оба компилируются в машинный код, Святой Грааль производительности. И с сегодняшней вычислительной мощностью разработчики могут делать удивительные вещи с любым из этих языков. Итак, вопрос в том, что вы должны написать: с Rust или с Go вы станете следующим большим достижением?

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

Rust или Go?

Иными словами, где же этот сладкий уголок простого кода и высочайшей производительности? И в данном случае это Rust или Go? Нет никаких аргументов: Rust быстрее Go. В приведенных выше тестах Rust был быстрее, а в некоторых случаях и на порядок быстрее.

Но прежде чем вы откажетесь писать все на Rust, примите во внимание, что Go не так уж сильно отстал от него во многих из этих тестов, и он все еще намного быстрее, чем подобные Java, C#, JavaScript, Python и т.д. Другими словами, по оси производительности Rust - это нечто среднее между Rust и Go. Теперь, если то, что вы создаете, требует максимальной производительности, тогда непременно выберите Rust. Но если вам нужна высочайшая производительность, вы будете впереди всех, выбрав любой из этих двух языков.

Итак, мы подошли к сложности кода. Здесь все может быть запутано, поскольку это может быть более субъективным, чем тесты производительности. Давайте посмотрим на простое упражнение: построим небольшой веб-сервер, который будет печатать «Hello World» при получении HTTP-запроса. Для этого в Rust это выглядит примерно так:

use std::net::{TcpStream, TcpListener};
use std::io::{Read, Write};
use std::thread;


fn handle_read(mut stream: &TcpStream) {
    let mut buf = [0u8; 4096];
    match stream.read(&mut buf) {
        Ok(_) => {
            let req_str = String::from_utf8_lossy(&buf);
            println!("{}", req_str);
            },
        Err(e) => println!("Unable to read stream: {}", e),
    }
}

fn handle_write(mut stream: TcpStream) {
    let response = b"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n<html><body>Hello world</body></html>\r\n";
    match stream.write(response) {
        Ok(n) => println!("Response sent: {} bytes", n),
        Err(e) => println!("Failed sending response: {}", e),
    }
}

fn handle_client(stream: TcpStream) {
    handle_read(&stream);
    handle_write(stream);
}

fn main() {
    let port = "8080";
    let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
    println!("Listening for connections on port {}", port);

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                thread::spawn(|| {
                    handle_client(stream)
                });
            }
            Err(e) => {
                println!("Unable to connect: {}", e);
            }
        }
    }
}

Нечто подобное в Go выглядит так:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

type handler struct{}

func (theHandler *handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	log.Printf("Received request: %s\n", request.URL)
	log.Printf("%v\n", request)
	io.WriteString(writer, "Hello world!")
}

const port = "8080"

func main() {
	server := http.Server{
		Addr:    fmt.Sprintf(":%s", port),
		Handler: &handler{},
	}

	server.ListenAndServe()
}

Теперь они не на 100% точно такие же, но достаточно близки. Разница между ними составляет ~ 20 строк кода. Rust определенно заставляет разработчика думать о большем и, следовательно, писать больше кода, чем Go.

Другой пример: рассмотрим один из наиболее сложных аспектов разработки программного обеспечения: многопоточность. Когда вы решаете что-то подобное, как вы, несомненно, делаете при создании HTTP-сервера, есть о чем подумать:

И Rust, и Go справляются с этими препятствиями действительно эффективно, но Go требует меньше усилий. С Rust у вас гораздо больше возможностей и, следовательно, больше возможностей при создании потоков. Просто взгляните на некоторую документацию по этому поводу. Вот только один способ создать поток в Rust:

use std::thread;

let handler = thread::spawn(|| {
    // thread code
});

handler.join().unwrap();

С другой стороны, вот как создать что-то подобное с помощью Go:

go someFunction(args)

Еще одна важная часть написания кода - это обработка ошибок. Здесь я думаю, что Rust и Go очень похожи. Rust позволяет разработчику обрабатывать случаи ошибок за счет использования возвращаемых типов перечисления: Option и Result<T, E>. Option вернет None или Some(T), тогда как Result<T, E> вернет Ok(T) или Err(T). Учитывая, что большинство собственных библиотек Rust, а также других сторонних библиотек возвращают один из этих типов, разработчику обычно приходится обрабатывать случай, когда ничего не возвращается или возвращается ошибка.

fn foo_divide(a: f32, b: f32) -> Result<f32, &'static str> {
    if b == 0.0 {
        Err("divide by zero error!")
    } else {
        Ok(a / b)
    }
}

fn main() {
    match foo_divide(5.0, 4.0) {
        Err(err) => println!("{}", err),
        Ok(result) => println!("5 / 4 = {}", result),
    }
}

Обратите внимание, что случай Err должен обрабатываться в операторе match.

Go, с другой стороны, оставляет это на усмотрение разработчика, поскольку ошибки можно игнорировать, используя _. Однако идиоматический Go настоятельно рекомендует возвращать ошибку, тем более что функции в Go могут возвращать несколько значений. Следовательно, легко заставить функции возвращать предполагаемое значение вместе с ошибкой, если таковая имеется.

Вот соответствующий пример сверху, сделанный в Go:

func fooDivide(a float32, b float32) (float32, error) {
    if b == 0 {
        return 0, errors.New("divide by zero error!")
    }    return a / b, nil
}

func main() {
    result, err := fooDivide(5, 4)
    if err != nil {
       log.Printf("an error occurred: %v", err)
    } else {
       log.Printf("The answer is: 5 / 4 = %f", result)
    }
}

Обратите внимание, что эта строка:

result, err := fooDivide(5, 4)

можно было бы написать как

result, _ := fooDivide(5, 4)

В последнем случае возвращенная ошибка была бы проигнорирована.

Честно говоря, они оба очень похожи, за исключением принудительной проверки ошибок в Rust. В остальном разница небольшая, и трудно найти преимущество одного перед другим. На мой взгляд, это ничья.

Я мог бы продолжить, углубляясь в другие языковые различия. Но в итоге, от потоков до каналов и обобщений, Rust предоставляет разработчику больше возможностей. В этом отношении Rust ближе к C++, чем Go. Делает ли это Rust более сложным по своей сути?

Я думаю, что да.

Итак, вот мои рекомендации:

Надеюсь, вам понравилось это читать!