强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Perl 完全指南 / 第 14 章:错误处理与异常

第 14 章:错误处理与异常

“未处理的错误是最危险的错误”

健壮的错误处理是生产代码的必要条件。本章介绍 Perl 的错误处理机制。


14.1 die 和 warn

use strict;
use warnings;

# die — 抛出致命错误(退出程序)
die "发生了错误!\n";       # 带换行:直接打印消息
die "发生了错误";           # 不带换行:自动附加文件名和行号

# warn — 发出警告(继续运行)
warn "这是一个警告!\n";

# $! — 系统错误信息
open my $fh, '<', 'nonexistent.txt'
    or die "无法打开文件: $!\n";

错误信息格式

# 带换行:打印消息并退出
die "错误消息\n";
# 输出: 错误消息

# 不带换行:附加 at file.pl line N
die "错误消息";
# 输出: 错误消息 at script.pl line 10.

# 使用 __FILE__ 和 __LINE__
die sprintf "错误 at %s line %d\n", __FILE__, __LINE__;

14.2 eval — 捕获错误

eval 块(推荐)

# eval 块捕获 die 的错误
eval {
    open my $fh, '<', 'data.txt' or die "文件不存在: $!";
    my $content = do { local $/; <$fh> };
    close $fh;
    # ... 更多操作
};

if ($@) {
    warn "捕获到错误: $@\n";
} else {
    print "操作成功\n";
}

# 推荐写法(更清晰)
my $result = eval {
    # ... 可能失败的操作
    return 42;
};

if (defined $result) {
    print "结果: $result\n";
} elsif ($@) {
    warn "错误: $@\n";
}

eval 字符串(不推荐)

# eval 字符串 — 执行字符串中的代码(危险!)
my $result = eval "2 + 2";    # 4

# 不推荐用于错误处理
# 安全风险:如果字符串来自用户输入

eval 的返回值

# eval 块返回最后一个表达式的值
my $val = eval { 1 / 0 };
# $val = undef, $@ = "Illegal division by zero"

if ($@) {
    warn "除零错误: $@";
}

14.3 Try::Tiny — 现代异常处理

Try::Tiny 是推荐的现代错误处理方式,解决了 eval 的各种陷阱:

use Try::Tiny;

try {
    open my $fh, '<', 'data.txt' or die "无法打开: $!";
    my $data = do { local $/; <$fh> };
    close $fh;
    print "成功读取\n";
} catch {
    warn "捕获错误: $_\n";     # $_ 包含错误消息
} finally {
    print "无论如何都会执行\n";  # 清理代码
};

Try::Tiny vs eval

特性eval 块Try::Tiny
语法eval { } / $@try { } / catch { }
$@ 污染可能不会
$@ 竞态
finally不支持支持
性能略快略慢
依赖内置需安装
# eval 的陷阱:
eval { die "error" };
# 如果在 die 和检查 $@ 之间有 DESTROY 方法修改了 $@...
# $@ 可能变成 undef!

# Try::Tiny 没有这个问题
try { die "error" } catch { print "捕获: $_" };

14.4 autodie — 自动错误处理

autodie 让系统调用在失败时自动 die

use autodie;

# 不需要 "or die" 了
open my $fh, '<', 'data.txt';     # 失败时自动 die
my @lines = <$fh>;
close $fh;

# 自动处理的函数包括:
# open, close, read, write, print
# mkdir, rmdir, unlink, rename
# chdir, flock, binmode
# system, exec
# ...

autodie 的作用域

# 文件级 autodie
use autodie;

# 块级 autodie
{
    no autodie;     # 此块内禁用 autodie
    open my $fh, '<', 'file.txt';   # 需要手动检查
}

# 只对特定函数启用
use autodie qw(open close);

autodie vs 手动 or die

# 手动检查(传统写法)
open my $fh, '<', 'file.txt' or die "打开失败: $!\n";
print $fh "data" or die "写入失败: $!\n";
close $fh or die "关闭失败: $!\n";

# autodie(推荐写法)
use autodie;
open my $fh, '<', 'file.txt';
print $fh "data";
close $fh;

14.5 自定义异常类

# 基础异常类
package MyApp::Error;

use overload
    '""' => sub { $_[0]->message },
    bool => sub { 1 },
    fallback => 1;

sub new {
    my ($class, %args) = @_;
    return bless {
        message => $args{message} // "Unknown error",
        code    => $args{code}    // 500,
        file    => $args{file}    // caller(0),
        line    => $args{line}    // caller(0),
    }, $class;
}

sub message { $_[0]->{message} }
sub code    { $_[0]->{code} }

# 具体异常类
package MyApp::Error::NotFound;
use parent 'MyApp::Error';

sub new {
    my ($class, $resource) = @_;
    return $class->SUPER::new(
        message => "资源未找到: $resource",
        code    => 404,
    );
}

package MyApp::Error::Permission;
use parent 'MyApp::Error';

sub new {
    my ($class, $action) = @_;
    return $class->SUPER::new(
        message => "权限不足: $action",
        code    => 403,
    );
}

使用自定义异常

use Try::Tiny;
use MyApp::Error;
use MyApp::Error::NotFound;

sub find_user {
    my ($id) = @_;
    # 模拟查询
    die MyApp::Error::NotFound->new("user:$id") unless $id > 0;
    return { id => $id, name => "User $id" };
}

try {
    my $user = find_user(0);
} catch {
    if ($_->isa('MyApp::Error::NotFound')) {
        warn "404: " . $_->message . "\n";
    } elsif ($_->isa('MyApp::Error')) {
        warn "Error " . $_->code . ": " . $_->message . "\n";
    } else {
        warn "未知错误: $_\n";
    }
};

14.6 异常处理模式

哨兵模式

sub process_file {
    my ($file) = @_;
    
    open my $fh, '<', $file or return undef;
    
    while (<$fh>) {
        chomp;
        # 处理...
    }
    
    close $fh;
    return 1;    # 成功
}

my $ok = process_file("data.txt");
warn "处理失败" unless defined $ok;

异常模式(推荐)

use Try::Tiny;

sub process_file {
    my ($file) = @_;
    open my $fh, '<', $file or die "打开失败: $!";
    
    while (<$fh>) {
        chomp;
        die "格式错误" unless /^valid/;
        # 处理...
    }
    
    close $fh;
}

try {
    process_file("data.txt");
    print "处理成功\n";
} catch {
    warn "错误: $_\n";
};

14.7 业务场景:API 错误处理

#!/usr/bin/env perl
use strict;
use warnings;
use Try::Tiny;
use JSON::XS;
use HTTP::Tiny;

my $client = HTTP::Tiny->new(timeout => 10);

sub api_request {
    my ($method, $url, $data) = @_;
    
    my $response;
    try {
        $response = $client->request($method, $url, {
            headers => { 'Content-Type' => 'application/json' },
            ($data ? (content => encode_json($data)) : ()),
        });
    } catch {
        die "网络错误: $_\n";
    };
    
    # 检查 HTTP 状态码
    die "HTTP $response->{status}: $response->{reason}\n"
        unless $response->{success};
    
    # 解析 JSON
    my $result;
    try {
        $result = decode_json($response->{content});
    } catch {
        die "JSON 解析失败: $_\n";
    };
    
    return $result;
}

# 使用
my $users = try {
    api_request('GET', 'https://jsonplaceholder.typicode.com/users');
} catch {
    warn "请求失败: $_";
    return [];
};

for my $user (@$users) {
    printf "%-30s %s\n", $user->{name}, $user->{email};
}

本章小结

要点内容
die抛出致命错误
warn发出警告(不退出)
eval { }捕获错误的旧方式
Try::Tiny推荐的现代错误处理
autodie自动让系统调用失败时报错
$@eval 错误信息
$!系统错误信息
自定义异常使用类层次结构

练习

  1. 使用 eval 和 Try::Tiny 分别实现除零错误处理
  2. 创建一个自定义异常类层次(Base → IOError → NotFound)
  3. 使用 autodie 重写一个文件操作脚本
  4. 实现一个带重试机制的函数(最多重试 3 次)
  5. 编写一个 API 客户端,使用异常类处理各种错误

扩展阅读