AI Library · 知识库

# 区分失败原因:不存在不等于不合法

学习目标:把可供机器判断的失败分类与供人阅读的错误消息分开,避免把所有读取失败都解释成“没有配置”。

前置知识:文件读取、函数返回、基本异常;可先读表达与传播失败。版本见主题基线。

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

# 问题与边界

配置文件只允许精确字节 fast 或 safe,无 BOM、空白或末尾换行。这是有意采用的小型 ASCII 协议;不是通用配置格式。读取成功但字节不同属于内容不合法,包括空文件、fast\n、非 ASCII 字节;文件不存在属于缺失;权限、目录误用和设备错误等仍属于其他 I/O 失败。

返回成功模式,或者可区分的失败;本篇不恢复。缺失、内容错误和其他 I/O 错误不能靠比较错误消息字符串判断。三个程序创建独立临时目录,覆盖两个合法值、三种坏内容和一个缺失路径。其他 I/O 错误保留传播,但不使用改权限这种跨平台不稳定的方法伪造运行证据。

# 关键差异

语言 内容错误 缺失 包装后的判断
Go 稳定 sentinel ErrInvalid os.ErrNotExist errors.Is 沿链查找
Java 专用受检异常 InvalidConfig NoSuchFileException 在原始类型边界捕获
Python InvalidConfig(ValueError) FileNotFoundError 子类异常先匹配

选 Go、Java、Python 分别展示错误值身份、受检异常分类和动态异常分类。与上一文不同,此处加载器不把不同原因全包装成同一外层类型;上下文可以由外部展示层添加,但不能破坏分类接口。

# 各语言实现

# Go

预期依次输出 fast、safe、三行 invalid、一行 missing。

Gopackage main

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

var ErrInvalid = errors.New("invalid config")

func load(path string) (string, error) {
    data, err := os.ReadFile(path)
    if err != nil { return "", fmt.Errorf("read config: %w", err) }
    value := string(data)
    if value != "fast" && value != "safe" {
        return "", fmt.Errorf("mode: %w", ErrInvalid)
    }
    return value, nil
}
func classify(path string) (string, error) {
    value, err := load(path)
    switch {
    case err == nil: return value, nil
    case errors.Is(err, ErrInvalid): return "invalid", nil
    case errors.Is(err, os.ErrNotExist): return "missing", nil
    default: return "", err
    }
}
func run() (err error) {
    dir, err := os.MkdirTemp("", "classify-")
    if err != nil { return err }
    defer func() { err = errors.Join(err, os.RemoveAll(dir)) }()
    path := filepath.Join(dir, "mode")
    for _, item := range []struct{ input, want string }{
        {"fast", "fast"}, {"safe", "safe"}, {"", "invalid"},
        {"fast\n", "invalid"}, {"\xff", "invalid"},
    } {
        if err := os.WriteFile(path, []byte(item.input), 0600); err != nil { return err }
        got, err := classify(path)
        if err != nil { return err }
        if got != item.want { return errors.New("wrong category") }
        fmt.Println(got)
    }
    if err := os.Remove(path); err != nil { return err }
    got, err := classify(path)
    if err != nil { return err }
    if got != "missing" { return errors.New("wrong missing category") }
    fmt.Println(got)
    return nil
}
func main() { if err := run(); err != nil { panic(err) } }

每次 errors.New("invalid config") 都会产生新的错误值,不能在分支里重新构造一个再比较。共享 sentinel 提供稳定身份;需要行号等数据时再用结构化错误和 errors.As,不要从消息中解析。参见 errors 包。

# 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;
import java.util.Arrays;

public class Main {
    static class InvalidConfig extends Exception {
        InvalidConfig() { super("invalid config"); }
    }
    static String load(Path path) throws IOException, InvalidConfig {
        byte[] data = Files.readAllBytes(path);
        if (Arrays.equals(data, new byte[]{'f', 'a', 's', 't'})) return "fast";
        if (Arrays.equals(data, new byte[]{'s', 'a', 'f', 'e'})) return "safe";
        throw new InvalidConfig();
    }
    static String classify(Path path) throws IOException {
        try { return load(path); }
        catch (InvalidConfig e) { return "invalid"; }
        catch (NoSuchFileException e) { return "missing"; }
    }
    public static void main(String[] args) throws Exception {
        Path dir = Files.createTempDirectory("classify-");
        Path path = dir.resolve("mode");
        try (AutoCloseable cleanup = () -> {
            Files.deleteIfExists(path);
            Files.delete(dir);
        }) {
            String[] inputs = {"fast", "safe", "", "fast\n", "é"};
            String[] wants = {"fast", "safe", "invalid", "invalid", "invalid"};
            for (int i = 0; i < inputs.length; i++) {
                Files.write(path, inputs[i].getBytes(StandardCharsets.UTF_8));
                String got = classify(path);
                if (!got.equals(wants[i])) throw new AssertionError("wrong category");
                System.out.println(got);
            }
            Files.delete(path);
            String got = classify(path);
            if (!got.equals("missing")) throw new AssertionError("wrong missing category");
            System.out.println(got);
        }
    }
}

NoSuchFileException 是文件系统操作报告缺失的专用异常;其他 IOException 不被这个捕获分支消费。这里面向默认本地文件系统,第三方文件系统提供者的异常分类须另查其契约。参见 NoSuchFileException 与 Files.readAllBytes。坏内容用受检异常使调用者看到不同于 I/O 的失败通道。

# Python

预期同 Go。

Pythonfrom pathlib import Path
from tempfile import TemporaryDirectory

class InvalidConfig(ValueError):
    pass

def load(path: Path) -> str:
    data = path.read_bytes()
    if data not in (b"fast", b"safe"):
        raise InvalidConfig("invalid config")
    return data.decode("ascii")

def classify(path: Path) -> str:
    try:
        return load(path)
    except InvalidConfig:
        return "invalid"
    except FileNotFoundError:
        return "missing"

with TemporaryDirectory(prefix="classify-") as directory:
    path = Path(directory) / "mode"
    for data, want in [(b"fast", "fast"), (b"safe", "safe"),
                       (b"", "invalid"), (b"fast\n", "invalid"),
                       (b"\xff", "invalid")]:
        path.write_bytes(data)
        got = classify(path)
        assert got == want
        print(got)
    path.unlink()
    got = classify(path)
    assert got == "missing"
    print(got)

先检查有限字节集合再解码,避免引入与 Java 替换解码策略不同的失败边界。FileNotFoundError、PermissionError、IsADirectoryError 都属于 OSError 家族,因此先捕获整个 OSError 再返回 missing 会误分类。参见 Python OS 异常层次。

# 迁移误区与适用限制

错误消息用于诊断,不是稳定协议。临时目录路径、操作系统语言、底层库升级都可能改变消息;错误类别才适合驱动策略。

“先 exists 再读取”也不是分类方案:检查与读取之间文件可能被删除,并且检查失败与不存在未必等价。应直接执行读取,再判断该次操作的结果。符号链接目标缺失、父目录缺失也可能报告不存在;本例只在自己创建的可信临时目录里演示,不把“缺失”解释成安全授权。

classify 返回的是演示界面的分类标签,不是建议库函数返回任意字符串枚举。真实库保持 load 的类型化失败,让各调用方自行决定报告还是恢复。

# 运行与验证

预期六行输出,语义检查覆盖合法集合两端、空值、尾随换行、非 ASCII 和真实缺失。文件内容按字节比较,清理仅移除自己的临时目录;不承诺刷新到稳定存储。执行事实与未覆盖的权限、设备和清理故障见验证记录,本文未单独运行示例。

# 相关阅读

表达与传播失败 · 恢复与降级 · 字符串转端口号