Rust против Go — тема, которая постоянно возникает, и о ней уже много написано. Частично это связано с тем, что разработчики ищут информацию, которая поможет им решить, какой язык использовать для своего следующего (веб) проекта.
В конце концов, оба языка можно использовать для написания быстрых и надежных веб-сервисов. С другой стороны, их подходы к достижению этой цели совершенно разные, и трудно найти хорошее сравнение, которое было бы справедливо по отношению к обоим языкам. Этот пост — моя попытка дать вам обзор различий между Go и Rust с акцентом на веб-разработку. Мы сравним синтаксис, веб-экосистему, способы решения типичных веб-задач, таких как маршрутизация, промежуточное программное обеспечение, шаблоны и многое другое. Мы также кратко рассмотрим модели параллелизма обоих языков и то, как они влияют на способ написания веб-приложений.
К концу этой статьи вы должны иметь четкое представление о том, какой язык вам подходит. Несмотря на то, что мы осознаем свои собственные предубеждения и предпочтения, мы постараемся быть максимально объективными и подчеркнуть сильные и слабые стороны обоих языков.
Создание небольшого веб-сервиса
Многие сравнения Go и Rust сосредоточены на синтаксисе и особенностях языка. Но в конечном итоге важно то, насколько легко их использовать для нетривиального проекта.
Поскольку мы являемся поставщиком платформы как услуги, мы считаем, что можем внести наибольший вклад, показав вам, как создать небольшой веб-сервис на обоих языках. Мы будем использовать одну и ту же задачу и популярные библиотеки для обоих языков, чтобы сравнить решения, чтобы вы могли принять собственное решение.
Мы рассмотрим следующие темы:
Маршрутизация
Шаблонизация
Доступ к базе данных
Деплой
Мы оставим такие темы, как рендеринг на стороне клиента или миграцию, и сосредоточимся только на стороне сервера.
Задание
Выбрать задачу, репрезентативную для веб-разработки, непросто: с одной стороны, мы хотим, чтобы она была достаточно простой, чтобы мы могли сосредоточиться на функциях языка и библиотеках. С другой стороны, мы хотим убедиться, что задача не слишком проста, чтобы мы могли показать, как использовать возможности языка и библиотеки в реалистичных условиях.
Мы решили создать сервис прогноза погоды. Пользователь должен иметь возможность ввести название города и получить текущий прогноз погоды для этого города. Сервис также должен показать список недавно найденных городов.
По мере расширения сервиса мы добавим следующие функции:
Простой пользовательский интерфейс для отображения прогноза погоды
База данных для хранения недавно найденных городов.
API погоды
Для прогноза погоды мы будем использовать API Open-Meteo, поскольку он имеет открытый исходный код, прост в использовании и предлагает щедрый уровень бесплатного использования для некоммерческих целей до 10 000 запросов в день.
Мы будем использовать эти два эндпоинта API:
API GeoCoding для получения координат города.
API прогноза погоды для получения прогноза погоды для заданных координат.
Существуют библиотеки как для Go (omgo), так и для Rust (openmeteo), которые мы будем использовать в производственном сервисе. Однако ради сравнения мы хотим посмотреть, что нужно, чтобы сделать «необработанный» HTTP-запрос на обоих языках и преобразовать ответ в идиоматическую структуру данных.
Веб-сервис Go
Выбор веб-фреймворка
Изначально созданный для упрощения создания веб-сервисов, Go имеет ряд отличных пакетов, связанных с вебом. Если стандартная библиотека не соответствует вашим потребностям, на выбор можно выбрать ряд популярных сторонних веб-фреймворков, таких как Gin , Echo или Chi.
Какой из них выбрать – вопрос личных предпочтений. Некоторые опытные разработчики Go предпочитают использовать стандартную библиотеку и добавлять поверх нее библиотеку маршрутизации, например Chi. Другие предпочитают подход, требующий большего количества плюшек, и используют полнофункциональную среду, такую как Gin или Echo.
Оба варианта хороши, но для целей этого сравнения мы выберем Gin, поскольку это один из самых популярных фреймворков, который поддерживает все функции, необходимые для нашего метеорологического сервиса.
Выполнение HTTP-запросов
Начнем с простой функции, которая отправляет HTTP-запрос к Open Meteo API и возвращает ответ в виде строки:
func getLatLong(city string) (*LatLong, error) {
endpoint := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1&language=en&format=json", url.QueryEscape(city))
resp, err := http.Get(endpoint)
if err != nil {
return nil, fmt.Errorf("error making request to Geo API: %w", err)
}
defer resp.Body.Close()
var response GeoResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("error decoding response: %w", err)
}
if len(response.Results) < 1 {
return nil, errors.New("no results found")
}
return &response.Results[0], nil
}
Функция получает название города в качестве аргумента и возвращает координаты города в виде структуры LatLong
.
Обратите внимание, как мы обрабатываем ошибки после каждого шага: мы проверяем, был ли HTTP-запрос успешным, можно ли декодировать тело ответа и содержит ли ответ какие-либо результаты. Если какой-либо из этих шагов завершается неудачей, мы возвращаем ошибку и прерываем функцию. До сих пор нам просто нужно было использовать стандартную библиотеку, и это здорово.
Этот оператор defer
гарантирует, что тело ответа закроется после возврата функции. Это распространенный шаблон в Go, позволяющий избежать утечек ресурсов. Компилятор не предупреждает нас, если мы забудем, поэтому здесь нужно быть осторожным.
Обработка ошибок занимает большую часть кода. Это просто, но писать может быть утомительно, а код становится труднее читать. Положительным моментом является то, что за обработкой ошибок легко следить, и понятно, что происходит в случае ошибки.
Поскольку API возвращает объект JSON со списком результатов, нам необходимо определить структуру, соответствующую этому ответу:
type GeoResponse struct {
// A list of results; we only need the first one
Results []LatLong `json:"results"`
}
type LatLong struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
Теги json
сообщают декодеру JSON, как сопоставить поля JSON с полями структуры. Дополнительные поля в ответе JSON по умолчанию игнорируются.
Давайте определим еще одну функцию, которая получает нашу структуру LatLong
и возвращает прогноз погоды для этого места:
func getWeather(latLong LatLong) (string, error) {
endpoint := fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%.6f&longitude=%.6f&hourly=temperature_2m", latLong.Latitude, latLong.Longitude)
resp, err := http.Get(endpoint)
if err != nil {
return "", fmt.Errorf("error making request to Weather API: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading response body: %w", err)
}
return string(body), nil
}
Для начала давайте вызовем эти две функции по порядку и выведем результат:
func main() func main() {
latlong, err := getLatLong("London") // you know it will rain
if err != nil {
log.Fatalf("Failed to get latitude and longitude: %s", err)
}
fmt.Printf("Latitude: %f, Longitude: %f\n", latlong.Latitude, latlong.Longitude)
weather, err := getWeather(*latlong)
if err != nil {
log.Fatalf("Failed to get weather: %s", err)
}
fmt.Printf("Weather: %s\n", weather)
}
Это напечатает следующее:
Latitude: 51.508530, Longitude: -0.125740
Weather: {"latitude":51.5,"longitude":-0.120000124, ... }
Хорошо! Мы получили прогноз погоды в Лондоне. Давайте сделаем это доступным как веб-сервис.
Маршрутизация
Маршрутизация — одна из самых основных задач веб-фреймворка. Для начала давайте добавим в наш проект Gin.
go mod init github.com/user/goforecast
go get -u github.com/gin-gonic/gin
Затем давайте заменим нашу функцию main()
сервером и маршрутом, который получает название города в качестве параметра и возвращает прогноз погоды для этого города.
Gin поддерживает параметры пути и параметры запроса.
// Path parameter
r.GET("/weather/:city", func(c *gin.Context) {
city := c.Param("city")
// ...
})
// Query parameter
r.GET("/weather", func(c *gin.Context) {
city := c.Query("city")
// ...
})
```go
Which one you want to use depends on your use case.
In our case, we want to submit the city name from a form in the end, so we will use a query parameter.
```go
func main() {
r := gin.Default()
r.GET("/weather", func(c *gin.Context) {
city := c.Query("city")
latlong, err := getLatLong(city)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
weather, err := getWeather(*latlong)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"weather": weather})
})
r.Run()
}
В отдельном терминале мы можем запустить сервер с помощью go run .
и сделать к нему запрос:
curl "localhost:8080/weather?city=Hamburg"
И получаем наш прогноз погоды:
{"weather":"{\"latitude\":53.550000,\"longitude\":10.000000, ... }
Мне нравится вывод логов, и он довольно быстрый!
[GIN] 2023/09/09 - 19:27:20 | 200 | 190.75625ms | 127.0.0.1 | GET "/weather?city=Hamburg"
[GIN] 2023/09/09 - 19:28:22 | 200 | 46.597791ms | 127.0.0.1 | GET "/weather?city=Hamburg"
Шаблоны
Мы получили наш эндпоинт, но необработанный JSON обычному пользователю не очень полезен. В реальном приложении мы, вероятно, отдаём ответ JSON на эндпоинте API (скажем /api/v1/weather/:city
) и добавим отдельный эндпоинт, который возвращает HTML-страницу. Для простоты мы просто вернем HTML-страницу напрямую.
Давайте добавим простую HTML-страницу, которая отображает прогноз погоды для данного города в виде таблицы. Мы будем использовать пакет html/template
из стандартной библиотеки для рендеринга HTML-страницы.
Во-первых, давайте добавим несколько структур для нашего представления:
type WeatherData struct
type WeatherResponse struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Timezone string `json:"timezone"`
Hourly struct {
Time []string `json:"time"`
Temperature2m []float64 `json:"temperature_2m"`
} `json:"hourly"`
}
type WeatherDisplay struct {
City string
Forecasts []Forecast
}
type Forecast struct {
Date string
Temperature string
}
Это просто прямое сопоставление соответствующих полей ответа JSON со структурой. Существуют такие инструменты, как Transform, которые упрощают преобразование структур JSON в структуры Go. Взгляните!
Далее мы определяем функцию, которая преобразует необработанный ответ JSON от API погоды в нашу новую структуру WeatherDisplay
:
func extractWeatherData(city string, rawWeather string) (WeatherDisplay, error) {
var weatherResponse WeatherResponse
if err := json.Unmarshal([]byte(rawWeather), &weatherResponse); err != nil {
return WeatherDisplay{}, fmt.Errorf("error decoding weather response: %w", err)
}
var forecasts []Forecast
for i, t := range weatherResponse.Hourly.Time {
date, err := time.Parse(time.RFC3339, t)
if err != nil {
return WeatherDisplay{}, err
}
forecast := Forecast{
Date: date.Format("Mon 15:04"),
Temperature: fmt.Sprintf("%.1f°C", weatherResponse.Hourly.Temperature2m[i]),
}
forecasts = append(forecasts, forecast)
}
return WeatherDisplay{
City: city,
Forecasts: forecasts,
}, nil
}
Обработка даты осуществляется встроенным пакетомtime
. Чтобы узнать больше об обработке дат в Go, прочтите эту статью «Go на Примере».
Мы расширяем наш обработчик маршрута для рендеринга HTML-страницы:
r.GET("/weather", func(c *gin.Context) {
city := c.Query("city")
latlong, err := getLatLong(city)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
weather, err := getWeather(*latlong)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
//////// NEW CODE STARTS HERE ////////
weatherDisplay, err := extractWeatherData(city, weather)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.HTML(http.StatusOK, "weather.html", weatherDisplay)
//////////////////////////////////////
})
Далее разберемся с шаблоном. Создайте каталог шаблонов под названием views
и сообщите об этом Gin:
r := gin.Default()
r.LoadHTMLGlob("views/*")
Наконец, мы можем создать файл шаблона weather.html
в каталоге views
:
<!DOCTYPE html>
<html>
<head>
<title>Weather Forecast</title>
</head>
<body>
<h1>Weather for {{ .City }}</h1>
<table border="1">
<tr>
<th>Date</th>
<th>Temperature</th>
</tr>
{{ range .Forecasts }}
<tr>
<td>{{ .Date }}</td>
<td>{{ .Temperature }}</td>
</tr>
{{ end }}
</table>
</body>
</html>
(Более подробную информацию о том, как использовать шаблоны, можно найти в документации Gin.)
Таким образом, у нас есть работающий веб-сервис, который возвращает прогноз погоды для данного города в виде HTML-страницы!
Ой! Возможно, мы также захотим создать индексную страницу с полем ввода, которое позволит нам ввести название города и отобразить прогноз погоды для этого города.
Добавим новый обработчик маршрута для индексной страницы:
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
И новый файл шаблона index.html
:
<!DOCTYPE html>
<html>
<head>
<title>Weather Forecast</title>
</head>
<body>
<h1>Weather Forecast</h1>
<form action="/weather" method="get">
<label for="city">City:</label>
<input type="text" id="city" name="city" />
<input type="submit" value="Submit" />
</form>
</body>
</html>
Теперь мы можем запустить наш веб-сервис и открыть http://localhost:8080 в нашем браузере:
Индексная страница
В качестве упражнения вы можете добавить некоторые стили к HTML-странице, но поскольку нас больше волнует серверная часть, мы оставим все как есть.
Доступ к базе данных
Наш сервис получает широту и долготу данного города из внешнего API при каждом отдельном запросе. Поначалу, вероятно, это нормально, но со временем нам может потребоваться кэшировать результаты в базе данных, чтобы избежать ненужных вызовов API.
Для этого давайте добавим базу данных в наш веб-сервис. Мы будем использовать PostgreSQL в качестве базы данных и pgx в качестве драйвера базы данных.
Сначала мы создаем файл с именем init.sql
, который будет использоваться для инициализации нашей базы данных:
CREATE TABLE IF NOT EXISTS cities (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
lat NUMERIC NOT NULL,
long NUMERIC NOT NULL
);
CREATE INDEX IF NOT EXISTS cities_name_idx ON cities (name);
Мы храним широту и долготу данного города. Тип SERIAL
представляет собой целое число с автоматическим приращением в PostgreSQL. В противном случае нам пришлось бы самим генерировать идентификаторы при вставке. Чтобы ускорить работу, мы также добавим индекс к столбцуname
.
Вероятно, проще всего использовать Docker или любого другого облачного провайдера. В конце концов, вам просто нужен URL-адрес базы данных , который вы можете передать своему веб-сервису в качестве переменной среды.
Мы не будем здесь вдаваться в подробности настройки базы данных, но простой способ локально запустить базу данных PostgreSQL с помощью Docker:
docker run -p 5432:5432 -e POSTGRES_USER=forecast -e POSTGRES_PASSWORD=forecast -e POSTGRES_DB=forecast -v `pwd`/init.sql:/docker-entrypoint-initdb.d/index.sql -d postgres
export DATABASE_URL="postgres://forecast:forecast@localhost:5432/forecast?sslmode=disable"
Однако, как только у нас есть база данных, нам нужно добавить зависимость sqlx в наш файл go.mod
:
go get github.com/jmoiron/sqlx
Теперь мы можем использовать пакет sqlx
для подключения к нашей базе данных, используя строку подключения из переменной среды DATABASE_URL
:
_ = sqlx.MustConnect("postgres", os.Getenv("DATABASE_URL"))
И благодаря этому у нас есть соединение с базой данных!
Добавим функцию для вставки города в нашу базу данных. Мы будем использовать нашу ранее созданную структуру LatLong
.
func insertCity(db *sqlx.DB, name string, latLong LatLong) error {
_, err := db.Exec("INSERT INTO cities (name, lat, long) VALUES ($1, $2, $3)", name, latLong.Latitude, latLong.Longitude)
return err
}
Давайте переименуем нашу старую функцию getLatLongfetchLatLong
и добавим новую функцию getLatLong
, которая использует базу данных вместо внешнего API:
func getLatLong(db *sqlx.DB, name string) (*LatLong, error) {
var latLong *LatLong
err := db.Get(&latLong, "SELECT lat, long FROM cities WHERE name = $1", name)
if err == nil {
return latLong, nil
}
latLong, err = fetchLatLong(name)
if err != nil {
return nil, err
}
err = insertCity(db, name, *latLong)
if err != nil {
return nil, err
}
return latLong, nil
}
Здесь мы напрямую передаем соединение db
нашей функции getLatLong
. В реальном приложении нам следует отделить доступ к базе данных от логики API, чтобы сделать возможным тестирование. Вероятно, мы также будем использовать кэш в памяти, чтобы избежать ненужных вызовов базы данных. Это просто для сравнения доступа к базе данных в Go и Rust.
Нам нужно обновить наш обработчик:
r.GET("/weather", func(c *gin.Context) {
city := c.Query("city")
// Pass in the db
latlong, err := getLatLong(db, city)
// ...
})
При этом у нас есть работающий веб-сервис, который хранит широту и долготу данного города в базе данных и извлекает их оттуда при последующих запросах.
Промежуточное ПО
Последний шаг — добавить промежуточное программное обеспечение к нашему веб-сервису. Мы уже получили неплохое логгирование от Gin бесплатно.
Давайте добавим промежуточное программное обеспечение базовой аутентификации и защитим наш эндпоинт /stats
, который мы будем использовать для вывода последних поисковых запросов.
r.GET("/stats", gin.BasicAuth(gin.Accounts{
"forecast": "forecast",
}), func(c *gin.Context) {
// rest of the handler
}
)
Вот и все!
Совет: вы также можете группировать маршруты, чтобы применять аутентификацию к нескольким маршрутам одновременно.
Вот логика получения последних поисковых запросов из базы данных:
func getLastCities(db *sqlx.DB) ([]string, error) {
var cities []string
err := db.Select(&cities, "SELECT name FROM cities ORDER BY id DESC LIMIT 10")
if err != nil {
return nil, err
}
return cities, nil
}
Теперь давайте подключим наш эндпоинт/stats
для вывода последних поисковых запросов:
r.GET("/stats", gin.BasicAuth(gin.Accounts{
"forecast": "forecast",
}), func(c *gin.Context) {
cities, err := getLastCities(db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.HTML(http.StatusOK, "stats.html", cities)
})
Наш шаблон stats.html
достаточно прост:
<!DOCTYPE html>
<html>
<head>
<title>Latest Queries</title>
</head>
<body>
<h1>Latest Lat/Long Lookups</h1>
<table border="1">
<tr>
<th>Cities</th>
</tr>
{{ range . }}
<tr>
<td>{{ . }}</td>
</tr>
{{ end }}
</table>
</body>
</html>
И теперь у нас есть работающий веб-сервис! Поздравляем!
Мы добились следующего:
Веб-сервис, который получает широту и долготу данного города из внешнего API.
Сохраняет широту и долготу в базе данных.
Извлекает широту и долготу из базы данных при последующих запросах.
Печатает последние поисковые запросы на эндпоинте
/stats
Базовая аутентификация для защиты эндпоинта
/stats
Использует промежуточное программное обеспечение для регистрации запросов
Шаблоны для рендеринга HTML
Это довольно много функциональности для нескольких строк кода! Давайте посмотрим, как сложится с Rust!
Веб-сервис Rust
Исторически сложилось так, что у Rust не было хорошей истории для веб-сервисов. Фреймворков было несколько, но они были довольно низкоуровневыми. Лишь недавно, с появлением async/await, веб-экосистема Rust начала по-настоящему развиваться. Внезапно стало возможным писать высокопроизводительные веб-сервисы без сборщика мусора и с бесстрашным параллелизмом.
Мы увидим, как Rust сравнивается с Go с точки зрения эргономики, производительности и безопасности. Но сначала нам нужно выбрать веб-фреймворк.
Какой веб-фреймворк?
Если вы хотите получить более полное представление о веб-фреймворках Rust, а также об их сильных и слабых сторонах, то недавно мы провели углубленное изучение веб-фреймворков Rust .
Для целей этой статьи мы рассматриваем два веб-фреймворка: Actix и Axum.
Actix — очень популярный веб-фреймворк в сообществе Rust. Он основан на модели актора и использует async/await под капотом. В тестах он регулярно оказывается одним из самых быстрых веб-фреймворков в мире .
Axum, с другой стороны, — это новый веб-фреймворк, основанный на Tower , библиотеке для создания асинхронных сервисов. Он быстро набирает популярность. Он также основан на async/await.
Обе платформы очень похожи с точки зрения эргономики и производительности. Они обе поддерживают промежуточное программное обеспечение и маршрутизацию. Каждый из них был бы хорошим выбором для нашего веб-сервиса, но мы выберем Axum, поскольку он хорошо связан с остальной частью экосистемы и в последнее время привлек к себе много внимания.
Маршрутизация
Давайте начнем проект с помощью cargo new forecast
и добавим в наш файл Cargo.toml
следующие зависимости :
[dependencies]
# web framework
axum = "0.6.20"
# async HTTP client
reqwest = { version = "0.11.20", features = ["json"] }
# serialization/deserialization for JSON
serde = "1.0.188"
# database access
sqlx = "0.7.1"
# async runtime
tokio = { version = "1.32.0", features = ["full"] }
Давайте создадим небольшой скелет нашего веб-сервиса, который мало что делает.
use std::net::SocketAddr;
use axum::{routing::get, Router};
// basic handler that responds with a static string
async fn index() -> &'static str {
"Index"
}
async fn weather() -> &'static str {
"Weather"
}
async fn stats() -> &'static str {
"Stats"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(index))
.route("/weather", get(weather))
.route("/stats", get(stats));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
Функция main
довольно проста. Создаём роутер и привязываем его к адресу сокета. Функции index
, weather
иstats
наши обработчики. Это асинхронные функции, возвращающие строку. Позже мы заменим их реальной логикой.
Давайте запустим веб-сервис с помощью cargo run
и посмотрим, что произойдет.
$ curl localhost:3000
Index
$ curl localhost:3000/weather
Weather
$ curl localhost:3000/stats
Stats
Хорошо, это работает. Давайте добавим немного реальной логики в наши обработчики.
Макросы Axum
Прежде чем мы продолжим, я хотел бы упомянуть, что у axum есть некоторые острые углы. Например, он будет кричать на вас, если вы забыли сделать функцию-обработчик асинхронной. Поэтому, если вы столкнетесь с ошибками Handler<_, _> is not implemented
, добавьте крейт axum-macros и аннотируйте свой обработчик с помощью #[axum_macros::debug_handler]
. Это даст вам гораздо лучшие сообщения об ошибках.
Получение широты и долготы
Давайте напишем функцию, которая получает широту и долготу данного города из внешнего API.
Вот структуры, представляющие ответ API:
use serde::Deserialize;
pub struct GeoResponse {
pub results: Vec<LatLong>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct LatLong {
pub latitude: f64,
pub longitude: f64,
}
По сравнению с Go, мы не используем теги для указания имен полей. Вместо этого мы используем атрибут #[derive(Deserialize)]
для автоматического получения признака Deserialize
для наших структур. Эти производные макросы (прим. пер.: аннотации) очень мощны и позволяют нам делать многое с очень небольшим количеством кода. Это очень распространенный шаблон в Rust.
Давайте воспользуемся новыми типами для получения широты и долготы данного города:
async fn fetch_lat_long(city: &str) -> Result<LatLong, Box<dyn std::error::Error>> {
let endpoint = format!(
"https://geocoding-api.open-meteo.com/v1/search?name={}&count=1&language=en&format=json",
city
);
let response = reqwest::get(&endpoint).await?.json::<GeoResponse>().await?;
response
.results
.get(0)
.cloned()
.ok_or("No results found".into())
}
Код немного менее подробный, чем версия Go. Нам не нужно писать конструкции if err != nil
, поскольку мы можем использовать оператор?
для распространения ошибок. Это также является обязательным, поскольку каждый шаг возвращает тип Result
. Если мы не обработаем ошибку, мы не получим доступа к значению.
Последняя часть может показаться немного незнакомой:
response
.results
.get(0)
.cloned()
.ok_or("No results found".into())
Здесь происходит несколько вещей:
response.results.get(0)
возвращаетOption<&LatLong>
. ЭтоOption
потому, что функцияget
может вернутьсяNone
, если вектор пуст.cloned()
клонирует значение внутриOption
и преобразуетOption<&LatLong>
вOption<LatLong>
. Это необходимо, потому что мы хотим вернутьLatLong
, а не ссылку. В противном случае нам пришлось бы добавить к сигнатуре функции спецификатор времени жизни, и это сделало бы код менее читабельным.ok_or("No results found".into())
преобразуетOption<LatLong>
вResult<LatLong, Box<dyn std::error::Error>>
. ЕслиOption
-None
, он вернет сообщение об ошибке. Функцияinto()
преобразует строку вBox<dyn std::error::Error>
.
Альтернативный способ написать это:
match response.results.get(0) {
Some(lat_long) => Ok(lat_long.clone()),
None => Err("No results found".into()),
}
Какая версия вам больше по душе — дело вкуса.
Rust — это язык, основанный на выражениях, а это означает, что нам не нужно использовать ключевое слово return
для возврата значения. Вместо этого возвращается последнее значение функции.
Теперь мы можем обновить нашу функцию weather
для использования fetch_lat_long
.
Наша первая попытка может выглядеть так:
async fn weather(city: String) -> String {
println!("city: {}", city);
let lat_long = fetch_lat_long(&city).await.unwrap();
format!("{}: {}, {}", city, lat_long.latitude, lat_long.longitude)
}
Сначала мы выводим в консоль город, затем получаем широту и долготу и разворачиваем (то есть «распаковываем») результат. Если результатом будет ошибка, программа впадёт в панику. Это не идеально, но мы исправим это позже.
Затем мы используем широту и долготу, чтобы создать строку и вернуть ее.
Запустим программу и посмотрим, что произойдет:
curl -v "localhost:3000/weather?city=Berlin"
* Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /weather?city=Berlin HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.1.2
> Accept: */*
>
* Empty reply from server
* Closing connection 0
curl: (52) Empty reply from server
Кроме того, мы получаем такой вывод:
city:
Параметр city
пуст. Что случилось?
Проблема в том, что мы используем тип String
для параметра city
. Этот тип не является допустимым экстрактором .
Вместо этого мы можем использовать экстрактор Query
:
async fn weather(Query(params): Query<HashMap<String, String>>) -> String {
let city = params.get("city").unwrap();
let lat_long = fetch_lat_long(&city).await.unwrap();
format!("{}: {}, {}", *city, lat_long.latitude, lat_long.longitude)
}
Это сработает, но это не очень своеобразно. Нам нужно использовать unwrap
для Option
чтобы получить город. Нам также нужно передать *city
format!
макросу, чтобы получить значение вместо ссылки. (На жаргоне Rust это называется «разыменование».)
Мы могли бы создать структуру, которая представляет параметры запроса:
#[derive(Deserialize)]
pub struct WeatherQuery {
pub city: String,
}
Затем мы можем использовать эту структуру в качестве экстрактора и избежать unwrap
:
async fn weather(Query(params): Query<WeatherQuery>) -> String {
let lat_long = fetch_lat_long(¶ms.city).await.unwrap();
format!("{}: {}, {}", params.city, lat_long.latitude, lat_long.longitude)
}
Чище! Это немного сложнее, чем версия Go, но она также более типобезопасна. Вы можете себе представить, что мы можем добавить ограничения в структуру, чтобы добавить проверку. Например, мы могли бы потребовать, чтобы длина города была не менее 3 символов.
Теперь оunwrap
в функции weather
. В идеале мы бы возвращали ошибку, если город не найден. Мы можем сделать это, изменив тип возвращаемого значения.
В axum все, что реализует IntoResponse
может быть возвращено из обработчиков, однако желательно возвращать конкретный тип, поскольку есть [некоторые предостережения при возврате impl IntoResponse
] (https://docs.rs/axum/latest/axum/response/index. html)
В нашем случае мы можем вернуть тип Result
:
async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, StatusCode> {
match fetch_lat_long(¶ms.city).await {
Ok(lat_long) => Ok(format!(
"{}: {}, {}",
params.city, lat_long.latitude, lat_long.longitude
)),
Err(_) => Err(StatusCode::NOT_FOUND),
}
}
Это вернет код состояния 404
, если город не найден. Мы используем match
для сопоставления результата fetch_lat_long
. Если это Ok
, мы возвращаем погоду в формате String
. Если этоErr
, мы возвращаем StatusCode::NOT_FOUND
.
Мы также могли бы использовать эту map_err
функцию для преобразования ошибки в StatusCode
:
async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, StatusCode> {
let lat_long = fetch_lat_long(¶ms.city)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
Ok(format!(
"{}: {}, {}",
params.city, lat_long.latitude, lat_long.longitude
))
}
Преимущество этого варианта заключается в том, что управление является более линейным: мы сразу обрабатываем ошибку и затем можем продолжить счастливый путь. С другой стороны, требуется время, чтобы привыкнуть к этим шаблонам комбинаторов, пока они не станут второй натурой.
В Rust обычно есть несколько способов сделать что-то. Какая версия вам больше по душе — дело вкуса. В общем, будьте проще и не зацикливайтесь на этом.
В любом случае, давайте протестируем нашу программу:
curl "localhost:3000/weather?city=Berlin"
Berlin: 52.52437, 13.41053
и
curl -I "localhost:3000/weather?city=abcdedfg"
HTTP/1.1 404 Not Found
Напишем вторую функцию, которая будет возвращать погоду для заданной широты и долготы:
async fn fetch_weather(lat_long: LatLong) -> Result<String, Box<dyn std::error::Error>> {
let endpoint = format!(
"https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m",
lat_long.latitude, lat_long.longitude
);
let response = reqwest::get(&endpoint).await?.text().await?;
Ok(response)
}
Здесь мы делаем запрос к API и возвращаем необработанное тело ответа в виде String
.
Мы можем расширить наш обработчик, чтобы он выполнял два вызова подряд:
async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, StatusCode> {
let lat_long = fetch_lat_long(¶ms.city)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
let weather = fetch_weather(lat_long)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(weather)
}
Это сработает, но вернет необработанное тело ответа из API Open Meteo. Давайте разберем ответ и вернем данные, аналогичные версии Go.
Напоминаем, вот определение Go:
type WeatherResponse struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Timezone string `json:"timezone"`
Hourly struct {
Time []string `json:"time"`
Temperature2m []float64 `json:"temperature_2m"`
} `json:"hourly"`
}
А вот версия Rust:
#[derive(Deserialize, Debug)]
pub struct WeatherResponse {
pub latitude: f64,
pub longitude: f64,
pub timezone: String,
pub hourly: Hourly,
}
#[derive(Deserialize, Debug)]
pub struct Hourly {
pub time: Vec<String>,
pub temperature_2m: Vec<f64>,
}
Пока мы этим занимаемся, давайте также определим другие структуры, которые нам нужны:
#[derive(Deserialize, Debug)]
pub struct WeatherDisplay {
pub city: String,
pub forecasts: Vec<Forecast>,
}
#[derive(Deserialize, Debug)]
pub struct Forecast {
pub date: String,
pub temperature: String,
}
Теперь мы можем разобрать тело ответа на наши структуры:
async fn fetch_weather(lat_long: LatLong) -> Result<WeatherResponse, Box<dyn std::error::Error>> {
let endpoint = format!(
"https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m",
lat_long.latitude, lat_long.longitude
);
let response = reqwest::get(&endpoint).await?.json::<WeatherResponse>().await?;
Ok(response)
}
Давайте настроим обработчик. Самый простой способ скомпилировать его — вернуть String
:
async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, StatusCode> {
let lat_long = fetch_lat_long(¶ms.city)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
let weather = fetch_weather(lat_long)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let display = WeatherDisplay {
city: params.city,
forecasts: weather
.hourly
.time
.iter()
.zip(weather.hourly.temperature_2m.iter())
.map(|(date, temperature)| Forecast {
date: date.to_string(),
temperature: temperature.to_string(),
})
.collect(),
};
Ok(format!("{:?}", display))
}
Обратите внимание, как мы смешиваем логику синтаксического анализа с логикой обработчика. Давайте немного исправим это, переместив логику синтаксического анализа в функцию-конструктор:
impl WeatherDisplay {
/// Create a new `WeatherDisplay` from a `WeatherResponse`.
fn new(city: String, response: WeatherResponse) -> Self {
let display = WeatherDisplay {
city,
forecasts: response
.hourly
.time
.iter()
.zip(response.hourly.temperature_2m.iter())
.map(|(date, temperature)| Forecast {
date: date.to_string(),
temperature: temperature.to_string(),
})
.collect(),
};
display
}
}```
That's a start. Our handler now looks like this:
```rust
async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, StatusCode> {
let lat_long = fetch_lat_long(¶ms.city)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
let weather = fetch_weather(lat_long)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let display = WeatherDisplay::new(params.city, weather);
Ok(format!("{:?}", display))
}
Это уже немного лучше. Что отвлекает, так это шаблонность map_err
. Мы можем устранить это, введя собственный тип ошибки. Например, мы можем последовать примеру из axum
репозитория и использовать anyhow, популярный крейт для обработки ошибок:
cargo add anyhow
Скопируем код из примера в наш проект:
// Make our own error that wraps `anyhow::Error`.
struct AppError(anyhow::Error);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
Вам не обязательно полностью понимать этот код. Достаточно сказать, что это настроит обработку ошибок для приложения, чтобы нам не приходилось иметь дело с ними в обработчике.
Нам нужно настроить функции fetch_lang_long
и fetch_weather
, чтобы они возвращали Result
с anyhow::Error
:
async fn fetch_lat_long(city: &str) -> Result<LatLong, anyhow::Error> {
let endpoint = format!(
"https://geocoding-api.open-meteo.com/v1/search?name={}&count=1&language=en&format=json",
city
);
let response = reqwest::get(&endpoint).await?.json::<GeoResponse>().await?;
response.results.get(0).cloned().context("No results found")
}
и
async fn fetch_weather(lat_long: LatLong) -> Result<WeatherResponse, anyhow::Error> {
// code stays the same
}
Ценой добавления зависимости и дополнительного шаблона для обработки ошибок нам удалось немного упростить наш обработчик:
async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, AppError> {
let lat_long = fetch_lat_long(¶ms.city).await?;
let weather = fetch_weather(lat_long).await?;
let display = WeatherDisplay::new(params.city, weather);
Ok(format!("{:?}", display))
}
Шаблоны
axum
не поставляется с шаблонизатором. Мы должны выбрать его сами. Обычно я использую либо tera, либо Askama с небольшим предпочтением askama
, потому что они поддерживают проверки синтаксиса во время компиляции. При этом вы не сможете случайно внести опечатки в шаблон. Каждая переменная, которую вы используете в шаблоне, должна быть определена в коде.
# Enable axum support
cargo add askama --features=with-axum
# I also needed to add this to make it compile
cargo add askama_axum
Давайте создадим каталог templates
и добавим шаблон weather.html
, аналогичный шаблону Go, который мы создали ранее:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Weather</title>
</head>
<body>
<h1>Weather for {{ city }}</h1>
<table>
<thead>
<tr>
<th>Date</th>
<th>Temperature</th>
</tr>
</thead>
<tbody>
{% for forecast in forecasts %}
<tr>
<td>{{ forecast.date }}</td>
<td>{{ forecast.temperature }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
Давайте преобразуем нашу структуру WeatherDisplay
в Template
:
#[derive(Template, Deserialize, Debug)]
#[template(path = "weather.html")]
struct WeatherDisplay {
city: String,
forecasts: Vec<Forecast>,
}
и наш обработчик становится:
async fn weather(Query(params): Query<WeatherQuery>) -> Result<WeatherDisplay, AppError> {
let lat_long = fetch_lat_long(¶ms.city).await?;
let weather = fetch_weather(lat_long).await?;
Ok(WeatherDisplay::new(params.city, weather))
}
Пришлось приложить немало усилий, чтобы прийти к этому, но теперь у нас есть хорошее разделение задач без слишком большого количества шаблонов.
Если вы откроете браузер на http://localhost:3000/weather?city=Berlin
, вы должны увидеть таблицу погоды.
Добавить маску ввода легко. Мы можем использовать тот же HTML-код, который мы использовали для версии Go:
<form action="/weather" method="get">
<!DOCTYPE html>
<html>
<head>
<title>Weather Forecast</title>
</head>
<body>
<h1>Weather Forecast</h1>
<form action="/weather" method="get">
<label for="city">City:</label>
<input type="text" id="city" name="city" />
<input type="submit" value="Submit" />
</form>
</body>
</html>
</form>
и вот обработчик:
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate;
async fn index() -> IndexTemplate {
IndexTemplate
}
Мы достигли «паритета функций» с версией Go. Перейдем к хранению широты и долготы в базе данных.
Доступ к базе данных
Мы будем использовать sqlx для доступа к базе данных. Это очень популярный крейт, поддерживающий несколько баз данных. В нашем случае мы будем использовать Postgres, как и в версии Go.
Добавьте это в свой Cargo.toml
:
sqlx = { version = "0.7", features = [
"runtime-tokio-rustls",
"macros",
"any",
"postgres",
] }
Нам нужно добавить переменную среды DATABASE_URL
в наш .env
файл:
export DATABASE_URL="postgres://forecast:forecast@localhost:5432/forecast?sslmode=disable"
Я предполагаю, что на вашем компьютере работает база данных Postgres и схема уже настроена. Если нет, вернитесь к версии Go и следуйте инструкциям.
При этом давайте настроим наш код для использования базы данных. Во-первых, функция main
:
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let db_connection_str = std::env::var("DATABASE_URL").context("DATABASE_URL must be set")?;
let pool = sqlx::PgPool::connect(&db_connection_str)
.await
.context("can't connect to database")?;
let app = Router::new()
.route("/", get(index))
.route("/weather", get(weather))
.route("/stats", get(stats))
.with_state(pool);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;
Ok(())
}
Вот что изменилось:
Мы добавили переменную среды
DATABASE_URL
и прочитали ее в форматеmain
.Мы создаем пул соединений с базой данных с помощью
sqlx::PgPool::connect
.Затем мы передаем пул в
with_state
, чтобы сделать его доступным для всех обработчиков.
В каждом маршруте мы можем (но не обязаны) получить доступ к пулу базы данных следующим образом:
async fn weather(
Query(params): Query<WeatherQuery>,
State(pool): State<PgPool>,
) -> Result<WeatherDisplay, AppError> {
let lat_long = fetch_lat_long(¶ms.city).await?;
let weather = fetch_weather(lat_long).await?;
Ok(WeatherDisplay::new(params.city, weather))
}
Чтобы узнать больше о State
, ознакомьтесь с документацией .
Чтобы наши данные можно было извлекать из базы данных, нам нужно добавить трейт FromRow
в наши структуры :
#[derive(sqlx::FromRow, Deserialize, Debug, Clone)]
pub struct LatLong {
pub latitude: f64,
pub longitude: f64,
}
Давайте добавим функцию для получения широты и долготы из базы данных:
async fn get_lat_long(pool: &PgPool, name: &str) -> Result<LatLong, anyhow::Error> {
let lat_long = sqlx::query_as::<_, LatLong>(
"SELECT lat AS latitude, long AS longitude FROM cities WHERE name = $1",
)
.bind(name)
.fetch_optional(pool)
.await?;
if let Some(lat_long) = lat_long {
return Ok(lat_long);
}
let lat_long = fetch_lat_long(name).await?;
sqlx::query("INSERT INTO cities (name, lat, long) VALUES ($1, $2, $3)")
.bind(name)
.bind(lat_long.latitude)
.bind(lat_long.longitude)
.execute(pool)
.await?;
Ok(lat_long)
}
и, наконец, давайте обновим наш маршрут weather
, чтобы использовать новую функцию:
async fn weather(
Query(params): Query<WeatherQuery>,
State(pool): State<PgPool>,
) -> Result<WeatherDisplay, AppError> {
let lat_long = fetch_lat_long(¶ms.city).await?;
let weather = fetch_weather(lat_long).await?;
Ok(WeatherDisplay::new(params.city, weather))
}
Вот и все! Теперь у нас есть работающее веб-приложение с серверной базой данных. Поведение идентично предыдущему, но теперь мы кэшируем широту и долготу.
Промежуточное ПО
Последняя функция, которой нам не хватает в нашей версии Go, — это эндпоинт/stats
. Помните, что он показывает последние запросы и находится за базовой аутентификацией.
Начнем с базовой аутентификации.
Мне потребовалось некоторое время, чтобы понять, как это сделать. Для axum существует множество библиотек аутентификации, но очень мало информации о том, как выполнить базовую аутентификацию.
В итоге я написал собственное промежуточное программное обеспечение, которое
проверьте, есть ли у запроса заголовок
Authorization
если да, проверьте, содержит ли заголовок действительное имя пользователя и пароль.
если это так, верните «несанкционированный» ответ и заголовок
WWW-Authenticate
, который инструктирует браузер показать диалоговое окно входа в систему.
Вот код:
/// A user that is authorized to access the stats endpoint.
///
/// No fields are required, we just need to know that the user is authorized. In
/// a production application you would probably want to have some kind of user
/// ID or similar here.
struct User;
#[async_trait]
impl<S> FromRequestParts<S> for User
where
S: Send + Sync,
{
type Rejection = axum::http::Response<axum::body::Body>;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
let auth_header = parts
.headers
.get("Authorization")
.and_then(|header| header.to_str().ok());
if let Some(auth_header) = auth_header {
if auth_header.starts_with("Basic ") {
let credentials = auth_header.trim_start_matches("Basic ");
let decoded = base64::decode(credentials).unwrap_or_default();
let credential_str = from_utf8(&decoded).unwrap_or("");
// Our username and password are hardcoded here.
// In a real app, you'd want to read them from the environment.
if credential_str == "forecast:forecast" {
return Ok(User);
}
}
}
let reject_response = axum::http::Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header(
"WWW-Authenticate",
"Basic realm=\"Please enter your credentials\"",
)
.body(axum::body::Body::from("Unauthorized"))
.unwrap();
Err(reject_response)
}
}
FromRequestParts — это трейт, который позволяет нам извлекать данные из запроса. Существует также FromRequest, который получает все тело запроса и поэтому может быть запущен для обработчиков только один раз. В нашем случае нам нужно просто прочитать заголовок Authorization
, FromRequestParts
для этого достаточно.
Прелесть в том, что мы можем просто добавить тип User
к любому обработчику, и он извлечет пользователя из запроса:
async fn stats(user: User) -> &'static str {
"We're authorized!"
}
Теперь о самой логике эндпоинта /stats
.
#[derive(Template)]
#[template(path = "stats.html")]
struct StatsTemplate {
pub cities: Vec<City>,
}
async fn stats(_user: User, State(pool): State<PgPool>) -> Result<StatsTemplate, AppError> {
let cities = get_last_cities(&pool).await?;
Ok(StatsTemplate { cities })
}
Деплой
Наконец, давайте поговорим о деплое.
Для Golang вы можете использовать любого облачного провайдера, поддерживающего Docker. Мы не будем здесь вдаваться в подробности, поскольку существует множество сервисов, которые это поддерживают.
Вы можете сделать то же самое с Rust, но есть и другие варианты. Конечно, одним из них является Shuttle, и его работа отличается от других сервисов: вам не нужно создавать образ Docker и помещать его в реестр. Вместо этого вы просто отправляете свой код в репозиторий git, и Shuttle запустит для вас двоичный файл.
Благодаря процедурным макросам Rust вы можете быстро расширить свой код дополнительными функциями.
Все, что нужно для начала, — это добавить #[shuttle_runtime::main]
в вашу main
функцию:
#[shuttle_runtime::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Rest of your code goes here
}
Для начала установите Shuttle CLI и зависимости:
cargo binstall cargo-shuttle
cargo add shuttle-axum shuttle-runtime
Давайте изменим нашу функцию main
для использования Shuttle. Обратите внимание, что нам больше не нужна привязка порта, поскольку Shuttle позаботится об этом за нас! Мы просто передаем ему маршрутизатор, а он позаботится обо всем остальном.
#[shuttle_runtime::main]
async fn main() -> shuttle_axum::ShuttleAxum {
let db_connection_str = std::env::var("DATABASE_URL").context("DATABASE_URL must be set")?;
let pool = sqlx::PgPool::connect(&db_connection_str)
.await
.context("can't connect to database")?;
let router = Router::new()
.route("/", get(index))
.route("/weather", get(weather))
.route("/stats", get(stats))
.with_state(pool);
Ok(router.into())
}
Далее давайте настроим нашу производственную базу данных Postgres. Для этого тоже есть макрос.
cargo add shuttle-shared-db --features=postgres
и
#[shuttle_runtime::main]
async fn main(#[shuttle_shared_db::Postgres] pool: PgPool) -> shuttle_axum::ShuttleAxum {
pool.execute(include_str!("../schema.sql"))
.await
.context("Failed to initialize DB")?;
let router = Router::new()
.route("/", get(index))
.route("/weather", get(weather))
.route("/stats", get(stats))
.with_state(pool);
Ok(router.into())
}
Видите ту часть о схеме? Вот как мы инициализируем нашу базу данных с помощью существующих определений таблиц. Миграции также поддерживаются через sqlx и sqlx-cli .
Мы избавились от большого количества шаблонного кода и теперь можем легко развернуть наше приложение.
# We only need to run this once
cargo shuttle project start
# Run as often as you like
cargo shuttle deploy
Когда это будет сделано, он напечатает URL-адрес службы. Он должен работать так же, как и раньше, но теперь он работает на сервере в облаке. ????
Какой язык подходит вам?
-
Go:
простой в освоении, быстрый, хорош для веб-сервисов
плюшки в комплекте. Мы многое сделали, используя только стандартную библиотеку.
Нашей единственной зависимостью был Gin, очень популярный веб-фреймворк.
-
Rust:
быстрый, безопасный, развивающаяся экосистема веб-сервисов
в комплекте нет плюшек. Нам пришлось добавить множество зависимостей, чтобы получить ту же функциональность, что и в Go, и написать собственное небольшое промежуточное программное обеспечение.
окончательный код обработчика не отвлекал от обработки ошибок, поскольку мы использовали собственный тип ошибки и оператор
?
. Это делает код очень читаемым за счет необходимости писать дополнительную логику адаптера.
Это ставит вопрос...
Rust лучше Go или Rust заменит Go?
Лично я большой поклонник Rust и считаю, что это отличный язык для веб-сервисов. Но в экосистеме все еще есть много неровностей и недостающих частей.
Если вы только начинаете новый проект и вы и ваша команда можете свободно выбирать язык для использования, вы можете задаться вопросом: «Должен ли я использовать Rust или Go в 2023 году?».
Это зависит от сроков реализации проекта и опыта вашей команды. Если вы хотите быстро начать работу, Go может быть лучшим выбором. Он предлагает среду разработки со всеми плюшками и отлично подходит для веб-приложений.
Однако не стоит недооценивать долгосрочные преимущества Rust. Его богатая система типов в сочетании с потрясающими механизмами обработки ошибок и проверками во время компиляции может помочь вам создавать приложения, которые не только быстры, но также надежны и расширяемы.
Что касается скорости разработки, Shuttle может существенно снизить операционную нагрузку, связанную с запуском кода Rust в рабочей среде. Как мы видели, вам не нужно писать Dockerfile, чтобы начать работу, и ваш код создается в облаке, что позволяет очень быстро выполнять циклы развертывания и итерации.
Так что, если вы ищете долгосрочное решение и готовы инвестировать в изучение Rust, я бы сказал, что это отличный выбор.
Предлагаю вам сравнить оба решения и решить для себя, какое из них вам больше нравится.
В любом случае было весело создать один и тот же проект на двух разных языках и посмотреть на различия в идиомах и экосистеме. Несмотря на то, что конечный результат тот же, путь, которым мы к нему пришли, был совершенно разным.
Комментарии (26)
jetu
28.09.2023 08:00+2Конечно, в статье есть отпечаток того, что автор больше поклонник Rust и поэтому некоторые его выводы, особенно про ошибки ... своеобразны, но общий посыл имеет место.
Я сам на Go пишу уже лет 8 и последние года 2 пописываю какие-то пет-проекты свои на Rust и мои мысли схожи с некоторыми выводами автора. Rust действительно более требователен к программисту чем Go, но если предметная область обязывает (то есть это не очередной микросервис/crud) и есть время для погружения в Rust, то я бы выбирал его. А вот для перекладывания json и Go вполне сгодится, да и любого "Васю" можно за пару недель научить.bel1k0v Автор
28.09.2023 08:00-1В go мне нравится их киллер-фича с каналами, но на практике так и не удалось применить к реальной задаче (пока).
Gorthauer87
28.09.2023 08:00+2В Rust тоже сложные приложения пишут с использованием каналов, это довольно фундаментальная вещь для асинхронного программирования.
NekoiNemo
28.09.2023 08:00+3Rust разработчики полностью согласны что это "киллер-фича", поэтому ее полностью растировали: как для синхронного/тредового использования в std и разных библиотеках, так и для async в tokio и других библиотеках
aegoroff
28.09.2023 08:00+1А не удалось использовать каналы вообще? или применительно к вебу? вообще применений масса - например ограничитель параллелизма при параллельном сканировании файловой системы я делал что-то подобное тут (https://github.com/aegoroff/dirstat/blob/master/scan/walker.go), множеством горутин, ну и вообще много где можно использовать каналы.
bel1k0v Автор
28.09.2023 08:00Применительно к вебу можно сделать чат на каналах, я даже тестил но, пока оставил всё равно вариант на node.
Когда дело доходит до реальных проектов нужны модели посложнее, методов побольше, уже не так удобно иметь под капотом аналог микрофреймворка, уже нужен или Laravel (Symfony) или Spring, всё-таки там удобные абстракции
Gorthauer87
28.09.2023 08:00+2Режет слух странный и прямо таки дословный перевод слова middleware, это точно не промежуточное ПО. Это скорее про промежуточный слой обработки запросов.
Ну и в целом, статью ещё на английском видел и она больше похожа на рекламу shuttle. Может заодно стоит тогда потом написать про него, что это за штука и с чем ее едят.
bel1k0v Автор
28.09.2023 08:00В контексте фреймворка да, это какой-то слой. В любом случае это какой-то код, он может быть и больше и в контексте систем, например, тогда уже это определение вполне имеет место быть. И тот и тот вариант длинный на русском. "Мидлвэа" тоже не хочется писать.
Shuttle - облако для rustовых серверов (публичных), это уже маркетинговая часть. Симпатичный продукт, но очень уж нишевый на мой взгляд. Вроде как heroku тоже даёт деплоить приложения на rust
Gorthauer87
28.09.2023 08:00+1Но как по мне, это крайне странное решение - делать чтобы код сервиса зависел напрямую от способа развертывания. И плюс кто-то же должен его компилировать, одно лишь добавление Кафки в зависимости и привет, удачного деплоя.
bel1k0v Автор
28.09.2023 08:00Для кафки много чего требуется со стороны облака, это да, для каждого такого приложения (со всеми версиями) нужно изрядно попотеть, чтобы предоставить сервис.
NekoiNemo
28.09.2023 08:00+4Интересный момент про sqlx который автор опустил - помимо фнукций query есть еще соответствующие макро `query!()/query_as!()`, которые позволяют при компиляции чекать синтаксис запросов и их валидность в схеме бд, в том числе соответствие типов колонок тому в какие поля struct они записываются.
Правда тогда пришлось бы посвятить еще один абзац использованию `cargo sqlx prepare` чтобы заранее собрать эти метаданные на живой базе для "оффлайн" разработки и компиляции в контейнере/CI...
Gorthauer87
28.09.2023 08:00+1Вот я больше огребал проблем с этой фичей, чем пользы. Как минимум, что сборка может стать двухпроводной, а как максимум, что надо держать запущенную постгрю ради этого.
NekoiNemo
28.09.2023 08:00+1Это да. К сожалению, дамп метаданных спасает только в тех случаях когда измененный код не трогает сами запросы. Если нужно поправить их или написать новый - нужна живая база, да еще и с накатанными миграциями. С Docker, это, конечно, проще, но определенное неудобство доставляет, да.
С другой стороны полноценный синтакс-чек для embedded SQL это ну уж очень приятно
Jolt
28.09.2023 08:00-1имхо вот реальный показатель
bel1k0v Автор
28.09.2023 08:00Это из тех, которые проплачены, реальное положение вещей несколько другое.
Надо же HRам делать вид, что они работают
NekoiNemo
28.09.2023 08:00+1Это инерция. Go уже был в задеплоен в продакшене когда у Rust вэб фреймвоки и игровые движки были еще в зачаточном состоянии. Тут наоборот, тот факт что даже у нас 5% вакансий это Rust уже радует
jetu
28.09.2023 08:00+2Rust -- это игра в долгую, по разным причинам, но одна из них -- это все же системный язык. В этом мире все происходит не так реактивно как в мире "микросервисов", но зато более основательно
DarkEld3r
28.09.2023 08:00Если вопрос стоит как "какой язык изучать, чтобы побыстрее и попроще найти работу", то да и то тут джаваскрипт выиграет с большим отрывом.
titan_pc
28.09.2023 08:00+2Странные вещи. А бенчмарк то где?
Нельзя любого Васю научить Go. Любой Вася с трудом освоит даже python.
А вот утечек наплодить - Вася запросто справится. В Go 100500 нюансов по работе с горутинами, каналами, тем как там работает GC, что и где аллоцируется и не грохается зачем и почему.
Не думаю что в Rust их меньше. Нюансов.
Взять популярный фреймворк, прикрутить дрова и не дай бог ОРМ и ещё шаблонизатор страниц запихать вместо нормальной микросервисной архитектуры, где фрон и бэк разные вообще сервисы - это собрать пет проект, ради пет проекта... В продакшене увольнять надо за такое.
А если выбираете для пет проекта что-то - есть python. 10 Минут и проект готов, написан поваром в перерывах между готовкой и лайками котиков на ютубе
bel1k0v Автор
28.09.2023 08:00В этом примере бенчмаркать особо нечего, большую часть времени занимает запрос-ответ к API погоды, синтетические видимо автору не захотелось. Написать то он может и напишет, а вот развернуть вряд ли сможет ;)
Crinax
Не хотели бы сравнить frontend-фреймворки?
bel1k0v Автор
React против Vue? Лично я выбрал Vue. Хотя React является библиотекой, как и jQuery.
Akuma
Svelte же, ну че вы
bel1k0v Автор
Не работал с ним, посмотрел бегло, по сравнению с Vue делает упор на производительность, но пока не оброс так сильно. Может быть что-то найдётся со временем для практического примера. Пока не дотягивает до полноценного фреймворка и вроде как им не является по факту. Можно с Ember.js и Angular, но они как-то не сильно популярны на мой взгляд. Мне не зашли в своё время.