Какое-то время назад я видел в небезызвестном блоге пост о том, как реализовать на Go простую программу, работающую с БД, а затем сделать на её базе целый REST-сервис. Я решил проверить, насколько сложно сделать аналогичную программу на Rust и поделиться результатами.
Мы начнём с работы с БД и создадим обычное консольное приложение, а затем добавим, так сказать, REST-фронтенд.
Несколько вступительных замечаний.
Для нетерпеливых — вот законченный проект на GitHub. Он включает в себя и реализацию REST-сервиса. Всех остальных же приглашаю читать дальше.
В целом, я постараюсь подробно проиллюстрировать процесс разработки со всеми ошибками, касающимися Rust, их причинами и способами устранения. Я думаю, знание типичных проблем и способов их решения сильно помогает новичкам в языке. Не бойтесь компилятора, он — ваш друг.
Вам понадобится установленный Rust (как установить). Должна работать любая версия после 1.0 — как stable, так и nightly. Я пробовал несколько в интервале 1.1-1.3.
Сам код здесь прототипного качества — я сейчас не пытаюсь сделать очень надёжную или читабельную программу. Разобравшись в нём, можно будет задуматься о правильности и стиле позже. Однако, и написана эта версия была весьма быстро.
Теперь к делу.
Как и любой проект на Rust, не требующий особых хитростей со сборкой, наша программа будет использовать Cargo. Создадим новый проект:
$ cargo new --bin rust-phonebook $ cd rust-phonebook
Cargo заботливо создаёт в директории репозиторий Git.
$ git status On branch master Initial commit Untracked files: (use "git add <file>..." to include in what will be committed) .gitignore Cargo.toml src/ nothing added to commit but untracked files present (use "git add" to track)
И мы можем сразу собрать и запустить нашу программу-заглушку:
$ cargo run Compiling rust-phonebook v0.1.0 (file:///home/mkpankov/rust-phonebook) Running `target/debug/rust-phonebook` Hello, world!
После чего закоммитим наши изменения в репозиторий и перейдём к сути нашей программы.
Начнём с простейшего прототипа, который подключается к базе, создаёт одну таблицу, добавляет туда одну запись и считывает её обратно.
Сначала я приведу весь код целиком, а затем объясню каждую его часть. Ниже — содержимое src/main.rs.
extern crate postgres;
use postgres::{Connection, SslMode};
struct Person {
id: i32,
name: String,
data: Option<Vec<u8>>
}
fn main() {
let conn =
Connection::connect(
"postgres://postgres:postgres@localhost",
&SslMode::None)
.unwrap();
conn.execute(
"CREATE TABLE person (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
data BYTEA
)",
&[])
.unwrap();
let me = Person {
id: 0,
name: "Михаил".to_string(),
data: None
};
conn.execute(
"INSERT INTO person (name, data) VALUES ($1, $2)",
&[&me.name, &me.data])
.unwrap();
let stmt = conn.prepare("SELECT id, name, data FROM person").unwrap();
for row in stmt.query(&[]).unwrap() {
let person = Person {
id: row.get(0),
name: row.get(1),
data: row.get(2)
};
println!("Нашли человека: {}", person.name);
}
}
Давайте разберём все по порядку.
fn main() {
let conn =
Connection::connect(
"postgres://postgres:postgres@localhost",
&SslMode::None)
.unwrap();
Первая строка в нашем новом main — подключение к БД. Тут стоит сразу рассказать подробнее.
Мы предполагаем, что локально запущен сервер PostgreSQL на порту по умолчанию, а имя пользователя и пароль — «postgres». Для этого нам, конечно, нужно установить PostgreSQL. Можно посмотреть, например, это руководство. Укажите ваше имя пользователя, имеющего доступ к базе, и его пароль вместо «postgres:postgres».
Помимо этого, не забудьте инициализировать базу данных.
Сам вышеупомянутый Connection — тип из контейнера postgres (документация). Поэтому мы запрашиваем его связывание вверху файла
extern crate postgres;
и вводим в область видимости Connection и SslMode
use postgres::{Connection, SslMode};
Если попробовать собрать программу прямо сейчас, вы получите другую ошибку:
$ cargo build Compiling rust-phonebook v0.1.0 (file:///home/mkpankov/rust-phonebook.finished) src/main.rs:1:1: 1:23 error: can't find crate for `postgres` src/main.rs:1 extern crate postgres; ^~~~~~~~~~~~~~~~~~~~~~ error: aborting due to previous error Could not compile `rust-phonebook`. To learn more, run the command again with --verbose.
Это означает, что компилятор не нашёл подходящий контейнер. Это потому, что мы не указали его в зависимостях нашего проекта. Давайте сделаем это в Cargo.toml (подробнее):
[dependencies] postgres = "0.9"
Теперь всё должно собираться. Но если вы не запустили сервер, то получите такую ошибку:
$ cargo run Running `target/debug/rust-phonebook` thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: IoError(Error { repr: Os { code: 111, message: "Connection refused" } })', ../src/libcore/result.rs:732
Это непосредственный результат нашего .unwrap() — он вызывает панику текущего потока, если Result был не Ok(_) — т.е. произошла ошибка соединения.
Кстати, backtrace для неё можно увидеть, если запустить программу с выставленным RUST_BACKTRACE=1 в окружении (работает только в отладочной версии программы!).
$ RUST_BACKTRACE=1 cargo run Running `target/debug/rust-phonebook` thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: IoError(Error { repr: Os { code: 111, message: "Connection refused" } })', ../src/libcore/result.rs:732 stack backtrace: 1: 0x56007b30a95e - sys::backtrace::write::haf6e4e635ac76143Ivs 2: 0x56007b30df06 - panicking::on_panic::ha085a58a08f78856lzx 3: 0x56007b3049ae - rt::unwind::begin_unwind_inner::hc90ee27246f12475C0w 4: 0x56007b304ee6 - rt::unwind::begin_unwind_fmt::ha4be06289e0df3dbIZw 5: 0x56007b30d8d6 - rust_begin_unwind 6: 0x56007b3390c4 - panicking::panic_fmt::he7875691f9cbe589SgC 7: 0x56007b25e58d - result::Result<T, E>::unwrap::h10659124002062427088 at ../src/libcore/macros.rs:28 8: 0x56007b25dcfd - main::h2f2e9aa4b99bad67saa at src/main.rs:13 9: 0x56007b30d82d - __rust_try 10: 0x56007b30fbca - rt::lang_start::hefba4015e797c325hux 11: 0x56007b27d1ab - main 12: 0x7fb3f21076ff - __libc_start_main 13: 0x56007b25db48 - _start 14: 0x0 - <unknown>
Фух, всего одна строчка, а столько способов накосячить! Надеюсь, вы не сильно напуганы и готовы продолжать.
Положительным моментом здесь является то, что мы явно говорим, что хотим уронить программу при ошибке соединения. Когда мы захотим сделать из нашей игрушки нормальный продукт, простой текстовый поиск по .unwrap() покажет, с чего стоит начать. Дальше я не буду останавливаться на этом моменте.
Создаём таблицу:
conn.execute(
"CREATE TABLE person (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
data BYTEA
)",
&[])
.unwrap();
Странное &[] в конце — это пустой срез. У данного запроса нет параметров, поэтому мы не передаём их.
Почему срез, а не массив? Хороший стиль в Rust — не принимать владение, если объекты нужны только для чтения. Иначе нам пришлось бы клонировать значение для передачи в функцию, т.е. она «поглотила» бы его. Подробнее о владении читайте тут.
Далее мы создаём структуру, представляющую собой нашу запись, которую мы будем добавлять в таблицу:
let me = Person {
id: 0,
name: "Михаил".to_string(),
data: None
};
Принципиально, сейчас смысла складывать эти данные в структуру нет, но дальше это нам поможет. Кстати, вот её объявление:
struct Person {
id: i32,
name: String,
data: Option<Vec<u8>>
}
Теперь выполним собственно вставку:
conn.execute(
"INSERT INTO person (name, data) VALUES ($1, $2)",
&[&me.name, &me.data])
.unwrap();
Здесь уже у нашего запроса есть параметры. Они подставляются с помощью строковой интерполяции в нумерованные поля $1, $2 и т.д. И теперь наш срез параметров не пуст — он содержит ссылки на соответствующие поля структуры.
Далее мы подготавливаем запрос к базе, чтобы прочитать то, что записали:
let stmt = conn.prepare("SELECT id, name, data FROM person").unwrap();
Думаю, ничего интересного. Это просто создание объекта-запроса. Повторяющиеся запросы имеет смысл не пересоздавать, а хранить, для увеличения производительности. Мы также могли бы сразу выполнить запрос без создания «приготовленного объекта».
В конце мы выполняем сам запрос. Пройдёмся по каждой строчке:
for row in stmt.query(&[]).unwrap() {
Здесь мы обходим массив результатов запроса. Как всегда, запрос мог бы завершиться ошибкой. Список параметров снова пуст — &[].
Теперь снова собираем структуру из результатов запроса.
let person = Person {
id: row.get(0),
name: row.get(1),
data: row.get(2)
};
Здесь мы просто берём поля по номерам, но вообще библиотека позволяет использовать и имена полей таблицы.
Наконец, печатаем сообщение с результатом:
println!("Нашли человека: {}", person.name);
}
}
Пост получился длинным, поскольку мы знакомились с инфраструктурой, терминологией и настраивали окружение, но, надеюсь, будет полезен в качестве иллюстрации рабочего процесса.
В следующей части мы добавим конфигурацию сервера в INI-файле. Оставайтесь с нами!
Комментарии (26)
vedenin1980
23.09.2015 17:07Я не совсем понял: Rust сумеет при этом коде защитить от SQL injection или выполнит все что придет из запросов?
mkpankov
23.09.2015 17:09Вы про законченную версию из репозитория? Потому что здесь-то запросы к базе статические и защищать не от чего.
vedenin1980
23.09.2015 17:25Да, вы пробовали отправлять sql injection в запросах законченной версии? Они нормально отсекались?
mkpankov
23.09.2015 18:03+3Это делает не Rust как таковой, а библиотека postgres, но да, он типобезопасен:
Заголовок спойлера$ RUST_BACKTRACE=1 cargo run -- del '-1 OR 1=1' Running `target/debug/rust-phonebook del -1\ OR\ 1=1` thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', ../src/libcore/result.rs:736 stack backtrace: 1: 0x555c75a62f49 - sys::backtrace::tracing::imp::write::ha2610c4d9f39e7d20qt 2: 0x555c75a62128 - panicking::on_panic::h0137eb721b989e66zqx 3: 0x555c75a551fe - sys_common::unwind::begin_unwind_inner::h9323ab6063ed22d03es 4: 0x555c75a55998 - sys_common::unwind::begin_unwind_fmt::hd6d1e3432b74d1dc9ds 5: 0x555c75a619a1 - rust_begin_unwind 6: 0x555c75a91aff - panicking::panic_fmt::h3b414ef7ca9e7ec0wZK 7: 0x555c7586bff6 - result::Result<T, E>::unwrap::h17631234665356226696 at ../src/libcore/macros.rs:28 8: 0x555c7586ae97 - main::closure.11714 at src/main.rs:85 9: 0x555c7586ae3c - ops::impls::_&'a mut F.FnOnce<A>::call_once::h16646540071590734845 at ../src/libcore/ops.rs:1795 10: 0x555c7586ade2 - option::Option<T>::map::h18348351940653665552 at ../src/libcore/option.rs:425 11: 0x555c7586ad9c - iter::Map<I, F>.Iterator::next::h6843473521919548427 at ../src/libcore/iter.rs:1686 12: 0x555c7586aadc - vec::Vec<T>.FromIterator<T>::from_iter::h16476383075473108179 at ../src/libcollections/vec.rs:1160 13: 0x555c7586aa79 - iter::Iterator::collect::h14488226810590600205 at ../src/libcore/iter.rs:567 14: 0x555c758627f7 - main::h89fbfb3db45c0143MAa at src/main.rs:84 15: 0x555c75a64b94 - sys_common::unwind::try::try_fn::h5089277237340746579 16: 0x555c75a61808 - __rust_try 17: 0x555c75a6482d - rt::lang_start::h377d03c3ab92e2f1kox 18: 0x555c758b9609 - main 19: 0x7ffb915336ff - __libc_start_main 20: 0x555c7581eaf8 - _start 21: 0x0 - <unknown>
VolCh
24.09.2015 07:39+1let me = Person { id: 0, name: "Михаил".to_string(), data: None };
для id нельзя задать None?
А «Михаил» не строка?!splav_asv
24.09.2015 10:39+1А «Михаил» не строка?!
Строковые константы имеют тип &'static str, т.е. являются string slice, а в структуре у поля тип String — heap-allocated string, изменяемая строка. Почему выбран именно он — отдельный вопрос, потому как для &'a str тоже есть трейт ToSql и в execute можно напрямую передавать &'static str.mkpankov
24.09.2015 12:07String выбран, чтобы не возиться с временами жизни в прототипе и не пугать лишний раз новичков.
stepik777
24.09.2015 11:48для id нельзя задать None?
id объявлен в структуре как i32, это обычное 32-битное знаковое число, у которого нет такого значения как None. Чтобы было None, нужно его объявить как Option<i32>, тогда возможными значениями были бы None, Some(0) (но не 0), Some(1), Some(-5), Some(99845) и т.д.
А "Михаил" не строка?!
Разница примерно как между const char* и std::string в С++, там можно написать std::string("Михаил"), только С++ может сам добавить неявный вызов этого конструктора. В Rust "Михаил" имеет тип &str, он содержит указатель на начало utf-8 строки и её длину, to_string() преобразует его в String.
mkpankov
24.09.2015 12:06-1Уже написали несколько объяснений, но я попробую ещё раз, потому что про строки любят спрашивать :)
«Михаил» — это строковый литерал, это не «объект». Его нельзя изменять, например. Это «unboxed» значение, и to_string превращает его в boxed значение на куче.
youROCK
Выглядит вполне ничего, за исключением постоянного unwrap(). Впрочем, в go есть такое же в виде if err != nil. Ещё обидно, что стек-трейсы можно увидеть только в отладочной версии программы (в go они будут видны всегда).
mkpankov
Насчёт .unwrap() — в этом-то и разница. В go ошибку можно не обрабатывать и это будет невидимо в исходном коде — так же, как и в Си. В Rust не проверенная ошибка (т.е. например Result, который не используется) вызывает предупреждение компилятора, а простейший способ её не обрабатывать — .unwrap() — помечает данное место как явно не обрабатывающее ошибки.
Представьте себе, что вам нужно в проекте на go из 10000 строк найти все места, где ошибки не обрабатываются, и оценить, сколько временеи нужно будет на приделывание правильной обработки. Вам придётся делать это, вручную вычитывая код. В Rust достаточно текстового поиска.
Antontzi
В го можно искать по коду "_".
mkpankov
А оно используется в go только в этом контексте?
alexeyknyshev
Обычно да
mirrr
Go линтеры, вроде errcheck отлично справляются с этой задачей
mkpankov
А как они работают?
mirrr
Анализирует код проекта и выдает список проблем с указанием файлов, номеров строк. В моем случае подсвечивает строки в редакторе (sublime), в которых есть необработанные ошибки.
mkpankov
Я имею ввиду, как анализируют. Знают, какие функции ошибки возвращают, или действительно, достаточно искать _ как выше предлагают?
mirrr
Ну не я разработчик линтера.
Вообще выявляет как при использовании методов из пакетов, подключенных с гитхаба, так и собственных методов в проекте и стандартных. А по _ можно искать только маскированные ошибки, ведь по сути можно значение даже не получать.
miolini
Они парсят весь код на Go согласно синтаксису и проверяют.