Justhavefun 2020-10-22
时下最流行、最具发展前途的的两门语言是Golang和Rust。Golang语言简洁、高效、并发、并且有个强大的囊括了常见功能标准库。与之相对比,Rust语言则主要是安全、高性能。虽然Rust没有golang那种"内置电池(Batteries included)"的标准库,但是Rust的第三方库(crate,板条箱)极大补充了Rust精炼基本库的功能。本文我们就介绍一下Golang和Rust常用的库功能。
参数处理
Golang标准库中提供了功能。flag库是非常基础的功能,在实践上也非常有用,在做命令行交互程序时候必不可少。
package main import "flag" var ( program = flag.String("p", "", "h program to compile/run") outFname = flag.String("o", "", "if specified, write the webassembly binary created by -p here") watFname = flag.String("o-wat", "", "if specified, write the uncompiled webassembly created by -p here") port = flag.String("port", "", "HTTP port to listen on") writeTao = flag.Bool("koan", false, "if true, print the h koan and then exit") writeVersion = flag.Bool("v", false, "if true, print the version of h and then exit") )
上述代码会生成一些程序包全局变量,其中将包含命令行参数的值。
在Rust中,常用的命令行解析包是structopt。但是,其工作方式与Golang flag程序包有所不同。structopt会选项加载到结构中,而非全局可变变量。主要因为Rust语言编程实践中,基本上都会避免全局可变的变量。在大多数情况下,带有包的全局变量flag是可以的,但前提是必须在程序真正开始执行其需要做的工作之前将它们写入到程序中。
一个简单示例源自于pahi库源码:
#[derive(Debug, StructOpt)] #[structopt( name = "pa'i", about = "A WebAssembly runtime in Rust meeting the Olin ABI." )] struct Opt { #[structopt(short, long, default_value = "cranelift")] backend: String, #[structopt(short, long)] function_log: bool, #[structopt(short, long)] no_cache: bool, #[structopt()] fname: String, #[structopt(short, long, default_value = "_start")] entrypoint: String, #[structopt()] args: Vec<String>, }
Rust编译器会生成所需的参数解析代码,然后可以使用:
fn main() { let opt = Opt::from_args(); debug!("args: {:?}", opt.args); if opt.backend != "cranelift" { return Err(format!( "wanted backend to be cranelift, got: {}", opt.backend )); }
错误处理
Golang的标准库具有error接口,可以创建一个描述类型的函数,函数描述为什么功能无法按预期执行,Golang程序必须先做好错误处理。比如:
func Write(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { log.Println("unable to write:", err) return err } return nil
Rust也具有Error 特性,它使还可以创建一个描述函数为何无法实现其预期功能的类型。这儿我们介绍更加易用的thiserror板条箱构建自定义错误类型:
[dependencies] thiserror = "1"
然后,在程序中使用:
use std::fmt; use thiserror::Error; #[derive(Debug, Error)] struct Divide0; impl fmt::Display for Divide0 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "不能被零整除!") } }
日志记录
Go标准库中也自带了log库。该库是一个非常有争议的记录器,它的日志记录缺乏日志记录级别和上下文感知值之类的功能。
package main import ( "log" ) func init() { log.SetPrefix("TRACE: ") log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile) }
func main() { log.Println("message") log.Fatalln("fatal message") log.Panicln("panic message") }
------------------------------- TRACE: 2020/09/09 14:24:32.868375 TestLog.go:15: message TRACE: 2020/09/09 14:24:32.962329 TestLog.go:18: fatal message Process finished with exit code 1
在Rust中,有一个板条箱,这是一个非常简单的包,它使用error!,warn!,info!,debug!和trace!宏分别关联到最高和最低日志打印水平。同样要使用log板条箱,先要增加的项目中,即添加到Cargo.toml的依赖部分。
[dependencies] log = "0.4"
然后,就可以使用了:
use log::{error, warn, info, debug, trace}; fn main() { trace!("starting main"); debug!("debug message"); info!("this is some information"); warn!("oh no something bad is about to happen"); error!("oh no it's an error"); }
注意,默认该库日志记录是不会记录到本地文件中的。要记录日志还需要其他库。
pretty_env_logger是和log买一送一,最常搭配使用的板条箱。同样,先添加项目依赖:
[dependencies] log = "0.4" pretty_env_logger = "0.4"
然后,在代码中使用:
use log::{error, warn, info, debug, trace}; fn main() { pretty_env_logger::init(); trace!("这是一个示例程序。"); debug!("调试信息xxx。"); info!("程序正在运行中。"); warn!("[WARN]程序有些参数配置有问题。"); error!("[ERROR]程序发生严重错误!");
然后在启动时候增加,日志级别参数RUST_LOG=trace 运行:
env RUST_LOG=trace cargo run Compiling errex v0.1.0 (/home/lz/test/rust/commoncrate/errex) Finished dev [unoptimized + debuginfo] target(s) in 1.32s Running `target/debug/errex` TRACE errex > 这是一个示例程序。 DEBUG errex > 调试信息xxx。 INFO errex > 程序正在运行中。 WARN errex > [WARN]程序有些参数配置有问题。 ERROR errex > [ERROR]程序发生严重错误!
序列化/反序列化
Golang在标准库内置了 包用来实现JSON编码/解码功能。我们可以定义可以轻松读取和写入JSON的类型。下面一个例子:
{ "id": 3137, "author": { "id": 420, "name": "Chongchong" }, "body": "Hello,This is Chongchong web!", "in_reply_to": 3135 }
在Golang中,可以生成如下的结构体:
type Author struct { ID int `json:"id"` Name string `json:"name"` }
type Comment struct { ID int `json:"id"` Author Author `json:"author"` Body string `json:"body"` InReplyTo int `json:"in_reply_to"` }
Rust没有开箱即用的功能,需要使用第三方板条箱,最常用的一个库是serde,可以跨JSON和能想到的所有其他序列化方法使用。
[dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1"
注意,上面serde的依赖配置,和其他包有差异。
Golang的JSON包通过使用struct标签作为元数据来工作,但是Rust没有,需要改用Rust的衍生功能。
因此,要将serde用于的注释类型:
use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Author { pub id: i32, pub name: String, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Comment { pub id: i32, pub author: Author, pub body: String, pub in_reply_to: i32, }
然后,使用以下代码解析Json:
fn main() { let data = r#" { "id": 3137, "author": { "id": 420, "name": "Chongchong" }, "body": "Hello,This is Chongchong web!", "in_reply_to": 3135 } "#; let c: Comment = serde_json::from_str(data).expect("json to parse"); println!("comment: {:#?}", c); }
cargo run
... Finished dev [unoptimized + debuginfo] target(s) in 0.04s Running `target/debug/serdeex` comment: Comment { id: 3137, author: Author { id: 420, name: "Chongchong", }, body: "Hello,This is Chongchong web!", in_reply_to: 3135, }
Web开发
在Web开发中HTTP包必不可少的。Golang中可使用net/http充当生产级HTTP客户端和服务器。
import ( "net/http" "fmt" "log" ) func sayhelloGolang(w http.ResponseWriter, r *http.Request) { r.ParseForm() fmt.Println("path", r.URL.Path) w.Write([]byte("Hello Chongchong!")) } func main() { http.HandleFunc("/",hello) err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } }
它可以让我们非常轻松地进行Web开发。Rust标准库没有开箱即用的HTTP功能,但是Web的框架也非常丰富。
客户端
对于HTTP客户端,可以使用。它还可以与serde无缝集成,以允许从HTTP解析JSON:
[dependencies] reqwest = { version = "0.10", features = ["json"] } tokio = { version = "0.2", features = ["full"] }
tokio默认情况下Rust不附带异步运行时,tokio大约等同于Golang运行时帮助处理的大多数重要事项。简单实例如下:
运行此命令:
cargo run ... Finished dev [unoptimized + debuginfo] target(s) in 3.31s Running `target/debug/webcer` Status: 200 OK Body: <!doctype html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="chrome=1"> <title>hyper.rs | hyper</title> <meta name="description" content="">
结合其他功能,reqwest可以做作为一个功能强大的HTTP客户端。
服务器端
至于HTTP服务器,可以使用warp板条箱。warp是一个建立在Rust的类型系统之上的HTTP服务器框架。
[dependencies] tokio = { version = "0.2", features = ["macros"] } warp = "0.2"
让我们写个简单的"Hello,Chongchong"示例:
use warp::Filter; #[tokio::main] async fn main() { // GET /hello/Chongchong=> 200 OK with body "Hello, Chongchong!" let hello = warp::path!("hello" / String) .map(|name| format!("Hello, {}!", name)); warp::serve(hello) .run(([127, 0, 0, 1], 3030)) .await; }
然后通过127.0.0.1:3030/hello/Chongchong,就可以提示Hello, Chongchong!。
对 warp应用可以使用其or模式构建多条Web路由:
let hello = warp::path!("hello" / String) .map(|name| format!("Hello, {}!", name)); let health = warp::path!(".within" / "health") .map(|| "OK"); let routes = hello.or(health);
还可通过过滤器将其他数据类型注入到处理程序中:
let fact = { let facts = pfacts::make(); warp::any().map(move || facts.clone()) }; let fact_handler = warp::get() .and(warp::path("fact")) .and(fact.clone()) .and_then(give_fact);
warp是功能强大的HTTP服务器,可以跨生产级Web应用程序所需的所有内容工作。
模版
Web开发中要常用模版来特定化页面的输出。Golang的标准库还包括HTML和纯文本模板包html/template和text/template。在Rust中有很多用于HTML模板化的解决方案,比如ructe板条箱。ructe使用Cargo的build.rs功能在编译时为其模板生成Rust代码。这样就可以将HTML模板编译成结果应用程序二进制文件,从而以惊人的速度呈现它们。
添加Cargo.toml:
[build-dependencies] ructe = { version = "0.12", features = ["warp02"] } 还依赖mime板条箱:
[dependencies] mime = "0.3.0"
完成此操作后,templates在当前工作目录中创建一个新文件夹。创建一个名为的文件hello.rs.html,并将以下内容放入其中:
@(title: String, message: String) <html> <head> <title>@title</title> </head> <body> <h1>@title</h1> <p>@message</p> </body> </html>
然后使用模板templates.rs:
use warp::{http::Response, Filter, Rejection, Reply}; async fn hello_html(message: String) -> Result<impl Reply, Rejection> { Response::builder() .html(|o| templates::index_html(o, "Hello".to_string(), message).unwrap().clone())) }
在src/main.rs底部,通过以下语句引入模版定义:
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
在main()函数中调用:
let hello_html_rt = warp::path!("hello" / "html" / String) .and_then(hello_html); let routes = hello_html_rt.or(health).or(hello);