AI Library · 知识库

# 恢复与降级:只为缺失的可选配置提供默认值

学习目标:把“能分类错误”推进到“有边界地恢复”,让错误配置继续失败,而不是被默认值掩盖。

前置知识:区分失败原因中的错误类别与基本文件操作。版本遵循主题基线。

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

# 问题与边界

可选配置只接受精确字节 fast、safe,缺失时返回默认 safe,并返回来源标记 default;真实文件产生 file 标记。空文件、坏内容、权限错误和其他 I/O 错误都必须失败。程序调用方知道配置是否降级,不能把一个默认值伪装成成功读取的值。

本例假定路径来自可信的本地配置目录,“可选”意味着读取操作报告不存在即可降级,包括末级文件或祖先目录不存在。若业务只允许末级缺失,则需要更强的目录生命周期约定,不能用一次 exists 检查消除竞态。本篇不重试,不添加缓存,不将默认值写回文件,不证明默认模式适合所有系统。

# 关键差异

语言 恢复位置 未恢复错误 成功结果
Go errors.Is 后的窄分支 原样返回错误 结构体和 nil
Java 只捕获 NoSuchFileException 受检异常继续传播 不可变 record
Python 只包围读取的 except 内容校验在恢复块外 元组

选择三种调用模型与分类篇对应,便于观察策略变化而不是增加语法列表。读取器处理事实,默认值属于可选配置的业务边界。硬编码默认值是本例契约,不是通用的“异常处理最佳实践”。

# 各语言实现

# Go

预期四行:safe default、fast file、safe file、invalid rejected。

Gopackage main

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

type Config struct { Mode, Source string }
var ErrInvalid = errors.New("invalid config")

func optional(path string) (Config, error) {
    data, err := os.ReadFile(path)
    if errors.Is(err, os.ErrNotExist) { return Config{"safe", "default"}, nil }
    if err != nil { return Config{}, fmt.Errorf("read optional config: %w", err) }
    mode := string(data)
    if mode != "fast" && mode != "safe" { return Config{}, ErrInvalid }
    return Config{mode, "file"}, nil
}
func run() (err error) {
    dir, err := os.MkdirTemp("", "optional-")
    if err != nil { return err }
    defer func() { err = errors.Join(err, os.RemoveAll(dir)) }()
    path := filepath.Join(dir, "mode")
    got, err := optional(path)
    if err != nil { return err }
    if got != (Config{"safe", "default"}) { return errors.New("wrong fallback") }
    fmt.Println(got.Mode, got.Source)
    for _, mode := range []string{"fast", "safe"} {
        if err := os.WriteFile(path, []byte(mode), 0600); err != nil { return err }
        got, err := optional(path)
        if err != nil { return err }
        if got != (Config{mode, "file"}) { return errors.New("wrong source") }
        fmt.Println(got.Mode, got.Source)
    }
    for _, bad := range []string{"", "fast\n", "broken"} {
        if err := os.WriteFile(path, []byte(bad), 0600); err != nil { return err }
        if _, err := optional(path); !errors.Is(err, ErrInvalid) {
            return fmt.Errorf("expected invalid, got %v", err)
        }
    }
    fmt.Println("invalid rejected")
    return nil
}
func main() { if err := run(); err != nil { panic(err) } }

不能将 if err != nil 一律替换成默认值;只有 errors.Is(err, os.ErrNotExist) 是恢复条件。Config{} 伴随错误时不代表有效模式。参见 os 错误与读取接口 和 errors.Is。

# Java

预期同 Go。

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 {
    record Config(String mode, String source) {}
    static class InvalidConfig extends Exception {
        InvalidConfig() { super("invalid config"); }
    }
    static Config optional(Path path) throws IOException, InvalidConfig {
        byte[] data;
        try { data = Files.readAllBytes(path); }
        catch (NoSuchFileException e) { return new Config("safe", "default"); }
        String mode = new String(data, StandardCharsets.US_ASCII);
        if (!mode.equals("fast") && !mode.equals("safe")) throw new InvalidConfig();
        return new Config(mode, "file");
    }
    static void show(Config got, Config want) {
        if (!got.equals(want)) throw new AssertionError("wrong config");
        System.out.println(got.mode() + " " + got.source());
    }
    public static void main(String[] args) throws Exception {
        Path dir = Files.createTempDirectory("optional-");
        Path path = dir.resolve("mode");
        try (AutoCloseable cleanup = () -> {
            Files.deleteIfExists(path);
            Files.delete(dir);
        }) {
            show(optional(path), new Config("safe", "default"));
            for (String mode : new String[]{"fast", "safe"}) {
                Files.write(path, mode.getBytes(StandardCharsets.US_ASCII));
                show(optional(path), new Config(mode, "file"));
            }
            for (String bad : new String[]{"", "fast\n", "broken"}) {
                Files.write(path, bad.getBytes(StandardCharsets.US_ASCII));
                try {
                    optional(path);
                    throw new AssertionError("invalid accepted");
                } catch (InvalidConfig expected) {
                    if (!expected.getMessage().equals("invalid config")) {
                        throw new AssertionError("wrong failure");
                    }
                }
            }
            System.out.println("invalid rejected");
        }
    }
}

这里只捕获一次读取的缺失异常,而不是用一个覆盖整个启动过程的 catch(IOException)。ASCII 解码的非 ASCII 替换字符不可能等于两个合法单词,因此不会放宽字节协议;本例不返回原始非法文本。record 表示固定的结果数据,并不替代业务校验,参见 Java record 规则。

# Python

预期同 Go。

Pythonfrom pathlib import Path
from tempfile import TemporaryDirectory

class InvalidConfig(ValueError):
    pass

def optional(path: Path) -> tuple[str, str]:
    try:
        data = path.read_bytes()
    except FileNotFoundError:
        return "safe", "default"
    if data not in (b"fast", b"safe"):
        raise InvalidConfig("invalid config")
    return data.decode("ascii"), "file"

with TemporaryDirectory(prefix="optional-") as directory:
    path = Path(directory) / "mode"
    got = optional(path)
    assert got == ("safe", "default")
    print(*got)
    for mode in ("fast", "safe"):
        path.write_bytes(mode.encode("ascii"))
        got = optional(path)
        assert got == (mode, "file")
        print(*got)
    for bad in (b"", b"fast\n", b"broken"):
        path.write_bytes(bad)
        try:
            optional(path)
        except InvalidConfig as error:
            assert str(error) == "invalid config"
        else:
            raise AssertionError("invalid accepted")
    print("invalid rejected")

缩小 try 范围防止今后在校验阶段增加其他文件访问时,意外把该访问的缺失也当成可选配置缺失。异常匹配规则见 Python 处理异常。元组是返回约定,不是静态运行时校验。

# 迁移误区与适用限制

# 运行与验证

三个程序均自建、清理临时目录。覆盖缺失降级、合法文件覆盖默认、相同 safe 值的不同来源、空值与尾随换行拒绝。Go 合并目录清理错误,Java 资源块按 suppressed 规则保留主错误,Python 使用上下文异常链;资源释放和错误优先级详见确定性清理。本文未执行示例,统一结果见验证记录。

# 相关阅读

区分失败原因 · 表达与传播失败 · 引用与共享修改:别名与可变性陷阱