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 を作るなど。

コードはこちら。

https://github.com/danny-yamamoto/rust-api-samples/tree/main/users

https://github.com/danny-yamamoto/go-api-samples/tree/main/cmd/users

## Rust Web Server: /users
実装の手順
1. Response 用の struct `User` を書く
1. Request 用の struct `UserQuery` を書く
1. Handler `users_handler` を書く。Query と `IntoResponse` 以外。
1. `main` を書く
1. Query と `IntoResponse` を書く
1. db との整合性をチェック `cargo sqlx prepare --database-url "sqlite:./local.db"`
1. `rust-analyzer` の警告に対応する

```toml: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"
```

```rust: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を実装しています。コードの主要な要素を分析し、その機能を説明します。
>
> 1. ライブラリのインポート:
> std、axum、serde、dotenv、sqlxといった複数のライブラリがインポートされています。これらは標準ライブラリ、HTTPサーバー機能、シリアライズ/デシリアライズ、環境変数の読み込み、データベース操作に必要です。
> 1. ユーザー構造体の定義:
> UserとUserQueryという2つの構造体が定義されています。Userはユーザー情報を表し、Serializeトレイトを使用してJSON形式で出力可能です。UserQueryはクエリパラメータを表し、DeserializeトレイトでHTTPリクエストからデータを取得します。
> 1. ユーザー情報取得ハンドラ:
> users_handler関数は、指定されたユーザーIDに基づいてデータベースからユーザー情報を取得します。QueryとExtensionは、それぞれクエリパラメータとデータベース接続プールを取得するために使用されます。
> 1. メイン関数:
> tokio::mainアトリビュートは、非同期ランタイムを表します。環境変数からデータベースURLを取得し、SqlitePoolを介してSQLiteデータベースに接続します。Routerを使用してエンドポイント（/users）を定義し、users_handlerを関連付けます。アドレス127.0.0.1:8080でサーバーを起動し、HTTPリクエストを待機します。このコードは、指定されたユーザーIDをクエリパラメータとして受け取り、そのIDに関連するユーザー情報をデータベースから検索してJSON形式で返す簡易的なREST APIを提供します。エラーハンドリングやセキュリティ機能は基本的ですが、これはサーバーサイドのWebアプリケーションの典型的な構造を示しています。

`sqlx` により、実行前に Query の検証を行う。
```bash
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
```

- 実行
```bash
vscode ➜ /workspaces/rust-api-samples/users (main) $ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 2.83s
     Running `target/debug/users`
```

- テスト
```bash
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
実装の手順
1. `User` 構造体を書く。Rust と違い colon が不要。
1. Handler を書く
1. Handler のインスタンスを書く
1. Response 用の関数 `respondWithJSON` を書く
1. `UserHandler` を書く
1. `main` を書く
1. blank import を忘れずに書く

```go: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))
}
```