让我们看看现代编译器和类型系统如何帮助防止许多错误,从而帮助提高每个人的安全性并降低软件生产和维护的成本。
资源泄露
很容易忘记关闭文件或连接:
resp, err := http.Get("http://kerkour.com") if err != nil { // ... } // defer resp.Body.Close() // DON'T forget this line
|
另一方面,Rust 强制执行RAII(资源获取即初始化),这使得泄漏资源几乎是不可能的:它们在被丢弃时会自动关闭。 let wordlist_file = File::open("wordlist.txt")?; // do something...
// we don't need to close wordlist_file // it will be closed when the variable goes out of scope
|
未释放的互斥锁
看看这个 Go 代码:
type App struct { mutex sync.Mutex data map[string]string }
func (app *App) DoSomething(input string) { app.mutex.Lock() defer app.mutex.Unlock() // do something with data and input }
|
到现在为止还挺好。但是当我们想要处理许多项目时,事情可能会很快变得非常糟糕func (app *App) DoManyThings(input []string) { for _, item := range input { app.mutex.Lock() defer app.mutex.Unlock() // do something with data and item } }
|
我们刚刚创建了一个死锁,因为互斥锁没有在预期的时候释放,而是在函数结束时释放。同样,Rust 中的 RAII 有助于防止未释放的互斥锁:
for item in input { let _guard = mutex.lock().expect("locking mutex"); // do something // mutex is released here as _guard is dropped }
|
缺少Switch
假设我们正在跟踪在线商店中产品的状态:
const ( StatusUnknown Status = 0 StatusDraft Status = 1 StatusPublished Status = 2 )
switch status { case StatusUnknown: // ... case StatusDraft: // ... case StatusPublished: // ... }
|
但是,如果我们添加了StatusArchived Status = 3变量而忘记更新这条switch语句,编译器仍然很乐意接受程序并让我们引入一个错误。在 Rust 中,非穷举match会产生编译时错误:
#[derive(Debug, Clone, Copy)] enum Platform { Linux, MacOS, Windows, Unknown, }
impl fmt::Display for Platform { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Platform::Linux => write!(f, "Linux"), Platform::Macos => write!(f, "macOS"), // Compile time error! We forgot Windows and Unknown } } }
|
无效的指针取消引用
据我所知,不可能在安全的 Rust 中创建对无效地址的引用。
type User struct { // ... Foo *Bar // is it intended to be used a a pointer, or as an optional field? }
|
甚至更好的是,因为 Rust 有Option枚举,你不必使用null指针来表示不存在的东西。struct User { // ... foor: Option<Bar>, // it's clear that this field is optional }
|
未初始化的变量
假设我们正在处理用户帐户:
type User struct { ID uuid.UUID CreatedAt time.Time UpdatedAt time.Time Email string }
func (app *App) CreateUser(email string) { // ... now := time.Now().UTC()
user := User { ID: uuid.New(), CreatedAt: now, UpdatedAt: now, Email: email, } err = app.repository.CreateUser(app.db, user) // ... }
|
很好,但是现在,我们需要将字段添加AllowedStorage int64到User结构中。如果我们忘记更新CreateUser函数,编译器仍然会愉快地接受代码而不做任何更改并使用int64:的默认值0,这可能不是我们想要的。
而下面的 Rust 代码
struct User { id: uuid::Uuid, created_at: DateTime<Utc>, updated_at: DateTime<Utc>, email: String, allowed_storage: i64, }
fn create_user(email: String) { let user = User { id: uuid::new(), created_at: now, updated_at: now, email: email, // we forgot to update the function to initialize allowed_storage }; }
|
产生一个编译时错误,阻止我们在脚下开枪。未处理的异常和错误
这听起来可能很愚蠢,但如果你没有异常,你就不能有未处理的异常......
panic!()Rust 中存在,但这不是处理可恢复错误的方式。
因此,通过强制程序员处理每个错误(或编译器拒绝编译程序),同时提供符合人体工程学的工具来处理错误(Result枚举和?运算符),Rust 编译器有助于防止大多数(如果不是全部) ) 与错误处理相关的错误。
数据竞赛
由于Sync和Send特性,Rust 的编译器可以静态断言不会发生数据竞争。
它是如何工作的?您可以在Jason McCampbell的这篇精彩文章中了解更多信息。
隐藏的流
在 Go 中,数据流隐藏在io.Writer界面后面。一方面,它可以简化它们的使用。另一方面,当与我们不希望成为流的类型一起使用时,它可以保留一些惊喜,bytes.Buffer例如。
这正是一个月前发生在我身上的事情: abytes.Buffer在循环中被重用以呈现模板,这导致模板被附加到缓冲区而不是要清理和重用的缓冲区。
这在 Rust 中永远不会发生,因为Streams是一种非常特殊的类型,并且永远不会在这种情况下使用。