Skip to content

Repository files navigation

English | 中文

GoTryCatch

Version Go

A type-safe exception handling library based on Go generics that brings try-catch-like capabilities to Go.

Features

  • 🎯 Type-safe: Uses Go generics to ensure type-safe exception handling
  • 🔗 Partial chaining: Supports chaining for CatchAny and Finally
  • 🏷️ Multiple error types: Built-in common error types (validation, database, network, business logic, auth, config, rate limit)
  • 📊 Structured output: All errors support ToMap() and ToJSON() for easy parsing by agents/logs
  • 🔍 Rich context: Errors include file, line, function, timestamp, and stack trace
  • 🔄 Finally support: Guarantees cleanup code execution
  • 🎁 TryWithResult: Support for functions with return values and OnSuccess/OnError/OrElse patterns
  • 🐛 Debug mode: Optional debug logging for type matching issues
  • 📦 Zero dependency: Pure Go implementation, no external dependencies
  • 🚀 High performance: Built on Go's panic/recover with minimal overhead

Important Note

⚠️ Chaining limitation: Due to Go's limitation that methods cannot have generic type parameters, you cannot write tb.Catch[ErrorType](handler). Use the functional form instead: gotrycatch.Catch[ErrorType](tb, handler). CatchAny and Finally do support chaining.

Semantic Boundaries

Read these before relying on this library in production:

  1. A panic inside a Catch handler propagates immediately and skips any later Finally call. The chain is plain function calls, not a deferred block. Keep handler bodies non-panicking, or wrap them in their own Try.
  2. A panic inside Finally replaces the original error — same as Go's native defer. If Finally must run fallible cleanup, guard it with its own recover.
  3. Try does not catch panics from goroutines started inside fn. recover only works within the same goroutine; an unhandled panic in a child goroutine still crashes the process.
  4. Catch[T] is an exact type assertion, not errors.As. panic(&err) will not match Catch[Err] (align value vs pointer), and errors wrapped with fmt.Errorf("...: %w", err) will not be unwrapped. Catch the wrapper type, or panic the specific type you intend to catch. Use CatchAs/OnAs for wrapping-aware matching.
  5. Run semantics: a panic inside a handler propagates (after Cleanup clauses have run, since they are deferred); a panic inside a Cleanup itself replaces whatever was propagating, same as Go's native defer. Child goroutine panics are never captured — recover is goroutine-local.

Installation

go get github.com/linkerlin/gotrycatch

The v2 Way: Run

Run is the recommended entry point. It is one call, impossible to forget cleanup, and results flow through ordinary Go error handling:

import (
    "fmt"

    "github.com/linkerlin/gotrycatch"
    "github.com/linkerlin/gotrycatch/errtypes"
)

func loadUser(id string) error {
    err := gotrycatch.Run(func() { queryUser(id) },
        // errors.As semantics: matches through fmt.Errorf("%w", ...) wrappers
        gotrycatch.OnAs(func(e errtypes.DatabaseError) {
            retry(id)
        }),
        gotrycatch.On(func(e errtypes.RateLimitError) {
            wait(e.RetryAfter)
        }),
        // always runs, LIFO, even if a handler panics
        gotrycatch.Cleanup(func() { conn.Close() }),
    )
    if err != nil {
        return fmt.Errorf("load user %s: %w", id, err)
    }
    return nil
}

Why it is hard to misuse:

  • Unhandled panics come back as error — silently swallowing is structurally impossible. Error panics pass through unchanged (so errors.Is/As work end-to-end); other values are wrapped in *PanicError.
  • Cleanup always runs — declared inline, no forgotten Finally.
  • Clauses are ordered values — no method/function chaining rules to memorize.
  • Run1 covers return values: v, err := gotrycatch.Run1(fn, clauses...).

Quick Start (classic chain API)

Prefer Run above; the classic API remains fully supported:

package main

import (
    "fmt"
    "github.com/linkerlin/gotrycatch"
    "github.com/linkerlin/gotrycatch/errtypes"
)

func main() {
    tb := gotrycatch.Try(func() {
        // Code that may panic
        gotrycatch.Throw(errtypes.NewValidationError("email", "invalid format", 1001))
    })

    tb = gotrycatch.Catch[errtypes.ValidationError](tb, func(err errtypes.ValidationError) {
        fmt.Printf("Validation error: %s (field: %s, code: %d)\n", err.Message, err.Field, err.Code)
    })

    tb.Finally(func() {
        fmt.Println("Cleanup done")
    })
}

TryWithResult - Functions with return values

// Execute function that returns a value
tb := gotrycatch.TryWithResult(func() int {
    return computeValue()
})

// Success callback
tb.OnSuccess(func(result int) {
    fmt.Println("Result:", result)
})

// Error callback
tb.OnError(func(err interface{}) {
    fmt.Println("Error:", err)
})

// Get result with default value
result := tb.OrElse(0)

// Or lazy evaluation of default
result := tb.OrElseGet(func() int { return computeDefault() })

Handling multiple error types

tb := gotrycatch.Try(func() {
    processUserData()
})

tb = gotrycatch.Catch[errors.ValidationError](tb, func(err errors.ValidationError) {
    fmt.Printf("Validation failed: %s\n", err.Message)
})

tb = gotrycatch.Catch[errors.DatabaseError](tb, func(err errors.DatabaseError) {
    fmt.Printf("Database error: %s on table %s\n", err.Operation, err.Table)
})

tb = gotrycatch.Catch[errors.NetworkError](tb, func(err errors.NetworkError) {
    if err.Timeout {
        fmt.Printf("Network timeout: %s\n", err.URL)
    } else {
        fmt.Printf("Network error %d: %s\n", err.StatusCode, err.URL)
    }
})

tb = tb.CatchAny(func(err interface{}) {
    fmt.Printf("Unknown error: %v\n", err)
})

tb.Finally(func() {
    fmt.Println("Processing done")
})

State query and debugging

tb := gotrycatch.Try(func() {
    riskyOperation()
})

// Query state
if tb.HasError() {
    fmt.Printf("Error type: %s\n", tb.GetErrorType())
    fmt.Printf("Error value: %v\n", tb.GetError())
}

if !tb.IsHandled() {
    // Decide how to handle based on error type
    switch tb.GetErrorType() {
    case "errors.ValidationError":
        // Handle validation error
    default:
        tb = tb.CatchAny(func(err interface{}) {
            logUnknownError(err)
        })
    }
}

// Enable debug mode to trace type matching
gotrycatch.SetDebug(true)

Structured error output (Agent-friendly)

tb := gotrycatch.Catch[errors.BusinessLogicError](tb, func(err errors.BusinessLogicError) {
    // JSON output for logging/agents
    jsonData, _ := err.ToJSON()
    log.Printf("ERROR: %s", string(jsonData))
    // Output: {"type":"BusinessLogicError","rule":"inventory_check","details":"Out of stock","file":"main.go","line":42,"function":"processOrder","timestamp":"2024-01-15T10:30:00Z","stack":[...]}
})

Assertion helpers

// Assert condition, throw error if false
gotrycatch.Assert(value != "", errors.NewValidationError("value", "cannot be empty", 1001))

// Assert no error, wrap and throw if error exists
gotrycatch.AssertNoError(err, "database operation failed")

Built-in Error Types

All error types include: File, Line, Function, Timestamp, Stack

Type Specific Fields Constructor Use Case
ValidationError Field, Message, Code NewValidationError(field, message, code) Data validation errors
DatabaseError Operation, Table, Cause NewDatabaseError(operation, table, cause) Database operation errors
NetworkError URL, StatusCode, Timeout NewNetworkError(url, code) HTTP errors
NetworkError URL, Timeout NewNetworkTimeoutError(url) Network timeouts
BusinessLogicError Rule, Details NewBusinessLogicError(rule, details) Business rule violations
ConfigError Key, Value, Reason NewConfigError(key, value, reason) Configuration errors
AuthError Operation, User, Reason NewAuthError(operation, user, reason) Authentication/authorization errors
RateLimitError Resource, Limit, Current, RetryAfter NewRateLimitError(resource, limit, current, retryAfter) Rate limiting errors

Error methods

var err errors.ValidationError

err.Error()     // string - Full error description with location
err.ToMap()     // map[string]interface{} - Structured data
err.ToJSON()    // ([]byte, error) - JSON output
err.Unwrap()    // error - Underlying error (DatabaseError returns Cause)
err.Is(target)  // bool - Error matching

API Reference

Core Functions

Function/Method Signature Description
Run func Run(fn func(), clauses ...Clause) error v2 Idiomatic clause-based execution; returns unhandled panics as error
Run1 func Run1[T any](fn func() T, clauses ...Clause) (T, error) v2 Run with a return value
On[E] func On[E any](handler func(E)) Clause v2 Clause: exact-type match (Catch semantics)
OnAs[E] func OnAs[E error](handler func(E)) Clause v2 Clause: errors.As match, penetrates %w wrapping
Any func Any(handler func(interface{})) Clause v2 Clause: fallback for any panic
Cleanup func Cleanup(fn func()) Clause v2 Clause: always runs, LIFO, even if a handler panics
Try func Try(fn func()) *TryBlock Execute function and capture any panic
Catch[T] func Catch[T any](tb *TryBlock, handler func(T)) *TryBlock Handle panics of exact type T
CatchAs[E] func CatchAs[E error](tb *TryBlock, handler func(E)) *TryBlock v2 Handle via errors.As (penetrates wrapping, E or *E)
CatchAny func (tb *TryBlock) CatchAny(handler func(interface{})) *TryBlock Handle any unhandled panic
Finally func (tb *TryBlock) Finally(fn func()) Execute cleanup code

TryBlock State Query

Method Return Type Description
HasError() bool Whether a panic was captured
GetError() interface{} Get the panic value
Err() error v2 Panic bridged to error (errors.Is/As-ready; non-error panics become *PanicError)
GetErrorType() string Get error type name (e.g., "errtypes.ValidationError")
CanonicalErrorType() string v2 Pointer-free short type name (e.g., "ValidationError")
IsHandled() bool Whether error was handled
String() string Friendly string representation

TryWithResult

Function/Method Signature Description
TryWithResult func TryWithResult[T any](fn func() T) *TryBlockWithResult[T] Execute function with return value
CatchWithResult func CatchWithResult[T, E any](tb *TryBlockWithResult[T], handler func(E)) *TryBlockWithResult[T] Typed catch for TryWithResult
CatchAnyWithResult func CatchAnyWithResult[T any](tb *TryBlockWithResult[T], handler func(interface{})) *TryBlockWithResult[T] Catch any for TryWithResult
GetResult() T Get the result value
OnSuccess func (tb *TryBlockWithResult[T]) OnSuccess(fn func(T)) *TryBlockWithResult[T] Callback on success
OnError func (tb *TryBlockWithResult[T]) OnError(fn func(interface{})) *TryBlockWithResult[T] Callback on error
OrElse func (tb *TryBlockWithResult[T]) OrElse(defaultValue T) T Get result or default
OrElseGet func (tb *TryBlockWithResult[T]) OrElseGet(supplier func() T) T Get result or lazy default

Debug & Assertions

Function Signature Description
SetDebug func SetDebug(enabled bool) Enable/disable debug logging
IsDebug func IsDebug() bool Check debug mode status
Throw func Throw(err interface{}) Throw an exception (panic)
Assert func Assert(condition bool, err interface{}) Assert condition, throw if false
AssertNoError func AssertNoError(err error, msg string) Assert no error, throw with message if error

Best Practices

  1. Order Catch blocks by specificity: Most specific types first, generic types later
  2. Always use Finally: Ensure resource cleanup
  3. Use predefined error types: Prefer structured errors over raw strings/numbers
  4. Use CatchAny as fallback: Handle unexpected error types gracefully
  5. Enable debug mode for troubleshooting: Use SetDebug(true) when type matching doesn't work as expected
  6. Use ToJSON for logging: Structured output is easier to parse and analyze

Performance

Measured with go test -bench . (see gotrycatch_bench_test.go; i9-13900HX, indicative only):

Scenario ns/op allocs/op
Error return (idiomatic baseline) ~0.2 0
Raw recover, no panic ~2 0
Run, no panic ~11 0
Run1, no panic ~9 0
Try, no panic ~63 1
TryWithResult + OrElse, no panic ~66 1
Run + panic + On hit ~200 0
Raw recover, panic path ~157 0
Try + panic + Catch hit ~469 1
Run unhandled panic → error ~1385 2
  • Run allocates nothing on the happy path (no TryBlock is created) — cheaper than Try
  • The unhandled-panic path pays one *PanicError + stack capture (error path only)
  • Fine for application-level flows; use Run over Try in hot loops

Compatibility

  • Requires Go 1.21+ (generics; panic(nil) detection semantics)
  • Fully compatible with the standard library

Migrating from v1.x

  1. gotrycatch/errorsgotrycatch/errtypes (renamed to avoid clashing with stdlib errors). The old import path keeps compiling — it is now a pure alias layer with identical behavior (types are aliases; constructors are direct bindings, so File/Line/Stack attribution is unchanged). GetErrorType() output changes accordingly: errors.ValidationErrorerrtypes.ValidationError.
  2. CatchWithReturn removed — replace with TryWithResult + CatchWithResult:
// v1
result, tb := gotrycatch.CatchWithReturn(tb, func(err string) interface{} { ... })

// v2
tb = gotrycatch.CatchWithResult[int, string](tb, func(err string) { ... })
result := tb.OrElse(defaultValue)
  1. Everything else is additive: Try/Catch/CatchAny/Finally/TryWithResult chains behave exactly as in v1.
  • Can coexist with existing error-handling code
  • Thread-safe for concurrent use

FAQ

Q: Why can't I use full method chaining?

A: Because methods cannot have generic type parameters in Go. So this is not supported:

// ❌ Not supported
tb := gotrycatch.Try(func() { ... }).Catch[ErrorType](handler)

Use the functional form instead:

// ✅ Correct
tb := gotrycatch.Try(func() { ... })
tb = gotrycatch.Catch[ErrorType](tb, handler)

But CatchAny and Finally support chaining:

// ✅ Supported
tb.CatchAny(handler).Finally(cleanup)

Q: How do I debug type matching issues?

A: Enable debug mode:

gotrycatch.SetDebug(true)
// Output: [gotrycatch] Catch: type errors.ValidationError does not match target type int
// Output: [gotrycatch] Catch: type errors.ValidationError matched, calling handler

Q: What happens to unhandled errors?

A: Unhandled errors are re-thrown after Finally executes. Always use CatchAny as a fallback if you don't want panics to propagate.

Examples

See the cmd/demo directory for more:

  • Basic usage
  • Handling multiple error types
  • Structured error output (ToMap/ToJSON)
  • TryWithResult patterns
  • Error chains (Unwrap/Is)
  • A real-world multi-catch scenario

Run examples

# Demo (10 detailed demos)
go run ./cmd/demo

# Run tests
go test -v ./...

# Run with coverage
go test -cover ./...

# Run with race detector
go test -race ./...

License

MIT License


GoTryCatch

一个基于 Go 泛型的类型安全异常处理库,为 Go 带来类似 try-catch 的异常处理能力。

特性

  • 🎯 类型安全: 使用 Go 泛型确保异常处理的类型安全
  • 🔗 部分链式调用: 支持 CatchAnyFinally 的链式调用
  • 🏷️ 七种异常类型: 内置常用异常类型(验证、数据库、网络、业务逻辑、认证、配置、限流)
  • 📊 结构化输出: 所有错误支持 ToMap()ToJSON(),便于 Agent 解析和日志记录
  • 🔍 丰富上下文: 错误自动包含文件名、行号、函数名、时间戳和调用堆栈
  • 🔄 Finally 支持: 保证清理代码的执行
  • 🎁 TryWithResult: 支持带返回值的函数,提供 OnSuccess/OnError/OrElse/OrElseGet 模式
  • 🐛 调试模式: 可选的调试日志,帮助排查类型匹配问题
  • 断言辅助: AssertAssertNoError 简化条件检查
  • 📦 零依赖: 纯 Go 实现,无外部依赖
  • 🚀 高性能: 基于 Go 的 panic/recover 机制,性能开销极小

重要说明

⚠️ 链式调用限制: 由于 Go 语言的限制,方法不能有泛型类型参数,因此不能直接写 tb.Catch[ErrorType](handler)。需要使用函数式调用:gotrycatch.Catch[ErrorType](tb, handler)。但是 CatchAnyFinally 方法支持链式调用。

语义边界

在生产环境依赖本库之前,请先阅读以下边界:

  1. Catch handler 内部发生 panic 会立即向上传播,并跳过之后才调用的 Finally。调用链是普通函数调用而非 defer 块。请保证 handler 本身不 panic,或为其单独包一层 Try
  2. Finally 内部的 panic 会覆盖原始错误——与 Go 原生 defer 语义一致。若 Finally 中的清理可能失败,请自行 recover 保护。
  3. Try 不捕获 fn 内启动的 goroutine 的 panicrecover 只在同一 goroutine 内有效;子 goroutine 的未处理 panic 仍会崩溃整个进程。
  4. Catch[T] 是精确类型断言,而非 errors.Aspanic(&err) 无法匹配 Catch[Err](注意值与指针对齐);用 fmt.Errorf("...: %w", err) 包装过的错误也不会被解包。请捕获包装类型,或直接 panic 你打算捕获的具体类型。需要穿透包装请用 CatchAs/OnAs
  5. Run 语义:handler 内的 panic 会向外传播(但 Cleanup 子句已通过 defer 注册,仍会执行);Cleanup 自身的 panic 会覆盖正在传播的错误——与 Go 原生 defer 一致。子 goroutine 的 panic 永远无法被捕获——recover 是 goroutine 局部的。

安装

go get github.com/linkerlin/gotrycatch

v2 推荐用法:Run

Run 是 v2 的推荐入口。一次调用、不可能忘记清理、结果直接进入 Go 惯用的 error 流:

import (
    "fmt"

    "github.com/linkerlin/gotrycatch"
    "github.com/linkerlin/gotrycatch/errtypes"
)

func loadUser(id string) error {
    err := gotrycatch.Run(func() { queryUser(id) },
        // errors.As 语义:可穿透 fmt.Errorf("%w", ...) 包装
        gotrycatch.OnAs(func(e errtypes.DatabaseError) {
            retry(id)
        }),
        gotrycatch.On(func(e errtypes.RateLimitError) {
            wait(e.RetryAfter)
        }),
        // 总是执行、LIFO,handler panic 也不会跳过
        gotrycatch.Cleanup(func() { conn.Close() }),
    )
    if err != nil {
        return fmt.Errorf("load user %s: %w", id, err)
    }
    return nil
}

为什么难以误用:

  • 未处理的 panic 以 error 返回——在结构上不可能静默吞错。error 型 panic 原样直通(errors.Is/As 全链路可用);其他值包装为 *PanicError
  • Cleanup 总是执行——内联声明,不存在忘记 Finally 的问题。
  • 子句是有序的值——无需记忆方法/函数混链规则。
  • Run1 支持返回值v, err := gotrycatch.Run1(fn, clauses...)

快速开始(经典链式 API)

推荐优先使用上面的 Run;经典 API 继续完整支持:

基本用法

package main

import (
    "fmt"
    "github.com/linkerlin/gotrycatch"
    "github.com/linkerlin/gotrycatch/errtypes"
)

func main() {
    tb := gotrycatch.Try(func() {
        // 可能会 panic 的代码
        gotrycatch.Throw(errtypes.NewValidationError("email", "格式无效", 1001))
    })

    tb = gotrycatch.Catch[errtypes.ValidationError](tb, func(err errtypes.ValidationError) {
        fmt.Printf("验证错误: %s (字段: %s, 代码: %d)\n", err.Message, err.Field, err.Code)
    })

    tb.Finally(func() {
        fmt.Println("清理工作完成")
    })
}

TryWithResult - 带返回值的函数

// 执行带返回值的函数
tb := gotrycatch.TryWithResult(func() int {
    return computeValue()
})

// 成功回调
tb.OnSuccess(func(result int) {
    fmt.Println("结果:", result)
})

// 错误回调
tb.OnError(func(err interface{}) {
    fmt.Println("错误:", err)
})

// 获取结果,有错误时返回默认值
result := tb.OrElse(0)

// 或者延迟计算默认值
result := tb.OrElseGet(func() int { return computeDefault() })

多种异常类型处理

tb := gotrycatch.Try(func() {
    processUserData()
})

tb = gotrycatch.Catch[errors.ValidationError](tb, func(err errors.ValidationError) {
    fmt.Printf("验证失败: %s\n", err.Message)
})

tb = gotrycatch.Catch[errors.DatabaseError](tb, func(err errors.DatabaseError) {
    fmt.Printf("数据库错误: %s on table %s\n", err.Operation, err.Table)
})

tb = gotrycatch.Catch[errors.NetworkError](tb, func(err errors.NetworkError) {
    if err.Timeout {
        fmt.Printf("网络超时: %s\n", err.URL)
    } else {
        fmt.Printf("网络错误 %d: %s\n", err.StatusCode, err.URL)
    }
})

tb = tb.CatchAny(func(err interface{}) {
    fmt.Printf("未知错误: %v\n", err)
})

tb.Finally(func() {
    fmt.Println("处理完成")
})

状态查询和调试

tb := gotrycatch.Try(func() {
    riskyOperation()
})

// 查询状态
if tb.HasError() {
    fmt.Printf("错误类型: %s\n", tb.GetErrorType())
    fmt.Printf("错误值: %v\n", tb.GetError())
}

if !tb.IsHandled() {
    // 根据错误类型决定处理方式
    switch tb.GetErrorType() {
    case "errors.ValidationError":
        // 处理验证错误
    default:
        tb = tb.CatchAny(func(err interface{}) {
            logUnknownError(err)
        })
    }
}

// 开启调试模式追踪类型匹配
gotrycatch.SetDebug(true)

结构化错误输出(Agent 友好)

tb := gotrycatch.Catch[errors.BusinessLogicError](tb, func(err errors.BusinessLogicError) {
    // JSON 输出便于日志和 Agent 解析
    jsonData, _ := err.ToJSON()
    log.Printf("ERROR: %s", string(jsonData))
    // 输出: {"type":"BusinessLogicError","rule":"inventory_check","details":"库存不足","file":"main.go","line":42,"function":"processOrder","timestamp":"2024-01-15T10:30:00Z","stack":[...]}
})

断言辅助函数

// 条件断言,false 时抛出错误
gotrycatch.Assert(value != "", errors.NewValidationError("value", "不能为空", 1001))

// 错误断言,有错误时包装并抛出
gotrycatch.AssertNoError(err, "数据库操作失败")

内置异常类型

所有错误类型都包含:FileLineFunctionTimestampStack

类型 专有字段 构造函数 用途
ValidationError Field, Message, Code NewValidationError(field, message, code) 数据验证错误
DatabaseError Operation, Table, Cause NewDatabaseError(operation, table, cause) 数据库操作错误
NetworkError URL, StatusCode, Timeout NewNetworkError(url, code) HTTP 错误
NetworkError URL, Timeout NewNetworkTimeoutError(url) 网络超时
BusinessLogicError Rule, Details NewBusinessLogicError(rule, details) 业务规则违规
ConfigError Key, Value, Reason NewConfigError(key, value, reason) 配置错误
AuthError Operation, User, Reason NewAuthError(operation, user, reason) 认证授权错误
RateLimitError Resource, Limit, Current, RetryAfter NewRateLimitError(resource, limit, current, retryAfter) 限流错误

错误方法

var err errors.ValidationError

err.Error()     // string - 完整错误描述(含位置信息)
err.ToMap()     // map[string]interface{} - 结构化数据
err.ToJSON()    // ([]byte, error) - JSON 输出
err.Unwrap()    // error - 底层错误(DatabaseError 返回 Cause)
err.Is(target)  // bool - 错误匹配

API 文档

核心函数

函数/方法 签名 说明
Try func Try(fn func()) *TryBlock 执行函数并捕获任何 panic
函数/方法 签名 说明
----------- ------ ------
Run func Run(fn func(), clauses ...Clause) error v2 惯用法子句式执行;未处理 panic 以 error 返回
Run1 func Run1[T any](fn func() T, clauses ...Clause) (T, error) v2 带返回值的 Run
On[E] func On[E any](handler func(E)) Clause v2 子句:精确类型匹配(Catch 语义)
OnAs[E] func OnAs[E error](handler func(E)) Clause v2 子句:errors.As 匹配,穿透 %w 包装
Any func Any(handler func(interface{})) Clause v2 子句:任意 panic 兜底
Cleanup func Cleanup(fn func()) Clause v2 子句:总是执行、LIFO、handler panic 也不跳过
Try func Try(fn func()) *TryBlock 执行函数并捕获 panic
Catch[T] func Catch[T any](tb *TryBlock, handler func(T)) *TryBlock 处理精确类型 T 的异常
CatchAs[E] func CatchAs[E error](tb *TryBlock, handler func(E)) *TryBlock v2 errors.As 匹配(穿透包装、E 或 *E)
CatchAny func (tb *TryBlock) CatchAny(handler func(interface{})) *TryBlock 处理任何未处理的异常
Finally func (tb *TryBlock) Finally(fn func()) 执行清理代码

TryBlock 状态查询

方法 返回类型 说明
HasError() bool 是否捕获了错误
GetError() interface{} 获取错误值
Err() error v2 panic 桥接为 error(支持 errors.Is/As;非 error panic 包装为 *PanicError
GetErrorType() string 获取错误类型名(如 "errtypes.ValidationError")
CanonicalErrorType() string v2 去指针短类型名(如 "ValidationError")
IsHandled() bool 错误是否已被处理
String() string 友好的字符串表示

TryWithResult

函数/方法 签名 说明
TryWithResult func TryWithResult[T any](fn func() T) *TryBlockWithResult[T] 执行带返回值的函数
CatchWithResult func CatchWithResult[T, E any](tb *TryBlockWithResult[T], handler func(E)) *TryBlockWithResult[T] TryWithResult 的类型捕获
CatchAnyWithResult func CatchAnyWithResult[T any](tb *TryBlockWithResult[T], handler func(interface{})) *TryBlockWithResult[T] TryWithResult 的任意捕获
GetResult() T 获取结果值
OnSuccess func (tb *TryBlockWithResult[T]) OnSuccess(fn func(T)) *TryBlockWithResult[T] 成功时回调
OnError func (tb *TryBlockWithResult[T]) OnError(fn func(interface{})) *TryBlockWithResult[T] 错误时回调
OrElse func (tb *TryBlockWithResult[T]) OrElse(defaultValue T) T 获取结果或默认值
OrElseGet func (tb *TryBlockWithResult[T]) OrElseGet(supplier func() T) T 获取结果或延迟计算默认值

调试与断言

函数 签名 说明
SetDebug func SetDebug(enabled bool) 开启/关闭调试日志
IsDebug func IsDebug() bool 查询调试模式状态
Throw func Throw(err interface{}) 抛出异常(panic)
Assert func Assert(condition bool, err interface{}) 条件断言,false 时抛出
AssertNoError func AssertNoError(err error, msg string) 错误断言,有错误时抛出

最佳实践

  1. 按特定性排序 Catch 块: 将最具体的异常类型放在前面,通用类型放在后面
  2. 总是使用 Finally: 确保资源清理代码被执行
  3. 使用预定义异常类型: 优先使用库提供的异常类型,而不是原始字符串或数字
  4. 使用 CatchAny 作为兜底: 优雅地处理未预期的错误类型
  5. 调试时开启调试模式: 当类型匹配不符合预期时,使用 SetDebug(true) 排查
  6. 日志使用 ToJSON: 结构化输出更易于解析和分析

性能考虑

使用 go test -bench . 实测(见 gotrycatch_bench_test.go;i9-13900HX,仅供参考):

场景 ns/op allocs/op
error 返回值(Go 惯用基线) ~0.2 0
原生 recover,无 panic ~2 0
Run,无 panic ~11 0
Run1,无 panic ~9 0
Try,无 panic ~63 1
TryWithResult + OrElse,无 panic ~66 1
Run + panic + On 命中 ~200 0
原生 recover,panic 路径 ~157 0
Try + panic + Catch 命中 ~469 1
Run 未处理 panic → error ~1385 2
  • Run 快乐路径零分配(不创建 TryBlock)——比 Try 更便宜
  • 未处理 panic 路径支付一次 *PanicError + 堆栈捕获(仅错误路径)
  • 适合应用级流程;热循环优先用 Run 而非 Try

兼容性

  • 需要 Go 1.21+(泛型;panic(nil) 检测语义)
  • 与标准库完全兼容

从 v1.x 迁移

  1. gotrycatch/errorsgotrycatch/errtypes(改名以避免与标准库 errors 冲突)。旧导入路径仍可编译——现在是纯别名层,行为完全一致(类型为别名;构造函数为直接绑定,File/Line/Stack 归因不变)。GetErrorType() 输出相应变化:errors.ValidationErrorerrtypes.ValidationError
  2. CatchWithReturn 已移除——用 TryWithResult + CatchWithResult 替代:
// v1
result, tb := gotrycatch.CatchWithReturn(tb, func(err string) interface{} { ... })

// v2
tb = gotrycatch.CatchWithResult[int, string](tb, func(err string) { ... })
result := tb.OrElse(defaultValue)
  1. 其余全部是增量:Try/Catch/CatchAny/Finally/TryWithResult 链式行为与 v1 完全一致。
  • 可以与现有的错误处理代码共存
  • 支持并发安全使用

常见问题 (FAQ)

Q: 为什么不能使用完全的链式调用?

A: 由于 Go 语言的限制,方法不能有泛型类型参数。因此不能写:

// ❌ 这样写是不支持的
tb := gotrycatch.Try(func() { ... }).Catch[ErrorType](handler)

只能使用函数式调用:

// ✅ 正确的写法
tb := gotrycatch.Try(func() { ... })
tb = gotrycatch.Catch[ErrorType](tb, handler)

但是 CatchAnyFinally 方法支持链式调用:

// ✅ 这样是可以的
tb.CatchAny(handler).Finally(cleanup)

Q: 如何调试类型匹配问题?

A: 开启调试模式:

gotrycatch.SetDebug(true)
// 输出: [gotrycatch] Catch: type errors.ValidationError does not match target type int
// 输出: [gotrycatch] Catch: type errors.ValidationError matched, calling handler

Q: 未处理的错误会怎样?

A: 未处理的错误会在 Finally 执行后重新抛出。如果不想让 panic 传播,请使用 CatchAny 作为兜底。

示例

查看 cmd/demo 目录获取详细示例,包括:

  • 基本用法演示
  • 多种异常类型处理
  • 结构化输出(ToMap/ToJSON)
  • TryWithResult 模式
  • 错误链(Unwrap/Is)
  • 真实场景多 Catch 示例

运行示例

# 演示程序(10个详细Demo)
go run ./cmd/demo

# 运行测试
go test -v ./...

# 查看覆盖率
go test -cover ./...

# 竞态检测
go test -race ./...

许可证

MIT License

About

A lib for using trycatch in Go!

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages