AI Library · 知识库

# 表达与传播失败:给读取配置补充上下文

学习目标:把底层读取失败沿调用链传递到展示层,既说明正在做什么,也保留原始原因。

前置知识:函数、文件路径、条件分支;异常链和 error 在本文就地说明。版本遵循主题基线。

导航:问题与边界 · 关键差异 · Go · Java · Python · 运行与验证

# 问题与边界

启动层调用加载层,加载层读取一个小配置文件。成功返回原始字节;失败包含“启动服务”和“读取配置”两层上下文,底层系统错误仍可访问。本篇不解析内容、不自动重试、不采用默认值;只比较 ASCII 字节 fast,避免混入文本解码差异。

每个程序在独立临时目录创建配置,先读取成功,再删除文件触发真实的缺失错误。调用方仅在成功分支使用结果;失败分支检查原因链,输出稳定的业务提示,不依赖本地化的系统错误文本或随机临时路径。创建和清理临时目录失败不属于预期业务失败,继续向程序边界传播。

# 关键差异

机制 跨层传播 保留底层原因 调用方责任
Go 每层检查并返回 error %w 包装 使用结果前检查错误
Java 抛出受检 IOException cause 构造参数 捕获或声明,选择展示边界
Python 异常沿栈传播 raise ... from ... 捕获约定的业务异常

选择这三种是为了对照显式多返回值、受检异常和动态异常;不是宣称其他语言没有错误处理。Rust 的类型化结果见端口解析,所有权问题另见 Rust 专题。

# 各语言实现

# Go

预期输出两行:fast、startup failed。

Gopackage main

import (
    "errors"
    "fmt"
    "os"
    "path/filepath"
)

func load(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil { return nil, fmt.Errorf("read config: %w", err) }
    return data, nil
}
func start(path string) ([]byte, error) {
    data, err := load(path)
    if err != nil { return nil, fmt.Errorf("start service: %w", err) }
    return data, nil
}
func run() (err error) {
    dir, err := os.MkdirTemp("", "config-")
    if err != nil { return err }
    defer func() { err = errors.Join(err, os.RemoveAll(dir)) }()
    path := filepath.Join(dir, "mode")
    if err := os.WriteFile(path, []byte("fast"), 0600); err != nil { return err }
    data, err := start(path)
    if err != nil { return err }
    if string(data) != "fast" { return errors.New("wrong data") }
    fmt.Println(string(data))
    if err := os.Remove(path); err != nil { return err }
    if _, err := start(path); err != nil {
        if !errors.Is(err, os.ErrNotExist) { return err }
        fmt.Println("startup failed")
    } else { return errors.New("missing file accepted") }
    return nil
}
func main() { if err := run(); err != nil { panic(err) } }

%v 只保留文本,改成它会破坏调用方的 errors.Is 判断;%w 把可解包的原因变成接口的一部分,必须有意承诺。参见 Go 官方错误包装说明。清理使用 errors.Join 同时保留主错误和目录移除错误;没有主错误时清理错误也不会消失。

# Java

预期同上;断言用显式检查,不依赖 -ea。

Javaimport java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;

public class Main {
    static byte[] load(Path path) throws IOException {
        try { return Files.readAllBytes(path); }
        catch (IOException e) { throw new IOException("read config", e); }
    }
    static byte[] start(Path path) throws IOException {
        try { return load(path); }
        catch (IOException e) { throw new IOException("start service", e); }
    }
    public static void main(String[] args) throws Exception {
        Path dir = Files.createTempDirectory("config-");
        Path path = dir.resolve("mode");
        try (AutoCloseable cleanup = () -> {
            Files.deleteIfExists(path);
            Files.delete(dir);
        }) {
            Files.write(path, "fast".getBytes(StandardCharsets.US_ASCII));
            String value = new String(start(path), StandardCharsets.US_ASCII);
            if (!value.equals("fast")) throw new AssertionError("wrong data");
            System.out.println(value);
            Files.delete(path);
            try {
                start(path);
                throw new AssertionError("missing file accepted");
            } catch (IOException e) {
                if (!(e.getCause().getCause() instanceof NoSuchFileException)) throw e;
                System.out.println("startup failed");
            }
        }
    }
}

固定两层的检查只为展示链形状;通用库不应让客户端依赖任意包装深度,而应提供稳定分类。IOException(message, cause) 保存原因,见 IOException API。本例展示层处理业务错误;资源块退出时的清理错误由 main 继续抛出,已有主异常时以 suppressed 形式附加。临时目录删除本身不是配置读取的一部分。

# Python

预期同上。

Pythonfrom pathlib import Path
from tempfile import TemporaryDirectory

class ConfigError(Exception):
    pass

class StartupError(Exception):
    pass

def load(path: Path) -> bytes:
    try:
        return path.read_bytes()
    except OSError as error:
        raise ConfigError("read config") from error

def start(path: Path) -> bytes:
    try:
        return load(path)
    except ConfigError as error:
        raise StartupError("start service") from error

with TemporaryDirectory(prefix="config-") as directory:
    path = Path(directory) / "mode"
    path.write_bytes(b"fast")
    data = start(path)
    assert data == b"fast"
    print(data.decode("ascii"))
    path.unlink()
    try:
        start(path)
    except StartupError as error:
        assert isinstance(error.__cause__, ConfigError)
        assert isinstance(error.__cause__.__cause__, FileNotFoundError)
        print("startup failed")
    else:
        raise AssertionError("missing file accepted")

只捕获本层能解释的异常,不把编程错误都重新标成读取失败。显式 from 将原因置于 __cause__,参见 Python 异常链。TemporaryDirectory 负责隔离和移除文件;若清理也失败,Python 的异常上下文保留先前异常,但并不自动采用 Java 的 suppressed 主次规则,详见确定性清理。

# 迁移误区与适用限制

# 运行与验证

三个程序分别放在独立目录运行,正常结束时得到上述两行输出。用例包含实际读取成功、删除后的缺失、两层上下文和底层原因可达;没有制造权限失败或磁盘故障。本次正文提供预期行为,未在撰写时执行;实际工具链与执行结论统一见验证记录。

# 相关阅读

区分失败原因 · 恢复与降级 · 确定性清理 · 保证由谁承担