Appearance
错误处理
本指南介绍 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 对比
错误处理方式
| 特性 | TypeScript | Targo |
|---|---|---|
| 错误传递 | throw / Promise.reject() | 返回 error | null |
| 错误捕获 | try/catch | 检查返回值 |
| 不可恢复错误 | throw | panic |
| 错误恢复 | catch | recover (仅在 defer 中) |
| 资源清理 | finally | defer |
代码对比
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 的代码...
}最佳实践
优先使用错误返回值
- 只在不可恢复的情况下使用 panic
- 让调用者决定如何处理错误
添加错误上下文
- 使用
fmt.Errorf包装错误 - 包含足够的信息用于调试
- 使用
定义哨兵错误
- 对于特定错误使用预定义的错误值
- 便于调用者进行错误判断
使用自定义错误类型
- 对于复杂错误,定义自定义类型
- 包含额外的上下文信息
使用 defer 清理资源
- 确保资源被正确释放
- 即使发生错误也能清理
不要忽略错误
- 始终检查错误返回值
- 至少记录错误日志