为了账号安全,请及时绑定邮箱和手机立即绑定

TypeScript也能用Go和Rust的错误处理方式?不要try/catch也能行?这也太叛逆了吧

Rust、TypeScript和Go的标志组合图像 | 图片由作者制作

所以,让我们从一点我的背景说起。我是一名软件开发者,大约有十年的工作经验,最初是做PHP开发,然后慢慢转向了JavaScript。

大概五年前我开始使用TypeScript,从那以后我就再也没有回头用过JavaScript。一开始使用它的时候,我觉得它就是最好的编程语言。大家都爱用它……它就是最好的,对吧?对吧?对吧?

嗯,然后我开始尝试玩弄其他更现代的语言,首先是 Go。我慢慢加入了 Rust(感谢 Prime 社区)。

当你不知道有些东西存在时,很难注意到它们。

我在谈论什么?Go 和 Rust 有什么共同点?最让我印象深刻的是错误处理。更具体地讲,它们是如何处理错误的。

JavaScript 依赖抛出异常来处理错误的方式,而 Go 和 Rust 则将错误当作值来处理。你可能会认为这没那么严重……但别小看了它,这其实是个重大变革。

咱们一个个来看这些语言。我们不会对每一种语言进行深入研究;我们只想了解一个大概的方法。

我们从JavaScript/TypeScript开始吧,玩个小游戏。

给自己五秒钟查看下面的代码,并说说为什么我们需要用 try/catch 来包裹它。

    try {  
      const request = { name: "test", value: 2n };  
      const body = JSON.stringify(request);  
      const response = await fetch("https://example.com", {  
        method: "POST",  
        body,  
      });  
      if (!response.ok) {  
        return;  
      }  
      // 处理响应数据  
    } catch (e) {  
      // 处理异常  
      return;  
    }

所以,我想你们大多数人猜到这一点了,即使我们在检查 response.okfetch 方法仍然可能抛出错误,因为 response.ok 只能捕获 4xx 和 5xx 的网络错误。但如果网络真的出了问题,会抛出一个错误。

但我不知道有多少人猜到过 JSON.stringify 也会抛出错误。原因是请求中的对象包含这样的 bigint (2n),因为 JSON 不知道如何将其字符串化。

所以,第一个问题也是我个人认为最大的JavaScript问题:我们不知道哪些代码会抛出错误。从JavaScript错误的角度来看,情况就好比这样。

    try {  
      let data = "你好";  
    } catch (err) {  
      console.error(err);  
    }

JavaScript不在乎也不知道。不过你应该知道。

第二点,这是完全可以运行的代码段。

    const request = { name: "test", value: 2n };  
    const body = JSON.stringify(request);  
    const response = await fetch("https://example.com", {  
      method: "POST",  
      body,  
    });  
    if (!response.ok) {  
      return;  
    }

没有错误,也没有代码规范检查工具的警告,但这样可能会让应用程序崩溃。

现在,我脑海里响起,“出了什么问题呢?到处用 try/catch 不就得了。”这里就是第三个问题:我们不知道是哪个抛出的。当然,我们可以通过错误信息来猜测,但对于这些服务或函数,有很多可能出错的地方,你能确定一个 try/catch 能正确处理所有情况吗?

好的,现在是时候停止吐槽JS了,转向其他语言。我们从这段Go代码开始吧。

f, err := os.Open("filename.ext 文件")  
if err != nil {  
    log.Fatal(err)  
}  
// 可以对打开的文件 f 进行一些操作

我们正在尝试打开一个会返回文件或错误的函数。你经常会看到这种情况,特别是当我们知道某些函数总是会返回错误时。绝对不会错过这种情况。这是将错误当作值来处理的第一个例子。你指定哪些函数可以返回它们,然后返回、赋值并检查它们,最后使用它们。

它也不够亮眼,这也让Go语言备受批评——繁琐的错误检查代码,比如 if err != nil { ... 有时甚至比其他部分的代码行数还要多。

    如果 err 不是 nil {  
      // 添加适当的代码注释或解释  
      如果 err 不是 nil {  
        // 添加适当的代码注释或解释  
        如果 err 不是 nil {  
          // 添加适当的代码注释或解释  
        }  
      }   
    }  
    如果 err 不是 nil {  
      // 添加适当的代码注释或解释  
    }  
    // 添加适当的代码注释或解释  
    如果 err 不是 nil {  
      // 添加适当的代码注释或解释  
    }

绝对值得,相信我,真的。

最后来谈谈,Rust:

    let greeting_file_result = File::open("hello.txt");  
    let greeting_file = match greeting_file_result {  
      Ok(file) => file,  
      Err(error) => panic!("打开文件时出错: {:?}", error),  
    };

在这三种展示的语言中,Rust 的代码虽然最啰嗦,但却可能是最好的。首先,Rust 使用其出色的枚举类型来处理错误(这与 TypeScript 的枚举类型不一样哦!)。简单点说,它使用一个名为 Result 的枚举,包含两个变体:OkErr。就像你猜的一样,Ok 包含一个值,而 Err 吗?就是一个错误哦 :D。

它也有很多方法可以更方便地处理这些问题,以缓解Go问题。最著名的是?运算符。

    let greeting_file_result = File::open("hello.txt"); // 尝试打开文件

这里的关键点是,Go 和 Rust 都能预知可能出错的地点,并且会让你在错误出现的地方立即处理它。没有隐藏的错误,无需猜测,不会因意外导致应用程序崩溃。

这种方法好太多了,简直不是一点两点的好。

好的,现在要诚实一点;我说了点谎。我们不能让 TypeScript 的错误像 Go 或 Rust 那样处理。这里的限制因素就是语言本身,它缺乏相应的工具来实现这一点。

但我们能做的就是尽量让它相似一些,并尽量简单化。

就这样开始。

    export type 安全的<T> =  
      | {  
        成功标志: true;  
        数据值: T;  
      }  
      | {  
        成功标志: false;  
        错误信息: string;  
      };

这里没啥特别的,只是一个简单的泛型类型。但这个小家伙完全可以改变代码。你可能已经注意到,这里最大的不同就是我们要么返回数据,要么返回错误信息。听起来熟悉吧?

还有……第二个说法是,确实需要几个 try-catch 结构。好在只需要大约两个,而不是十万左右。

    export function safe<T>(promise: Promise<T>, err?: string): Promise<Safe<T>>;  
    export function safe<T>(func: () => T, err?: string): Safe<T>;  
    export function safe<T>(  
      promiseOrFunc: Promise<T> | (() => T),  
      err?: string,  
    ): Promise<Safe<T>> | Safe<T> {  
      if (promiseOrFunc instanceof Promise) {  
        return safeAsync(promiseOrFunc, err);  
      }  
      return safeSync(promiseOrFunc, err);  
    }  

    async function safeAsync<T>(  
      promise: Promise<T>,   
      err?: string  
    ): Promise<Safe<T>> {  
      try {  
        const data = await promise;  
        return { data, success: true };  
      } catch (e) {  
        console.error(e);  
        if (err !== undefined) {  
          return { success: false, error: err };  
        }  
        if (e instanceof Error) {  
          return { success: false, error: e.message };  
        }  
        return { success: false, error: "出了点问题" };  
      }  
    }  

    function safeSync<T>(  
      func: () => T,   
      err?: string  
    ): Safe<T> {  
      try {  
        const data = func();  
        return { data, success: true };  
      } catch (e) {  
        console.error(e);  
        if (err !== undefined) {  
          return { success: false, error: err };  
        }  
        if (e instanceof Error) {  
          return { success: false, error: e.message };  
        }  
        return { success: false, error: "出了点问题" };  
      }  
    }

“哇,真聪明。他为 try/catch 实现了一个包装器。”没错,你说得对,这只是一个以 Safe 类型作为返回类型的包装器。但有时候简单的解决方案就足够了。让我们结合上面提到的例子来使用它。

老版本(16行代码):

    try {  
      const request = { name: "test", value: 2n };  
      const body = JSON.stringify(request);  
      const response = await fetch("https://example.com", {  
        method: "POST",  
        body,  
      });  
      if (!response.ok) {  
        // 网络出错时处理  
        return;  
      }  
      // 处理返回  
    } catch (e) {  
      // 异常处理  
      return;  
    }

新内容(20行):

    const request = { name: 'test', value: 2n };  
    const body = safe(  
      () => JSON.stringify(request),  
      '未序列化请求失败',  
    );  
    if (!body.success) {  
      // 处理错误 ({body.error})  
      return;  
    }  
    const response = await safe(  
      fetch("https://example.com", {  
        method: 'POST',  
        body: body.data,  
      }),  
    );  
    if (!response.success) {  
      // 处理错误 ({response.error})  
      return;  
    }  
    if (!response.data.ok) {  
      // 处理网络错误:{response.data.ok}  
      return;  
    }  
    // 处理响应:{response.data}

虽然我们的新解决方案更长,但是因为有以下几个原因表现更好。

  • 不使用 try/catch
  • 我们直接在每个出错的地方处理错误
  • 我们可以针对特定函数设置错误信息
  • 我们的逻辑是自上而下的,清晰明了,所有的错误信息都放在最上面,然后才是响应部分

但现在来了王牌。如果我们忘了检查这一项会怎样?

    if (!body.success) {  
      // Handle error (body.error)  
      return;  
    }

问题是……我们不能这么干。是的,我们必须进行那个检查。如果我们不验证,body.data 就不会有值。LSP 会抛出“Property ‘data’ does not exist on type ‘Safe<string>’”的错误来提醒我们。这多亏了我们创建的那个简单的 Safe 类型。对于错误信息也是一样的情况,我们无法读取 body.error,除非我们先确认 !body.success

我们应当欣赏一下TypeScript,看看它是如何改变了JavaScript世界的。

以下也是一样。

    if (!response.success) {  
      // 处理错误信息(响应错误)
      return;  
    }

我们不能去掉 !response.success 的检查,否则 response.data 就可能不存在。

当然,我们的解决方案也有一些问题。最大的问题是您必须记得用我们的safe包装器包裹那些可能会引发错误的Promise或函数。然而,这个“我们需要知道”的限制是语言上的局限性,我们无法克服。

听起来这可能很难,但实际上并不难。很快你就会发现,你代码里的几乎所有 Promise 都可能抛出错误,而那些可能抛出错误的同步函数你也熟悉,这样的同步函数其实不多。

不过你可能会问,这样做真的值得吗?我们认为值得。它在我们团队中运作得非常顺利 :). 当你看更大的服务文件,里面没有 try/catches,每个错误都在其出现的地方得到妥善处理,逻辑流程也很流畅……这看起来真的很棒。

这里有一个真实的 SvelteKit FormAction 示例,比如:

    export const actions = {  
      createEmail: async ({ locals, request }) => {  
        const end = perf("CreateEmail");  
        const form = await safe(request.formData());  
        if (!form.success) {  
          return fail(400, { error: form.error });  
        }  
        const schema = z  
          .object({  
            emailTo: z.string().email(),  
            emailName: z.string().min(1),  
            emailSubject: z.string().min(1),  
            emailHtml: z.string().min(1),  
          })  
        .safeParse({  
          emailTo: form.data.get("emailTo"),  
          emailName: form.data.get("emailName"),  
          emailSubject: form.data.get("emailSubject"),  
          emailHtml: form.data.get("emailHtml"),  
        });  
        if (!schema.success) {  
          console.error(schema.error.flatten());  
          return fail(400, { form: schema.error.flatten().fieldErrors });  
        }  
        const metadata = createMetadata(URI_GRPC, locals.user.key)  
        if (!metadata.success) {  
          return fail(400, { error: metadata.error });  
        }  
        const response = await new Promise<Safe<Email__Output>>((res) => {  
          usersClient.createEmail(schema.data, metadata.data, grpcSafe(res));  
        });  
        if (!response.success) {  
          return fail(400, { error: response.error });  
        }  
        end();  
        return {  
          email: response.data,  
        };  
      },  
    } satisfies Actions;

这里有几个需要注意的地方:

  • 我们用自定义函数 grpcSafe 来帮助处理 gGRPC 回调。
  • createMetadata 返回 Safe,所以不用再额外处理它。
  • zod 库也采用了同样的方法 :) 如果我们不检查 schema.success,就无法获取 schema.data

看起来挺干净的,试试看吧!也许它也非常适合你哦 :)

谢谢您的阅读。

附:看起来差不多?

    f, err := os.Open(“filename.ext”)  
    if err != nil {  
      log.Fatal(err)  
    }  
    // 可以对打开的文件 f 做点事情
const response = await safe(fetch("https://example.com"));  
if (!response.success) {  
  console.error(response.error);  
  return;  
}  
// 可以在这里处理 response.data

好的,结束了。

希望你玩得开心!

一如既往地,稍微自我夸奖一下 :)

订阅我在 GithubTwitter 获取最新通知。

我也是GoFast的创始人之一,这是构建现代应用程序的终极基础,利用了Golang和SvelteKit/Next.js的强大之处。它也完全利用了这里提到的所有功能。有兴趣的话,欢迎加入我们!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消