rust面试经验1

自我介绍

  1. 介绍自己
  2. 介绍教育经历
  3. 介绍项目经历

介绍项目

  1. redis rust实现
  2. 反向代理服务器rust实现

追问细节

  1. 介绍一下tokio::select!
    1.

    tokio::select! 宏简介

    tokio::select! 是 Tokio 异步运行时提供的一个宏,用于同时等待多个异步任务,并在其中任意一个完成时立即执行对应的分支。它类似于 match 表达式,但用于异步任务的选择。

    基本用法

    rust 复制代码
    use tokio::sync::oneshot;
    
    #[tokio::main]
    async fn main() {
        let (tx1, rx1) = oneshot::channel();
        let (tx2, rx2) = oneshot::channel();
    
        tokio::spawn(async move {
            tx1.send("one").unwrap();
        });
    
        tokio::spawn(async move {
            tx2.send("two").unwrap();
        });
    
        tokio::select! {
            val = rx1 => {
                println!("rx1 completed first with {:?}", val);
            }
            val = rx2 => {
                println!("rx2 completed first with {:?}", val);
            }
        }
    }

    主要特性

    分支匹配 每个分支的格式为 pattern = future => { code },当 future 完成时,执行对应的 code 块。

    默认分支 可以添加 else 分支作为默认处理:

    rust 复制代码
    tokio::select! {
        _ = async {} => {}
        else => {
            println!("All other branches are disabled");
        }
    }

    分支优先级 tokio::select! 会随机选择分支进行检查以避免饥饿问题。如果需要固定优先级,可以使用 biased; 前缀:

    rust 复制代码
    tokio::select! {
        biased;
        _ = future1 => {}
        _ = future2 => {}
    }

    常见使用场景

    超时控制 结合 tokio::time::sleep 实现超时:

    rust 复制代码
    use tokio::time::{sleep, Duration};
    
    async fn some_async_task() {
        sleep(Duration::from_secs(1)).await;
    }
    
    #[tokio::main]
    async fn main() {
        tokio::select! {
            _ = some_async_task() => {
                println!("task completed");
            }
            _ = sleep(Duration::from_millis(500)) => {
                println!("timeout");
            }
        }
    }

    取消任务 通过 oneshot 通道发送取消信号:

    rust 复制代码
    use tokio::sync::oneshot;
    
    async fn long_running_task(cancel: oneshot::Receiver<()>) {
        tokio::select! {
            _ = async {
                // 模拟长时间任务
                tokio::time::sleep(Duration::from_secs(10)).await;
            } => {}
            _ = cancel => {
                println!("task was cancelled");
            }
        }
    }

    注意事项

    1. 所有分支的 future 必须实现 Future trait。
    2. 未选中的分支会被取消,可能引发资源清理问题。
    3. 避免在分支中执行阻塞操作,以免影响其他异步任务。
    4. tokio::select! 是编写高效异步代码的重要工具,合理使用可以显著提升程序的并发性能。
  2. #[dervice()]如何实现

    1. 在 Rust 中,#[derive(feature)] 并不是一个内置的宏或语法,但可以通过自定义派生宏或属性宏实现类似功能。以下是实现自定义派生宏或属性宏的方法:

      自定义派生宏实现

      派生宏允许为结构体或枚举自动生成代码。通过 proc_macro_derive 可以创建一个自定义派生宏,结合 feature 参数控制生成的代码逻辑。

      rust 复制代码
      use proc_macro::TokenStream;
      use quote::quote;
      use syn::{parse_macro_input, DeriveInput};
      
      #[proc_macro_derive(MyDerive, attributes(feature))]
      pub fn my_derive(input: TokenStream) -> TokenStream {
          let input = parse_macro_input!(input as DeriveInput);
          let feature_enabled = input.attrs.iter().any(|attr| attr.path().is_ident("feature"));
      
          if feature_enabled {
              quote! {
                  impl MyTrait for #input {
                      fn my_method(&self) {
                          println!("Feature enabled!");
                      }
                  }
              }.into()
          } else {
              quote! {
                  impl MyTrait for #input {
                      fn my_method(&self) {
                          println!("Feature disabled!");
                      }
                  }
              }.into()
          }
      }

      使用时可以通过 #[feature] 属性标记结构体或枚举:

      rust 复制代码
      #[derive(MyDerive)]
      #[feature]
      struct MyStruct;

      属性宏实现

      属性宏允许在任意项上添加自定义属性,并通过宏展开生成代码。以下是一个支持 feature 参数的属性宏示例:

      rust 复制代码
      use proc_macro::TokenStream;
      use quote::quote;
      use syn::{parse_macro_input, Item};
      
      #[proc_macro_attribute]
      pub fn my_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
          let attr_str = attr.to_string();
          let item = parse_macro_input!(item as Item);
      
          if attr_str.contains("feature") {
              quote! {
                  #item
      
                  impl MyTrait for #item {
                      fn my_method(&self) {
                          println!("Feature enabled!");
                      }
                  }
              }.into()
          } else {
              quote! {
                  #item
              }.into()
          }
      }

      使用时可以通过 #[my_attr(feature)] 启用功能:

      rust 复制代码
      #[my_attr(feature)]
      struct MyStruct;

      条件编译与特性结合

      如果目标是基于 Rust 的 feature 机制(如 Cargo.toml 中定义的特性)控制派生行为,可以通过 cfg_attr 实现:

      rust 复制代码
      #[cfg_attr(feature = "my_feature", derive(MyTrait))]
      struct MyStruct;

      Cargo.toml 中定义特性:

      toml 复制代码
      [features]
      my_feature = []

      关键点总结

    2. 派生宏通过 proc_macro_derive 实现,结合属性判断逻辑。

    3. 属性宏通过 proc_macro_attribute 实现,支持更灵活的语法。

    4. 条件编译通过 cfg_attr 直接与 Cargo 特性系统集成。

    5. 宏的实现需依赖 synquote 库解析和生成代码。

    6. 以上方法可根据实际需求选择,派生宏适合自动生成 trait 实现,属性宏适合更复杂的代码生成逻辑。

  3. 在thiserror中的#[from ]
    1.

    #[from] 的作用

    thiserror 库中,#[from] 是一个派生宏属性,用于自动实现 std::convert::From trait。它允许将一个错误类型自动转换为另一个错误类型,简化错误处理中的类型转换。

    #[from] 的使用场景

    当需要将一个底层错误类型包装到自定义错误类型中时,#[from] 可以自动生成 From trait 的实现代码。例如,将 std::io::Error 转换为自定义错误类型。

    基本语法

    thiserrorError 派生宏中,#[from] 通常与字段声明一起使用:

    rust 复制代码
    #[derive(Error, Debug)]
    pub enum MyError {
        #[error("IO error occurred")]
        IoError(#[from] std::io::Error),
    }

    上述代码会自动生成 From<std::io::Error> 的实现,允许直接使用 ? 操作符将 std::io::Error 转换为 MyError

    实现原理

    #[from] 属性会为枚举变体生成类似以下的代码:

    rust 复制代码
    impl From<std::io::Error> for MyError {
        fn from(err: std::io::Error) -> Self {
            MyError::IoError(err)
        }
    }

    多层级错误转换

    #[from] 支持多层级错误转换。例如:

    rust 复制代码
    #[derive(Error, Debug)]
    pub enum TopLevelError {
        #[error("Mid level error")]
        MidLevel(#[from] MidLevelError),
    }
    
    #[derive(Error, Debug)]
    pub enum MidLevelError {
        #[error("IO error")]
        Io(#[from] std::io::Error),
    }

    此时 std::io::Error 可以通过 MidLevelError 自动转换为 TopLevelError

    注意事项

    1. #[from] 只能用于包含单个字段的变体。
    2. 被转换的类型必须实现了 std::error::Error trait。
    3. 当多个变体使用 #[from] 转换相同类型时,需要明确指定转换路径。

    与手动实现的对比

    使用 #[from] 可以避免手动编写 From trait 实现的样板代码,使错误定义更加简洁。例如,以下手动实现:

    rust 复制代码
    impl From<std::io::Error> for MyError {
        fn from(err: std::io::Error) -> Self {
            MyError::IoError(err)
        }
    }

    可以简化为:

    rust 复制代码
    #[derive(Error, Debug)]
    pub enum MyError {
        #[error("IO error occurred")]
        IoError(#[from] std::io::Error),
    }

    错误消息定制

    #[from] 可以与 #[error] 属性结合使用,定制错误消息:

    rust 复制代码
    #[derive(Error, Debug)]
    pub enum MyError {
        #[error("Custom IO error: {0}")]
        IoError(#[from] std::io::Error),
    }
  4. 在tracing中如何进行日志2次过滤
    1.

    Rust中的Tracing简介

    Tracing是Rust生态中一个强大的诊断工具,专注于高性能、结构化的日志记录和分布式追踪。它通过tracing库提供了一套灵活的API,支持事件(events)、跨度(spans)和层级化的上下文传播。核心特点包括:
    2. 结构化日志:支持键值对形式的元数据,便于机器解析。
    3. 低开销:通过编译时优化减少运行时性能损耗。
    4. 可扩展性:支持自定义订阅者(subscriber)处理日志数据。
    5.

    日志的二次过滤

    tracing中,日志过滤通常通过EnvFilter实现初次过滤(如按日志级别)。二次过滤需在订阅者逻辑中进一步处理:

    方法1:自定义Layer过滤
    rust 复制代码
    use tracing_subscriber::{Layer, registry::Registry};
    use tracing::{Event, Subscriber, Metadata};
    
    struct CustomFilter;
    
    impl<S: Subscriber> Layer<S> for CustomFilter {
        fn on_event(&self, event: &Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) {
            let metadata = event.metadata();
            // 示例:过滤包含特定字段的事件
            if metadata.fields().field("special_field").is_some() {
                println!("Filtered event: {:?}", metadata);
            }
        }
    }
    
    // 注册时添加自定义Layer
    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::new("info"))
        .with(CustomFilter)
        .init();
    方法2:动态修改过滤规则

    通过reload机制动态调整过滤条件:

    rust 复制代码
    use tracing_subscriber::{filter, prelude::*};
    
    let (filter, reload_handle) = filter::dynamic();
    let subscriber = tracing_subscriber::registry().with(filter);
    
    // 初始设置为info级别
    reload_handle.reload("info").unwrap();
    
    // 运行时修改为debug级别
    reload_handle.reload("debug").unwrap();

    对日志的操作

    操作1:自定义日志格式

    通过tracing-subscriberfmt层修改输出格式:

    rust 复制代码
    use tracing_subscriber::fmt::format::FmtSpan;
    
    tracing_subscriber::fmt()
        .with_span_events(FmtSpan::CLOSE) // 记录span生命周期
        .with_target(false) // 隐藏目标模块路径
        .init();
    操作2:日志数据转换

    在自定义订阅者中修改或提取日志字段:

    rust 复制代码
    use tracing_subscriber::Layer;
    
    struct FieldModifier;
    
    impl<S: Subscriber> Layer<S> for FieldModifier {
        fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
            let mut visitor = JsonVisitor::default();
            event.record(&mut visitor);
            // 转换字段值
            let modified_data = visitor.0.replace("sensitive", "***");
            println!("Modified: {}", modified_data);
        }
    }
    
    #[derive(Default)]
    struct JsonVisitor(String);
    
    impl tracing::field::Visit for JsonVisitor {
        fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
            self.0.push_str(&format!("{}={},", field.name(), value));
        }
    }
    操作3:日志聚合与转发

    将日志发送到外部系统(如Elasticsearch):

    rust 复制代码
    use tracing_subscriber::Layer;
    
    struct ElasticsearchLayer;
    
    impl<S: Subscriber> Layer<S> for ElasticsearchLayer {
        fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
            let mut buffer = vec![];
            let mut serializer = serde_json::Serializer::new(&mut buffer);
            let mut visitor = JsonVisitor(&mut serializer);
            event.record(&mut visitor);
            // 异步发送到ES
            tokio::spawn(async move {
                reqwest::Client::new()
                    .post("http://es-host:9200/logs")
                    .json(&buffer)
                    .send()
                    .await;
            });
        }
    }

    关键注意事项

    1. 性能影响:复杂过滤或操作可能增加开销,建议在发布版本中禁用非关键逻辑。
    2. 线程安全 :确保自定义操作是线程安全的,避免使用Rc等非线程安全类型。
    3. 错误处理:日志处理链中的错误应妥善捕获,避免中断主流程。
    4. 通过组合tracing的Layer系统和自定义逻辑,可以实现灵活的日志处理流水线,满足从简单调试到复杂监控的各种需求。
  5. tracing日志会对线程有影响,谈谈有什么影响
    1.

    影响分析

    线程阻塞风险

    tracing的日志记录机制在高频日志场景下可能引发线程阻塞。同步日志记录(如直接写入文件)会阻塞调用线程,尤其是使用tracing_subscriber::fmt默认配置时。异步日志记录(如搭配tracing_appender)虽能缓解,但仍可能因缓冲区满或I/O延迟导致间接阻塞。

    性能开销

    动态日志级别过滤(如EnvFilter)在每次日志调用时检查过滤规则,高频日志场景下会增加CPU开销。跨线程日志传递(如多线程向同一目标写入)可能引发锁竞争,特别是在高并发环境下。

    线程局部存储干扰

    tracing的Span依赖线程局部存储(TLS),若业务代码同时使用TLS(如thread_local!宏),可能引发不可预期的交互问题。跨线程Span传播(如Span::enter后跨线程)会导致日志上下文丢失或混乱。


    实际问题案例

    案例1:异步任务日志丢失

    某项目使用tokio::spawn生成大量异步任务,未显式传递Span导致子任务日志脱离父Span上下文。解决方案是通过Instrument trait主动绑定Span:

    rust 复制代码
    task.instrument(span).await;

    案例2:锁竞争导致延迟

    高频日志场景下,多个线程争用同一日志文件的写入锁,观测到平均延迟上升15%。解决方案是改用异步非阻塞的tracing_appender::non_blocking

    rust 复制代码
    let (writer, guard) = tracing_appender::non_blocking(std::io::stdout());
    tracing_subscriber::fmt().with_writer(writer).init();

    案例3:动态过滤性能劣化

    生产环境使用EnvFilter::from_default_env()导致日志调用开销增加,QPS下降8%。优化方案是预编译过滤规则或限制动态更新频率:

    rust 复制代码
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info"));

    优化策略

    异步日志写入

    强制使用非阻塞写入器避免I/O阻塞主线程。推荐组合tracing-appender的异步写入与日志轮转:

    rust 复制代码
    let appender = tracing_appender::rolling::daily("/logs", "app.log");
    let (non_blocking, _guard) = tracing_appender::non_blocking(appender);

    Span显式传递

    在跨线程或异步边界手动传递Span上下文。对于tokio运行时,结合tracing-futuresInstrument

    rust 复制代码
    use tracing_futures::Instrument;
    tokio::spawn(async move { 
        // 业务代码
    }.instrument(current_span));

    静态过滤配置

    生产环境优先采用编译期确定的日志级别,或通过热加载控制动态过滤的更新频率:

    rust 复制代码
    tracing_subscriber::fmt()
        .with_env_filter("info,hyper=warn,tower=debug")
        .init();

    资源隔离

    对关键路径线程禁用高频率日志,或通过专用线程处理日志I/O。示例配置分离业务与日志线程:

    rust 复制代码
    let (log_sender, log_receiver) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        while let Ok(event) = log_receiver.recv() {
            // 处理日志写入
        }
    });

    通过上述措施可显著降低tracing对线程的影响,具体实施需结合场景的QPS要求与可观测性需求进行权衡。

  6. 在tokio::select!中的停止信号为什么要使用& mut shutdown而不是其他的什么

    tokio::select! 中使用 &mut shutdown 的原因

    tokio::select! 宏用于在多个异步操作中选择最先完成的一个。当处理停止信号时,使用 &mut shutdown 的主要原因涉及所有权和可变性的设计考量。

    所有权与可变性

    shutdown 通常是一个 bool 或类似的状态标志,需要在运行时被修改。通过 &mut shutdown 传递可变引用,确保可以在 select! 分支中安全地修改其值。直接传递 shutdown 会导致所有权的转移,而 select! 要求所有分支的变量必须实现 Copy 或能够共享所有权。

    rust 复制代码
    let mut shutdown = false;
    tokio::select! {
        _ = async_task() => {},
        _ = &mut shutdown => { shutdown = true; } // 需要可变引用修改
    }
    避免 Arc<Mutex<bool>> 的复杂性

    虽然可以使用 Arc<Mutex<bool>> 共享状态,但引入互斥锁会增加运行时开销和复杂性。&mut shutdown 在单线程上下文中是零成本的,且更符合 Rust 的借用规则。

    rust 复制代码
    // 不推荐:过度设计
    let shutdown = Arc::new(Mutex::new(false));
    let shutdown_clone = shutdown.clone();
    tokio::select! {
        _ = async_task() => {},
        _ = *shutdown_clone.lock().unwrap() => {} // 不必要的锁
    }
    tokio::sync::watch 的对比

    tokio::sync::watch 是另一种常见模式,适合跨线程通知。但对于单线程内的简单停止信号,&mut shutdown 更轻量:

    rust 复制代码
    // watch 适合跨线程
    let (tx, mut rx) = tokio::sync::watch::channel(false);
    tokio::select! {
        _ = async_task() => {},
        _ = rx.changed() => { let _ = rx.borrow(); }
    }
    生命周期安全性

    &mut shutdown 显式标注了生命周期的依赖关系,确保在 select! 执行期间 shutdown 不会被意外释放或重复借用。Rust 的借用检查器会阻止以下错误代码:

    rust 复制代码
    let mut shutdown = false;
    let shutdown_ref = &mut shutdown;
    tokio::select! {
        _ = async_task() => {},
        _ = shutdown_ref => { *shutdown_ref = true; } // 明确的生命周期
    }
    // shutdown_ref 不再可用,避免悬垂引用
    性能优化

    直接操作 &mut bool 避免了动态分发或智能指针的开销。tokio::select! 会对分支进行优化,&mut 引用在编译期即可确定其行为。


    其他替代方案的局限性

    使用 Cell<bool>

    Cell 允许内部可变性,但需要 shutdown 实现 Copy,且无法在异步上下文中直接用于 select!

    rust 复制代码
    let shutdown = Cell::new(false);
    // 无法直接用于 select!,因为不实现 Future
    使用 AtomicBool

    跨线程安全,但需要原子操作,且仍需通过 Arc 共享,不如 &mut 直接:

    rust 复制代码
    let shutdown = Arc::new(AtomicBool::new(false));
    let shutdown_clone = shutdown.clone();
    tokio::select! {
        _ = async_task() => {},
        _ = shutdown_clone.load(Ordering::Relaxed) => {} // 无法直接等待变化
    }

    总结

    1. &mut shutdown:轻量、零开销,适合单线程内修改状态。
    2. watch/AtomicBool:适合跨线程场景,但引入额外复杂度。
    3. 所有权明确 :避免 select! 分支中意外转移所有权或触发 Clone

做题目

要求实操建立一个解析get请求, 获取其中内容,要有响应的结构体,并要求有错误机制

  1. 以下是一个用Rust实现的GET请求解析示例,包含结构体定义、错误处理和完整代码实现:

    依赖添加

    Cargo.toml中添加以下依赖:

    toml 复制代码
    [dependencies]
    reqwest = { version = "0.11", features = ["json"] }
    tokio = { version = "1.0", features = ["full"] }
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"
    thiserror = "1.0"

    结构体定义与错误处理

    rust 复制代码
    use serde::Deserialize;
    use thiserror::Error;
    
    #[derive(Debug, Deserialize)]
    struct ApiResponse {
        user_id: u32,
        id: u32,
        title: String,
        completed: bool,
    }
    
    #[derive(Error, Debug)]
    enum RequestError {
        #[error("HTTP请求失败: {0}")]
        ReqwestError(#[from] reqwest::Error),
        #[error("JSON解析失败: {0}")]
        JsonError(#[from] serde_json::Error),
        #[error("无效的状态码: {0}")]
        StatusCodeError(reqwest::StatusCode),
    }

    GET请求实现

    rust 复制代码
    async fn fetch_data(url: &str) -> Result<ApiResponse, RequestError> {
        let response = reqwest::get(url).await?;
        
        if !response.status().is_success() {
            return Err(RequestError::StatusCodeError(response.status()));
        }
    
        let response_body = response.json::<ApiResponse>().await?;
        Ok(response_body)
    }

    主函数示例

    rust 复制代码
    #[tokio::main]
    async fn main() {
        let api_url = "https://jsonplaceholder.typicode.com/todos/1";
        
        match fetch_data(api_url).await {
            Ok(data) => {
                println!("获取数据成功:");
                println!("User ID: {}", data.user_id);
                println!("Title: {}", data.title);
                println!("Completed: {}", data.completed);
            }
            Err(e) => eprintln!("错误发生: {}", e),
        }
    }

    关键点说明

  2. 使用reqwest库处理HTTP请求,需启用tokio运行时

  3. ApiResponse结构体通过serde实现JSON反序列化

  4. 自定义RequestError枚举统一处理三种错误类型:

    • 网络请求错误
    • JSON解析错误
    • 非成功状态码错误
  5. 错误处理使用thiserror简化实现

复制代码
  #### 测试运行

  执行`cargo run`将获取示例API数据并打印结果,若修改URL为无效地址会触发错误处理。

  #### 扩展建议

  对于生产环境使用,建议添加:
  1. 请求超时设置
  2. 自定义请求头
  3. 重试机制
  4. 更完善的状态码处理
  5. 日志记录
  6. 以上实现提供了Rust中处理GET请求的完整范例,包含类型安全的结构体解析和全面的错误处理机制。
相关推荐
禧西2 小时前
面试准备——agent和大模型
面试·职场和发展
北风toto2 小时前
Spring Boot / Spring Cloud 配置文件加密详解:使用 jasypt-spring-boot 实现 ENC() 加密
spring boot·后端·spring cloud
本地化文档3 小时前
rust-nomicon-l10n
rust·github·gitcode
代码羊羊3 小时前
Rust 格式化输出完全攻略:从入门到精通
开发语言·后端·rust
Rust研习社3 小时前
Rust + PostgreSQL 极简技术栈应用开发
开发语言·数据库·后端·http·postgresql·rust
geovindu3 小时前
go:Template Method Pattern
开发语言·后端·设计模式·golang·模板方法模式
白晨并不是很能熬夜3 小时前
【RPC】第 4 篇:服务发现 — Zookeeper + 缓存容错
java·后端·程序人生·缓存·zookeeper·rpc·服务发现
我这一拳20年的功力3 小时前
深入解析 XXL-JOB 核心原理:从 Quartz 到自研时间轮
后端
MgArcher3 小时前
一个下划线表示“别动”,两个下划线表示“真别动”!Python属性访问控制,看懂这篇就够了
后端