Skip to content

错误处理

本指南介绍 Targo 中的错误处理模式,包括错误返回值、panic 和 recover。

目标读者

  • 熟悉 TypeScript try/catch 的开发者
  • 想要理解 Go 错误处理哲学的开发者
  • 需要编写健壮程序的开发者

核心概念

Targo 采用 Go 的错误处理模型:

  • 错误返回值 - 函数通过返回值传递错误
  • error | null - 错误类型,null 表示无错误
  • 显式检查 - 必须显式检查每个错误
  • panic/recover - 用于不可恢复的错误

与 TypeScript 的区别

TypeScript 使用 try/catch 处理异常。Targo 使用错误返回值,只在不可恢复的情况下使用 panic。

错误返回值模式

基本用法

函数通过多返回值传递错误:

typescript
import { os } from "os";

function readFile(path: string): [slice<byte>, error | null] {
    return os.ReadFile(path);
}

// 使用
let [data, err] = readFile("config.json");
if (err != null) {
    console.log(`Error: ${err.Error()}`);
    return;
}

// 使用 data...
console.log(string(data));

编译为 Go:

go
func readFile(path string) ([]byte, error) {
    return os.ReadFile(path)
}

data, err := readFile("config.json")
if err != nil {
    fmt.Printf("Error: %v\n", err)
    return
}

fmt.Println(string(data))

error 接口

error 是一个内置接口:

typescript
interface error extends GoInterface {
    Error(): string;
}

任何实现了 Error() 方法的类型都满足 error 接口。

创建错误

使用 errors.New()fmt.Errorf() 创建错误:

typescript
import { errors } from "errors";
import { fmt } from "fmt";

// 简单错误
let err1 = errors.New("something went wrong");

// 格式化错误
let err2 = fmt.Errorf("failed to open file: %s", filename);

// 包装错误(Go 1.13+)
let err3 = fmt.Errorf("failed to process: %w", originalErr);

自定义错误类型

创建自定义错误类型:

typescript
class ValidationError implements error {
    Field: string;
    Message: string;
    
    constructor(field: string, message: string) {
        this.Field = field;
        this.Message = message;
    }
    
    Error(): string {
        return `${this.Field}: ${this.Message}`;
    }
}

function validateAge(age: int): error | null {
    if (age < 0) {
        return new ValidationError("age", "must be non-negative");
    }
    if (age > 150) {
        return new ValidationError("age", "must be less than 150");
    }
    return null;
}

// 使用
let err = validateAge(-5);
if (err != null) {
    console.log(err.Error());  // "age: must be non-negative"
}

错误检查模式

1. 立即返回

最常见的模式:检查错误后立即返回:

typescript
function processFile(path: string): error | null {
    let [data, err] = os.ReadFile(path);
    if (err != null) {
        return err;
    }
    
    let [result, err2] = process(data);
    if (err2 != null) {
        return err2;
    }
    
    return save(result);
}

2. 包装错误

添加上下文信息:

typescript
function loadConfig(path: string): [Config, error | null] {
    let [data, err] = os.ReadFile(path);
    if (err != null) {
        return [null, fmt.Errorf("failed to load config: %w", err)];
    }
    
    let [config, err2] = parseConfig(data);
    if (err2 != null) {
        return [null, fmt.Errorf("failed to parse config: %w", err2)];
    }
    
    return [config, null];
}

3. 错误聚合

收集多个错误:

typescript
function validateUser(user: User): error[] {
    let errors: error[] = [];
    
    if (user.Name == "") {
        errors.push(errors.New("name is required"));
    }
    
    if (user.Email == "") {
        errors.push(errors.New("email is required"));
    }
    
    if (user.Age < 18) {
        errors.push(errors.New("must be 18 or older"));
    }
    
    return errors;
}

// 使用
let errs = validateUser(user);
if (errs.length > 0) {
    for (const err of errs) {
        console.log(err.Error());
    }
    return;
}

4. 哨兵错误

预定义的错误值:

typescript
import { io } from "io";

// 哨兵错误
const ErrNotFound = errors.New("not found");
const ErrInvalidInput = errors.New("invalid input");

function findUser(id: int): [User, error | null] {
    // ...
    if (!found) {
        return [null, ErrNotFound];
    }
    return [user, null];
}

// 使用
let [user, err] = findUser(123);
if (err == ErrNotFound) {
    console.log("User not found");
} else if (err == io.EOF) {
    console.log("End of file");
} else if (err != null) {
    console.log(`Other error: ${err.Error()}`);
}

Panic 和 Recover

Panic

panic 用于不可恢复的错误,会立即停止当前函数的执行:

typescript
function divide(a: int, b: int): int {
    if (b == 0) {
        panic("division by zero");
    }
    return a / b;
}

// 调用
let result = divide(10, 0);  // panic: division by zero

编译为 Go:

go
func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

何时使用 Panic

只在以下情况使用 panic:

  • 程序初始化失败(如配置文件损坏)
  • 不可能发生的情况(编程错误)
  • 无法继续执行的严重错误

大多数情况应该返回 error。

Recover

recover 用于捕获 panic,只能在 defer 函数中使用:

typescript
function safeDiv(a: int, b: int): [int, error | null] {
    let result = 0;
    let err: error | null = null;
    
    defer(() => {
        let r = recover();
        if (r != null) {
            err = fmt.Errorf("panic: %v", r);
        }
    });
    
    result = divide(a, b);  // 可能 panic
    return [result, err];
}

// 使用
let [result, err] = safeDiv(10, 0);
if (err != null) {
    console.log(err.Error());  // "panic: division by zero"
} else {
    console.log(result);
}

编译为 Go:

go
func safeDiv(a, b int) (int, error) {
    var result int
    var err error
    
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic: %v", r)
        }
    }()
    
    result = divide(a, b)
    return result, err
}

Defer 语句

defer 延迟执行函数,常用于资源清理:

typescript
function processFile(path: string): error | null {
    let [file, err] = os.Open(path);
    if (err != null) {
        return err;
    }
    defer(() => file.Close());  // 确保文件被关闭
    
    // 处理文件...
    let [data, err2] = io.ReadAll(file);
    if (err2 != null) {
        return err2;  // file.Close() 仍会被调用
    }
    
    return process(data);
}

TypeScript 对比

错误处理方式

特性TypeScriptTargo
错误传递throw / Promise.reject()返回 error | null
错误捕获try/catch检查返回值
不可恢复错误throwpanic
错误恢复catchrecover (仅在 defer 中)
资源清理finallydefer

代码对比

TypeScript

typescript
async function loadUser(id: number): Promise<User> {
    try {
        const response = await fetch(`/api/users/${id}`);
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return await response.json();
    } catch (error) {
        console.error("Failed to load user:", error);
        throw error;
    }
}

// 使用
try {
    const user = await loadUser(123);
    console.log(user);
} catch (error) {
    console.error("Error:", error);
}

Targo

typescript
function loadUser(id: int): [User, error | null] {
    let [resp, err] = http.Get(`/api/users/${id}`);
    if (err != null) {
        return [null, fmt.Errorf("failed to load user: %w", err)];
    }
    defer(() => resp.Body.Close());
    
    if (resp.StatusCode != 200) {
        return [null, fmt.Errorf("HTTP %d", resp.StatusCode)];
    }
    
    let [user, err2] = parseUser(resp.Body);
    if (err2 != null) {
        return [null, fmt.Errorf("failed to parse user: %w", err2)];
    }
    
    return [user, null];
}

// 使用
let [user, err] = loadUser(123);
if (err != null) {
    console.log(`Error: ${err.Error()}`);
    return;
}
console.log(user);

实际示例

HTTP 服务器错误处理

typescript
import { http } from "net/http";
import { json } from "encoding/json";

interface ErrorResponse {
    error: string;
    code: int;
}

function handleError(w: http.ResponseWriter, err: error, code: int): void {
    w.WriteHeader(code);
    let response: ErrorResponse = {
        error: err.Error(),
        code: code
    };
    json.NewEncoder(w).Encode(response);
}

function getUserHandler(w: http.ResponseWriter, r: http.Request): void {
    let id = r.URL.Query().Get("id");
    if (id == "") {
        handleError(w, errors.New("id is required"), 400);
        return;
    }
    
    let [user, err] = findUser(parseInt(id));
    if (err == ErrNotFound) {
        handleError(w, err, 404);
        return;
    } else if (err != null) {
        handleError(w, err, 500);
        return;
    }
    
    w.WriteHeader(200);
    json.NewEncoder(w).Encode(user);
}

数据库事务

typescript
function transferMoney(from: int, to: int, amount: float64): error | null {
    let [tx, err] = db.Begin();
    if (err != null) {
        return err;
    }
    
    // 确保事务被提交或回滚
    let committed = false;
    defer(() => {
        if (!committed) {
            tx.Rollback();
        }
    });
    
    // 扣款
    let err2 = debit(tx, from, amount);
    if (err2 != null) {
        return fmt.Errorf("failed to debit: %w", err2);
    }
    
    // 入账
    let err3 = credit(tx, to, amount);
    if (err3 != null) {
        return fmt.Errorf("failed to credit: %w", err3);
    }
    
    // 提交事务
    let err4 = tx.Commit();
    if (err4 != null) {
        return fmt.Errorf("failed to commit: %w", err4);
    }
    
    committed = true;
    return null;
}

重试逻辑

typescript
function retryWithBackoff<T>(
    fn: () => [T, error | null],
    maxRetries: int
): [T, error | null] {
    let lastErr: error | null = null;
    
    for (let i = 0; i < maxRetries; i++) {
        let [result, err] = fn();
        if (err == null) {
            return [result, null];
        }
        
        lastErr = err;
        console.log(`Attempt ${i + 1} failed: ${err.Error()}`);
        
        if (i < maxRetries - 1) {
            let backoff = time.Duration(1 << i) * time.Second;
            time.Sleep(backoff);
        }
    }
    
    return [null, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)];
}

// 使用
let [data, err] = retryWithBackoff(() => fetchData(), 3);
if (err != null) {
    console.log(`Failed: ${err.Error()}`);
    return;
}

常见陷阱

1. 忽略错误

typescript
// ❌ 错误:忽略错误
let [data, _] = os.ReadFile("config.json");
// 如果读取失败,data 是 null,使用会 panic

// ✅ 正确:检查错误
let [data, err] = os.ReadFile("config.json");
if (err != null) {
    console.log(`Error: ${err.Error()}`);
    return;
}

2. 过度使用 Panic

typescript
// ❌ 错误:对可恢复错误使用 panic
function parseAge(s: string): int {
    let [age, err] = strconv.Atoi(s);
    if (err != null) {
        panic(err);  // 不应该 panic
    }
    return age;
}

// ✅ 正确:返回错误
function parseAge(s: string): [int, error | null] {
    let [age, err] = strconv.Atoi(s);
    if (err != null) {
        return [0, fmt.Errorf("invalid age: %w", err)];
    }
    return [age, null];
}

3. 错误信息不清晰

typescript
// ❌ 错误:错误信息缺少上下文
let [data, err] = os.ReadFile(path);
if (err != null) {
    return err;  // 不知道是哪个文件失败
}

// ✅ 正确:添加上下文
let [data, err] = os.ReadFile(path);
if (err != null) {
    return fmt.Errorf("failed to read %s: %w", path, err);
}

4. Recover 使用不当

typescript
// ❌ 错误:在非 defer 函数中使用 recover
function bad(): void {
    let r = recover();  // 无效,recover 只在 defer 中有效
    if (r != null) {
        console.log("recovered");
    }
}

// ✅ 正确:在 defer 中使用 recover
function good(): void {
    defer(() => {
        let r = recover();
        if (r != null) {
            console.log("recovered");
        }
    });
    
    // 可能 panic 的代码...
}

最佳实践

  1. 优先使用错误返回值

    • 只在不可恢复的情况下使用 panic
    • 让调用者决定如何处理错误
  2. 添加错误上下文

    • 使用 fmt.Errorf 包装错误
    • 包含足够的信息用于调试
  3. 定义哨兵错误

    • 对于特定错误使用预定义的错误值
    • 便于调用者进行错误判断
  4. 使用自定义错误类型

    • 对于复杂错误,定义自定义类型
    • 包含额外的上下文信息
  5. 使用 defer 清理资源

    • 确保资源被正确释放
    • 即使发生错误也能清理
  6. 不要忽略错误

    • 始终检查错误返回值
    • 至少记录错误日志

下一步

参考