招聘网官方网站,莱芜网站开发,百度怎么优化网站关键词,重庆网站制作和推广公司Go 和 Rust 之间的许多比较都强调它们在语法和初始学习曲线上的差异。然而#xff0c;最终的决定性因素是重要项目的易用性。
“Rust 与 Go”争论
Rust vs Go 是一个不断出现的话题#xff0c;并且已经有很多关于它的文章。部分原因是开发人员正在寻找信息来帮助他们决定下…
Go 和 Rust 之间的许多比较都强调它们在语法和初始学习曲线上的差异。然而最终的决定性因素是重要项目的易用性。
“Rust 与 Go”争论
Rust vs Go 是一个不断出现的话题并且已经有很多关于它的文章。部分原因是开发人员正在寻找信息来帮助他们决定下一个 Web 项目使用哪种语言而这两种语言在这种情况下都经常被提及。我们环顾四周但确实没有太多关于该主题的深入内容因此开发人员只能自己解决这个问题并冒着由于误导性原因而过早放弃某个选项的风险。
这两个社区都经常面临误解和偏见。一些人将 Rust 主要视为一种系统编程语言质疑它是否适合 Web 开发。与此同时其他人认为 Go 过于简单怀疑它处理复杂 Web 应用程序的能力。然而这些都只是表面的判断。
事实上这两种语言都可以用来编写快速可靠的 Web 服务。然而他们的方法截然不同很难找到一个对两者都公平的比较。
这篇文章是我们试图通过用两种语言构建一个重要的现实世界中的应用程序来概述 Go 和 Rust 之间的差异重点是 Web 开发。我们将超越语法并仔细研究这些语言如何处理典型的 Web 任务如路由、中间件、模板、数据库访问等。
读完本文后您应该清楚哪种语言适合您。
尽管我们知道自己的偏见和偏好但我们将尽力保持客观并强调两种语言的优点和缺点。
构建一个小型web服务
我们将讨论以下主题
Routing 路由Templating 模板Database access 数据库访问Deployment 部署
我们将省略客户端渲染或数据库迁移等主题只关注服务器端。
任务
选择一个代表 Web 开发的任务并不容易一方面我们希望保持它足够简单以便我们可以专注于语言功能和库。另一方面我们希望确保任务不会太简单以便我们可以展示如何在现实环境中使用语言功能和库。
我们决定建立天气预报服务。用户应该能够输入城市名称并获取该城市当前的天气预报。该服务还应该显示最近搜索过的城市列表。
随着我们扩展服务我们将添加以下功能
一个简单的 UI 显示天气预报用于存储最近搜索的城市的数据库
The Weather API 天气 API
对于天气预报我们将使用 Open-Meteo API因为它是开源的、易于使用并且为非商业用途提供慷慨的免费套餐每天最多可处理 10,000 个请求。
我们将使用这两个 API 接口
用于获取城市坐标的 GeoCoding API 。Weather Forecast API 用于获取给定坐标的天气预报。
这两种语言都有现成的库可用Go (omgo) 和 Rust (openmeteo) 我们将在生产服务中使用它们。然而为了进行比较我们希望了解如何用两种语言发出“原始”HTTP 请求并将响应转换为常用的数据结构。
Go Web 服务
选择网络框架
Go 最初是为了简化构建 Web 服务而创建的它拥有许多很棒的与 Web 相关的包。如果标准库不能满足您的需求还有许多流行的第三方 Web 框架可供选择例如 Gin、Echo 或 Chi。
选择哪一个是个人喜好问题。一些经验丰富的 Go 开发人员更喜欢使用标准库并在其之上添加像 Chi 这样的路由库。其他人则更喜欢包含更多内置功能的方法使用功能齐全的框架例如 Gin 或 Echo。
这两个选项都很好但为了比较的目的我们将选择 Gin因为它是最流行的框架之一并且它支持我们的天气服务所需的所有功能。
HTTP 请求
让我们从一个简单的函数开始该函数向 Open Meteo API 发出 HTTP 请求并以字符串形式返回响应正文
func getLatLong(city string) (*LatLong, error) {endpoint : fmt.Sprintf(https://geocoding-api.open-meteo.com/v1/search?name%scount1languageenformatjson, 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 GeoResponseif 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 oneResults []LatLong json:results
}type LatLong struct {Latitude float64 json:latitudeLongitude float64 json:longitude
}json 标签(tag)告诉 JSON 解码器如何将 JSON 字段映射到结构体字段。默认情况下JSON 响应中的额外字段将被忽略。
让我们定义另一个函数它采用 LatLong 结构并返回该位置的天气预报
func getWeather(latLong LatLong) (string, error) {endpoint : fmt.Sprintf(https://api.open-meteo.com/v1/forecast?latitude%.6flongitude%.6fhourlytemperature_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() {latlong, err : getLatLong(London) // you know it will rainif 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, ... }漂亮我们得到了伦敦的天气预报。让我们将其作为 Web 服务提供。
Routing 路由
路由是 Web 框架最基本的任务之一。首先让我们将 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)// ...
})您想使用哪一种取决于您的用例。在我们的例子中我们希望最终从表单提交城市名称因此我们将使用查询参数。
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?cityHamburg我们得到天气预报
{weather:{\latitude\:53.550000,\longitude\:10.000000, ... }我喜欢日志输出而且速度也很快
[GIN] 2023/09/09 - 19:27:20 | 200 | 190.75625ms | 127.0.0.1 | GET /weather?cityHamburg
[GIN] 2023/09/09 - 19:28:22 | 200 | 46.597791ms | 127.0.0.1 | GET /weather?cityHamburgTemplates 模板
我们完成了api服务端但原始 JSON 对于普通用户来说并不是很有用。在现实应用程序中我们可能会在 API 端点例如 /api/v1/weather/:city 上提供 JSON 响应并返回一个 HTML 页面。为了简单起见我们直接返回 HTML 页面。
让我们添加一个简单的 HTML 页面以表格形式显示给定城市的天气预报。我们将使用标准库中的 html/template 包来呈现 HTML 页面。
首先我们为视图添加一些结构
type WeatherData struct
type WeatherResponse struct {Latitude float64 json:latitudeLongitude float64 json:longitudeTimezone string json:timezoneHourly struct {Time []string json:timeTemperature2m []float64 json:temperature_2m} json:hourly
}type WeatherDisplay struct {City stringForecasts []Forecast
}type Forecast struct {Date stringTemperature string
}这只是 JSON 响应中相关字段到结构的直接映射。有一些工具例如transform可以使从JSON 到Go 结构的转换变得更容易。你可以试一下
接下来我们定义一个函数它将来自天气 API 的原始 JSON 响应转换为新的 WeatherDisplay 结构
func extractWeatherData(city string, rawWeather string) (WeatherDisplay, error) {var weatherResponse WeatherResponseif err : json.Unmarshal([]byte(rawWeather), weatherResponse); err ! nil {return WeatherDisplay{}, fmt.Errorf(error decoding weather response: %w, err)}var forecasts []Forecastfor 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 中日期处理的更多信息请查看这篇文章。
我们扩展路由处理程序来呈现 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/*)最后我们可以在 views 目录下创建一个模板文件 weather.html
!DOCTYPE html
htmlheadtitleWeather Forecast/title/headbodyh1Weather for {{ .City }}/h1table border1trthDate/ththTemperature/th/tr{{ range .Forecasts }}trtd{{ .Date }}/tdtd{{ .Temperature }}/td/tr{{ end }}/table/body
/html有关如何使用模板的更多详细信息请参阅 Gin 文档。
这样我们就有了一个可用的 Web 服务它以 HTML 页面的形式返回给定城市的天气预报
差点忘了也许我们还想创建一个带有输入字段的index页面它允许我们输入城市名称并显示该城市的天气预报。
让我们为index页添加一个新的路由处理程序
r.GET(/, func(c *gin.Context) {c.HTML(http.StatusOK, index.html, nil)
})和一个新的模板文件 index.html
!DOCTYPE html
htmlheadtitleWeather Forecast/title/headbodyh1Weather Forecast/h1form action/weather methodgetlabel forcityCity:/labelinput typetext idcity namecity /input typesubmit valueSubmit //form/body
/html现在我们可以启动 Web 服务并在浏览器中打开 http://localhost:8080 伦敦的天气预报是这样的。它不漂亮但是…实用 它无需 JavaScript 即可在终端浏览器中运行 作为练习您可以向 HTML 页面添加一些样式但由于我们更关心后端因此我们将保留它。
数据访问
我们的服务根据每个请求从外部 API 获取给定城市的纬度和经度。一开始这可能没问题但最终我们可能希望将结果缓存在数据库中以避免不必要的 API 调用。
为此我们将数据库添加到我们的 Web 服务中。我们将使用 PostgreSQL 作为数据库使用 sqlx 作为数据库驱动程序。
首先我们创建一个名为 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您可以将其作为环境变量传递到您的 Web 服务。
我们不会在这里详细介绍设置数据库的细节但在本地使用 Docker 运行 PostgreSQL 数据库的一个简单方法是
docker run -p 5432:5432 -e POSTGRES_USERforecast -e POSTGRES_PASSWORDforecast -e POSTGRES_DBforecast -v pwd/init.sql:/docker-entrypoint-initdb.d/index.sql -d postgres
export DATABASE_URLpostgres://forecast:forecastlocalhost:5432/forecast?sslmodedisable然而一旦我们有了数据库我们需要将 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
}让我们将旧的 getLatLong 函数重命名为 fetchLatLong 并添加一个新的 getLatLong 函数该函数使用数据库而不是外部 API
func getLatLong(db *sqlx.DB, name string) (*LatLong, error) {var latLong *LatLongerr : 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 dblatlong, err : getLatLong(db, city)// ...
})这样我们就有了一个可用的 Web 服务它将给定城市的纬度和经度存储在数据库中并在后续请求时从那里获取它。
Middleware 中间件
最后一点是向我们的 Web 服务添加一些中间件。我们已经从 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 []stringerr : 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
htmlheadtitleLatest Queries/title/headbodyh1Latest Lat/Long Lookups/h1table border1trthCities/th/tr{{ range . }}trtd{{ . }}/td/tr{{ end }}/table/body
/html这样我们就有了一个可以运行的web服务恭喜
我们取得了以下成就
从外部 API 获取给定城市的纬度和经度的 Web 服务将纬度和经度存储在数据库中在后续请求中从数据库获取纬度和经度打印 /stats 端点上的最后一个搜索查询用于保护 /stats 端点的基本身份验证使用中间件记录请求用于呈现 HTML 的模板
对于几行代码来说这已经是相当多的功能了让我们看看 Rust 表现如何
Rust Web 服务
从历史上看Rust 对于 Web 服务并没有一个好的支持。有一些框架但它们的级别相当低。直到 async/await 的出现Rust Web 生态系统才真正起飞。突然间无需垃圾收集器且具有无所畏惧的并发性就可以编写高性能的 Web 服务。
我们将了解 Rust 与 Go 在人体工程学、性能和安全性方面的比较。但首先我们需要选择一个 Web 框架。
哪个网络框架
如果您希望更好地了解 Rust Web 框架及其优缺点我们最近对 Rust Web 框架进行了深入研究。
出于本文的目的我们考虑两个 Web 框架Actix 和 Axum。
Actix 是 Rust 社区中非常流行的 Web 框架。它基于 Actor 模型并在底层使用 async/await。在基准测试中它经常被评为世界上最快的 Web 框架之一。
另一方面Axum 是一个基于 tower 的新 Web 框架tower 是一个用于构建异步服务的库。它正在迅速流行。它也是基于async/await。
两个框架在人体工程学和性能方面非常相似。它们都支持中间件和路由。对于我们的网络服务来说它们都是不错的选择但我们会选择 Axum因为它与生态系统的其他部分紧密结合并且最近得到了很多关注。
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] }让我们为我们的 Web 服务创建一个小框架它的作用不大。
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 运行 Web 服务看看会发生什么。
$ curl localhost:3000
Index
$ curl localhost:3000/weather
Weather
$ curl localhost:3000/stats
Stats好吧可以运行。让我们向处理程序添加一些实际逻辑。
Axum macros
在我们继续之前我想提一下 axum 有一些粗糙的地方。例如。如果忘记把处理程序函数标记为 async它会报出很多错误。因此如果您遇到 Handler_, _ is not implemented 错误请添加 axum-macros crate并使用 #[axum_macros::debug_handler] 注释您的处理程序。这将为您提供更好的错误消息。
获取纬度和经度
让我们编写一个函数从外部 API 获取给定城市的纬度和经度。
以下是表示 API 响应的结构
use serde::Deserialize;pub struct GeoResponse {pub results: VecLatLong,
}#[derive(Deserialize, Debug, Clone)]
pub struct LatLong {pub latitude: f64,pub longitude: f64,
}与 Go 相比我们不使用标签来指定字段名称。相反我们使用 serde 中的 #[derive(Deserialize)] 属性来自动派生结构的 Deserialize 特征。这些派生宏非常强大允许我们用很少的代码做很多事情包括处理类型的解析错误。这是 Rust 中非常常见的模式。
让我们使用新类型来获取给定城市的纬度和经度
async fn fetch_lat_long(city: str) - ResultLatLong, Boxdyn std::error::Error {let endpoint format!(https://geocoding-api.open-meteo.com/v1/search?name{}count1languageenformatjson,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) 返回 OptionLatLong 。它是 Option 因为如果向量为空 get 函数可能会返回 None 。cloned() 拷贝Option 内的值并将 OptionLatLong 转换为 OptionLatLong 。这是必要的因为我们想要返回 LatLong 而不是引用。否则我们必须在函数签名中添加生命周期说明符这会降低代码的可读性。ok_or(No results found.into()) 将 OptionLatLong 转换为 ResultLatLong, Boxdyn std::error::Error 。如果 Option 为 None 则会返回错误信息。 into() 函数将字符串转换为 Boxdyn 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)
}首先我们将城市打印到控制台然后获取纬度和经度并unwrap即“unwrap”结果。如果结果错误程序就会出现恐慌。这并不理想但我们稍后会修复它。
然后我们使用纬度和经度创建一个字符串并返回它。
让我们运行该程序看看会发生什么
curl -v localhost:3000/weather?cityBerlin
* Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)GET /weather?cityBerlin HTTP/1.1Host: localhost:3000User-Agent: curl/8.1.2Accept: */** Empty reply from server
* Closing connection 0
curl: (52) Empty reply from server此外我们得到这个输出
city:city 参数为空。what
问题是我们使用 String 类型作为 city 参数。此类型不是有效的提取器(extractor)。
我们可以使用 Query 提取器来代替
async fn weather(Query(params): QueryHashMapString, 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,
}然后我们可以使用这个结构作为提取器(extractor)并避免 unwrap
async fn weather(Query(params): QueryWeatherQuery) - String {let lat_long fetch_lat_long(params.city).await.unwrap();format!({}: {}, {}, params.city, lat_long.latitude, lat_long.longitude)
}整洁多了它比 Go 版本稍微复杂一些但它也更类型安全。您可以想象我们可以向结构添加约束以添加验证。例如我们可能要求城市的长度至少为 3 个字符。
现在介绍 weather 函数中的 unwrap 。理想情况下如果找不到城市我们会返回错误。我们可以通过更改返回类型来做到这一点。
在 axum 中任何实现 IntoResponse 的内容都可以从处理程序返回但是建议返回具体类型因为[返回 impl IntoResponse 时有一些注意事项]https:// docs.rs/axum/latest/axum/response/index.html)
在我们的例子中我们可以返回 Result 类型
async fn weather(Query(params): QueryWeatherQuery) - ResultString, 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 状态代码。我们使用 match 来匹配 fetch_lat_long 的结果。如果是 Ok 我们将天气作为 String 返回。如果是 Err 我们返回 StatusCode::NOT_FOUND 。
我们还可以使用 map_err 函数将错误转换为 StatusCode
async fn weather(Query(params): QueryWeatherQuery) - ResultString, 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))
}这种变体的优点是我们的控制流更加线性我们立即处理错误然后可以继续正常的路径。另一方面需要一段时间才能习惯这些组合器模式直到它们成为第二种天性。
在 Rust 中通常有多种方法可以做事。您喜欢哪个版本只是品味问题。一般来说不要想太多,保持简单就行。
无论如何让我们测试一下程序
curl localhost:3000/weather?cityBerlin
Berlin: 52.52437, 13.41053和
curl -I localhost:3000/weather?cityabcdedfg
HTTP/1.1 404 Not Found让我们编写第二个函数它将返回给定纬度和经度的天气
async fn fetch_weather(lat_long: LatLong) - ResultString, Boxdyn std::error::Error {let endpoint format!(https://api.open-meteo.com/v1/forecast?latitude{}longitude{}hourlytemperature_2m,lat_long.latitude, lat_long.longitude);let response reqwest::get(endpoint).await?.text().await?;Ok(response)
}在这里我们发出 API 请求并以 String 形式返回原始响应正文。
我们可以扩展我们的处理程序以连续进行两个调用
async fn weather(Query(params): QueryWeatherQuery) - ResultString, 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 返回原始响应正文。让我们解析响应并返回类似于 Go 版本的数据。
提醒一下这是 Go 的定义
type WeatherResponse struct {Latitude float64 json:latitudeLongitude float64 json:longitudeTimezone string json:timezoneHourly struct {Time []string json:timeTemperature2m []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: VecString,pub temperature_2m: Vecf64,
}在这样做的同时还可以定义需要的其他结构
#[derive(Deserialize, Debug)]
pub struct WeatherDisplay {pub city: String,pub forecasts: VecForecast,
}#[derive(Deserialize, Debug)]
pub struct Forecast {pub date: String,pub temperature: String,
}现在可以将响应主体解析为结构
async fn fetch_weather(lat_long: LatLong) - ResultWeatherResponse, Boxdyn std::error::Error {let endpoint format!(https://api.open-meteo.com/v1/forecast?latitude{}longitude{}hourlytemperature_2m,lat_long.latitude, lat_long.longitude);let response reqwest::get(endpoint).await?.json::WeatherResponse().await?;Ok(response)
}让我们调整处理程序。使其编译的最简单方法是返回 String
async fn weather(Query(params): QueryWeatherQuery) - ResultString, 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))
}请注意我们如何将解析逻辑与处理程序逻辑混合在一起。让我们通过将解析逻辑移至构造函数中来清理一下
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}
}这是一个开始。我们的处理程序现在看起来像这样:
async fn weather(Query(params): QueryWeatherQuery) - ResultString, 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))
}这已经好一点了。令人分心的是 map_err 样板代码。我们可以通过引入自定义错误类型来删除它。例如我们可以按照 axum 存储库中的示例无论如何使用一个流行的错误处理包
cargo add anyhowLet’s copy the code from the example into our project: 让我们将示例中的代码复制到我们的项目中
// 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 dont need to do that manually.
implE FromE for AppError
whereE: Intoanyhow::Error,
{fn from(err: E) - Self {Self(err.into())}
}您不必完全理解这段代码。可以说这将为应用程序设置错误处理这样我们就不必在处理程序中处理它。
我们必须调整 fetch_lang_long 和 fetch_weather 函数以返回 Result 和 anyhow::Error
async fn fetch_lat_long(city: str) - ResultLatLong, anyhow::Error {let endpoint format!(https://geocoding-api.open-meteo.com/v1/search?name{}count1languageenformatjson,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) - ResultWeatherResponse, anyhow::Error {// code stays the same
}以添加依赖项并添加用于错误处理的附加样板为代价我们设法简化了我们的处理程序
async fn weather(Query(params): QueryWeatherQuery) - ResultString, 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 --featureswith-axum
# I also needed to add this to make it compile
cargo add askama_axum让我们创建一个 templates 目录并添加一个 weather.html 模板类似于我们之前创建的 Go 表模板
!DOCTYPE html
html langenheadmeta charsetUTF-8 /titleWeather/title/headbodyh1Weather for {{ city }}/h1tabletheadtrthDate/ththTemperature/th/tr/theadtbody{% for forecast in forecasts %}trtd{{ forecast.date }}/tdtd{{ forecast.temperature }}/td/tr{% endfor %}/tbody/table/body
/html让我们将 WeatherDisplay 结构转换为 Template
#[derive(Template, Deserialize, Debug)]
#[template(path weather.html)]
struct WeatherDisplay {city: String,forecasts: VecForecast,
}处理程序变成
async fn weather(Query(params): QueryWeatherQuery) - ResultWeatherDisplay, AppError {let lat_long fetch_lat_long(params.city).await?;let weather fetch_weather(lat_long).await?;Ok(WeatherDisplay::new(params.city, weather))
}到达这里需要做一些工作但我们现在已经很好地分离了关注点没有太多的样板代码。
如果您在 http://localhost:3000/weather?cityBerlin 打开浏览器您应该会看到天气表。
添加我们的输入很容易。我们可以使用与 Go 版本完全相同的 HTML
form action/weather methodget!DOCTYPE htmlhtmlheadtitleWeather Forecast/title/headbodyh1Weather Forecast/h1form action/weather methodgetlabel forcityCity:/labelinput typetext idcity namecity /input typesubmit valueSubmit //form/body/html
/form这是处理程序
#[derive(Template)]
#[template(path index.html)]
struct IndexTemplate;async fn index() - IndexTemplate {IndexTemplate
}让我们继续将纬度和经度存储在数据库中。
数据访问
我们将使用 sqlx 进行数据库访问。这是一个非常受欢迎的包支持多个数据库。在我们的例子中我们将使用 Postgres就像在 Go 版本中一样。
Add this to your Cargo.toml: 将其添加到您的 Cargo.toml 中
sqlx { version 0.7, features [runtime-tokio-rustls,macros,any,postgres,
] }需要将 DATABASE_URL 环境变量添加到 .env 文件中
export DATABASE_URLpostgres://forecast:forecastlocalhost:5432/forecast?sslmodedisable如果 Postgres 尚未运行您可以使用 Go 部分中的相同 Docker 代码片段来启动它。
这样调整代码以使用数据库。首先是 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(cant 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 创建一个数据库连接池。然后将pool传递给 with_state 以使其可供所有处理程序使用。
在每个路由中可以但不必像这样访问数据库池
async fn weather(Query(params): QueryWeatherQuery,State(pool): StatePgPool,
) - ResultWeatherDisplay, AppError {let lat_long fetch_lat_long(params.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) - ResultLatLong, 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): QueryWeatherQuery,State(pool): StatePgPool,
) - ResultWeatherDisplay, AppError {let lat_long fetch_lat_long(params.city).await?;let weather fetch_weather(lat_long).await?;Ok(WeatherDisplay::new(params.city, weather))
}就是这样我们现在有了一个带有数据库后端的可用 Web 应用程序。功能与之前相同但现在我们缓存纬度和经度。
Middleware 中间件
比Go 版本中缺少的最后一个功能是 /stats api。请记住它显示最近的查询并且支持基本身份验证。
让我们从基本的身份验证开始。
我花了一段时间才弄清楚如何做到这一点。 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]
implS FromRequestPartsS for User
whereS: Send Sync,
{type Rejection axum::http::Responseaxum::body::Body;async fn from_request_parts(parts: mut Parts, _: S) - ResultSelf, 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, youd 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 {Were authorized!
}现在了解 /stats api的实际逻辑。
#[derive(Template)]
#[template(path stats.html)]
struct StatsTemplate {pub cities: VecCity,
}async fn get_last_cities(pool: PgPool) - ResultVecCity, 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): StatePgPool) - ResultStatsTemplate, AppError {let cities get_last_cities(pool).await?;Ok(StatsTemplate { cities })
}Deployment 部署
最后我们来谈谈部署。
由于这两种语言都编译为静态链接的二进制文件因此它们可以托管在任何虚拟机 (VM) 或虚拟专用服务器 (VPS) 上。这是令人惊奇的因为这意味着如果您愿意您可以在裸机上本机运行您的应用程序。
另一种选择是使用容器它在隔离的环境中运行您的应用程序。它们非常受欢迎因为它们易于使用并且几乎可以部署在任何地方。
对于 Golang您可以使用任何支持运行静态二进制文件或容器的云提供商。更受欢迎的选项之一是 Google Cloud Run。
当然您也可以使用容器来运送 Rust但还有其他选择。当然其中之一就是 Shuttle它的工作方式与其他服务不同您不需要构建 Docker 映像并将其推送到注册表。相反您只需将代码推送到 Git 存储库Shuttle 就会为您构建并运行二进制文件。
借助 Rust 的过程宏您可以通过附加功能快速增强代码。
只需在 main 函数上使用 #[shuttle_runtime::main] 即可开始
#[shuttle_runtime::main]
async fn main() - Result(), Boxdyn 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 函数以使用 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(cant 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 --featurespostgres然后
#[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 deployWhen 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. 完成后它将打印服务的 URL。它应该像以前一样工作但现在它在云中的服务器上运行。
Go 和 Rust 的对比
让我们看看这两个版本如何相互比较。
Go 版本
Go 版本非常简单明了。我们只需要添加两个依赖项 Gin Web 框架和 sqlx 数据库驱动程序。除此之外标准库提供了所有内容模板引擎、JSON 解析器、日期时间处理等。
尽管我个人不太喜欢 Go 的模板引擎和错误处理机制但我在整个开发过程中都感到富有成效。我们本来可以使用外部模板库但我们不需要这样做因为内置模板库非常适合我们的用例。如果您希望在项目中利用 Go 的强大功能您可能需要聘请 Golang 开发人员。
Rust 版本
Rust 代码涉及更多一些。我们需要添加很多依赖项才能获得与 Go 中相同的功能。例如我们需要添加模板引擎、JSON 解析器、日期时间库、数据库驱动程序和 Web 框架。
这是设计使然。 Rust 的标准库非常小只提供最基本的构建块。这个想法是您可以选择项目所需的依赖项。它有助于生态系统更快地发展并允许进行更多实验同时语言核心保持稳定。
尽管开始花费了更长的时间但我很享受逐步提升到更高抽象级别的过程。我从来没有觉得自己陷入了次优的解决方案之中。有了适当的抽象例如 ? 运算符和 FromRequest 特征代码就很容易阅读没有任何样板文件或不必要的冗长错误处理。
Summary 总结 Go: 易于学习、快速、适合 Web 服务丰富的内置功能。我们仅使用标准库就做了很多事情。例如我们不需要添加模板引擎或单独的身份验证库。我们唯一的外部依赖项是 Gin 和 sqlx Rust: 锈 快速、安全、不断发展的网络服务生态系统内置功能较少。我们必须添加大量依赖项才能获得与 Go 中相同的功能并编写我们自己的小型中间件。最终的处理程序代码没有分散注意力的错误处理因为我们使用了自己的错误类型和 ? 运算符。这使得代码非常可读但代价是必须编写额外的适配器逻辑。处理程序很简洁并且存在自然的关注点分离。
这就引出了一个问题…
Rust 比 Go 更好还是 Rust 会取代 Go
就我个人而言我是 Rust 的忠实粉丝我认为它是一种很棒的 Web 服务语言。但生态系统中仍然存在一些粗糙的边缘和缺失的部分。
特别是对于新手来说使用 axum 时的错误消息有时可能非常神秘。例如常见的是以下错误消息该消息发生在由于类型不匹配而未实现处理程序特征的路由上
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 它大大简化了错误消息。在他们的文档中相关信息。
与Go相比授权部分也涉及更多。在 Go 中我们只需使用中间件即可完成。在 Rust 中我们必须编写自己的中间件和错误类型。这不一定是坏事但需要对 axum 文档进行一些研究才能找到正确的解决方案。诚然基本身份验证并不是现实应用程序的常见用例并且有大量高级身份验证库可供选择。
上述问题并不影响主要功能主要是与特定crate 相关的问题。 Core Rust 已经达到了稳定和成熟的程度适合生产使用。生态系统仍在不断发展但已经处于良好状态。
另一方面我个人认为最终的 Go 代码有点过于冗长。错误处理非常明确但也分散了实际业务逻辑的注意力。总的来说我发现自己在 Go 中达到了更高级别的抽象如前面提到的 Rust 版本中的 FromRequest 特征。最终的 Rust 代码感觉更加简洁。感觉 Rust 编译器在整个过程中默默地引导我走向更好的设计。使用 Rust 的前期成本肯定较高但一旦你度过了最初的脚手架阶段人体工程学就会很棒。
我不认为一种语言比另一种语言更好。这是品味和个人喜好的问题。这两种语言的理念截然不同但它们都允许您构建快速可靠的 Web 服务。
应该使用 Rust 还是 Go
如果您刚刚开始一个新项目并且您和您的团队可以自由选择要使用的语言您可能想知道选择哪一种。
这取决于项目的时间范围和您团队的经验。如果您希望快速入门Go 可能是更好的选择。它提供了一个包含丰富内置功能的开发环境非常适合web应用程序。
但是不要低估 Rust 的长期好处。其丰富的类型系统与其出色的错误处理机制和编译时检查相结合可以帮助您构建不仅快速而且健壮且可扩展的应用程序。
因此如果您正在寻找长期解决方案并且愿意投资学习 Rust我认为这是一个不错的选择。
我邀请您比较这两种解决方案并自行决定您更喜欢哪一种。
无论如何用两种不同的语言构建同一个项目并查看习惯用法和生态系统的差异是很有趣的。尽管最终的结果是一样的但我们到达那里的方式却截然不同。 原文:Rust Vs Go: A Hands-On Comparison 文章转载自: http://www.morning.ssglh.cn.gov.cn.ssglh.cn http://www.morning.tbrnl.cn.gov.cn.tbrnl.cn http://www.morning.qgfhr.cn.gov.cn.qgfhr.cn http://www.morning.rlwcs.cn.gov.cn.rlwcs.cn http://www.morning.wqhlj.cn.gov.cn.wqhlj.cn http://www.morning.kjyqr.cn.gov.cn.kjyqr.cn http://www.morning.gzgwn.cn.gov.cn.gzgwn.cn http://www.morning.pmftz.cn.gov.cn.pmftz.cn http://www.morning.wqbfd.cn.gov.cn.wqbfd.cn http://www.morning.srgnd.cn.gov.cn.srgnd.cn http://www.morning.kqnwy.cn.gov.cn.kqnwy.cn http://www.morning.qkqhr.cn.gov.cn.qkqhr.cn http://www.morning.fndmk.cn.gov.cn.fndmk.cn http://www.morning.ntnml.cn.gov.cn.ntnml.cn http://www.morning.bhjyh.cn.gov.cn.bhjyh.cn http://www.morning.dtnjr.cn.gov.cn.dtnjr.cn http://www.morning.ljfjm.cn.gov.cn.ljfjm.cn http://www.morning.lhjmq.cn.gov.cn.lhjmq.cn http://www.morning.lcjw.cn.gov.cn.lcjw.cn http://www.morning.jtmql.cn.gov.cn.jtmql.cn http://www.morning.dtmjn.cn.gov.cn.dtmjn.cn http://www.morning.kphsp.cn.gov.cn.kphsp.cn http://www.morning.spghj.cn.gov.cn.spghj.cn http://www.morning.gqnll.cn.gov.cn.gqnll.cn http://www.morning.fwzjs.cn.gov.cn.fwzjs.cn http://www.morning.mlyq.cn.gov.cn.mlyq.cn http://www.morning.zqmdn.cn.gov.cn.zqmdn.cn http://www.morning.ey3h2d.cn.gov.cn.ey3h2d.cn http://www.morning.bnbtp.cn.gov.cn.bnbtp.cn http://www.morning.wmqxt.cn.gov.cn.wmqxt.cn http://www.morning.xmtzk.cn.gov.cn.xmtzk.cn http://www.morning.wgdnd.cn.gov.cn.wgdnd.cn http://www.morning.mwjwy.cn.gov.cn.mwjwy.cn http://www.morning.zdwjg.cn.gov.cn.zdwjg.cn http://www.morning.qdxkn.cn.gov.cn.qdxkn.cn http://www.morning.xinxianzhi005.com.gov.cn.xinxianzhi005.com http://www.morning.xylxm.cn.gov.cn.xylxm.cn http://www.morning.grxyx.cn.gov.cn.grxyx.cn http://www.morning.wmhlz.cn.gov.cn.wmhlz.cn http://www.morning.qynnw.cn.gov.cn.qynnw.cn http://www.morning.fbmrz.cn.gov.cn.fbmrz.cn http://www.morning.kqfdrqb.cn.gov.cn.kqfdrqb.cn http://www.morning.xpmwt.cn.gov.cn.xpmwt.cn http://www.morning.fbmjw.cn.gov.cn.fbmjw.cn http://www.morning.gsqw.cn.gov.cn.gsqw.cn http://www.morning.fsnhz.cn.gov.cn.fsnhz.cn http://www.morning.fhqdb.cn.gov.cn.fhqdb.cn http://www.morning.zxdhp.cn.gov.cn.zxdhp.cn http://www.morning.njqpg.cn.gov.cn.njqpg.cn http://www.morning.fdhwh.cn.gov.cn.fdhwh.cn http://www.morning.kbynw.cn.gov.cn.kbynw.cn http://www.morning.bkpbm.cn.gov.cn.bkpbm.cn http://www.morning.qyfrd.cn.gov.cn.qyfrd.cn http://www.morning.qlckc.cn.gov.cn.qlckc.cn http://www.morning.xhhzn.cn.gov.cn.xhhzn.cn http://www.morning.lyrgp.cn.gov.cn.lyrgp.cn http://www.morning.pwdmz.cn.gov.cn.pwdmz.cn http://www.morning.lzsxp.cn.gov.cn.lzsxp.cn http://www.morning.frqtc.cn.gov.cn.frqtc.cn http://www.morning.cnwpb.cn.gov.cn.cnwpb.cn http://www.morning.rmryl.cn.gov.cn.rmryl.cn http://www.morning.fmrd.cn.gov.cn.fmrd.cn http://www.morning.zckhn.cn.gov.cn.zckhn.cn http://www.morning.ghrlx.cn.gov.cn.ghrlx.cn http://www.morning.thbnt.cn.gov.cn.thbnt.cn http://www.morning.bby45.cn.gov.cn.bby45.cn http://www.morning.jpnw.cn.gov.cn.jpnw.cn http://www.morning.rqnhf.cn.gov.cn.rqnhf.cn http://www.morning.fwkpp.cn.gov.cn.fwkpp.cn http://www.morning.rglzy.cn.gov.cn.rglzy.cn http://www.morning.mhmdx.cn.gov.cn.mhmdx.cn http://www.morning.gbpanel.com.gov.cn.gbpanel.com http://www.morning.ysmw.cn.gov.cn.ysmw.cn http://www.morning.dyght.cn.gov.cn.dyght.cn http://www.morning.kyjpg.cn.gov.cn.kyjpg.cn http://www.morning.pabxcp.com.gov.cn.pabxcp.com http://www.morning.kpwdt.cn.gov.cn.kpwdt.cn http://www.morning.qfgxk.cn.gov.cn.qfgxk.cn http://www.morning.srwny.cn.gov.cn.srwny.cn http://www.morning.stph.cn.gov.cn.stph.cn