国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Rust Vs Go:從頭構(gòu)建一個web服務(wù)

這篇具有很好參考價值的文章主要介紹了Rust Vs Go:從頭構(gòu)建一個web服務(wù)。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

rust語言最新消息,rust,golang,前端

Go 和 Rust 之間的許多比較都強調(diào)它們在語法和初始學(xué)習(xí)曲線上的差異。然而,最終的決定性因素是重要項目的易用性。

“Rust 與 Go”爭論

Rust vs Go 是一個不斷出現(xiàn)的話題,并且已經(jīng)有很多關(guān)于它的文章。部分原因是開發(fā)人員正在尋找信息來幫助他們決定下一個 Web 項目使用哪種語言,而這兩種語言在這種情況下都經(jīng)常被提及。我們環(huán)顧四周,但確實沒有太多關(guān)于該主題的深入內(nèi)容,因此開發(fā)人員只能自己解決這個問題,并冒著由于誤導(dǎo)性原因而過早放棄某個選項的風(fēng)險。

這兩個社區(qū)都經(jīng)常面臨誤解和偏見。一些人將 Rust 主要視為一種系統(tǒng)編程語言,質(zhì)疑它是否適合 Web 開發(fā)。與此同時,其他人認為 Go 過于簡單,懷疑它處理復(fù)雜 Web 應(yīng)用程序的能力。然而,這些都只是表面的判斷。

事實上,這兩種語言都可以用來編寫快速可靠的 Web 服務(wù)。然而,他們的方法截然不同,很難找到一個對兩者都公平的比較。

這篇文章是我們試圖通過用兩種語言構(gòu)建一個重要的現(xiàn)實世界中的應(yīng)用程序來概述 Go 和 Rust 之間的差異,重點是 Web 開發(fā)。我們將超越語法,并仔細研究這些語言如何處理典型的 Web 任務(wù),如路由、中間件、模板、數(shù)據(jù)庫訪問等。

讀完本文后,您應(yīng)該清楚哪種語言適合您。

盡管我們知道自己的偏見和偏好,但我們將盡力保持客觀并強調(diào)兩種語言的優(yōu)點和缺點。

構(gòu)建一個小型web服務(wù)

我們將討論以下主題:

  • Routing 路由
  • Templating 模板
  • Database access 數(shù)據(jù)庫訪問
  • Deployment 部署

我們將省略客戶端渲染或數(shù)據(jù)庫遷移等主題,只關(guān)注服務(wù)器端。

任務(wù)

選擇一個代表 Web 開發(fā)的任務(wù)并不容易:一方面,我們希望保持它足夠簡單,以便我們可以專注于語言功能和庫。另一方面,我們希望確保任務(wù)不會太簡單,以便我們可以展示如何在現(xiàn)實環(huán)境中使用語言功能和庫。

我們決定建立天氣預(yù)報服務(wù)。用戶應(yīng)該能夠輸入城市名稱并獲取該城市當(dāng)前的天氣預(yù)報。該服務(wù)還應(yīng)該顯示最近搜索過的城市列表。

隨著我們擴展服務(wù),我們將添加以下功能:

  • 一個簡單的 UI 顯示天氣預(yù)報
  • 用于存儲最近搜索的城市的數(shù)據(jù)庫

The Weather API 天氣 API

對于天氣預(yù)報,我們將使用 Open-Meteo API,因為它是開源的、易于使用,并且為非商業(yè)用途提供慷慨的免費套餐,每天最多可處理 10,000 個請求。

我們將使用這兩個 API 接口:

  • 用于獲取城市坐標的 GeoCoding API 。
  • Weather Forecast API ,用于獲取給定坐標的天氣預(yù)報。

這兩種語言都有現(xiàn)成的庫可用,Go (omgo) 和 Rust (openmeteo) ,我們將在生產(chǎn)服務(wù)中使用它們。然而,為了進行比較,我們希望了解如何用兩種語言發(fā)出“原始”HTTP 請求并將響應(yīng)轉(zhuǎn)換為常用的數(shù)據(jù)結(jié)構(gòu)。

Go Web 服務(wù)

選擇網(wǎng)絡(luò)框架

Go 最初是為了簡化構(gòu)建 Web 服務(wù)而創(chuàng)建的,它擁有許多很棒的與 Web 相關(guān)的包。如果標準庫不能滿足您的需求,還有許多流行的第三方 Web 框架可供選擇,例如 Gin、Echo 或 Chi。

選擇哪一個是個人喜好問題。一些經(jīng)驗豐富的 Go 開發(fā)人員更喜歡使用標準庫,并在其之上添加像 Chi 這樣的路由庫。其他人則更喜歡包含更多內(nèi)置功能的方法,使用功能齊全的框架,例如 Gin 或 Echo。

這兩個選項都很好,但為了比較的目的,我們將選擇 Gin,因為它是最流行的框架之一,并且它支持我們的天氣服務(wù)所需的所有功能。

HTTP 請求

讓我們從一個簡單的函數(shù)開始,該函數(shù)向 Open Meteo API 發(fā)出 HTTP 請求并以字符串形式返回響應(yīng)正文:

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
}

該函數(shù)將城市名稱作為參數(shù),并以 LatLong 結(jié)構(gòu)體的形式返回城市的坐標。

請注意我們在每個步驟之后如何處理錯誤:我們檢查 HTTP 請求是否成功、響應(yīng)正文是否可以解碼以及響應(yīng)是否包含任何結(jié)果。如果這些步驟中的任何一個失敗,我們將返回錯誤并中止該函數(shù)。到目前為止,我們只需要使用標準庫,這樣挺好。

defer 語句確保響應(yīng)正文在函數(shù)返回后關(guān)閉。這是 Go 中避免資源泄漏的常見模式。如果我們忘記了,編譯器不會警告我們,所以我們在這里需要小心。

錯誤處理占據(jù)了代碼的很大一部分。它很簡單,但編寫起來可能很乏味,并且會使代碼更難閱讀。從好的方面來說,錯誤處理很容易遵循,并且很清楚發(fā)生錯誤時會發(fā)生什么。

由于 API 返回帶有結(jié)果列表的 JSON 對象,因此我們需要定義一個與該響應(yīng)匹配的結(jié)構(gòu):

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 標簽(tag)告訴 JSON 解碼器如何將 JSON 字段映射到結(jié)構(gòu)體字段。默認情況下,JSON 響應(yīng)中的額外字段將被忽略。

讓我們定義另一個函數(shù),它采用 LatLong 結(jié)構(gòu)并返回該位置的天氣預(yù)報:

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
}

首先,讓我們按順序調(diào)用這兩個函數(shù)并打印結(jié)果:

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)
}

This will print the following output:
這將打印以下輸出:

Latitude: 51.508530, Longitude: -0.125740
Weather: {"latitude":51.5,"longitude":-0.120000124, ... }

漂亮!我們得到了倫敦的天氣預(yù)報。讓我們將其作為 Web 服務(wù)提供。

Routing 路由

路由是 Web 框架最基本的任務(wù)之一。首先,讓我們將 gin 添加到我們的項目中。

go mod init github.com/user/goforecast
go get -u github.com/gin-gonic/gin

然后,我們將 main() 函數(shù)替換為服務(wù)器和路由,該路由將城市名稱作為參數(shù)并返回該城市的天氣預(yù)報。

Gin 支持路徑參數(shù)和查詢參數(shù)。

// 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")
    // ...
})

您想使用哪一種取決于您的用例。在我們的例子中,我們希望最終從表單提交城市名稱,因此我們將使用查詢參數(shù)。

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 . 啟動服務(wù)器并向其發(fā)出請求:

curl "localhost:8080/weather?city=Hamburg"

我們得到天氣預(yù)報:

{"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"
Templates 模板

我們完成了api服務(wù)端,但原始 JSON 對于普通用戶來說并不是很有用。在現(xiàn)實應(yīng)用程序中,我們可能會在 API 端點(例如 /api/v1/weather/:city )上提供 JSON 響應(yīng),并返回一個 HTML 頁面。為了簡單起見,我們直接返回 HTML 頁面。

讓我們添加一個簡單的 HTML 頁面,以表格形式顯示給定城市的天氣預(yù)報。我們將使用標準庫中的 html/template 包來呈現(xiàn) HTML 頁面。

首先,我們?yōu)橐晥D添加一些結(jié)構(gòu):

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 響應(yīng)中相關(guān)字段到結(jié)構(gòu)的直接映射。有一些工具,例如transform,可以使從JSON 到Go 結(jié)構(gòu)的轉(zhuǎn)換變得更容易。你可以試一下!

接下來我們定義一個函數(shù),它將來自天氣 API 的原始 JSON 響應(yīng)轉(zhuǎn)換為新的 WeatherDisplay 結(jié)構(gòu):

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
}

日期處理是通過內(nèi)置的 time 包完成的。要了解有關(guān) Go 中日期處理的更多信息,請查看這篇文章。

我們擴展路由處理程序來呈現(xiàn) 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)
    //
})

接下來讓我們處理模板。創(chuàng)建一個名為 views 的模板目錄并告訴 Gin:

r := gin.Default()
r.LoadHTMLGlob("views/*")

最后,我們可以在 views 目錄下創(chuàng)建一個模板文件 weather.html

<!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>

(有關(guān)如何使用模板的更多詳細信息,請參閱 Gin 文檔。)

這樣,我們就有了一個可用的 Web 服務(wù),它以 HTML 頁面的形式返回給定城市的天氣預(yù)報!

差點忘了!也許我們還想創(chuàng)建一個帶有輸入字段的index頁面,它允許我們輸入城市名稱并顯示該城市的天氣預(yù)報。

讓我們?yōu)閕ndex頁添加一個新的路由處理程序:

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>

現(xiàn)在我們可以啟動 Web 服務(wù)并在瀏覽器中打開 http://localhost:8080:

rust語言最新消息,rust,golang,前端

倫敦的天氣預(yù)報是這樣的。它不漂亮,但是…實用! (它無需 JavaScript 即可在終端瀏覽器中運行?。?/p>

rust語言最新消息,rust,golang,前端

作為練習(xí),您可以向 HTML 頁面添加一些樣式,但由于我們更關(guān)心后端,因此我們將保留它。

數(shù)據(jù)訪問

我們的服務(wù)根據(jù)每個請求從外部 API 獲取給定城市的緯度和經(jīng)度。一開始這可能沒問題,但最終我們可能希望將結(jié)果緩存在數(shù)據(jù)庫中以避免不必要的 API 調(diào)用。

為此,我們將數(shù)據(jù)庫添加到我們的 Web 服務(wù)中。我們將使用 PostgreSQL 作為數(shù)據(jù)庫,使用 sqlx 作為數(shù)據(jù)庫驅(qū)動程序。

首先,我們創(chuàng)建一個名為 init.sql 的文件,它將用于初始化我們的數(shù)據(jù)庫:

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);

我們存儲給定城市的緯度和經(jīng)度。 SERIAL 類型是 PostgreSQL 自增整數(shù)。為了加快速度,我們還將在 name 列上添加索引。

使用 Docker 或任何云提供商可能是最簡單的。最終,您只需要一個數(shù)據(jù)庫 URL,您可以將其作為環(huán)境變量傳遞到您的 Web 服務(wù)。

我們不會在這里詳細介紹設(shè)置數(shù)據(jù)庫的細節(jié),但在本地使用 Docker 運行 PostgreSQL 數(shù)據(jù)庫的一個簡單方法是:

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"

然而,一旦我們有了數(shù)據(jù)庫,我們需要將 sqlx 依賴項添加到 go.mod 文件中:

go get github.com/jmoiron/sqlx

現(xiàn)在,我們可以使用 sqlx 包通過 DATABASE_URL 環(huán)境變量中的連接字符串連接到我們的數(shù)據(jù)庫:

_ = sqlx.MustConnect("postgres", os.Getenv("DATABASE_URL"))

這樣,我們就獲取了一個數(shù)據(jù)庫連接!

讓我們添加一個函數(shù)來將城市插入到我們的數(shù)據(jù)庫中。我們將使用之前的 LatLong 結(jié)構(gòu)。

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
}

讓我們將舊的 getLatLong 函數(shù)重命名為 fetchLatLong 并添加一個新的 getLatLong 函數(shù),該函數(shù)使用數(shù)據(jù)庫而不是外部 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 函數(shù)。在實際應(yīng)用中,我們應(yīng)該將數(shù)據(jù)庫訪問與API邏輯解耦,以使測試成為可能。我們可能還會使用內(nèi)存緩存來避免不必要的數(shù)據(jù)庫調(diào)用。這只是為了比較 Go 和 Rust 中的數(shù)據(jù)庫訪問。

我們需要更新我們的處理程序:

r.GET("/weather", func(c *gin.Context) {
    city := c.Query("city")
    // Pass in the db
    latlong, err := getLatLong(db, city)
    // ...
})

這樣,我們就有了一個可用的 Web 服務(wù),它將給定城市的緯度和經(jīng)度存儲在數(shù)據(jù)庫中,并在后續(xù)請求時從那里獲取它。

Middleware 中間件

最后一點是向我們的 Web 服務(wù)添加一些中間件。我們已經(jīng)從 Gin 免費獲得了一些不錯的日志記錄。

讓我們添加一個基本身份驗證中間件并保護我們的 /stats 端點,我們將使用它來打印最后的搜索查詢。

r.GET("/stats", gin.BasicAuth(gin.Accounts{
        "forecast": "forecast",
    }), func(c *gin.Context) {
        // rest of the handler
    }
)

就這樣!

專業(yè)提示:您還可以將路由分組在一起,以便一次對多個路由應(yīng)用身份驗證。

以下是從數(shù)據(jù)庫中獲取最后搜索查詢的邏輯:

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
}

現(xiàn)在讓我們連接 /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>

這樣,我們就有了一個可以運行的web服務(wù)!恭喜!

我們?nèi)〉昧艘韵鲁删停?/p>

  • 從外部 API 獲取給定城市的緯度和經(jīng)度的 Web 服務(wù)
  • 將緯度和經(jīng)度存儲在數(shù)據(jù)庫中
  • 在后續(xù)請求中從數(shù)據(jù)庫獲取緯度和經(jīng)度
  • 打印 /stats 端點上的最后一個搜索查詢
  • 用于保護 /stats 端點的基本身份驗證
  • 使用中間件記錄請求
  • 用于呈現(xiàn) HTML 的模板

對于幾行代碼來說,這已經(jīng)是相當(dāng)多的功能了!讓我們看看 Rust 表現(xiàn)如何!

Rust Web 服務(wù)

從歷史上看,Rust 對于 Web 服務(wù)并沒有一個好的支持。有一些框架,但它們的級別相當(dāng)?shù)?。直?async/await 的出現(xiàn),Rust Web 生態(tài)系統(tǒng)才真正起飛。突然間,無需垃圾收集器且具有無所畏懼的并發(fā)性就可以編寫高性能的 Web 服務(wù)。

我們將了解 Rust 與 Go 在人體工程學(xué)、性能和安全性方面的比較。但首先,我們需要選擇一個 Web 框架。

哪個網(wǎng)絡(luò)框架?

如果您希望更好地了解 Rust Web 框架及其優(yōu)缺點,我們最近對 Rust Web 框架進行了深入研究。

出于本文的目的,我們考慮兩個 Web 框架:Actix 和 Axum。

Actix 是 Rust 社區(qū)中非常流行的 Web 框架。它基于 Actor 模型,并在底層使用 async/await。在基準測試中,它經(jīng)常被評為世界上最快的 Web 框架之一。

另一方面,Axum 是一個基于 tower 的新 Web 框架,tower 是一個用于構(gòu)建異步服務(wù)的庫。它正在迅速流行。它也是基于async/await。

兩個框架在人體工程學(xué)和性能方面非常相似。它們都支持中間件和路由。對于我們的網(wǎng)絡(luò)服務(wù)來說,它們都是不錯的選擇,但我們會選擇 Axum,因為它與生態(tài)系統(tǒng)的其他部分緊密結(jié)合,并且最近得到了很多關(guān)注。

Routing 路由

讓我們從 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"] }

讓我們?yōu)槲覀兊?Web 服務(wù)創(chuàng)建一個小框架,它的作用不大。

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 函數(shù)非常簡單。我們創(chuàng)建一個路由器并將其綁定到一個套接字地址。 index 、 weatherstats 函數(shù)是我們的處理程序。它們是返回字符串的異步函數(shù)。稍后我們將用實際邏輯替換它們。

讓我們使用 cargo run 運行 Web 服務(wù),看看會發(fā)生什么。

$ curl localhost:3000
Index
$ curl localhost:3000/weather
Weather
$ curl localhost:3000/stats
Stats

好吧,可以運行。讓我們向處理程序添加一些實際邏輯。

Axum macros

在我們繼續(xù)之前,我想提一下 axum 有一些粗糙的地方。例如。如果忘記把處理程序函數(shù)標記為 async,它會報出很多錯誤。因此,如果您遇到 Handler<_, _> is not implemented 錯誤,請?zhí)砑?axum-macros crate并使用 #[axum_macros::debug_handler] 注釋您的處理程序。這將為您提供更好的錯誤消息。

獲取緯度和經(jīng)度

讓我們編寫一個函數(shù),從外部 API 獲取給定城市的緯度和經(jīng)度。

以下是表示 API 響應(yīng)的結(jié)構(gòu):

use serde::Deserialize;

pub struct GeoResponse {
    pub results: Vec<LatLong>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct LatLong {
    pub latitude: f64,
    pub longitude: f64,
}

與 Go 相比,我們不使用標簽來指定字段名稱。相反,我們使用 serde 中的 #[derive(Deserialize)] 屬性來自動派生結(jié)構(gòu)的 Deserialize 特征。這些派生宏非常強大,允許我們用很少的代碼做很多事情,包括處理類型的解析錯誤。這是 Rust 中非常常見的模式。

讓我們使用新類型來獲取給定城市的緯度和經(jīng)度:

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 構(gòu)造,因為我們可以使用 ? 運算符來傳播錯誤。這也是強制性的,因為每個步驟都會返回一個 Result 類型。如果我們不處理錯誤,我們將無法訪問該值。

最后一部分可能看起來有點陌生:

response
    .results
    .get(0)
    .cloned()
    .ok_or("No results found".into())

這里發(fā)生了一些事情:

  • response.results.get(0) 返回 Option<&LatLong> 。它是 Option ,因為如果向量為空, get 函數(shù)可能會返回 None
  • cloned() 拷貝Option 內(nèi)的值并將 Option<&LatLong> 轉(zhuǎn)換為 Option<LatLong> 。這是必要的,因為我們想要返回 LatLong 而不是引用。否則,我們必須在函數(shù)簽名中添加生命周期說明符,這會降低代碼的可讀性。
  • ok_or("No results found".into())Option<LatLong> 轉(zhuǎn)換為 Result<LatLong, Box<dyn std::error::Error>> 。如果 OptionNone ,則會返回錯誤信息。 into() 函數(shù)將字符串轉(zhuǎn)換為 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 從函數(shù)返回值。相反,返回函數(shù)的最后一個值。

我們現(xiàn)在可以更新 weather 函數(shù)以使用 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)
}

首先,我們將城市打印到控制臺,然后獲取緯度和經(jīng)度并unwrap(即“unwrap”)結(jié)果。如果結(jié)果錯誤,程序就會出現(xiàn)恐慌。這并不理想,但我們稍后會修復(fù)它。

然后,我們使用緯度和經(jīng)度創(chuàng)建一個字符串并返回它。

讓我們運行該程序,看看會發(fā)生什么:

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 參數(shù)為空。what???

問題是我們使用 String 類型作為 city 參數(shù)。此類型不是有效的提取器(extractor)。

我們可以使用 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 術(shù)語中稱為“解引用”。)

我們可以創(chuàng)建一個表示查詢參數(shù)的結(jié)構(gòu):

#[derive(Deserialize)]
pub struct WeatherQuery {
    pub city: String,
}

然后我們可以使用這個結(jié)構(gòu)作為提取器(extractor)并避免 unwrap

async fn weather(Query(params): Query<WeatherQuery>) -> String {
    let lat_long = fetch_lat_long(&params.city).await.unwrap();
    format!("{}: {}, {}", params.city, lat_long.latitude, lat_long.longitude)
}

整潔多了!它比 Go 版本稍微復(fù)雜一些,但它也更類型安全。您可以想象我們可以向結(jié)構(gòu)添加約束以添加驗證。例如,我們可能要求城市的長度至少為 3 個字符。

現(xiàn)在介紹 weather 函數(shù)中的 unwrap 。理想情況下,如果找不到城市,我們會返回錯誤。我們可以通過更改返回類型來做到這一點。

在 axum 中,任何實現(xiàn) IntoResponse 的內(nèi)容都可以從處理程序返回,但是建議返回具體類型,因為[返回 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(&params.city).await {
        Ok(lat_long) => Ok(format!(
            "{}: {}, {}",
            params.city, lat_long.latitude, lat_long.longitude
        )),
        Err(_) => Err(StatusCode::NOT_FOUND),
    }
}

如果未找到城市,這將返回 404 狀態(tài)代碼。我們使用 match 來匹配 fetch_lat_long 的結(jié)果。如果是 Ok ,我們將天氣作為 String 返回。如果是 Err ,我們返回 StatusCode::NOT_FOUND 。

我們還可以使用 map_err 函數(shù)將錯誤轉(zhuǎn)換為 StatusCode

async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, StatusCode> {
    let lat_long = fetch_lat_long(&params.city)
        .await
        .map_err(|_| StatusCode::NOT_FOUND)?;
    Ok(format!(
        "{}: {}, {}",
        params.city, lat_long.latitude, lat_long.longitude
    ))
}

這種變體的優(yōu)點是我們的控制流更加線性:我們立即處理錯誤,然后可以繼續(xù)正常的路徑。另一方面,需要一段時間才能習(xí)慣這些組合器模式,直到它們成為第二種天性。

在 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

讓我們編寫第二個函數(shù),它將返回給定緯度和經(jīng)度的天氣:

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)
}

在這里,我們發(fā)出 API 請求并以 String 形式返回原始響應(yīng)正文。

我們可以擴展我們的處理程序以連續(xù)進行兩個調(diào)用:

async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, StatusCode> {
    let lat_long = fetch_lat_long(&params.city)
        .await
        .map_err(|_| StatusCode::NOT_FOUND)?;
    let weather = fetch_weather(lat_long)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    Ok(weather)
}

這可行,但它會從 Open Meteo API 返回原始響應(yīng)正文。讓我們解析響應(yīng)并返回類似于 Go 版本的數(shù)據(jù)。

提醒一下,這是 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>,
}

在這樣做的同時,還可以定義需要的其他結(jié)構(gòu):

#[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,
}

現(xiàn)在可以將響應(yīng)主體解析為結(jié)構(gòu):

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)
}

讓我們調(diào)整處理程序。使其編譯的最簡單方法是返回 String

async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, StatusCode> {
    let lat_long = fetch_lat_long(&params.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))
}

請注意我們?nèi)绾螌⒔馕鲞壿嬇c處理程序邏輯混合在一起。讓我們通過將解析邏輯移至構(gòu)造函數(shù)中來清理一下:

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
    }
}

這是一個開始。我們的處理程序現(xiàn)在看起來像這樣:

async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, StatusCode> {
    let lat_long = fetch_lat_long(&params.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))
}

這已經(jīng)好一點了。令人分心的是 map_err 樣板代碼。我們可以通過引入自定義錯誤類型來刪除它。例如,我們可以按照 axum 存儲庫中的示例,無論如何使用一個流行的錯誤處理包:

cargo add anyhow

Let’s copy the code from the example into our project:
讓我們將示例中的代碼復(fù)制到我們的項目中:

// 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())
    }
}

您不必完全理解這段代碼??梢哉f,這將為應(yīng)用程序設(shè)置錯誤處理,這樣我們就不必在處理程序中處理它。

我們必須調(diào)整 fetch_lang_longfetch_weather 函數(shù)以返回 Resultanyhow::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
}

以添加依賴項并添加用于錯誤處理的附加樣板為代價,我們設(shè)法簡化了我們的處理程序:

async fn weather(Query(params): Query<WeatherQuery>) -> Result<String, AppError> {
    let lat_long = fetch_lat_long(&params.city).await?;
    let weather = fetch_weather(lat_long).await?;
    let display = WeatherDisplay::new(params.city, weather);
    Ok(format!("{:?}", display))
}
Templates 模板

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

讓我們創(chuàng)建一個 templates 目錄并添加一個 weather.html 模板,類似于我們之前創(chuàng)建的 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 結(jié)構(gòu)轉(zhuǎn)換為 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(&params.city).await?;
    let weather = fetch_weather(lat_long).await?;
    Ok(WeatherDisplay::new(params.city, weather))
}

到達這里需要做一些工作,但我們現(xiàn)在已經(jīng)很好地分離了關(guān)注點,沒有太多的樣板代碼。

如果您在 http://localhost:3000/weather?city=Berlin 打開瀏覽器,您應(yīng)該會看到天氣表。

添加我們的輸入很容易。我們可以使用與 Go 版本完全相同的 HTML:

<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
}

讓我們繼續(xù)將緯度和經(jīng)度存儲在數(shù)據(jù)庫中。

數(shù)據(jù)訪問

我們將使用 sqlx 進行數(shù)據(jù)庫訪問。這是一個非常受歡迎的包,支持多個數(shù)據(jù)庫。在我們的例子中,我們將使用 Postgres,就像在 Go 版本中一樣。

Add this to your Cargo.toml:
將其添加到您的 Cargo.toml 中:

sqlx = { version = "0.7", features = [
    "runtime-tokio-rustls",
    "macros",
    "any",
    "postgres",
] }

需要將 DATABASE_URL 環(huán)境變量添加到 .env 文件中:

export DATABASE_URL="postgres://forecast:forecast@localhost:5432/forecast?sslmode=disable"

如果 Postgres 尚未運行,您可以使用 Go 部分中的相同 Docker 代碼片段來啟動它。

這樣,??調(diào)整代碼以使用數(shù)據(jù)庫。首先是 main 函數(shù):

#[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 環(huán)境變量并在 main 中讀取它。
  • 使用 sqlx::PgPool::connect 創(chuàng)建一個數(shù)據(jù)庫連接池。
  • 然后將pool傳遞給 with_state 以使其可供所有處理程序使用。

在每個路由中,可以(但不必)像這樣訪問數(shù)據(jù)庫池:

async fn weather(
    Query(params): Query<WeatherQuery>,
    State(pool): State<PgPool>,
) -> Result<WeatherDisplay, AppError> {
    let lat_long = fetch_lat_long(&params.city).await?;
    let weather = fetch_weather(lat_long).await?;
    Ok(WeatherDisplay::new(params.city, weather))
}

要了解有關(guān) State 的更多信息,請查看文檔。

為了使我們的數(shù)據(jù)可以從數(shù)據(jù)庫中獲取,我們需要向結(jié)構(gòu)添加 FromRow 特征:

#[derive(sqlx::FromRow, Deserialize, Debug, Clone)]
pub struct LatLong {
    pub latitude: f64,
    pub longitude: f64,
}

讓我們添加一個函數(shù)來從數(shù)據(jù)庫中獲取緯度和經(jīng)度:

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 路由以使用新函數(shù):

async fn weather(
    Query(params): Query<WeatherQuery>,
    State(pool): State<PgPool>,
) -> Result<WeatherDisplay, AppError> {
    let lat_long = fetch_lat_long(&params.city).await?;
    let weather = fetch_weather(lat_long).await?;
    Ok(WeatherDisplay::new(params.city, weather))
}

就是這樣!我們現(xiàn)在有了一個帶有數(shù)據(jù)庫后端的可用 Web 應(yīng)用程序。功能與之前相同,但現(xiàn)在我們緩存緯度和經(jīng)度。

Middleware 中間件

比Go 版本中缺少的最后一個功能是 /stats api。請記住,它顯示最近的查詢并且支持基本身份驗證。

讓我們從基本的身份驗證開始。

我花了一段時間才弄清楚如何做到這一點。 axum 有許多身份驗證庫,但有關(guān)如何進行基本身份驗證的信息很少。

我最終編寫了一個自定義中間件,這將

  • 檢查請求是否有 Authorization 標頭
  • 如果是,檢查標頭是否包含有效的用戶名和密碼
  • 如果是,則返回“未經(jīng)授權(quán)”響應(yīng)和 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 是一個允許我們從請求中提取數(shù)據(jù)的特征。還有 FromRequest,它消耗整個請求正文,因此只能為處理程序運行一次。在我們的例子中,我們只需要讀取 Authorization 標頭,因此 FromRequestParts 就足夠了。

美妙之處在于,我們可以簡單地將 User 類型添加到任何處理程序中,它將從請求中提取用戶:

async fn stats(user: User) -> &'static str {
    "We're authorized!"
}

現(xiàn)在了解 /stats api的實際邏輯。

#[derive(Template)]
#[template(path = "stats.html")]
struct StatsTemplate {
    pub cities: Vec<City>,
}

async fn get_last_cities(pool: &PgPool) -> Result<Vec<City>, AppError> {
    let cities = sqlx::query_as::<_, City>("SELECT name FROM cities ORDER BY id DESC LIMIT 10")
        .fetch_all(pool)
        .await?;
    Ok(cities)
}

async fn stats(_user: User, State(pool): State<PgPool>) -> Result<StatsTemplate, AppError> {
    let cities = get_last_cities(&pool).await?;
    Ok(StatsTemplate { cities })
}

Deployment 部署

最后,我們來談?wù)劜渴稹?/p>

由于這兩種語言都編譯為靜態(tài)鏈接的二進制文件,因此它們可以托管在任何虛擬機 (VM) 或虛擬專用服務(wù)器 (VPS) 上。這是令人驚奇的,因為這意味著如果您愿意,您可以在裸機上本機運行您的應(yīng)用程序。

另一種選擇是使用容器,它在隔離的環(huán)境中運行您的應(yīng)用程序。它們非常受歡迎,因為它們易于使用并且?guī)缀蹩梢圆渴鹪谌魏蔚胤健?/p>

對于 Golang,您可以使用任何支持運行靜態(tài)二進制文件或容器的云提供商。更受歡迎的選項之一是 Google Cloud Run。

當(dāng)然,您也可以使用容器來運送 Rust,但還有其他選擇。當(dāng)然,其中之一就是 Shuttle,它的工作方式與其他服務(wù)不同:您不需要構(gòu)建 Docker 映像并將其推送到注冊表。相反,您只需將代碼推送到 Git 存儲庫,Shuttle 就會為您構(gòu)建并運行二進制文件。

借助 Rust 的過程宏,您可以通過附加功能快速增強代碼。

只需在 main 函數(shù)上使用 #[shuttle_runtime::main] 即可開始:

#[shuttle_runtime::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Rest of your code goes here
}

首先,安裝 Shuttle CLI 和依賴項。

您可以使用 Cargo binstall,這是一個 Cargo 插件,旨在安裝來自 crates.io 的二進制文件。首先,確保您已安裝該插件。之后,您將能夠安裝 Shuttle CLI:

cargo binstall cargo-shuttle
cargo add shuttle-axum shuttle-runtime

讓我們修改 main 函數(shù)以使用 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())
}

接下來,讓我們設(shè)置生產(chǎn) postgres 數(shù)據(jù)庫。也有一個宏。

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())
}

看到關(guān)于架構(gòu)的部分了嗎?這就是我們?nèi)绾问褂矛F(xiàn)有的表定義來初始化數(shù)據(jù)庫。還通過 sqlx 和 sqlx-cli 支持遷移。

我們擺脫了很多樣板代碼,現(xiàn)在可以輕松部署我們的應(yīng)用程序。

# We only need to run this once
cargo shuttle project start

# Run as often as you like
cargo shuttle deploy

When it’s done, it will print the URL to the service. It should work just like before, but now it’s running on a server in the cloud. ??
完成后,它將打印服務(wù)的 URL。它應(yīng)該像以前一樣工作,但現(xiàn)在它在云中的服務(wù)器上運行。 ??

Go 和 Rust 的對比

讓我們看看這兩個版本如何相互比較。

Go 版本

Go 版本非常簡單明了。我們只需要添加兩個依賴項: Gin (Web 框架)和 sqlx (數(shù)據(jù)庫驅(qū)動程序)。除此之外,標準庫提供了所有內(nèi)容:模板引擎、JSON 解析器、日期時間處理等。

盡管我個人不太喜歡 Go 的模板引擎和錯誤處理機制,但我在整個開發(fā)過程中都感到富有成效。我們本來可以使用外部模板庫,但我們不需要這樣做,因為內(nèi)置模板庫非常適合我們的用例。如果您希望在項目中利用 Go 的強大功能,您可能需要聘請 Golang 開發(fā)人員。

Rust 版本

Rust 代碼涉及更多一些。我們需要添加很多依賴項才能獲得與 Go 中相同的功能。例如,我們需要添加模板引擎、JSON 解析器、日期時間庫、數(shù)據(jù)庫驅(qū)動程序和 Web 框架。

這是設(shè)計使然。 Rust 的標準庫非常小,只提供最基本的構(gòu)建塊。這個想法是,您可以選擇項目所需的依賴項。它有助于生態(tài)系統(tǒng)更快地發(fā)展,并允許進行更多實驗,同時語言核心保持穩(wěn)定。

盡管開始花費了更長的時間,但我很享受逐步提升到更高抽象級別的過程。我從來沒有覺得自己陷入了次優(yōu)的解決方案之中。有了適當(dāng)?shù)某橄螅?? 運算符和 FromRequest 特征,代碼就很容易閱讀,沒有任何樣板文件或不必要的冗長錯誤處理。

Summary 總結(jié)

  • Go:

    • 易于學(xué)習(xí)、快速、適合 Web 服務(wù)
    • 豐富的內(nèi)置功能。我們僅使用標準庫就做了很多事情。例如,我們不需要添加模板引擎或單獨的身份驗證庫。
    • 我們唯一的外部依賴項是 Ginsqlx
  • Rust: 銹:

    • 快速、安全、不斷發(fā)展的網(wǎng)絡(luò)服務(wù)生態(tài)系統(tǒng)
    • 內(nèi)置功能較少。我們必須添加大量依賴項才能獲得與 Go 中相同的功能并編寫我們自己的小型中間件。
    • 最終的處理程序代碼沒有分散注意力的錯誤處理,因為我們使用了自己的錯誤類型和 ? 運算符。這使得代碼非??勺x,但代價是必須編寫額外的適配器邏輯。處理程序很簡潔,并且存在自然的關(guān)注點分離。

這就引出了一個問題…

Rust 比 Go 更好,還是 Rust 會取代 Go?

就我個人而言,我是 Rust 的忠實粉絲,我認為它是一種很棒的 Web 服務(wù)語言。但生態(tài)系統(tǒng)中仍然存在一些粗糙的邊緣和缺失的部分。

特別是對于新手來說,使用 axum 時的錯誤消息有時可能非常神秘。例如,常見的是以下錯誤消息,該消息發(fā)生在由于類型不匹配而未實現(xiàn)處理程序特征的路由上:

error[E0277]: the trait bound `(): Handler<_, _>` is not satisfied
   --> src\router.rs:22:50
    |
22  |         router = router.route("/", get(handler));
    |                                    --- ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Handler<_, _>` is not implemented for `()`
    |                                    |
    |                                    required by a bound introduced by this call
    |
note: required by a bound in `axum::routing::get`

對于這種情況,我推薦 axum debug_handler ,它大大簡化了錯誤消息。在他們的文檔中閱讀更多相關(guān)信息。

與Go相比,授權(quán)部分也涉及更多。在 Go 中,我們只需使用中間件即可完成。在 Rust 中,我們必須編寫自己的中間件和錯誤類型。這不一定是壞事,但需要對 axum 文檔進行一些研究才能找到正確的解決方案。誠然,基本身份驗證并不是現(xiàn)實應(yīng)用程序的常見用例,并且有大量高級身份驗證庫可供選擇。

上述問題并不影響主要功能,主要是與特定crate 相關(guān)的問題。 Core Rust 已經(jīng)達到了穩(wěn)定和成熟的程度,適合生產(chǎn)使用。生態(tài)系統(tǒng)仍在不斷發(fā)展,但已經(jīng)處于良好狀態(tài)。

另一方面,我個人認為最終的 Go 代碼有點過于冗長。錯誤處理非常明確,但也分散了實際業(yè)務(wù)邏輯的注意力??偟膩碚f,我發(fā)現(xiàn)自己在 Go 中達到了更高級別的抽象(如前面提到的 Rust 版本中的 FromRequest 特征)。最終的 Rust 代碼感覺更加簡潔。感覺 Rust 編譯器在整個過程中默默地引導(dǎo)我走向更好的設(shè)計。使用 Rust 的前期成本肯定較高,但一旦你度過了最初的腳手架階段,人體工程學(xué)就會很棒。

我不認為一種語言比另一種語言更好。這是品味和個人喜好的問題。這兩種語言的理念截然不同,但它們都允許您構(gòu)建快速可靠的 Web 服務(wù)。

應(yīng)該使用 Rust 還是 Go?

如果您剛剛開始一個新項目,并且您和您的團隊可以自由選擇要使用的語言,您可能想知道選擇哪一種。

這取決于項目的時間范圍和您團隊的經(jīng)驗。如果您希望快速入門,Go 可能是更好的選擇。它提供了一個包含豐富內(nèi)置功能的開發(fā)環(huán)境,非常適合web應(yīng)用程序。

但是,不要低估 Rust 的長期好處。其豐富的類型系統(tǒng)與其出色的錯誤處理機制和編譯時檢查相結(jié)合,可以幫助您構(gòu)建不僅快速而且健壯且可擴展的應(yīng)用程序。

因此,如果您正在尋找長期解決方案,并且愿意投資學(xué)習(xí) Rust,我認為這是一個不錯的選擇。

我邀請您比較這兩種解決方案并自行決定您更喜歡哪一種。

無論如何,用兩種不同的語言構(gòu)建同一個項目并查看習(xí)慣用法和生態(tài)系統(tǒng)的差異是很有趣的。盡管最終的結(jié)果是一樣的,但我們到達那里的方式卻截然不同。


原文:Rust Vs Go: A Hands-On Comparison文章來源地址http://www.zghlxwxcb.cn/news/detail-835162.html

到了這里,關(guān)于Rust Vs Go:從頭構(gòu)建一個web服務(wù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • 用Rust設(shè)計一個并發(fā)的Web服務(wù):常用Rust庫如Tokio、Hyper等,基于TCP/IP協(xié)議棧,實現(xiàn)了一個簡單的并發(fā)Web服務(wù)器,并結(jié)合具體的代碼講解如何編寫并發(fā)Web服務(wù)器的程序

    作者:禪與計算機程序設(shè)計藝術(shù) 1994年,互聯(lián)網(wǎng)泡沫破裂,一批優(yōu)秀的程序員、工程師紛紛加入到web開發(fā)領(lǐng)域。而其中的Rust語言卻備受矚目,它是一種現(xiàn)代系統(tǒng)編程語言,專注于安全和并發(fā)。因此,Rust在當(dāng)下成為最流行的編程語言之一,很多框架也開始使用Rust重構(gòu),這使得

    2024年02月06日
    瀏覽(34)
  • Rust vs Go:常用語法對比(十三)

    Rust vs Go:常用語法對比(十三)

    題圖來自 Go vs. Rust: The Ultimate Performance Battle 241. Yield priority to other threads Explicitly decrease the priority of the current process, so that other execution threads have a better chance to execute now. Then resume normal execution and call function busywork . 將優(yōu)先權(quán)讓給其他線程 After Gosched, the execution of the current gorout

    2024年02月15日
    瀏覽(21)
  • Rust vs Go:常用語法對比(八)

    Rust vs Go:常用語法對比(八)

    題目來自 Golang vs. Rust: Which Programming Language To Choose in 2023? [1] 141. Iterate in sequence over two lists Iterate in sequence over the elements of the list items1 then items2. For each iteration print the element. 依次迭代兩個列表 依次迭代列表項1和項2的元素。每次迭代打印元素。 1 2 3 a b c 142. Hexadecimal digits of

    2024年02月15日
    瀏覽(25)
  • Rust vs Go:常用語法對比(二)

    21. Swap values 交換變量a和b的值 輸出 a: 10, b: 3 or 輸出 22. Convert string to integer 將字符串轉(zhuǎn)換為整型 or 輸出 or 輸出 or 輸出 23. Convert real number to string with 2 decimal places Given a real number x, create its string representation s with 2 decimal digits following the dot. 給定一個實數(shù),小數(shù)點后保留兩位小數(shù)

    2024年02月16日
    瀏覽(27)
  • Rust vs Go:常用語法對比(九)

    Rust vs Go:常用語法對比(九)

    題圖來自 Golang vs Rust - The Race to Better and Ultimate Programming Language 161. Multiply all the elements of a list Multiply all the elements of the list elements by a constant c 將list中的每個元素都乘以一個數(shù) [4.0, 7.0, 8.0] 162. Execute procedures depending on options execute bat if b is a program option and fox if f is a program optio

    2024年02月15日
    瀏覽(26)
  • Rust vs Go:常用語法對比(四)

    Rust vs Go:常用語法對比(四)

    題圖來自 Go vs. Rust performance comparison: The basics 61. Get current date 獲取當(dāng)前時間 Now is 2009-11-10 23:00:00 +0000 UTC m=+0.000000001 or SystemTime { tv_sec: 1526318418, tv_nsec: 699329521 } 62. Find substring position 字符串查找 查找子字符串位置 i is the byte index of y in x, not the character (rune) index. i will be -1 if y i

    2024年02月16日
    瀏覽(19)
  • Rust vs Go:常用語法對比(七)

    Rust vs Go:常用語法對比(七)

    題圖來自 Go vs Rust: Which will be the top pick in programming? [1] 121. UDP listen and read Listen UDP traffic on port p and read 1024 bytes into buffer b. 聽端口p上的UDP流量,并將1024字節(jié)讀入緩沖區(qū)b。 122. Declare enumeration Create an enumerated type Suit with 4 possible values SPADES, HEARTS, DIAMONDS, CLUBS. 聲明枚舉值 Hearts

    2024年02月15日
    瀏覽(20)
  • Rust vs Go:常用語法對比(十)

    Rust vs Go:常用語法對比(十)

    題圖來自 Rust vs. Golang: Which One is Better? [1] 182. Quine program Output the source of the program. 輸出程序的源代碼 輸出: 另一種寫法: //go:embed 入門 [2] Quine 是一種可以輸出自身源碼的程序。利用 go:embed 我們可以輕松實現(xiàn) quine 程序: 輸出: or 輸出: fn main(){print!(\\\"{},{0:?})}}\\\",\\\"fn main(){pri

    2024年02月15日
    瀏覽(23)
  • Rust vs Go:常用語法對比(十一)

    Rust vs Go:常用語法對比(十一)

    題目來自 Rust Vs Go: Which Language Is Better For Developing High-Performance Applications? [1] 202. Sum of squares Calculate the sum of squares s of data, an array of floating point values. 計算平方和 +1.094200e+000 32.25 205. Get an environment variable Read an environment variable with the name \\\"FOO\\\" and assign it to the string variable foo. If i

    2024年02月15日
    瀏覽(19)
  • Rust vs Go:常用語法對比(十二)

    Rust vs Go:常用語法對比(十二)

    題圖來自 Rust vs Go in 2023 [1] 221. Remove all non-digits characters Create string t from string s, keeping only digit characters 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. 刪除所有非數(shù)字字符 168 [src/main.rs:7] t = \\\"14\\\" 222. Find first index of an element in list Set i to the first index in list items at which the element x can be found, or -1 if items doe

    2024年02月15日
    瀏覽(16)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包