Rust异步编程的错误处理艺术

Rust异步编程的错误处理艺术

Rust异步编程的错误处理艺术

在这里插入图片描述

一、异步错误的本质与分类

1.1 异步错误与同步错误的区别

💡在Rust同步编程中,错误通常是通过Result<T, E>类型返回的,Err变体包含了错误信息,程序会阻塞线程直到操作完成。而在异步编程中,操作的结果是一个Future<Output = Result<T, E>>,程序会暂停任务直到操作完成,Err变体可能是IO错误、超时错误、取消错误等异步场景特有的错误。

同步错误示例:

usestd::fs::File;usestd::io::Read;// 同步读取文件,阻塞线程fnread_file_sync()->Result<String,std::io::Error>{letmut file =File::open("test.txt")?;letmut content =String::new(); file.read_to_string(&mut content)?;Ok(content)}fnmain(){matchread_file_sync(){Ok(content)=>println!("File content: {}", content),Err(e)=>println!("Error reading file: {}", e),}}

异步错误示例:

usetokio::fs::File;usetokio::io::AsyncReadExt;// 异步读取文件,暂停任务asyncfnread_file_async()->Result<String,std::io::Error>{letmut file =File::open("test.txt").await?;letmut content =String::new(); file.read_to_string(&mut content).await?;Ok(content)}#[tokio::main]asyncfnmain(){matchread_file_async().await{Ok(content)=>println!("File content: {}", content),Err(e)=>println!("Error reading file: {}", e),}}

1.2 异步错误的分类

异步错误可以分为以下几类:

1. IO错误

IO错误是异步编程中最常见的错误类型,包括网络连接失败、文件读取失败、数据库连接失败等。这类错误通常由std::io::Error表示。

usetokio::net::TcpStream;asyncfnconnect_to_server()->Result<TcpStream,std::io::Error>{let stream =TcpStream::connect("127.0.0.1:8080").await?;Ok(stream)}
2. 超时错误

超时错误是指异步操作在指定时间内没有完成。Tokio提供了tokio::time::timeout函数来设置超时,超时会返回tokio::time::error::Elapsed错误。

usetokio::time::{timeout,Duration};asyncfnasync_operation(){tokio::time::sleep(Duration::from_secs(3)).await;println!("Operation completed");}#[tokio::main]asyncfnmain(){let result =timeout(Duration::from_secs(2),async_operation()).await;match result {Ok(_)=>println!("Success"),Err(e)=>println!("Error: {}", e),}}
3. 取消错误

取消错误是指异步任务被主动取消。Tokio的tokio::task::JoinHandle::abort方法可以取消任务,取消会返回tokio::task::JoinError错误。

usetokio::time::sleep;usestd::time::Duration;asyncfntask(){sleep(Duration::from_secs(5)).await;println!("Task completed");}#[tokio::main]asyncfnmain(){let handle =tokio::spawn(task());sleep(Duration::from_secs(2)).await; handle.abort();let result = handle.await;match result {Ok(_)=>println!("Success"),Err(e)=>println!("Error: {}", e),}}
4. 业务逻辑错误

业务逻辑错误是指应用程序的业务规则违反,例如用户不存在、余额不足、密码错误等。这类错误需要我们自定义错误类型来表示。

// 自定义业务逻辑错误类型#[derive(Debug)]enumBusinessError{UserNotFound,InsufficientBalance,InvalidPassword,}implstd::fmt::DisplayforBusinessError{fnfmt(&self, f:&mutstd::fmt::Formatter)->std::fmt::Result{matchself{BusinessError::UserNotFound=>write!(f,"User not found"),BusinessError::InsufficientBalance=>write!(f,"Insufficient balance"),BusinessError::InvalidPassword=>write!(f,"Invalid password"),}}}implstd::error::ErrorforBusinessError{}asyncfnlogin(username:&str, password:&str)->Result<(),BusinessError>{if username !="admin"{returnErr(BusinessError::UserNotFound);}if password !="password"{returnErr(BusinessError::InvalidPassword);}Ok(())}
5. 系统错误

系统错误是指操作系统级别的错误,例如内存不足、权限不足等。这类错误通常由std::io::Error表示。

usetokio::fs::File;asyncfnread_sensitive_file()->Result<String,std::io::Error>{letmut file =File::open("/etc/shadow").await?;letmut content =String::new(); file.read_to_string(&mut content).await?;Ok(content)}

二、异步上下文中的标准Result处理

2.1 ?操作符的异步应用

💡在异步函数中,?操作符的使用方式与同步函数相同。当遇到Err变体时,它会将错误包装在Result<_, Box<dyn Error>>类型中并返回。

usetokio::fs::File;usetokio::io::AsyncReadExt;usestd::error::Error;asyncfnread_file_and_parse()->Result<Vec<u32>,Box<dynError>>{letmut file =File::open("numbers.txt").await?;letmut content =String::new(); file.read_to_string(&mut content).await?;let numbers:Vec<u32>= content .lines().map(|line| line.parse::<u32>()).collect::<Result<_, _>>()?;Ok(numbers)}#[tokio::main]asyncfnmain(){matchread_file_and_parse().await{Ok(numbers)=>println!("Numbers: {:?}", numbers),Err(e)=>println!("Error: {}", e),}}

2.2 unwrap/expect的注意事项

在异步任务中使用unwrap()expect()是非常危险的,因为它们会导致任务崩溃。我们应该尽量使用match语句或?操作符来处理错误。

危险示例:

usetokio::fs::File;usetokio::io::AsyncReadExt;asyncfnread_file(){letmut file =File::open("nonexistent.txt").await.unwrap();// 会导致任务崩溃letmut content =String::new(); file.read_to_string(&mut content).await.unwrap();println!("File content: {}", content);}#[tokio::main]asyncfnmain(){read_file().await;}

安全示例:

usetokio::fs::File;usetokio::io::AsyncReadExt;asyncfnread_file()->Result<(),Box<dynstd::error::Error>>{letmut file =File::open("nonexistent.txt").await?;letmut content =String::new(); file.read_to_string(&mut content).await?;println!("File content: {}", content);Ok(())}#[tokio::main]asyncfnmain(){matchread_file().await{Ok(_)=>println!("Success"),Err(e)=>println!("Error: {}", e),}}

2.3 Box的应用

Box<dyn Error>是Rust中用于统一错误类型的常用方法。它可以容纳任何实现了Error trait的类型,包括标准库的错误类型和自定义错误类型。

usetokio::time::{timeout,Duration};usestd::error::Error;asyncfnasync_operation()->Result<(),Box<dynError>>{// 模拟超时错误let result =timeout(Duration::from_secs(2),tokio::time::sleep(Duration::from_secs(3))).await?;Ok(result)}asyncfnread_file()->Result<String,Box<dynError>>{letmut file =tokio::fs::File::open("test.txt").await?;letmut content =String::new(); file.read_to_string(&mut content).await?;Ok(content)}asyncfncombined_operation()->Result<(),Box<dynError>>{async_operation().await?;let content =read_file().await?;println!("File content: {}", content);Ok(())}#[tokio::main]asyncfnmain(){matchcombined_operation().await{Ok(_)=>println!("Success"),Err(e)=>println!("Error: {}", e),}}

三、自定义异步错误类型的设计

3.1 thiserror与anyhow的对比

💡在Rust中,有两个常用的错误处理库:thiserroranyhow

  • thiserror:用于定义自定义错误类型,支持Error trait的实现,适合库开发。
  • anyhow:用于快速原型开发,支持Error链,适合应用开发。

在Cargo.toml中添加依赖:

[dependencies] thiserror = "1.0" anyhow = "1.0" 

3.2 用thiserror定义自定义错误类型

thiserror提供了#[derive(Error)]宏,可以快速生成Error trait的实现。

usethiserror::Error;usetokio::time::error::Elapsed;#[derive(Error, Debug)]pubenumAppError{#[error("IO error: {0}")]Io(#[from]std::io::Error),#[error("Timeout error: {0}")]Timeout(#[from]Elapsed),#[error("Business error: {0}")]Business(#[from]BusinessError),#[error("System error: {0}")]System(String),}#[derive(Error, Debug)]pubenumBusinessError{#[error("User not found")]UserNotFound,#[error("Insufficient balance")]InsufficientBalance,#[error("Invalid password")]InvalidPassword,}asyncfnlogin(username:&str, password:&str)->Result<(),AppError>{if username !="admin"{returnErr(AppError::Business(BusinessError::UserNotFound));}if password !="password"{returnErr(AppError::Business(BusinessError::InvalidPassword));}Ok(())}asyncfnconnect_to_server()->Result<tokio::net::TcpStream,AppError>{let stream =tokio::net::TcpStream::connect("127.0.0.1:8080").await?;Ok(stream)}

3.3 用anyhow处理错误

anyhow提供了Result<T>类型,它是Result<T, anyhow::Error>的别名。使用anyhow可以快速处理各种类型的错误。

useanyhow::Result;usetokio::time::{timeout,Duration};asyncfnasync_operation()->Result<()>{let result =timeout(Duration::from_secs(2),tokio::time::sleep(Duration::from_secs(3))).await?;Ok(result)}asyncfnread_file()->Result<String>{letmut file =tokio::fs::File::open("test.txt").await?;letmut content =String::new(); file.read_to_string(&mut content).await?;Ok(content)}asyncfncombined_operation()->Result<()>{async_operation().await?;let content =read_file().await?;println!("File content: {}", content);Ok(())}#[tokio::main]asyncfnmain(){ifletErr(e)=combined_operation().await{println!("Error: {}", e);println!("Chain:");for(i, cause)in e.chain().enumerate(){println!(" {}: {}", i, cause);}}}

3.4 自定义错误类型的实战应用

我们可以结合thiserroranyhow,在库中使用thiserror定义自定义错误类型,在应用中使用anyhow处理错误。

库代码(my-lib/src/lib.rs):

usethiserror::Error;#[derive(Error, Debug)]pubenumMyLibError{#[error("Invalid input: {0}")]InvalidInput(String),#[error("Network error: {0}")]Network(#[from]reqwest::Error),#[error("Database error: {0}")]Database(#[from]sqlx::Error),}pubasyncfnfetch_data()->Result<String,MyLibError>{let response =reqwest::get("https://example.com/api/data").await?;let data = response.text().await?;Ok(data)}

应用代码(src/main.rs):

usemy_lib::fetch_data;useanyhow::Result;asyncfnprocess_data()->Result<()>{let data =fetch_data().await?;println!("Data: {}", data);Ok(())}#[tokio::main]asyncfnmain(){ifletErr(e)=process_data().await{println!("Error: {}", e);}}

四、超时与取消错误的处理

4.1 超时错误的处理

Tokio的tokio::time::timeout函数会返回Result<T, Elapsed>类型,我们需要对Elapsed错误进行处理。

usetokio::time::{timeout,Duration};usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Timeout error")]Timeout,#[error("Other error: {0}")]Other(#[from]anyhow::Error),}asyncfnasync_operation()->Result<(),AppError>{// 模拟长时间运行的操作tokio::time::sleep(Duration::from_secs(5)).await;Ok(())}asyncfnrun_with_timeout()->Result<(),AppError>{let result =timeout(Duration::from_secs(3),async_operation()).await;match result {Ok(_)=>Ok(()),Err(_)=>Err(AppError::Timeout),}}#[tokio::main]asyncfnmain(){matchrun_with_timeout().await{Ok(_)=>println!("Success"),Err(AppError::Timeout)=>println!("Error: Operation timed out"),Err(e)=>println!("Error: {}", e),}}

4.2 取消错误的处理

Tokio的tokio::task::JoinHandle::abort方法会取消任务,取消会返回tokio::task::JoinError错误。

usetokio::time::sleep;usestd::time::Duration;usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Task canceled")]Canceled,#[error("Other error: {0}")]Other(#[from]anyhow::Error),}asyncfntask()->Result<(),AppError>{for i in0..5{println!("Task iteration {}", i);sleep(Duration::from_secs(1)).await;}Ok(())}asyncfnrun_with_cancel()->Result<(),AppError>{let handle =tokio::spawn(task());sleep(Duration::from_secs(2)).await; handle.abort();let result = handle.await;match result {Ok(_)=>Ok(()),Err(e)=>{if e.is_cancelled(){Err(AppError::Canceled)}else{Err(AppError::Other(e.into()))}},}}#[tokio::main]asyncfnmain(){matchrun_with_cancel().await{Ok(_)=>println!("Success"),Err(AppError::Canceled)=>println!("Error: Task canceled"),Err(e)=>println!("Error: {}", e),}}

4.3 超时与取消的组合处理

在实际应用中,我们可能需要同时处理超时和取消错误。

usetokio::time::{timeout,Duration};usetokio::sync::oneshot;usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Timeout error")]Timeout,#[error("Task canceled")]Canceled,#[error("Other error: {0}")]Other(#[from]anyhow::Error),}asyncfnasync_operation(mut cancel_rx:oneshot::Receiver<()>)->Result<(),AppError>{for i in0..5{println!("Task iteration {}", i);tokio::select!{ _ =tokio::time::sleep(Duration::from_secs(1))=>{}, _ =&mut cancel_rx =>returnErr(AppError::Canceled),}}Ok(())}asyncfnrun_with_timeout_and_cancel()->Result<(),AppError>{let(cancel_tx, cancel_rx)=oneshot::channel();let handle =tokio::spawn(async_operation(cancel_rx));let result =timeout(Duration::from_secs(3),asyncmove{let _ = cancel_tx.send(()); handle.await}).await;match result {Ok(Ok(_))=>Ok(()),Ok(Err(e))=>{if e.is_cancelled(){Err(AppError::Canceled)}else{Err(AppError::Other(e.into()))}},Err(_)=>Err(AppError::Timeout),}}#[tokio::main]asyncfnmain(){matchrun_with_timeout_and_cancel().await{Ok(_)=>println!("Success"),Err(AppError::Timeout)=>println!("Error: Operation timed out"),Err(AppError::Canceled)=>println!("Error: Task canceled"),Err(e)=>println!("Error: {}", e),}}

五、并发任务的错误聚合

5.1 join!与try_join!的错误处理

  • join!:等待所有任务完成,不处理错误。
  • try_join!:有一个任务失败就返回错误。
usetokio::join;usetokio::try_join;usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Task1 error")]Task1,#[error("Task2 error")]Task2,}asyncfntask1()->Result<(),AppError>{tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;Ok(())}asyncfntask2()->Result<(),AppError>{tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;Err(AppError::Task2)}#[tokio::main]asyncfnmain(){println!("Using join!:");let(res1, res2)=join!(task1(),task2());println!("Task1 result: {:?}", res1);println!("Task2 result: {:?}", res2);println!("\nUsing try_join!:");let result =try_join!(task1(),task2());println!("Result: {:?}", result);}

5.2 多个任务的错误聚合

我们可以使用futures::future::try_join_all函数聚合多个任务的错误。

usefutures::future::try_join_all;usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Task {0} error")]Task(usize),}asyncfntask(i:usize)->Result<usize,AppError>{tokio::time::sleep(tokio::time::Duration::from_secs(i asu64)).await;if i %2==0{Ok(i)}else{Err(AppError::Task(i))}}#[tokio::main]asyncfnmain(){let tasks =(0..5).map(|i|task(i));let result =try_join_all(tasks).await;println!("Result: {:?}", result);}

5.3 spawn的错误处理

Tokio的tokio::spawn函数会返回tokio::task::JoinHandle<T>类型,我们需要使用join()方法等待任务完成并处理错误。

usetokio::time::sleep;usestd::time::Duration;usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Task error")]Task,}asyncfntask()->Result<(),AppError>{sleep(Duration::from_secs(2)).await;Err(AppError::Task)}#[tokio::main]asyncfnmain(){let handle =tokio::spawn(task());let result = handle.await;println!("Task result: {:?}", result);}

六、异步错误的传递与传播

6.1 错误链的构建

错误链是指错误之间的因果关系,我们可以使用thiserror#[source]属性来构建错误链。

usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Failed to fetch data: {0}")]FetchData(#[source]reqwest::Error),#[error("Failed to parse data: {0}")]ParseData(#[source]serde_json::Error),}asyncfnfetch_and_parse_data()->Result<serde_json::Value,AppError>{let response =reqwest::get("https://example.com/api/data").await.map_err(AppError::FetchData)?;let data = response.text().await.map_err(AppError::FetchData)?;let json =serde_json::from_str(&data).map_err(AppError::ParseData)?;Ok(json)}#[tokio::main]asyncfnmain(){let result =fetch_and_parse_data().await;match result {Ok(json)=>println!("Data: {}",serde_json::to_string_pretty(&json).unwrap()),Err(e)=>{println!("Error: {}", e);println!("Cause chain:");letmut cause = e.source();whileletSome(c)= cause {println!(" {}", c); cause = c.source();}},}}

6.2 异步任务间的错误传递

我们可以使用tokio::sync::oneshot通道在异步任务间传递错误。

usetokio::sync::oneshot;usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Task error")]Task,}asyncfntask(tx:oneshot::Sender<Result<(),AppError>>){tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;let _ = tx.send(Err(AppError::Task));}#[tokio::main]asyncfnmain(){let(tx, rx)=oneshot::channel();tokio::spawn(task(tx));let result = rx.await.unwrap();println!("Task result: {:?}", result);}

6.3 父子任务间的错误传递

当父任务创建子任务时,我们可以将子任务的错误传递到父任务。

usetokio::time::sleep;usestd::time::Duration;usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Child task error")]ChildTask,}asyncfnchild_task()->Result<(),AppError>{sleep(Duration::from_secs(1)).await;Err(AppError::ChildTask)}asyncfnparent_task()->Result<(),AppError>{let handle =tokio::spawn(child_task());let result = handle.await?;Ok(result)}#[tokio::main]asyncfnmain(){let result =parent_task().await;println!("Parent task result: {:?}", result);}

七、实战项目:异步API的错误处理体系

7.1 项目需求与架构设计

我们将构建一个异步RESTful API,支持用户管理功能,包括创建用户、查询用户、更新用户和删除用户。我们将设计完整的错误处理体系,包括请求验证错误、数据库操作错误、业务逻辑错误的统一响应。

项目架构设计:

  • 使用Axum作为HTTP框架
  • 使用SQLx作为数据库访问库
  • 使用thiserror定义自定义错误类型
  • 使用serde定义API响应结构
  • 实现错误中间件,统一处理所有错误

7.2 依赖配置与项目初始化

创建项目:

cargo new rust-async-api-error-handling cd rust-async-api-error-handling 

在Cargo.toml中添加依赖:

[dependencies] axum = { version = "0.5", features = ["ws"] } tokio = { version = "1.0", features = ["full"] } sqlx = { version = "0.6", features = ["postgres", "runtime-tokio-rustls"] } thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.1", features = ["v4"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } 

7.3 数据库连接与模型定义

创建src/db.rs:

usesqlx::PgPool;pubasyncfncreate_pool(database_url:&str)->PgPool{PgPool::connect(database_url).await.unwrap()}

创建src/models.rs:

useserde::{Deserialize,Serialize};useuuid::Uuid;#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]pubstructUser{pub id:Uuid,pub name:String,pub email:String,pub password:String,pub created_at:chrono::DateTime<chrono::Utc>,pub updated_at:chrono::DateTime<chrono::Utc>,}#[derive(Debug, Serialize, Deserialize)]pubstructCreateUserRequest{pub name:String,pub email:String,pub password:String,}#[derive(Debug, Serialize, Deserialize)]pubstructUpdateUserRequest{pub name:Option<String>,pub email:Option<String>,pub password:Option<String>,}

7.4 错误类型与响应结构

创建src/errors.rs:

usethiserror::Error;useaxum::{http::StatusCode,response::{IntoResponse,Response}};useserde::{Serialize,Deserialize};#[derive(Error, Debug)]pubenumAppError{#[error("Database error: {0}")]Database(#[from]sqlx::Error),#[error("Request validation error: {0}")]Validation(String),#[error("User not found")]UserNotFound,#[error("User already exists")]UserAlreadyExists,#[error("Internal server error")]InternalServerError,}#[derive(Debug, Serialize, Deserialize)]pubstructErrorResponse{pub error:String,pub code:u16,pub message:String,}implIntoResponseforAppError{fninto_response(self)->Response{let(status, code, message)=matchself{AppError::Database(e)=>{tracing::error!("Database error: {:?}", e);(StatusCode::INTERNAL_SERVER_ERROR,500,"Internal server error")},AppError::Validation(msg)=>(StatusCode::BAD_REQUEST,400, msg.as_str()),AppError::UserNotFound=>(StatusCode::NOT_FOUND,404,"User not found"),AppError::UserAlreadyExists=>(StatusCode::CONFLICT,409,"User already exists"),AppError::InternalServerError=>(StatusCode::INTERNAL_SERVER_ERROR,500,"Internal server error"),};let response =ErrorResponse{ error:self.to_string(), code: code, message: message.to_string(),};(status,axum::Json(response)).into_response()}}

7.5 业务逻辑实现

创建src/repository.rs:

usesqlx::PgPool;useuuid::Uuid;usecrate::models::*;usecrate::errors::AppError;pubasyncfncreate_user(pool:&PgPool, user:CreateUserRequest)->Result<User,AppError>{// 检查用户是否已经存在let exists =sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)", user.email).fetch_one(pool).await?;if exists {returnErr(AppError::UserAlreadyExists);}// 创建用户let user =sqlx::query_as!(User,"INSERT INTO users (id, name, email, password, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *",Uuid::new_v4(), user.name, user.email, user.password,chrono::Utc::now(),chrono::Utc::now()).fetch_one(pool).await?;Ok(user)}pubasyncfnget_user_by_id(pool:&PgPool, id:Uuid)->Result<User,AppError>{let user =sqlx::query_as!(User,"SELECT * FROM users WHERE id = $1", id).fetch_one(pool).await.map_err(|e|{if e.as_database_error().is_some()&& e.to_string().contains("no row found"){AppError::UserNotFound}else{AppError::Database(e)}})?;Ok(user)}pubasyncfnupdate_user(pool:&PgPool, id:Uuid, user:UpdateUserRequest)->Result<User,AppError>{let updated_user =sqlx::query_as!(User,"UPDATE users SET name = COALESCE($1, name), email = COALESCE($2, email), password = COALESCE($3, password), updated_at = $4 WHERE id = $5 RETURNING *", user.name, user.email, user.password,chrono::Utc::now(), id ).fetch_one(pool).await.map_err(|e|{if e.as_database_error().is_some()&& e.to_string().contains("no row found"){AppError::UserNotFound}else{AppError::Database(e)}})?;Ok(updated_user)}pubasyncfndelete_user(pool:&PgPool, id:Uuid)->Result<(),AppError>{let result =sqlx::query!("DELETE FROM users WHERE id = $1", id).execute(pool).await?;if result.rows_affected()==0{returnErr(AppError::UserNotFound);}Ok(())}

创建src/service.rs:

usecrate::repository::*;usecrate::models::*;usecrate::errors::AppError;usesqlx::PgPool;pubasyncfncreate_user_service(pool:&PgPool, user:CreateUserRequest)->Result<User,AppError>{// 验证请求参数if user.name.is_empty(){returnErr(AppError::Validation("Name is required".to_string()));}if user.email.is_empty(){returnErr(AppError::Validation("Email is required".to_string()));}if!user.email.contains("@"){returnErr(AppError::Validation("Invalid email format".to_string()));}if user.password.len()<6{returnErr(AppError::Validation("Password must be at least 6 characters long".to_string()));}create_user(pool, user).await}pubasyncfnget_user_by_id_service(pool:&PgPool, id:uuid::Uuid)->Result<User,AppError>{get_user_by_id(pool, id).await}pubasyncfnupdate_user_service(pool:&PgPool, id:uuid::Uuid, user:UpdateUserRequest)->Result<User,AppError>{// 验证请求参数ifletSome(email)=&user.email {if!email.contains("@"){returnErr(AppError::Validation("Invalid email format".to_string()));}}ifletSome(password)=&user.password {if password.len()<6{returnErr(AppError::Validation("Password must be at least 6 characters long".to_string()));}}update_user(pool, id, user).await}pubasyncfndelete_user_service(pool:&PgPool, id:uuid::Uuid)->Result<(),AppError>{delete_user(pool, id).await}

7.6 API路由与中间件

创建src/routes.rs:

useaxum::{routing::{get, post, put, delete},extract::Path,extract::State,Json,};useuuid::Uuid;usecrate::models::*;usecrate::service::*;usecrate::errors::AppError;usesqlx::PgPool;pubfncreate_routes()->axum::Router<PgPool>{axum::Router::new().route("/users",post(create_user_handler)).route("/users/:id",get(get_user_handler)).route("/users/:id",put(update_user_handler)).route("/users/:id",delete(delete_user_handler))}asyncfncreate_user_handler(State(pool):State<PgPool>,Json(req):Json<CreateUserRequest>,)->Result<Json<User>,AppError>{let user =create_user_service(&pool, req).await?;Ok(Json(user))}asyncfnget_user_handler(State(pool):State<PgPool>,Path(id):Path<Uuid>,)->Result<Json<User>,AppError>{let user =get_user_by_id_service(&pool, id).await?;Ok(Json(user))}asyncfnupdate_user_handler(State(pool):State<PgPool>,Path(id):Path<Uuid>,Json(req):Json<UpdateUserRequest>,)->Result<Json<User>,AppError>{let user =update_user_service(&pool, id, req).await?;Ok(Json(user))}asyncfndelete_user_handler(State(pool):State<PgPool>,Path(id):Path<Uuid>,)->Result<(),AppError>{delete_user_service(&pool, id).await}

7.7 应用程序入口

创建src/main.rs:

useaxum::Router;usetracing_subscriber::prelude::*;usetracing::info;usecrate::db::create_pool;usecrate::routes::create_routes;moddb;moderrors;modmodels;modrepository;modroutes;modservice;#[tokio::main]asyncfnmain(){tracing_subscriber::registry().with(tracing_subscriber::EnvFilter::new("info")).with(tracing_subscriber::fmt::layer()).init();let database_url ="postgresql://user:password@localhost:5432/mydb";let pool =create_pool(database_url).await;info!("Database pool created");let app:Router<sqlx::PgPool>=create_routes();let listener =tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();info!("API server running on http://0.0.0.0:3000");axum::serve(listener, app.with_state(pool)).await.unwrap();}

7.8 数据库初始化

创建migrations目录:

mkdir-p migrations/20230101000000_create_users 

创建migrations/20230101000000_create_users/up.sql:

CREATETABLE users ( id UUID PRIMARYKEY, name VARCHAR(255)NOTNULL, email VARCHAR(255)NOTNULLUNIQUE, password VARCHAR(255)NOTNULL, created_at TIMESTAMPNOTNULL, updated_at TIMESTAMPNOTNULL);

创建migrations/20230101000000_create_users/down.sql:

DROPTABLE users;

运行数据库迁移:

cargo sqlx migrate run 

7.9 测试API

我们可以使用curl工具测试API:

  1. 创建用户:
curl-X POST -H"Content-Type: application/json"-d'{"name": "Alice", "email": "[email protected]", "password": "password123"}' http://localhost:3000/users 
  1. 查询用户:
curl-X GET http://localhost:3000/users/{user_id}
  1. 更新用户:
curl-X PUT -H"Content-Type: application/json"-d'{"name": "Alice Smith"}' http://localhost:3000/users/{user_id}
  1. 删除用户:
curl-X DELETE http://localhost:3000/users/{user_id}

八、异步错误调试与排查

8.1 用tracing记录错误

我们可以使用tokio-tracing库记录错误的详细信息,包括错误的位置、堆栈信息等。

usetracing::error;usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Database error: {0}")]Database(#[from]sqlx::Error),}asyncfnfetch_data()->Result<(),AppError>{let result =sqlx::query!("SELECT * FROM nonexistent_table").execute(&sqlx::PgPool::connect("postgresql://user:password@localhost:5432/mydb").await?).await; result?;Ok(())}#[tokio::main]asyncfnmain(){tracing_subscriber::registry().with(tracing_subscriber::EnvFilter::new("info")).with(tracing_subscriber::fmt::layer()).init();ifletErr(e)=fetch_data().await{error!("Error fetching data: {:?}", e);error!("Backtrace: {:?}", e.backtrace());}}

8.2 用tokio-console定位错误

tokio-console是Tokio提供的调试工具,可以用于定位异步任务的错误。

安装tokio-console:

cargoinstall tokio-console 

运行程序并使用tokio-console:

RUSTFLAGS="--cfg tokio_unstable"cargo run tokio-console 

8.3 错误信息的格式化输出

我们可以使用format!serde_json库格式化错误信息,使错误信息更容易阅读。

use serde_json;usethiserror::Error;#[derive(Error, Debug)]pubenumAppError{#[error("Database error: {0}")]Database(#[from]sqlx::Error),}asyncfnfetch_data()->Result<(),AppError>{let result =sqlx::query!("SELECT * FROM nonexistent_table").execute(&sqlx::PgPool::connect("postgresql://user:password@localhost:5432/mydb").await?).await; result?;Ok(())}#[tokio::main]asyncfnmain(){ifletErr(e)=fetch_data().await{let error_json =serde_json::json!({"error": e.to_string(),"code":500,"message":"Internal server error","details":format!("{:?}", e)});println!("{}",serde_json::to_string_pretty(&error_json).unwrap());}}

九、常见问题与最佳实践

9.1 异步任务崩溃

问题:异步任务崩溃导致程序终止。

解决方案

  1. 在任务中添加错误处理,使用match语句或?操作符处理错误。
  2. 使用tokio::spawnJoinHandle来捕获任务的崩溃。
  3. 使用tokio-tracing库记录崩溃的详细信息。

9.2 错误信息不够详细

问题:错误信息不够详细,无法定位问题所在。

解决方案

  1. 使用thiserror#[source]属性构建错误链。
  2. 使用tokio-tracing库记录错误的位置和堆栈信息。
  3. 使用serde_json库格式化错误信息。

9.3 错误处理代码重复

问题:错误处理代码在多个地方重复。

解决方案

  1. 定义统一的错误类型,使用thiserror库。
  2. 实现IntoResponse trait,统一处理错误响应。
  3. 使用中间件处理全局错误。

9.4 异步任务泄漏

问题:异步任务泄漏导致内存泄漏。

解决方案

  1. 正确处理任务的取消,使用tokio::task::JoinHandle::abort方法。
  2. 确保任务有明确的终止条件。
  3. 使用tokio-console工具定位任务泄漏。

十、总结

异步错误处理是Rust异步编程的重要组成部分。通过深入理解异步错误的本质与分类、异步上下文中的标准Result处理、自定义异步错误类型的设计、超时与取消错误的处理、并发任务的错误聚合、异步错误的传递与传播、实战项目的错误处理体系以及异步错误调试与排查,我们可以编写出更高效、更安全的异步代码。

在实际项目中,我们应该根据项目的需求选择合适的错误处理方式。对于库开发,我们应该使用thiserror定义自定义错误类型,支持Error trait的实现。对于应用开发,我们可以使用anyhow快速处理各种类型的错误。同时,我们应该使用tokio-tracing库记录错误的详细信息,使用tokio-console工具定位异步任务的错误。

希望本章的内容能够帮助您深入掌握Rust异步编程的错误处理艺术,并在实际项目中应用。

Read more

Python 微信小程序的班级学生奖惩成绩信息管理系统

Python 微信小程序的班级学生奖惩成绩信息管理系统

文章目录 * 技术架构设计 * 数据库设计 * 前端功能模块 * 后端API开发 * 权限控制系统 * 数据统计功能 * 部署方案 * 性能优化策略 * 系统设计与实现的思路 * 主要技术与实现手段 * 源码lw获取/同行可拿货,招校园代理 :文章底部获取博主联系方式! 技术架构设计 微信小程序前端采用微信原生框架,后端使用Python Flask/Django框架,数据库选用MySQL或SQLite。前后端通过RESTful API交互,数据格式采用JSON。 数据库设计 学生表包含学号、姓名、班级等基础字段;奖惩记录表关联学生ID,包含类型(奖励/惩罚)、分值、原因、时间等字段;成绩表关联学生ID,包含科目、分数、学期等字段。 前端功能模块 微信小程序页面分为学生信息展示页、奖惩记录页、成绩查询页。使用WXML/WXSS构建界面,通过wx.request调用后端API获取数据。实现下拉刷新、分页加载等交互功能。 后端API开发

By Ne0inhk

CCF GESP C++讲义和真题汇总5级完整版

CCF GESP C++讲义和真题汇总5级完整版 序 言 当下各类编程和算法相关竞赛层出不穷,但多数比赛难度低、缺乏含金量;甚至个别比赛并非比拼学生能力,而是依赖老师带队编写程序,学生仅体验流程、花钱获取证书,此类比赛意义甚微。 GESP由举办CSP、NOIP和NOI竞赛的中国计算机学会(CCF)主办,可看作是“分期”版本的CSP-J。CSP-J一年仅一次考试,且难度较高、初赛通过率低,例如北京初赛通过率仅20%~30%,个别南方省份初赛分数线高达80多分,写错两道选择题便无法通过,错失当年考试机会。 GESP将CSP-J难度的内容划分为8个等级,学生可按顺序逐级报考,相当于分8次体验完整的CSP-J内容,避免难度陡增;且每年3、6、9、12月各有一次考试机会,一次未考好可学习三个月后再次报考,能为学生提供及时反馈,避免长期学习却难以入门。同时,GESP虽拆分了学习和考试阶段,但难度、内容与CSP-J基本一致,保有同等含金量。 目录 CCF编程能力等级认证概述 第一课 初等数论 * 知识点01:

By Ne0inhk

SketchUp STL插件终极指南:从数字设计到实体打印的完整教程

还在为SketchUp作品无法直接3D打印而烦恼吗?SketchUp STL插件就是你的完美解决方案!这个强大的Ruby扩展为SketchUp添加了完整的STL格式支持,让你的创意轻松转化为实体模型。🎯 【免费下载链接】sketchup-stlA SketchUp Ruby Extension that adds STL (STereoLithography) file format import and export. 项目地址: https://gitcode.com/gh_mirrors/sk/sketchup-stl 🚀 三步搞定插件安装 想要快速上手?跟着这个超简单的安装流程走: 第一步:获取插件文件 下载最新的RBZ格式安装包,这是SketchUp插件的标准打包格式。 第二步:安装扩展 打开SketchUp → 窗口 → 扩展管理器 → 安装扩展,选择下载的RBZ文件即可。 第三步:验证功能 重启SketchUp后,检查文件菜单是否新增了STL导入导出选项,确认插件安装成功! 💡 两大核心功能深度体验 导入功能:外部模型的完美融合 当你需要编辑现

By Ne0inhk
【STL】深度剖析 C++ string:从 0 到 1 的模拟实现与细节解析

【STL】深度剖析 C++ string:从 0 到 1 的模拟实现与细节解析

前言 string是 C++ 中最常用的字符串工具,但多数人只懂用、不懂其底层逻辑。 这篇会带你手搓一个简易string:从内存管理的构造 / 析构,到深拷贝的拷贝构造 / 赋值重载,再到基础接口封装,帮你吃透string的核心机制,同时掌握 C++ 类设计的关键思路。 📚 C++ 初阶 【……】 【 类和对象(下篇)】 【 C/C++内存管理 】 【 C++模版初阶 】 【 stl_string高频接口测试 】 目录 一、前置工作 二、默认成员函数 1、构造函数 2、析构函数 3、拷贝构造函数 4、赋值运算符重载 三、字符串操作接口 1、reserve 2、push_back 3、append 4、

By Ne0inhk