Краткое руководство по Rust для разработчиков Go
Перевод | Автор оригинала: Matt Butcher
Вы писали на Go. Но вы чувствуете желание попробовать новое с помощью Rust. Это руководство, чтобы упростить этот переход.
Как соавтор «Go in Practice», я чувствовал определенное обязательство поехать. Но я готов к переменам. Rust возглавил опрос удовлетворенности в обзоре языков (скриншот выше). Я решил попробовать. Хотя go и rust часто сравнивают, это совершенно разные языки.
Исходя из прошлого опыта, в Rust есть вещи, которые кажутся очень естественными, и вещи (например, управление памятью), которые кажутся совершенно чужими. И так, как я изучаю Rust, я каталогизирую, каково это программисту Go. И вместо того, чтобы побуждать других «погрузиться в самую суть», как это сделал я (когда я пытался написать полноценный веб-сервис), я решил подойти к Rust, начав с сходства и работая над различиями.
Набор инструментов Rust
В первую очередь следует начать с набора инструментов Rust. Как и положено, Rust (основной дистрибутив) поставляется с настоящим рогом изобилия инструментов. В отличие от go, rust считает управление пакетами важным, поэтому его инструментарий на самом деле намного легче освоить, чем go.
Для начала установите Rust, как рекомендовано. Не придумывайте. Не пытайтесь найти альтернативные установщики. Инструмент rustup прост и широко используется. и, пользуясь популярным гоуизмом, это «идиоматический» способ работы с Rust. Позже вы можете пофантазировать, если захотите.
Инструмент rustup попытается внести все необходимые изменения и установить необходимые переменные среды. Если вы будете следовать инструкциям, к концу настройки вы сможете открыть новый терминал, ввести cargo и распечатать путь.
Если вы пройдете через руководства по Rust, они (как и руководства go) проведут вас по инструментам, которые вы вряд ли будете использовать непосредственно в повседневной жизни. Я пропущу это и сделаю одно смелое заявление:
В мире Rust инструмент, который вам небезразличен, - это cargo.
Вы будете использовать его для сборки, вы будете использовать его для управления зависимостями, вы будете использовать его для отладки и вы будете использовать его для выпуска. Да, вам, вероятно, когда-нибудь понадобится rustc, чтобы что-нибудь отладить. Но не сегодня.
Создаем проект
Во-первых, у Rust нет эквивалента $gopath. Программы Rust могут работать где угодно. Во-вторых, вам не нужно следовать какому-либо конкретному шаблону в именовании пути (хотя Cargo создаст вам идиоматическую структуру пакета).
Так что cd туда, где вы хотите хранить свой код, и запустите это:
$ cargo new --bin hello
created binary (application) `hello` project
Теперь у вас будет каталог с именем hello, в нем будут каталог src/и cargo.toml.
Cargo.toml будет очень похож на файл gopkg.toml от dep (что неудивительно, учитывая, что на dep сильно повлиял Cargo). Если вы привыкли к glide, dep или godep, Cargo отслеживает зависимости, подобные этим инструментам. Если вы привыкли к идиоматическому методу go get, который позволяет вам «взлетать», то Cargo.toml - это то, что избавит вас от необходимости путешествовать по аду зависимостей каждый раз, когда кто-то обновляет свой код. добро пожаловать в лучшую жизнь!
Позже мы добавим зависимости. А пока мы начнем с быстрого перевода программы go в программу rust.
привет, давай ... эээ ... Rust
Начнем с программы go с golang.org:
package main
import "fmt"
func main() {
fmt.println("hello, 世界")
}
А пока поместите это в src/main.go, затем запустите свою верную старую команду go run:
$ go run src/main.go
hello, 世界
Хорошо, давайте удалим кое-что и превратим это в программу для Rust. Сначала скопируйте программу в main.rs (который должен был быть создан для вас).
Затем сделайте следующее:
- удалить строку пакета и строку импорта
- изменить func на fn
- измените fmt.println на println!
- добавьте точку с запятой в конце строки с println!
Итак, ваша программа теперь должна выглядеть так:
fn main() {
println!("hello, 世界");
}
Помимо очевидного, из приведенного выше кода можно извлечь четыре вещи:
- rust не требует явных имен пакетов для некоторых вещей (см. мод для модулей rust).
- точки с запятой обычно требуются (тогда как на ходу они почти всегда необязательны). Подсказка: большинство моих ошибок компиляции при первом запуске - результат забытых точек с запятой.
- у Rust есть макросы, из которых println! тот, который встроен.
- на данный момент в сообществе rust все согласны с тем, что разработчики должны использовать пробелы, а не табуляции, с отступом в 4 пробела (да, есть rustfmt, и да, вы обычно запускаете его с Cargo fmt). теперь выполнить прогон cargo:
$ cargo run
compiling hello v0.1.0 (file:///users/mbutcher/code/rust/hello)
finished dev [unoptimized + debuginfo] target(s) in 2.1 secs
running `target/debug/hello`
hello, 世界
Команда Cargo run компилирует и выполняет вашу программу. Если вы посмотрите на содержимое вашего каталога hello, вы заметите, что на этапе компиляции Cargo просто добавил Cargo.lock и каталог target /.
Файл cargo.lock выполняет те же важные функции, что и gopkg.lock или glide.lock в программах go.
идиоматично отслеживать Cargo.lock в vcs только для исполняемых файлов, но опускается для библиотек.
Каталог target/ будет содержать все ваши скомпилированные полезности, организованные по типу развертывания (отладка, выпуск, ...). поэтому, если мы посмотрим в target/debug, мы увидим наш двоичный файл.
Хорошо, мы только что сделали основу. А теперь давайте углубимся еще немного.
использование библиотек, переменных и макросов печати
В конце концов, мы хотим создать программу, которая переходит от знакомой программы "Hello world" к программе, которая говорит "Доброе утро, мир!" или «добрый день, мир!», в зависимости от времени. Но сначала мы сделаем более короткий шаг.
Давайте изменим нашу программу, чтобы распечатать время. Это даст нам представление о некоторых важных аспектах Rust, таких как использование библиотек.
use std::time::systemtime;
fn main() {
let now = systemtime::now();
println!("it is now {:?}", now);
}
Если мы запустим эту программу, то получим:
$ cargo run
compiling hello v0.1.0 (file:///users/mbutcher/code/rust/hello)
finished dev [unoptimized + debuginfo] target(s) in 1.62 secs
running `target/debug/hello`
it is now systemtime { tv_sec: 1527461839, tv_nsec: 389866000 }
Хорошо, теперь давайте посмотрим, что мы обнаружили на этом примере.
Во-первых, мы хотим работать со стандартной библиотекой времени. Говоря языком Rust, мы используем использование, чтобы что-то осознать. поскольку нам нужно получить доступ к системному времени, мы вводим его в область видимости следующим образом:
use std::time::systemtime;
Технически говоря, нам не нужно использовать use, чтобы сделать библиотеку доступной для нас (другими словами, компоновщик не полагается на строки use). Поэтому мы можем опустить строку use и вызвать std::time::systemtime::new() в нашем коде. Или мы могли бы использовать std::time, а затем вызвать time::systemtime::new(). Но наиболее распространенной практикой является импорт того или иного артифакта или вещей, которые вы используете, чтобы сделать ваш код как можно более легким для чтения.
systemtime - это структура. А systemtime::now() создает новое системное время с его содержимым, равным текущему системному времени. В отличие от go, но, как и в большинстве языков, rust представляет время как время, прошедшее с эпохи unix.
Следующая полезная вещь, которую мы видим в этом примере, - это как объявить переменную: let now = // ....
в Rust переменные по умолчанию неизменяемы. Это означает, что если бы мы сейчас попытались увеличить его на две секунды, это не удаласться:
use std::time::{systemtime, duration};
fn main() {
let now = systemtime::now();
now = now + duration::from_secs(2);
println!("it is now {:?}", now);
}
Выполнение прогона cargo приведет к ошибке, в которой говорится, что нельзя дважды присвоить неизменной переменной now
.
Идиоматическое именование: модули в нижнем регистре. Конструкции camel_case. Переменные, методы и функции - это snake_case.
Чтобы изменить переменную с неизменяемой на изменяемую, мы добавляем mut между let и оператором присваивания:
use std::time::{systemtime, duration};
fn main() {
let mut now = systemtime::now();
now = now + duration::from_secs(2);
println!("it is now {:?}", now);
}
Из этого примера можно быстро почерпнуть еще две вещи, а затем мы перейдем к печати.
- мы можем увидеть, как обрабатывать длительности в Rust (используя методы duration::from_ *).
- мы видим, что оператор + может использоваться на временах, которые не являются примитивными типами. Это потому, что systemtime::add реализует трейт add
(вот так). Из этого Rust может определить, как применить системное время + продолжительность. это очень полезная функция, которой нет в go.
Трэйты похожи на заряженные интерфейсы Go. Реализация трейта в Rust дает примерно тот же эффект, что и реализация интерфейса в go. В Rust, однако, реализация трейта должна выполняться явно (impl mytrait для mytype {}).
Хорошо, мы подошли к последней интересной строке нашего принтера времени:
println!("it is now {:?}", now);
Как отмечалось ранее, println! это макрос. Он выполняет ту же роль, что и fmt.println в go, за исключением того, что он также позволяет форматировать строки (вроде как воображаемая функция fmt.printlnf).
Все аналогичные функции реализованы в виде макросов:
- print! эквивалентно fmt.printf
- eprint! эквивалентно fmt.fprintf (os.stderr, ...)
- format! эквивалентно fmt.sprintf
Форматирование в Rust немного сложнее, чем в пакете go fmt. но основы довольно просты:
- {} распечатает объект в "режиме отображения" (например, с намерением показать его пользователям).
- {:?} распечатает вещь в "режиме отладки".
- {2} напечатает третий переданный параметр.
- {last} напечатает параметр, названный последним
Вот краткий пример того, как все четыре используются вместе:
println!("{} {:?} {2} {last}", "first", "second", "third", last="fourth");
Это производит:
first "second" third fourth
В нашем коде мы использовали println! ("{:?}"), потому что структура std::time::systemtime не реализует отображение, что означает (на практике), что println! ("{}") не работает. println! ("сейчас {}", сейчас); приводит к ошибке std::time::systemtime не реализует std::fmt::display.
Но он реализует Debug
, поэтому мы можем использовать "{:?}" и увидеть представление объекта системного времени:
it is now systemtime { tv_sec: 1527473298, tv_nsec: 570487000 }
с использованием внешних библиотек
Наша цель на данный момент - создать приложение «Доброе утро/добрый вечер». Мы только что увидели, как работать с необработанным временем, так что теперь мы можем заняться созданием нашего маленького инструмента.
Я собираюсь в чем-то признаться. Пожалуйста, не злись. Я клянусь, что (а) это для вашего же блага, и (б) я действительно не понимал, когда начинал, что нам придется это сделать. но ... вот ... в отличие от пакета времени go, пакет rust std::time не имеет средства форматирования. На самом деле все немного хуже. Стандартная библиотека времени в Rust не имеет встроенной концепции единиц времени, кроме секунд и наносекунд.
Так что ... мы собираемся заняться математикой!
Просто шучу. Мы собираемся использовать библиотеку, которая дает более широкое представление о времени. Она называется chrono
.
В предыдущем примере мы использовали внутреннюю стандартную библиотеку. Теперь мы собираемся использовать внешнюю библиотеку. Это означает, что на практическом уровне нам придется сделать три вещи:
- указать Cargo, что нам нужна библиотека.
- сообщить компилятору / компоновщику, что мы используем эту внешнюю библиотеку (в main.rs).
- воспользоваться библиотекой.
Начнем с первого. Rust использует стандартную систему управления пакетами, в которой на пакеты (крэйты) можно ссылаться по имени и автоматически отслеживать по версии. В мире Rust идиоматическим (он никогда не устареет!) местом для поиска пакетов является crates.io.
Я просмотрел различные временные модули на crates.io и обнаружил, что крэйт chrono мне подходит для моих потребностей в дате/времени (разоблачение: все, что я действительно делал, это искал «time» и смотрел количество загрузок).
Первая строка страницы chrono
инструктирует меня поместить chrono = "0.4.2" в мой файл cargo.toml. Хорошо я следую указаниям.
[package]
name = "hello"
version = "0.1.0"
authors = ["matt butcher <me@example.com>"]
[dependencies]
chrono = "0.4.2"
Он не говорит делать что-либо еще. Я умею не следовать несуществующим инструкциям.
Затем пришло время добавить этот пакет в наш код, а затем провести небольшую модернизацию, чтобы наш последний пример снова заработал:
use chrono::prelude::*;
fn main() {
let now = local::now();
println!("it is now {:?}", now);
}
Обратите внимание, что мы изменили всего пару вещей:
use chrono::prelude::*
сообщает Rust о необходимости импорта всего содержимого модуля chrono, называемого prelude (удачно резюмированного как «материал, который вам обычно нужен»). На данный момент мы просто используем local, поэтому мы могли бы упростить это до chrono::prelude::local.
local::now() - это конструктор chrono
для создания нового локализованного времени (в отличие от utc::now(), который не устанавливает часовой пояс).
Что-то интересное происходит, когда мы запускаем это:
$ cargo run
updating registry `https://github.com/rust-lang/crates.io-index`
downloading chrono v0.4.2
downloading num-integer v0.1.38
downloading num-traits v0.2.4
downloading time v0.1.40
downloading libc v0.2.41
compiling num-traits v0.2.4
compiling num-integer v0.1.38
compiling libc v0.2.41
compiling time v0.1.40
compiling chrono v0.4.2
compiling hello v0.1.0 (file:///users/mbutcher/code/rust/hello)
finished dev [unoptimized + debuginfo] target(s) in 7.50 secs
running `target/debug/hello`
it is now 2018-05-27t20:35:07.581600-06:00
Напомним, что мы добавили chrono = "0.4.2" в наш cargo.toml, но больше ничего не сделали. На тот момент у нас фактически не было установленной библиотеки. Только после запуска Cargo Run система обнаружила, что нам нужна библиотека, которой не было. Сначала он получил копию индекса crates.io, нашел chrono, а затем установил его. chrono также имеет ряд зависимостей, поэтому Cargo загрузил и установил их.
Затем, наконец, он скомпилировал нашу программу и запустил ее.
Далее, вместо того, чтобы выводить время в секундах и наносекундах, наша новая версия распечатала дату rfc8601:
it is now 2018-05-27t20:35:07.581600-06:00
Это отладочное представление метки времени chrono
.
Мы почти закончили с нашей программой.
совпадение
Наш следующий проход будет иметь интересное отклонение с самого начала. У нас есть локализованная дата/время, и мы хотим определить, что сейчапас время до полудня (чтобы мы могли сказать «доброе утро, мир») или после полудня (чтобы мы могли сказать «добрый день, мир»).
Объект chrono locale реализует временную трэйт (опять же, подумайте об интерфейсе go), которая имеет метод, называемый hour12(). а hour12() возвращает два значения: логическое значение для am(ложь) или pm(истина) и 32-разрядное целое число без знака (в Rust этот тип называется u32) со значением от 1 до 12.
В Go мы бы справились с ситуацией следующим образом:
if am_pm, _ := hour12(); am_pm {
// it's pm
} else {
// it's am
}
Идиоматически в Rust немного другая. Во-первых, у if в Rust нет отдельного инициализатора. Rust следует С-подобному формату if: if condition {} else if condition {} else {}.
Чтобы мы могли обработать случай hour12 следующим образом:
use chrono::prelude::*;
fn main() {
let now = local::now();
let (is_pm, _) = now.hour12();
if is_pm {
println!("good day, world!");
} else {
println!("good morning, world!");
}
}
Здесь мы просто выполняем присваивание в одной строке, а условие - в следующих строках. Обратите внимание, что для присвоения нескольких возвращаемых значений мы создаем кортеж (is_pm, _), сообщая rust присвоить логическое значение is_pm и игнорировать час. Нам, программистам, это более или менее удобно.
Но теперь у нас есть почти повторяющиеся вызовы println!. А Rust позволяет нам присваивоить значение условно.
use chrono::prelude::*;
fn main() {
let now = local::now();
let (is_pm, _) = now.hour12();
let am_pm = if is_pm {
"day"
} else {
"morning"
};
println!("good {}, world!", am_pm);
}
Слегка изменив синтаксис нашего условного оператора, мы использовали его для присваивания. Обратите внимание, что «день» и «утро» не имеют точки с запятой, но что блок if/else
заканчивается символом };
. В основном, когда блок выполняется, возвращается последний оператор. Поэтому, когда выполняется блок if/else, присваиванию let am_pm =
возвращается либо «день», либо «утро».
Это круто и полезно. Yо мы можем сделать это более компактно без if/else
:
use chrono::prelude::*;
fn main() {
let now = local::now();
let am_pm = match now.hour12() {
(false, _) => "morning",
(_, _) => "day",
};
println!("good {}, world!", am_pm);
}
Теперь мы используем сопоставления вместо if/else
. match
структурно похож на оператор case в go. Фраза сопоставления сообщает нам, что мы пытаемся сопоставить, и каждая запись внутри сопоставления предоставляет критерии сопоставления. Но match
должен охватывать все возможные случаи.
Поэтому простое совпадение может выглядеть так:
let int = 3;
match int {
1 => {
println!("first")
},
2 => {
println!("second")
}
_ => {
println!("something else")
}
}
Мы можем установить для int разные значения, чтобы увидеть, как это ведет себя. Но, по сути, он будет соответствовать значению int первому условию, которому оно удовлетворяет. Если let int = 1
, то будет напечатано first
, 2
- напечатает second
. Любой другой номер (соответствующий _
) напечатает something else
.
Мы используем наше соответствие для присвоения am_pm, и мы сопоставляем кортеж. Сделаем это:
let am_pm = match now.hour12() {
(false, _) => "morning",
(_, _) => "day",
};
И что мы имеем в виду:
Присвоить am_pm «утро», если первое возвращаемое значение hour12() ложно. Во всех остальных случаях присвойте "день" am_pm.
Поскольку я одержим идеей сделать код компактным, есть еще один способ сделать этот код еще короче. Мы можем вернуться к нашему оператору if
, но просто проиндексировать кортеж:
use chrono::prelude::*;
fn main() {
let now = local::now();
let am_pm = if now.hour12().0 { "day" } else { "morning" };
println!("good {}, world!", am_pm);
}
Часть .0
сообщает Rust использовать первого (0-й) элемента в кортеже, который вернул hour12().
Так что наиболее идиоматично для Rust? Я просмотрел все документы, включая руководство по стилю, и пришел к выводу, что никого особенно не волнует, какое решение вы выберете. Просто выберите удобочитаемость. Это приятное чувство.
Вывод
Go и Rust часто сравнивают друг с другом, вероятно, несправедливо. На самом деле, это очень разные языки, каждый из которых создан с разными целями. Но я попытался начать с некоторых общих черт и представить основы Rust, сравнивая ее с Go.
Я хотел бы сделать последующие публикации, посвященные таким вещам, как тестирование, обработка ошибок и работа с типами и структурами данных. Но, возможно, вышеизложенного достаточно, чтобы заинтересовать вас чтением настоящей книги о Rust.
Отказ от ответственности: я полный новичок в Rust и не очень хорошо знаю всю терминологию и механику. Мои извинения.