Skip to content

第 9 章:失败 — 当事情不按预期发生时

从一段代码的两种写法开始

下面是同一个功能——"读取优惠规则文件,解析内容"——在 Go 和 Java 里的两种写法:

go
// Go
func loadCouponRules(path string) (CouponRules, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return CouponRules{}, fmt.Errorf("load coupon rules failed: %w", err)
    }
    return parseRules(data)
}
java
// Java
CouponRules loadCouponRules(Path path) throws CouponRuleException {
    try {
        var text = Files.readString(path);
        return parseRules(text);
    } catch (IOException e) {
        throw new CouponRuleException("load coupon rules failed", e);
    }
}

这两段代码做的是同一件事。但看它们的程序员脑子里在想不同的事。

Go 程序员每写一行都在问:"这一步可能失败吗?" Java 程序员倾向于先写主流程,再决定 "在哪里兜住失败"。

这不是风格。这是两种不同的失败治理制度。


9.1 语法基础:8 种语言如何面对失败

Python

python
# 异常机制
def load_coupon_rules(path: str) -> CouponRules:
    try:
        with open(path, "r") as f:
            return parse_rules(f.read())
    except FileNotFoundError:
        raise CouponRuleException(f"file not found: {path}")
    except json.JSONDecodeError as e:
        raise CouponRuleException(f"invalid json: {e}") from e
    except OSError as e:
        raise CouponRuleException(f"read failed: {e}") from e

# EAFP:It's Easier to Ask Forgiveness than Permission
# Python 鼓励先尝试,失败了再处理

# LBYL 也可以,但不是 Python 主流
if os.path.exists(path):
    # ...

JavaScript

javascript
// 同步异常
function loadCouponRules(path) {
    try {
        const text = fs.readFileSync(path, "utf8");
        return parseRules(text);
    } catch (e) {
        throw new Error(`load coupon rules failed: ${e.message}`, { cause: e });
    }
}

// 异步版本
async function loadCouponRulesAsync(path) {
    try {
        const text = await fs.promises.readFile(path, "utf8");
        return parseRules(text);
    } catch (e) {
        throw new Error(`load coupon rules failed: ${e.message}`, { cause: e });
    }
}

// 未捕获的 Promise rejection——Node 里会打印警告,浏览器里可能静默
// 全局兜底:
process.on("unhandledRejection", (reason, promise) => {
    console.error("Unhandled Rejection at:", promise, "reason:", reason);
});

Java

java
// 受检异常(Checked Exception)——Java 唯一的语言特性
CouponRules loadCouponRules(Path path) throws CouponRuleException {
    try {
        var text = Files.readString(path);
        return parseRules(text);
    } catch (IOException e) {
        throw new CouponRuleException("load coupon rules failed", e);
    }
}

// 自定义异常层次
class CouponRuleException extends Exception {  // 受检——调用方必须处理
    CouponRuleException(String msg, Throwable cause) { super(msg, cause); }
}

class CouponNotFoundException extends RuntimeException {  // 非受检——调用方可选
    CouponNotFoundException(String code) {
        super("coupon not found: " + code);
    }
}

C++

cpp
// 异常机制。C++ 中没有受检异常
CouponRules load_coupon_rules(const std::string& path) {
    try {
        std::ifstream in(path);
        if (!in) throw std::runtime_error("open failed");
        return parse_rules(in);
    } catch (const std::exception& e) {
        throw std::runtime_error(
            std::string("load coupon rules failed: ") + e.what()
        );
    }
}

// C++23 的 std::expected(类似 Rust 的 Result)
std::expected<CouponRules, std::string> load_coupon_rules_expected(const std::string& path) {
    std::ifstream in(path);
    if (!in) return std::unexpected("open failed");
    return parse_rules(in);
}

Rust

rust
// Result<T, E>:失败是类型的一部分
fn load_coupon_rules(path: &str) -> Result<CouponRules, AppError> {
    let text = std::fs::read_to_string(path)
        .map_err(|e| AppError::Io(e))?;  // ? = 如果是 Err,立即向上传播
    parse_rules(&text).map_err(|e| AppError::Parse(e))
}

// 定义错误类型
enum AppError {
    Io(std::io::Error),
    Parse(ParseError),
}

// 为 AppError 实现 Display 和 Error trait
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
        }
    }
}

// panic:不可恢复的错误
fn must_succeed() -> CouponRules {
    load_coupon_rules("rules.json").unwrap()  // Err 时 panic
}

Go

go
// 显式错误返回
func LoadCouponRules(path string) (CouponRules, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return CouponRules{},
            fmt.Errorf("load coupon rules failed: %w", err)
    }
    return parseRules(data)
}

// 错误包装和检查
func HandleRequest() {
    rules, err := LoadCouponRules("rules.json")
    if err != nil {
        if errors.Is(err, fs.ErrNotExist) {  // 错误链检查
            // 文件不存在——返回 404
        }
        // 其他错误——返回 500
    }
}

// panic/recover:Go 的异常机制——仅用于不可恢复的错误
func mustLoad(path string) CouponRules {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("recovered: %v", r)
        }
    }()
    // 正常情况下不应该用 panic 做控制流
}

Swift

swift
// throw / try / catch
func loadCouponRules(path: String) throws -> CouponRules {
    do {
        let text = try String(contentsOfFile: path, encoding: .utf8)
        return try parseRules(text)
    } catch let error as NSError {
        throw AppError.io(error)
    }
}

// 自定义错误
enum AppError: Error {
    case io(Error)
    case parse(String)
    case notFound(String)
}

// try? 把错误转成 nil
let rules = try? loadCouponRules(path: "rules.json")

// try! 断言不失败(失败就 crash)
let rules = try! loadCouponRules(path: "safe_path.json")

Kotlin

kotlin
// 异常机制
fun loadCouponRules(path: String): CouponRules {
    return try {
        val text = File(path).readText()
        parseRules(text)
    } catch (e: IOException) {
        throw CouponRuleException("load coupon rules failed", e)
    }
}

// Kotlin 的 Result 类型
fun loadCouponRulesResult(path: String): Result<CouponRules> = runCatching {
    val text = File(path).readText()
    parseRules(text)
}

// 使用
loadCouponRulesResult("rules.json")
    .onSuccess { rules -> processRules(rules) }
    .onFailure { e -> log.error("failed", e) }

语法速查:错误处理

能力PythonJavaScriptJavaC++RustGoSwiftKotlin
错误机制异常异常受检 + 非受检异常异常 + expected(23)Result<T,E>显式 error 返回throw/try异常 + Result
错误在类型里?否(throws 不写进类型)(受检异常在签名里)否(noexcept可选)是(核心)(error 是返回值)throws 在签名里)
调用方被迫处理?受检异常是(不 ? 就是 Result文化强制(不检查 lint 会报)(必须 try
错误包装raise ... from enew Error(msg, {cause:e})new Ex(msg, e)throw std::runtime_error(msg+...)map_err + ?fmt.Errorf(": %w", e)throw AppError.io(e)catch (e: X) throw Y(e)
错误链检查isinstance(e, X)e instanceof Xe instanceof Xcatch (X& e)match eerrors.Is(err, X)catch let e as Xe is X
recover/rescueexceptcatchcatchcatchunwrap_or / matchif err != nilcatchcatch

9.2 深度对照一:三种失败治理制度

制度一:异常(Python、JavaScript、Java、C++、Swift、Kotlin)

正常路径是主文本,失败路径是脚注。

python
def process_order(order_id):
    order = load_order(order_id)        # 可能抛 OrderNotFound
    payment = charge(order)             # 可能抛 PaymentFailed
    ship(order, payment.transaction_id) # 可能抛 ShipmentFailed
    return "success"

如果你不看函数签名,你不知道哪些行会抛异常、抛什么异常。主流程非常干净——但也非常不透明。

优点:主路径代码像散文一样流畅。适合 "正常情况占 99%" 的系统。

代价:失败路径不在眼前。你需要文档、注释、或 "看源码" 来知道可能出什么错。异常可能在你没准备的地方穿透。

制度二:显式错误返回(Go)

失败是正文,和成功平起平坐。

go
func ProcessOrder(orderID string) error {
    order, err := LoadOrder(orderID)
    if err != nil {
        return fmt.Errorf("process order: %w", err)
    }

    payment, err := Charge(order)
    if err != nil {
        return fmt.Errorf("process order: %w", err)
    }

    err = Ship(order, payment.TransactionID)
    if err != nil {
        return fmt.Errorf("process order: %w", err)
    }

    return nil
}

每一行都可能失败,每一行都显式处理。你无法 "忘记" 错误——它就在你眼前。

优点:失败路径可见性极强。code review 时一眼看到错误处理。新人不会漏。

代价:样板代码多。if err != nil 占据屏幕的 1/3 到 1/2。容易写成机械的 return err 而不补充上下文。

制度三:类型化失败(Rust)

失败是函数契约的一部分,编译器强制执行。

rust
fn process_order(order_id: &str) -> Result<Success, AppError> {
    let order = load_order(order_id)?;     // ? = 失败时立即返回 Err
    let payment = charge(&order)?;
    ship(&order, &payment.transaction_id)?;
    Ok(Success { order_id: order_id.into() })
}

? 操作符让代码看起来像异常风格的主流程,但 Result<T, E> 在类型层面保证了调用方必须处理失败——你不能忽略一个 Result(编译器警告)。

优点:既有异常的简洁(? 代替三行 if-err-return),又有显式返回的安全性(失败在类型里)。调用方不能假装失败不会发生。

代价:你需要定义错误类型。不同错误类型之间需要转换。? 操作符要求调用链上的所有错误类型兼容。


9.3 深度对照二:? 操作符——同一个符号,不同的世界

? 在 Rust、Swift、Kotlin 中都存在,但语义完全不同。

Rust 的 ?:早期返回 + 类型转换

rust
fn load_rules() -> Result<CouponRules, AppError> {
    let text = std::fs::read_to_string("rules.json")?;  // std::io::Error → AppError
    // 等价于:
    // let text = match std::fs::read_to_string("rules.json") {
    //     Ok(t) => t,
    //     Err(e) => return Err(AppError::from(e)),
    // };
    parse_rules(&text)?  // ParseError → AppError
}

? 做两件事:如果是 Err,立即从函数返回 Err;同时通过 From trait 自动转换错误类型。

Swift 的 try?:错误变 nil

swift
let rules = try? loadCouponRules(path: "rules.json")
// 如果 loadCouponRules 抛异常 → rules = nil
// 和 Rust 的 ? 完全不同——这是把错误吃掉了

Swift 还有一个 try!——失败就 crash。这完全和 Rust 的 unwrap() 对标。

Kotlin 的 ?::空值合并(和 Rust 的 ? 完全不同)

kotlin
val name = order.couponCode ?: "NO_COUPON"  // 如果 null 就用默认值
// 这和错误处理无关——它是空值处理
// Rust 没有这个操作符

关键认知:看到 ? 在不同语言里,你要知道它在做什么——Rust 的 ? 是 "失败就返回",Swift 的 try? 是 "失败就 nil",Kotlin 的 ?: 是 "null 就默认值"。三个符号,三种语义。


9.4 深度对照三:错误分层——不只是语法问题

成熟的错误处理不是选边站队("异常好还是返回值好"),而是给失败世界分层

三个错误层

层次例子处理方式丢失这一层会发生什么
领域错误库存不足、优惠券过期返回稳定业务码,用户可理解消息用户看到 "database timeout",一头雾水
技术错误数据库超时、磁盘满、网络抖动保留 cause,做监控、重试、降级运维不知道发生了什么,无法报警
边界错误对外 API 的状态码、MQ 的死信统一翻译,不泄漏内部结构调用方看到内部 toString,耦合到实现

8 门语言对错误分层的支持力度

python
# Python:异常链支持良好(raise ... from e)
try:
    charge(order)
except PaymentTimeout as e:
    raise OrderServiceError("payment failed") from e
# OrderServiceError 和 PaymentTimeout 的因果链被保留了
go
// Go:用 fmt.Errorf + %w 保留链
func ProcessOrder(id string) error {
    err := charge(id)
    if err != nil {
        return fmt.Errorf("process order %s: %w", id, err)  // %w = wrap
    }
    return nil
}

// 调用方:
if errors.Is(err, ErrPaymentTimeout) {  // 可以检查底层错误
    // ...
}
rust
// Rust:用 thiserror 或 anyhow 做错误分层
#[derive(Error, Debug)]
pub enum AppError {
    #[error("order not found: {0}")]
    NotFound(String),

    #[error("payment failed")]
    PaymentFailed(#[source] PaymentError),
    // #[source] 保留错误链
}
java
// Java:异常链天然保留(构造函数接受 Throwable cause)
throw new OrderServiceException("payment failed", paymentTimeoutException);

9.5 边界探索:跨语言错误处理陷阱

陷阱一:Go → Java:把错误返回值当成了 Java 的受检异常

Go 开发者切到 Java 后,容易觉得 Java 的受检异常和 Go 的 error 返回值是一回事——都是 "调用方必须处理"。

但不是。

Go 的 error 是值——你可以把它存进变量、放进 channel、传递给另一个 goroutine。Java 的受检异常是控制流机制——它们沿调用栈向上传播,你不能把它们存起来稍后处理。

陷阱二:Java → Go:受检异常的心智带过来

Java 程序员习惯了:编译器告诉我哪里需要 try/catch。切到 Go 后,编译器不会告诉你 "这个函数返回了 error 但你忽略了它",只有 linter 会提醒。从 "编译器强制" 到 "lint 提醒",中间差着整个纪律体系。

陷阱三:Python → Rust:except 变成 match

python
# Python
try:
    result = do_something()
except ValueError as e:
    handle_value_error(e)
except IOError as e:
    handle_io_error(e)
rust
// Rust
match do_something() {
    Ok(result) => use(result),
    Err(AppError::Value(e)) => handle_value_error(e),
    Err(AppError::Io(e)) => handle_io_error(e),
}

exceptmatch,表面上是语法改变,深层上是思维转变:在 Python 里你沿着 try 块思考主路径,except 是兜底的。在 Rust 里 Result 的两个可能性在类型层面完全平等——你不能 "先写主路径、再补错误"。

陷阱四:Rust → Go:失去 ? 操作符

Rust 开发者习惯了 ? 的简洁。切到 Go 后,每两行一个 if err != nil 会让人沮丧。但这正是 Go 刻意的——让你正视每一个失败点。


9.6 洞察:错误处理是语言对 "不确定性" 的姿态

如果你只用一门语言处理错误,你不会发现:

错误处理不是附属语法。它是一门语言对 "不确定性" 的治理立场。

异常语言(Python、Java、JS、Swift)的姿态是:相信你会记得处理错误——所以先让你把主路径说清楚。 代价是,如果你忘了,错误会在不恰当的地方出现。

显式错误返回(Go)的姿态是:不相信你会记得——所以让每一步都可能失败的证据留在你眼前。 代价是,样板代码很多,而且在简单的业务流程里 "显得" 啰嗦。

类型化失败(Rust)的姿态是:不相信你会记得,但也给你一个省力的工具(?)——所以失败被写入类型,同时让你不至于被样板淹死。 代价是,你要为错误类型设计投入更多前期工作。

这三种姿态没有谁比谁 "正确"。它们对应着三种不同的团队现实:

  • 如果你的系统以正常路径为主,失败是少数情况——异常让你快。
  • 如果你的系统充满了各种可能的失败(网络、磁盘、超时)——显式错误让你不遗漏。
  • 如果你的系统要求正确性优先——类型化失败让编译器替你打仗。

本章小结

  • 三种失败治理制度:异常、显式返回、类型化失败。 它们分别是 "失败是脚注"、"失败是正文"、"失败是契约" 三种世界观的体现。

  • 错误分层比语法选边更重要。 再优雅的错误机制,没有分层也会陷入混乱:用户看到底层异常,监控收不到可聚合的错误码,技术错误被当业务错误重试。

  • ? 操作符在不同语言里意味完全不同。 Rust 的 ? 是早期返回,Swift 的 try? 是错误变 nil,Kotlin 的 ?: 是空值合并。

  • 跨语言迁移时,错误文化是最难切换的事情之一。 它不像语法可以靠肌肉记忆——它是你在整个开发生涯中形成的 "什么事该在什么时候处理" 的整套直觉。


如果你只用一门语言,你不会学到这个:

你怎么处理错误,比你怎么处理成功,更能暴露你用的语言对 "人" 的假设——它相信你会记得失败,还是相信你会忘记?它用编译器提醒你,用约定提醒你,还是用生产环境的事故提醒你?