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 был быстрее, а в некоторых случаях и на порядок быстрее.
Но прежде чем вы откажетесь писать все на 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
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 более сложным по своей сути?
Я думаю, что да.
Итак, вот мои рекомендации:
- Либо. Если вы создаете веб-сервис, который обрабатывает высокую нагрузку, и хотите, чтобы его можно было масштабировать как по вертикали, так и по горизонтали, любой язык вам идеально подойдет.
- Идти. Но если вы хотите писать быстрее, возможно, потому, что вам нужно написать много разных сервисов или у вас большая команда разработчиков, тогда Go - ваш предпочтительный язык. Go дает вам возможность параллелизма как первоклассного гражданина и не допускает небезопасного доступа к памяти (как и Rust), но не заставляет вас управлять всеми мельчайшими деталями. Go быстрый и мощный, но он не утомляет разработчика, вместо этого сосредоточиваясь на простоте и единообразии.
- Rust. Если, с другой стороны, необходимо выжать максимум из производительности, тогда Rust должен быть вашим выбором. Rust больше конкурирует с C++, чем с Go. Сражаясь с C++, Rust кажется таким же мощным, но с множеством приятных улучшений. Rust дает разработчикам возможность контролировать каждую деталь того, как их потоки ведут себя с остальной частью системы, как следует обрабатывать ошибки и даже время жизни их переменных!
- Rust. Rust был разработан для взаимодействия с C. Go, но он многое теряет для достижения этой цели, и это не совсем его фокус. Идти. Если требуется удобочитаемость, выбирайте Go. Слишком легко сделать ваш код сложным для понимания Rust другими.
Надеюсь, вам понравилось это читать!