第 9 章:失败 — 当事情不按预期发生时
从一段代码的两种写法开始
下面是同一个功能——"读取优惠规则文件,解析内容"——在 Go 和 Java 里的两种写法:
// 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
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
# 异常机制
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
// 同步异常
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
// 受检异常(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++
// 异常机制。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
// 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
// 显式错误返回
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
// 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
// 异常机制
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) }语法速查:错误处理
| 能力 | Python | JavaScript | Java | C++ | Rust | Go | Swift | Kotlin |
|---|---|---|---|---|---|---|---|---|
| 错误机制 | 异常 | 异常 | 受检 + 非受检异常 | 异常 + expected(23) | Result<T,E> | 显式 error 返回 | throw/try | 异常 + Result |
| 错误在类型里? | 否(throws 不写进类型) | 否 | 是(受检异常在签名里) | 否(noexcept可选) | 是(核心) | 是(error 是返回值) | 是(throws 在签名里) | 否 |
| 调用方被迫处理? | 否 | 否 | 受检异常是 | 否 | 是(不 ? 就是 Result) | 文化强制(不检查 lint 会报) | 是(必须 try) | 否 |
| 错误包装 | raise ... from e | new 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 X | e instanceof X | catch (X& e) | match e | errors.Is(err, X) | catch let e as X | e is X |
| recover/rescue | except | catch | catch | catch | unwrap_or / match | if err != nil | catch | catch |
9.2 深度对照一:三种失败治理制度
制度一:异常(Python、JavaScript、Java、C++、Swift、Kotlin)
正常路径是主文本,失败路径是脚注。
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)
失败是正文,和成功平起平坐。
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)
失败是函数契约的一部分,编译器强制执行。
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 的 ?:早期返回 + 类型转换
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
let rules = try? loadCouponRules(path: "rules.json")
// 如果 loadCouponRules 抛异常 → rules = nil
// 和 Rust 的 ? 完全不同——这是把错误吃掉了Swift 还有一个 try!——失败就 crash。这完全和 Rust 的 unwrap() 对标。
Kotlin 的 ?::空值合并(和 Rust 的 ? 完全不同)
val name = order.couponCode ?: "NO_COUPON" // 如果 null 就用默认值
// 这和错误处理无关——它是空值处理
// Rust 没有这个操作符关键认知:看到 ? 在不同语言里,你要知道它在做什么——Rust 的 ? 是 "失败就返回",Swift 的 try? 是 "失败就 nil",Kotlin 的 ?: 是 "null 就默认值"。三个符号,三种语义。
9.4 深度对照三:错误分层——不只是语法问题
成熟的错误处理不是选边站队("异常好还是返回值好"),而是给失败世界分层。
三个错误层
| 层次 | 例子 | 处理方式 | 丢失这一层会发生什么 |
|---|---|---|---|
| 领域错误 | 库存不足、优惠券过期 | 返回稳定业务码,用户可理解消息 | 用户看到 "database timeout",一头雾水 |
| 技术错误 | 数据库超时、磁盘满、网络抖动 | 保留 cause,做监控、重试、降级 | 运维不知道发生了什么,无法报警 |
| 边界错误 | 对外 API 的状态码、MQ 的死信 | 统一翻译,不泄漏内部结构 | 调用方看到内部 toString,耦合到实现 |
8 门语言对错误分层的支持力度
# Python:异常链支持良好(raise ... from e)
try:
charge(order)
except PaymentTimeout as e:
raise OrderServiceError("payment failed") from e
# OrderServiceError 和 PaymentTimeout 的因果链被保留了// 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:用 thiserror 或 anyhow 做错误分层
#[derive(Error, Debug)]
pub enum AppError {
#[error("order not found: {0}")]
NotFound(String),
#[error("payment failed")]
PaymentFailed(#[source] PaymentError),
// #[source] 保留错误链
}// 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
try:
result = do_something()
except ValueError as e:
handle_value_error(e)
except IOError as e:
handle_io_error(e)// Rust
match do_something() {
Ok(result) => use(result),
Err(AppError::Value(e)) => handle_value_error(e),
Err(AppError::Io(e)) => handle_io_error(e),
}从 except 到 match,表面上是语法改变,深层上是思维转变:在 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 的?:是空值合并。跨语言迁移时,错误文化是最难切换的事情之一。 它不像语法可以靠肌肉记忆——它是你在整个开发生涯中形成的 "什么事该在什么时候处理" 的整套直觉。
如果你只用一门语言,你不会学到这个:
你怎么处理错误,比你怎么处理成功,更能暴露你用的语言对 "人" 的假设——它相信你会记得失败,还是相信你会忘记?它用编译器提醒你,用约定提醒你,还是用生产环境的事故提醒你?