Rust入门教学+实战《使用axum实现一个http server》
前言
Rust最近两年很活跃,作为走在时尚前沿的前端er,了解一下还是很有必要的,毕竟技多不压身嘛。 为了让大家快速理解,我会结合TypeScript做类比,帮助大家更好的理解。因为我本身也是刚学不久,有错误的地方,欢迎大家指出。 如果觉得对你有帮助的话,帮我点点赞。
Rust特点
语言层面
Rust 通过结合高性能和内存安全,提供了一个适合系统编程、嵌入式编程和并发编程的强大工具。其所有权系统和借用检查器在编译时捕获了许多常见的编程错误,使得开发者可以编写高效且安全的代码。
- Rust 速度惊人且内存利用率极高(由于没有运行时和垃圾回收)
- Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误。
- 包管理器和构建工具, 智能地自动补全和类型检验的多编辑器支持
使用领域
命令行、WebAssembly 、网络应用 、嵌入式应用等,都可以使用Rust构建。
hello world
环境安装
mac电脑可以直接通过brew安装
brew install rust
安装之后执行
rustc --version
cargo --version
rust和cargo,就像node和npm的关系。
初始化项目
cargo init 项目名
├── Cargo.toml
├── src
│ └── main.rs
Cargo.toml
类似于package.json
,
src/main.rs
就是默认的入口文件,类似package.json
的main
字段。
(其实就是进程的主线程)
dev运行
cargo run
(类似于npm run dev
或者npm run start
)
结果如下:
Hello, world!
运行成功,至此rust
这门语言我们已经掌握50%了😄。
开发编辑器
编辑器vscode
和RustRover
(推荐这个,jetbrain免费的,不需要安装很多插件)
语法速讲
ps: 语法讲解方面,我只把一些关键的特性讲一下,其他方面需要你自己探索了,主打的是一个“一笔带过,不求甚解。”
基础语法
变量
类型
rust代码中类型,不强制开发者提供,未提供的编译器会做类型推断。 如下:s和s1的类型都为String
let s = String::from("I am xxx");
let s1:String = String::from("I am xxx");
rust类型推断非常强大,不单单是错误校验,有时候还会影响运行结果。 这点后文会提到的
声明与修改
声明变量使用关键字let
fn main() {
let a = "asdasda";
println!("{}",a);
}
注意:每一行代码结束强制使用分号;
执行cargo run
(后文,不做特殊说明都是这个命令)
不过let
和js的let并不一样的,默认情况下,let有点像js const,声明之后不能改,不过可以声明多次。
如下
fn main() {
let a = "1";
println!("{}", a);
let a = "2";
println!("{}", a);
let a = "3";
println!("{}", a);
// 会报错
// a = "4"
}
想要修改怎么办?
使用mut
关键字
fn main() {
let mut a = "1";
println!("{}", a);
a = "2";
println!("{}", a);
// a = "1";
}
所有权
这个是Rust的一个难点之一
所有权其实就是,某一段内存只能归属于一个变量,如果复制给另一个变量,就会发生所有权转移。(这点其实和变量的生命周期有关,js里面的闭包就会存在这种情况,函数执行结束了,变量还没有被回收。rust因为没有gc(垃圾回收),通过引入所有权和生命周期来控制内存的开销)
看两个例子
fn main() {
let a = 10;
let b = a;
println!("{a}");
println!("{b}");
}
打印两个 10,没有任何问题
下面这个例子就有点问题了
fn main() {
let s1 = String::from("I am a superman.");
let s2 = s1;
println!("{s1}");
println!("{s2}");
}
对于固定尺寸类型,会默认放在栈上;而非固定尺寸类型,会默认创建在堆上,成为堆上的一个资源,然后在栈上用一个局部变量来指向它。
这里和js的浅拷贝与深拷贝有点像。不过在let s2 = s1;
会将这段内存的所有权从s1转移给了s2
变量作为实参也会所有权发生转移:
fn foo(s: String) {
println!("{s}");
}
fn main() {
let s1 = String::from("I am xxx");
foo(s1);
println!("{s1}"); // 这里加了一行
}
上面这段代码将会报错。
引用
“引用”这词,我们在回答面试题的时候,都回答过好多遍了。 计算机术语中定义如下
引用是一个数据对象,它包含了另一个对象的位置(地址)。引用本身并不直接包含数据,而是指向数据的存储位置。
在js中我们只知道某个行为是引用造成(比如浅拷贝),没有使用过,属于是看不见摸不着。rust中可以使用引用。
上一节的例子,我们改成这样就可以运行了:
fn main() {
let s1 = String::from("I am xxx");
let s2 = &s1; // 这里加了一行
println!("{}", s1);
println!("{}", s2);
}
引用不会发生所有权转换,只是指向存储地址。
这个例子引用其实有点像es6的getter
。
这时候有同学就要问了是不是还有setter
?
就是我们想通过改变引用,改变原来内存的值。正常情况下肯定是不会让你修改,就像你通过es6设置了getter
一样,想改变值是不是还得设置setter
,rust想改变原来的值的行为就叫解引用。
fn main() {
let mut a = 10;
let b = &mut a;
*b = 20;
println!("{a}"); // 加了一句打印语句
}
注意&mut
表示可变引用。
问题:
下面两个例子加一行代码:都会报错 例子1
fn main() {
let mut a = 10;
let b = &mut a;
*b = 20;
println!("{a}"); // 加了一句打印语句
a = 30;
println!("{b}"); // 加了一句打
}
例子2
fn main() {
let mut a = 10;
let b = &mut a;
*b = 20;
println!("{a}"); // 加了一句打印语句
println!("{b}"); // 加了一句打
}
会报错 这里我们记住一条规律:对于同一个内存(一开始定义的变量a)读写权限不能交叉。如例子2 a=10到打印a(读),交叉了声明b打印b,所以会报错,感兴趣的可以自己研究一下。
字符串
在了解rust的字符串之后,你就发现js字符串背后做了很多的事情。 字符串单引号和双引号对值而言没有区别。 看下面这段代码:
let a = '123'
console.log(a)
a.includes(2)
打印a的时候会是一个字符串原始类型,但是a.include()
又当成对象在使用了。
(js引擎在背后帮我们做了封箱操作)。
单双引号
看下面这段代码:
fn main() {
let a = '2';
let b = "abc";
}
单引号声明的类型为char
类型,且长度只能一位。
双引号声明的为&str
。
字符串类型转换
这里我们重点讲一下双引号字符串的关系,看个例子。
fn main() {
let s1:&str = "I am a superman.";
let s2: String = s1.to_string();
println!("{s2}")
}
str
表示静态数据(也叫字符串切片),写在内存上的一段静态数据,比如字符串“abc”。
&str
就好理解了,指向“abc”的引用。
&str
具有to_string
方法,调用的话变成String
类型。
对象
如下为声明一个对象(rust称为结构)
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle { // 就像这样去实现
fn area(self) -> u32 { // area就是方法,被放在impl实现体中
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("{:?}", rect1);
println!(
"The area of the rectangle is {} square pixels.",
rect1.area() // 使用点号操作符调用area方法
);
// Rectangle { width: 30, height: 50 }
// The area of the rectangle is 1500 square pixels.
}
这里有些知识点需要讲一下:
#[derive(Debug)]
为宏,和ts的装饰器很像,就是安装隐形的翅膀。不过宏是编译阶段(可以理解为帮你把代码写好),装饰器是运行时的。- struct只能定义属性,定义方法通过impl关键字,有点像js对象的原型。
- rust使用组合实现了继承而并非面向对象的那种。
- 方法中也有带有
!
,其实也是宏。 println!("{:?}", rect1);
,{:?}
为debug占位符。
关于宏,后面会讲到。这里先不展开。
构造函数
想自定义构造函数可以这样写:
impl Rectangle {
pub fn new(width: u32, height: u32) -> Self {
Rectangle {
width,
height,
}
}
}
调用构造函数的方式为Rectangle::new(10,10)
pub类似于es6的static
关键字,不过调用方式有些差异。
上文提到的String::from
,from
就是pub
方法。
枚举与模式匹配
看下面一段代码:
enum Number { Zero = 0, One = 1, Two = 2}
fn main() {
let a = Number::One;
match a {
Number::One => {
println!("1");
},
Number::Two => {
println!("2");
},
Number::Zero => {
println!("0");
}
}
}
又有非常多的知识点需要我们学习了:
enum Number
定义一个枚举。这点和ts很像。match
就是模式匹配,类似于js
的switch case
,不过作为强类型语言需要你处理所有的可能情况(毕竟要跑在系统底层的代码,这点后面很多语法都是如此)。比如我们将Number::Zero => { println!("0"); }
移除就会编译不通过。
我们上面也说过类似于js switch
, 可以使用_
关键字达到default
的效果。_
在模式匹配这里就是通配符的作用。
enum Number { Zero = 0, One = 1, Two = 2}
fn main() {
let a = Number::One;
match a {
Number::One => {
println!("1");
},
_ => {
println!("else");
}
}
}
Option
rust中Option
是一个非常关键的枚举
定义如下:
pub enum Option<T> {
None,
Some(T),
}
T就是类似于ts的泛型
有点类似于我在ts中定义某个类型:
function fn(a?:string){}
,(a为undefind
或者string
)
或者是ts的Option util type
。
Option为枚举取值为None
或者Some
看一下这段代码:
fn main() {
let vec = vec![1, 2, 3, 4, 5];
let result = vec.iter().find(|&&x| x % 2 == 0);
match result {
Some(&x) => println!("Found an even number: {}", x),
None => println!("No even number found"),
}
}
又有新的知识点了
vec
表示动态数组(动态的意思是:长度可变的);find
类似于js
的数组find
- 变量
result
的类型就是Option<&i32>
,find可能找到了,也可能没有找到。和js不同,result的取值要么是null
,要么就是找到的那个元素。rust不同,返回一个Option
类型的值 - 我们通过模式匹配检测结果。
在ts中我们能确定那个元素一定存在,可以使用“!”(告诉编译器一定存在的) :
let arr = [1,2,3]
arr.find(v => v ===1)!.
rust中也有类似的操作,我们称为解包
可以使用unwrap
,或者其他api:
fn main() {
let vec = vec![1, 2, 3, 4, 5];
let result = vec.iter().find(|&&x| x % 2 == 0).unwrap();
}
这样result的类型就变成了&i32
,数组元素的类型了。
unwrap
称为:解包。(是不是有点像薛定谔的猫,必须要打开盒子才知道 猫是死(None
)的还是活的(Some
))。
还有内置的Result
枚举类型,就不展开了,感兴趣的可以自行了解。
元组
长度固定的数组,类似于ts tuple
let arr = [1,2,3]
约束
trait
trait
在类型约束方面类似于ts的interface
,但又能力不仅于此。
看一段代码:
struct Point<T> {
x: T,
y: T,
}
fn print<td::fmt::Display>(p: Point<T>) {
println!("Point {}, {}", p.x, p.y);
}
fn main() {
let p = Point {x: 10, y: 20};
print(p);
let p = Point {x: 10.2, y: 20.4};
print(p);
}
知识点又来了:
Point<T>
T类似于ts泛型,使用时可以指定类型。- td::fmt::Display为一个
trait
,print
函数对T进行了约束,T必须实现了Display
,x=10
为i32
可以直接使用println!()方法
, rust帮我们实现了这个trait
;
下面这样调用就会报错:
println!("Point{}",p);
可以为point实现 Display trait
:
use std::fmt;
// 定义 Point 结构体
struct Point<T> {
x: T,
y: T,
}
// 为 Point<T> 实现 Display trait,其中 T 需要实现 Display trait
impl<T: fmt::Display> fmt::Display for Point<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
// 测试整数类型的 Point
let int_point = Point { x: 5, y: 10 };
println!("{}", int_point); // 输出: (5, 10)
}
我们也可以通过调用 debug宏
,debug宏
我们上文提到在编译时帮我们填充代码的,其实就是自动实现了Debug trait
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
fn main() {
let p = Point {x: 10, y: 20};
print(p);
let p = Point {x: 10.2, y: 20.4};
// print(p);
println!("Point{:?}",p);
}
前面我们提到了,rust使用组合实现了类似于面向对象的效果。 trait也可以组合:
T: TraitA + TraitB + TraitC + TraitD
类似于ts的 &,type T = A & B & C
条件表达式:
if
fn main() {
let number = 5;
if number > 0 {
println!("The number is positive.");
} else if number < 0 {
println!("The number is negative.");
} else {
println!("The number is zero.");
}
}
if 比较简单,和js类似。
match
fn main() {
let number = 7;
match number {
1 | 2 | 3 => println!("One, Two, or Three!"),
4..=6 => println!("Four to Six!"),
_ => println!("Other!"),
}
}
match比较灵活。
遍历
for
数组
fn main() {
let array = [1, 2, 3, 4, 5];
for element in array.iter() {
println!("Element: {}", element);
}
}
Range范围
fn main() {
// 使用 for 循环遍历区间 1..10
for i in 1..10 {
println!("{}", i);
}
}
1..10
的类似其实是个Range
类型,可以使用for
循环迭代。
while
fn main() {
let array = [1, 2, 3, 4, 5];
let mut index = 0;
while index < array.len() {
println!("Element: {}", array[index]);
index += 1;
}
}
类型推断
看下面的代码:翻转一个字符串。
fn main() {
let original = "hello, world!";
let reversed: String = original.chars().rev().collect();
let reversed2: Vec<char>= original.chars().rev().collect();
println!("Original: {}", original);
println!("Reversed: {}", reversed);
println!("Reversed2: {:?}", reversed2);
}
打印结果为:
Original: hello, world!
Reversed: !dlrow ,olleh
Reversed2: ['!', 'd', 'l', 'r', 'o', 'w', ' ', ',', 'o', 'l', 'l', 'e', 'h']
其中reversed
与reversed2
操作完全相同,只是标注的变量类型不同,返回结果却差别很大,这样这是rust的编译器的强大之处,不单单是类型约束!
宏
宏的解析和执行是在 Rust 代码的编译阶段之前。你可以理解成,在 Rust 代码编译之前有一个宏展开的过程,这个过程的输出结果就是完整版的 Rust 代码,然后 Rust 编译器再来编译这个输出的代码。
声明宏
vec!就是一个声明宏
let v = vec![1,2,3];
展开为
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
过程宏
派生宏
对于派生这个词我们并不陌生,比如es6关键词extends
或者ts的interface extends
,就是继承。前面我们也提到多次,rust通过组合实现继承。
上文中的#[derive(Debug)]
就是派生宏。
属性宏
只在单元测试时生效
// 声明下面这个函数为单元测试函数,这个属性只作用在test_foo()函数上
#[test]
fn test_foo() {
/* ... */
}
函数宏
在 Rust 中,函数宏(Function-like Macros)是一种允许开发者定义宏看起来像函数调用的语法结构。它们接受输入并生成代码,通常用于代码生成、模板化和元编程。函数宏的定义和使用在 Rust 中非常灵活且强大。
看一段代码:
// 这是伪代码
use sql_macros::sqlbuilder;
fn foo() {
sqlbuilder!(select title, content from article where id='1111111';);
}
上面的select title, content from article where id='1111111';)
非rust语法,通过宏的能力,可以让我们书写sql非常舒服,不需要做字符串拼接了。
http-server
tokio
Tokio 是一个用于异步编程的 Rust 库。它提供了必要的工具和运行时,用于构建高性能的异步 I/O 应用程序,例如网络服务器、客户端、和其他需要并发处理的应用。Tokio 的设计目标是高效地处理大量 I/O 操作,同时保持较低的延迟和高吞吐量。
看一段代码:
use tokio::fs::File;
use tokio::io::AsyncWriteExt; // 引入AsyncWriteExt trait
async fn doit() -> std::io::Result<()> {
let mut file = File::create("foo.txt").await.unwrap(); // 创建文件
file.write_all(b"hello, world!").await.unwrap(); // 写入内容
Ok(())
}
#[tokio::main]
async fn main() {
let result = doit().await; // 注意这里的.await
}
上面的这段代码的作用是:创建文本文件、写入文本到文件。
代码风格是不是很熟悉?很像js的async await
,事实也是如此,确实很像。
不过rust本身并不支持这种语法,tokio
在底层帮我们做了很多事情。
#[tokio::main] // 这个是tokio库里面提供的一个属性宏标注
async fn main() { // 注意 main 函数前面有 async
println!("Hello world");
}
展开之后:
fn main() {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async { // 注意这里block_on,里面是异步代码
println!("Hello world");
})
}
上面这段代码,我们看函数名能略知一二:new_multi_thread()
起多线程来这些任务。和nodejs
的异步api很像(ps: node启动时,会启动一个线程池,有异步任务了,就会在线程池里面取一个线程,来执行任务,执行完成了通知(kqueue、epoll等系统级别通知)主线程 ,nginx
也是如此)。
Axum
Axum 是一个基于 Tokio 的高性能异步 HTTP 服务器框架,用于构建 Rust 中的 Web 应用程序和 API。Axum 旨在提供一种简单、灵活且高效的方式来构建现代 Web 服务,充分利用 Rust 的所有权模型和类型系统来实现安全且高效的代码。
hello world
src/main.rs
use axum::{ // 引入 Axum 库
routing::get, // 引入用于定义 GET 路由的模块
Router, // 引入 Router 类型,用于构建路由器
};
use std::net::SocketAddr; // 引入标准库中的 SocketAddr 类型,用于定义服务器的监听地址
use tokio::main; // 引入 Tokio 库中的 main 宏,用于定义异步主函数
#[main] // 使用 Tokio 的 main 宏,表示该函数是异步的主函数
async fn main() {
// 构建路由器,将根路径 "/" 映射到 hello_world 处理程序
let app = Router::new().route("/", get(hello_world));
// 定义服务器的监听地址,127.0.0.1 表示本地主机,端口号为 3000
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
// 打印服务器的监听地址
println!("Listening on http://{}", addr);
// 启动服务器并开始监听指定的地址,将请求路由到相应的处理程序
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await // 等待服务器异步运行
.unwrap(); // 如果服务器运行过程中出现错误,将其解包并 panic
}
// 定义一个异步处理程序,处理根路径 "/" 的 GET 请求
async fn hello_world() -> &'static str {
"Hello, World!" // 返回一个静态字符串作为响应内容
}
use
类似于es的import
,可以使用三方包或者是标准库代码,使用来use之后,就可以使用这个全局变量(命名空间)了。
需要注意的是:
use axum::{ // 引入 Axum 库
routing::get, // 引入用于定义 GET 路由的模块
Router, // 引入 Router 类型,用于构建路由器
};
只会“注册”最后一级空间,比如上面的代码,可以使用get
,不可以使用routing
Cargo.toml
包的版本请保存一致,可能存在break change
[package]
name = "rust-study"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.5"
tokio = { version = "1", features = ["full"] }
上面代码展示写一个简单http server
,写了一个简单的get
请求。
每一行都有注释,就不过多解释了。代码书写风格很像nodejs
的一些http server
框架。
日志
引入日志(注意版本号和我的保持一致)
cargo add tracing
cargo add tracing-subscriber
Cargo.toml
[package]
name = "rust-study"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.5.0", features = ["fs", "trace"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
main.rs
use axum::{ routing::get, Router};
use tower_http::{
trace::TraceLayer,
};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/foo", get(handler))
.layer(TraceLayer::new_for_http());
// run it
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
async fn handler() -> &'static str {
"<h1>Hello, World!</h1>"
}
启动服务(包含日志):
RUST_LOG=trace cargo run
日志级别有:eror、warn、info、debug、trace,级别由左到右从高到低。
get请求
get请求需要处理两种参数,一种是param
,一种是query
route path
伪代码如下:
path: '/data/detail/:id'
我们请求时: get('/data/detail/123?name=liyunlong&age=40')
id
就是123, query
就是{name: 'liyunlong', age: '40'}
在cargo.toml
,加一个序列化的包
serde = { version = "1.0.204", features = ["derive"] }
main.rs
use axum::{ extract::{Query, Path},routing::get, Router};
use tower_http::{
trace::TraceLayer,
};
use serde::Deserialize;
use axum::response::Html;
use axum::response::IntoResponse;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/foo", get(handler))
.route("/data/detail/:id", get(query))
.layer(TraceLayer::new_for_http());
// run it
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
async fn handler() -> &'static str {
"<h1>Hello, World!</h1>"
}
#[derive(Debug, Deserialize)]
struct InputParams {
age: i32,
name: String,
job: Option<String>,
}
async fn query(Path(id): Path<i32>,Query(params): Query<InputParams>) -> impl IntoResponse {
tracing::debug!("id {:?}", id);
tracing::debug!("query params {:?}", params);
Html("<h3>Test query</h3>")
}
这样就可以提取 id
和query
了
而且参数类型和InputParams
不一致的话,直接帮我们做校验了,不需要额外的配置。
http://localhost:3000/data/detail/123?name=李云龙
post请求
运行: RUST_LOG=trace cargo run
控制台就可以看到body payload
解析成功了!。
同时,请求参数和我们struct定义的不一样也会报错的。
完整代码:
use axum::{ extract::{Query, Path, Json},routing::{get, post}, Router};
use tower_http::{
trace::TraceLayer,
};
use serde::Deserialize;
use axum::response::Html;
use axum::response::IntoResponse;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/foo", get(handler))
.route("/data/detail/:id", get(query))
.route("/json", post(accept_json))
.layer(TraceLayer::new_for_http());
// run it
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
async fn handler() -> &'static str {
"<h1>Hello, World!</h1>"
}
#[derive(Debug, Deserialize)]
struct InputParams {
age: i32,
name: String,
job: Option<String>,
}
async fn query(Path(id): Path<i32>,Query(params): Query<InputParams>) -> impl IntoResponse {
tracing::debug!("id {:?}", id);
tracing::debug!("query params {:?}", params);
Html("<h3>Test query</h3>")
}
#[derive(Deserialize, Debug)]
struct Input {
name: String,
email: String,
}
async fn accept_json(Json(input): Json<Input>) -> Html<&'static str> {
tracing::debug!("json params {:?}", input);
Html("<h3>Json posted</h3>")
}
连接数据库
通过docker
启动 pg
容器:
docker run --name some-postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=changeme -e POSTGRES_DB=todolist -p 5432:5432 -d postgres
可以使用navicat
等工具连接测试一下。
数据库: todolist
用户名: postgres
密码: changeme
也可以使用免费的工具:dbeaver等。
开发一个todoList
创建表
create table todo (
id varchar not null,
description varchar not null,
completed bool not null);
连接数据库
可以使用bb8
和bb8_postgres
来进行数据库连接与查询操作(也可以选择其他rustORM
工具。)
todo list
完整代码参考:(有详细的注释)
使用postman调用结果:
docker 构建
Dockerfile
bash
代码解读
复制代码
# 使用官方的最新Rust镜像作为基础镜像
FROM rust:latest
# 安装构建工具和依赖项
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libssl-dev \
pkg-config \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /usr/src/rust-study
# 将Cargo.toml和Cargo.lock复制到工作目录
COPY Cargo.toml Cargo.lock ./
# 创建空的src目录,以便缓存依赖项
RUN mkdir src
# 预先编译依赖项
RUN cargo build --release
RUN rm -rf src
# 复制项目的所有源文件到工作目录
COPY . .
# 再次构建项目
RUN cargo build --release
# 暴露应用运行的端口
EXPOSE 3000
# 运行构建好的二进制文件
CMD ["./target/release/rust-study"]
执行bash build.sh
构建镜像。
注意:
- 构建时需要注意运行时的处理器架构。(建议在目标机器构建)。
- 有些库linux基础镜像可能不具备,在构建时进行安装。
- 注意网络问题,能不能拉到docker官方库的镜像、apt-get能不能拉得到,拉不到要想其他方案了。
结语
- 虽然rust出现的时间不长,但是生态正在蓬勃发展。
- 因为比较新,国内资料较少,后面技术选型,维护成本也是一个考量因素。
- 学习rust,“深度优先”找到一个“根结点”,写一个功能的demo,过分纠结语法细节,会一致卡住,特别是rust这种概念多,语法复杂的语言。
转载自:https://juejin.cn/post/7389912762049642548