🚀
Rust vs. Go: Implementing a REST API in SQLite
Backend の開発言語選定について、現職では Go を推す声が強い。
なぜなら、現状の技術選定に、規律がないからだ。Java、Python、Ruby、PHP、Node.js、Go、Kotlin and more.
Web 業界では、Go が popular になっているし、現職の 2B・2C 向けサービスも Go を多く使っている。
僕個人として、Go を選択することに異論はない。
一方で、次を見据えて考えておく必要もある。
何が言いたいかというと、より最高な選択肢は何かを常に考えていきたい。思考停止は退化。
Rust について、
- tutorial
- gRPC
- Rust + Wasm + Cloudflare Workers
- REST
と続けてきて、syntax は身について来た。
より実践的な課題を解けるように実践を重ねていく。そして、AtCoder の algorithm 問題についても解いていく。
本題
今回も、REST + Database の API を Rust で実装し、それを Go で rewrite した。
database は、扱いやすさの観点から SQLite にした。
Rust、Go のコードを全部消して、ゼロから実装できるところまで完了した。
次は、Advent Calendar に向けて、何かお題を考えていく。Rust で Otel の tracer を作るなど。
コードはこちら。
Rust Web Server: /users
実装の手順
- Response 用の struct
User
を書く - Request 用の struct
UserQuery
を書く - Handler
users_handler
を書く。Query とIntoResponse
以外。 -
main
を書く - Query と
IntoResponse
を書く - db との整合性をチェック
cargo sqlx prepare --database-url "sqlite:./local.db"
-
rust-analyzer
の警告に対応する
Cargo.toml
[package]
name = "users"
version = "0.1.0"
edition = "2021"
[dependencies]
dotenv = "0.15.0"
serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108"
sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio-native-tls", "chrono", "runtime-tokio", "macros"] }
tokio = { version = "1.33.0", features = ["full"] }
axum = "0.6.20"
main.rs
use std::{env, sync::Arc, net::{SocketAddr, IpAddr, Ipv4Addr}};
use axum::{extract::Query, Router, routing::get, Extension, http::StatusCode, Json, response::IntoResponse};
use serde::{Serialize, Deserialize};
use dotenv::dotenv;
use sqlx::SqlitePool;
#[derive(Serialize)]
struct User {
user_id: i64,
email_address: Option<String>,
created_at: Option<i64>,
deleted: Option<i64>,
settings: Option<String>,
}
#[derive(Deserialize)]
struct UserQuery {
user_id: i64,
}
async fn users_handler(Query(query):Query<UserQuery>, Extension(pool):Extension<Arc<SqlitePool>>) -> impl IntoResponse {
let selected_user_id = query.user_id;
match sqlx::query_as!(User, "select user_id, email_address, created_at, deleted, settings from users where user_id = ?", selected_user_id).fetch_optional(&*pool).await {
Ok(user) => (StatusCode::OK, Json(user)),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, Json(None::<User>)),
}
}
#[tokio::main]
async fn main() {
dotenv().ok();
let key = "DATABASE_URL";
let db_url = env::var(key).expect("key not found.");
let pool = SqlitePool::connect(&db_url).await.expect("cannot connect.");
let shared_pool = Arc::new(pool);
let app = Router::new()
.route("/users", get(users_handler))
.layer(Extension(shared_pool));
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
GPT-4 の解説
このRustコードは、Webサーバとして機能し、SQLiteデータベースを使用してユーザー情報を提供するAPIを実装しています。コードの主要な要素を分析し、その機能を説明します。
- ライブラリのインポート:
std、axum、serde、dotenv、sqlxといった複数のライブラリがインポートされています。これらは標準ライブラリ、HTTPサーバー機能、シリアライズ/デシリアライズ、環境変数の読み込み、データベース操作に必要です。- ユーザー構造体の定義:
UserとUserQueryという2つの構造体が定義されています。Userはユーザー情報を表し、Serializeトレイトを使用してJSON形式で出力可能です。UserQueryはクエリパラメータを表し、DeserializeトレイトでHTTPリクエストからデータを取得します。- ユーザー情報取得ハンドラ:
users_handler関数は、指定されたユーザーIDに基づいてデータベースからユーザー情報を取得します。QueryとExtensionは、それぞれクエリパラメータとデータベース接続プールを取得するために使用されます。- メイン関数:
tokio::mainアトリビュートは、非同期ランタイムを表します。環境変数からデータベースURLを取得し、SqlitePoolを介してSQLiteデータベースに接続します。Routerを使用してエンドポイント(/users)を定義し、users_handlerを関連付けます。アドレス127.0.0.1:8080でサーバーを起動し、HTTPリクエストを待機します。このコードは、指定されたユーザーIDをクエリパラメータとして受け取り、そのIDに関連するユーザー情報をデータベースから検索してJSON形式で返す簡易的なREST APIを提供します。エラーハンドリングやセキュリティ機能は基本的ですが、これはサーバーサイドのWebアプリケーションの典型的な構造を示しています。
sqlx
により、実行前に Query の検証を行う。
vscode ➜ /workspaces/rust-api-samples/users (main) $ cargo sqlx prepare --database-url "sqlite:./local.db"
Checking users v0.1.0 (/workspaces/rust-api-samples/users)
Finished dev [unoptimized + debuginfo] target(s) in 5.14s
query data written to .sqlx in the current directory; please check this into version control
- 実行
vscode ➜ /workspaces/rust-api-samples/users (main) $ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 2.83s
Running `target/debug/users`
- テスト
vscode ➜ /workspaces/rust-api-samples (main) $ curl "localhost:8080/users?user_id=1" -i
HTTP/1.1 200 OK
content-type: application/json
content-length: 90
date: Sun, 12 Nov 2023 03:23:25 GMT
{"user_id":1,"email_address":"maria@example.com","created_at":0,"deleted":0,"settings":""}
vscode ➜ /workspaces/rust-api-samples (main) $
Rust to Go Conversion
実装の手順
-
User
構造体を書く。Rust と違い colon が不要。 - Handler を書く
- Handler のインスタンスを書く
- Response 用の関数
respondWithJSON
を書く -
UserHandler
を書く -
main
を書く - blank import を忘れずに書く
main.go
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
UserId int64 `json:"user_id"`
EmailAddress string `json:"email_address"`
CreatedAt int64 `json:"created_at"`
Deleted int64 `json:"deleted"`
Settings string `json:"settings"`
}
type Handler struct {
db *sql.DB
}
func NewHandler(db *sql.DB) *Handler {
return &Handler{db: db}
}
func respondWithJSON(w http.ResponseWriter, statusCode int, payload interface{}) {
response, _ := json.Marshal(payload)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(statusCode)
w.Write(response)
}
func (h Handler) UserHandler(w http.ResponseWriter, r *http.Request) {
userId := r.URL.Query().Get("user_id")
var user User
err := h.db.QueryRow("select * from users where user_id = ?", userId).Scan(&user.UserId, &user.EmailAddress, &user.CreatedAt, &user.Deleted, &user.Settings)
if err != nil {
fmt.Println(err)
respondWithJSON(w, http.StatusInternalServerError, err)
return
}
respondWithJSON(w, http.StatusOK, user)
}
func main() {
key := "DATABASE_URL"
dbUrl := os.Getenv(key)
client, err := sql.Open("sqlite3", dbUrl)
if err != nil {
fmt.Printf("Failed to create connection: %s", err)
return
}
defer client.Close()
handler := NewHandler(client)
http.HandleFunc("/users", handler.UserHandler)
port := "8080"
addr := fmt.Sprintf("0.0.0.0:%s", port)
fmt.Printf("Listening on http://%s\n", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}
Discussion