Rust 经常被视为仅仅是一种系统编程语言,但实际上它是一种多用途的通用语言。像 Tauri(用于桌面应用)、Leptos(用于前端开发)和 Axum(用于后端开发)这样的项目表明 Rust 的用途远不止于系统编程。
class User(APIView): def post(self, request): body = request.data这段代码大多数时候都能正常工作。然而,当我意外地发送了一个格式不正确的请求体时,它就不再起作用了。访问数据时抛出了异常,导致返回了 500 状态码的响应。我没有意识到这种访问可能会抛出异常,并且也没有明确的提示。
// 堆代码 duidaima.com let body: RequestBody = serde_json::from_slice(&requestData)?;问号 (?) 表示你想在调用函数中处理错误,将错误向上一级传播。我认为任何将错误作为值来处理的语言都是正确处理错误的方式。这种方法允许你编写代码时避免出现意外的情况,就像 Python 示例中的那样。
newClient( WithHTTPClient(httpClient), // &http.Client{} WithEndpoint(config.ApiBasePath), )突然间,集成测试开始抛出竞态条件错误,他搞不清楚为什么会这样。他向我求助,我们一起追踪问题回到了这行代码。我们在其他客户端之间共享了这个HTTP客户端,这导致了错误的发生。多个协程在读取客户端,而 WithHttpClient 函数修改了客户端的状态。在同一资源上同时有读线程和写线程会导致未定义的行为或在 Go 语言中引发恐慌。
fn with_httpclient(client: &mut reqwest::Client) {}「宏」
sqlx::query_as!(Student, "DELETE FROM student WHERE id = ? RETURNING *", id)Rust 中的宏会在后台生成代码,编译器在构建过程中会检查这些代码的正确性。通过宏,你甚至可以在编译时扩展编译器检查并验证 SQL 查询,方法是在编译期间生成运行查询的真实数据库上的代码。这种能够在编译时检查代码正确性的能力开辟了新的可能性,特别是在 web 开发中,我们经常编写原始的数据库语句或 HTML 和 CSS 代码。它帮助我们写出更少 bug 的代码。
#[instrument(name = "UserRepository::begin")] pub async fn begin(&self) {}核心思想保持不变:在后台生成代码,并在方法调用前后执行一些逻辑,从而确保代码更加健壮和易于维护。
let key_value = request.into_inner() .key_value .ok_or_else(|| ServerError::InvalidArgument("key_value must be set".to_string()))?;与这种更为冗长的方法相比:
Optional<KeyValue> keyValueOpt = request.getInner().getKeyValue(); if (!keyValueOpt.isPresent()) { throw new IllegalArgumentException("key_value must be set"); } KeyValue keyValue = keyValueOpt.get();在 Rust 中,我们可以将操作链接在一起,从而得到简洁且易读的代码。但是,为了实现这种流畅的语法,我们通常需要实现诸如 From 这样的特质。功能性技术大佬们可能会认识并欣赏这种方法,他们有这样的见解是有道理的。我认为任何允许混合函数式和过程式编程的语言都是走在正确的道路上。它为开发者提供了灵活性,让他们可以选择最适合其特定应用场景的方式。
type repo struct { m map[int]int } func (r *repo) Create(i int) { r.m[i] = i } type Server struct { repo *repo } func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { s.repo.Create(1) }没有显式启动任何线程,乍一看,一切似乎都很好。然而实际上,HTTP 服务器是在多个线程上运行的,这些线程被抽象隐藏了起来。在 web 开发中,这种抽象可能会掩盖与多线程相关的潜在问题。现在,让我们用 Rust 实现相同的功能:
struct repo { m: std::collections::HashMap<i8, i8> } #[post("/maps")] async fn crate_entry(r: web::Data<repo>) -> HttpResponse { r.m.insert(1, 2); HttpResponse::Ok().json(MessageResponse { message: "good".to_string(), }) }当我们尝试编译这个程序时,Rust 编译器将会抛出一个错误:
error[E0596]: cannot borrow data in an `Arc` as mutable --> src\main.rs:117:5 | 117 | r.m.insert(1, 2); | ^^^ cannot borrow as mutable | = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Arc<repo>`很多人说 Rust 的错误信息通常是很有帮助的,这通常是正确的。然而,在这种情况下,错误信息可能会让人感到困惑并且不是立即就能明白。幸运的是,如果知道如何解决,修复方法很简单:只需要添加一个小的互斥锁:
struct repo { m: HashMap<i8, i8> } #[post("/maps")] async fn create_entry(r: web::Data<Mutex<repo>>) -> HttpResponse { let mut map = r.lock().await(); map.m.insert(1, 2); HttpResponse::Ok().json(MessageResponse { message: "good".to_string(), }) }确实非常美妙,编译器能够帮助我们避免这些问题,让我们的代码保持安全和可靠。
type valueObject struct { value *int } func getValue(vo *valueObject) int { return *vo.value }你可能会说,“在使用值之前检查它是否为 nil 就好了。”这是 Go 语言中最大的陷阱之一 —— 它的指针机制。有时候我们会优化内存分配,有时候我们使用指针来表示可选值。
type Repository interface { Get() int } func getValue(r Repository) int { return r.Get() } func main() { getValue(nil) }在许多语言中,将空值作为接口的有效选项传递是可以的。虽然代码审查通常会发现这类问题,但我还是见过一些空接口进入开发阶段的情况。在 Rust 中,这类问题是不可能发生的,这是对我们错误的另一层保护:
trait Repository { fn get(&self) -> i32; } fn get_value(r: impl Repository) -> i32 { r.get() } fn main() { get_value(std::ptr::null()); }「更不用说这段代码根本无法编译。」
@Service public class StudentServiceImpl implements StudentService { private final StudentRepository studentRepository; @Autowired public StudentServiceImpl(StudentRepository studentRepository) { this.studentRepository = studentRepository; } }Spring 为我们处理了很多幕后的事务。其中一个特性就是使用 @Autowired 注解来进行依赖注入。当应用程序启动时,Spring 会进行类路径扫描和反射。然而,这种便利性却伴随着性能成本。在 Rust 中,我们可以创建这些清晰的抽象而不付出性能代价,这得益于所谓的零成本抽象:
struct ServiceImpl<T: Repository> { repo: T, } trait Service{} fn new_service<T: Repository>(repo: T) -> impl Service { ServiceImpl { repo: repo } }这些抽象在编译时就被处理好,确保在运行时不会有任何性能开销。这使我们能够在不牺牲性能的情况下保持代码的整洁和高效。
impl From<UserRequest> for domain::DomainUser { fn from(user: UserRequest) -> Self { domain::DomainUser {} } } impl From<domain::DomainUser> for UserResponse { fn from(user: domain::DomainUser) -> Self { UserResponse {} } } fn create_user(user: UserRequest) -> Result<()> { let domain_user = domain::upsert_user(user.into()); send_response(domain_user.into())?; Ok(()) }通过在需要的地方实现 From 特质,Rust 提供了一种一致且直接的方式来处理数据转换,减少了不一致性,并使代码库更加易于维护。
想象一下,有一个 Lambda 函数被创建用来列出 AWS 账户中的所有存储桶,并确定每个存储桶所在的区域。你可能会认为,进行一些 REST API 调用并使用 for 循环在性能上不会有太大的区别。任何语言都应该能够合理地处理这个任务,对吧?然而,测试显示 Rust 在执行这项任务时比 Python 快得多,并且使用更少的内存来达到这些执行时间。事实上,他们每百万次调用节省了 6 美元。