Go 入门到精通-15-错误处理

目录

  • [🔵 Go 入门到精通:错误处理](#🔵 Go 入门到精通:错误处理)
    • [15.1 Go的错误处理哲学](#15.1 Go的错误处理哲学)
      • [15.1.1 核心理念](#15.1.1 核心理念)
      • [15.1.2 与其他语言的对比](#15.1.2 与其他语言的对比)
      • [15.1.3 为什么Go选择这种方式](#15.1.3 为什么Go选择这种方式)
    • [15.2 error 接口](#15.2 error 接口)
      • [15.2.1 error 的零值](#15.2.1 error 的零值)
      • [15.2.2 基本使用模式](#15.2.2 基本使用模式)
      • [15.2.3 标准模式](#15.2.3 标准模式)
    • [15.3 创建错误](#15.3 创建错误)
      • [15.3.1 errors.New](#15.3.1 errors.New)
      • [15.3.2 fmt.Errorf](#15.3.2 fmt.Errorf)
      • [15.3.3 错误创建方式对比](#15.3.3 错误创建方式对比)
    • [15.4 错误包装(Error Wrapping)](#15.4 错误包装(Error Wrapping))
      • [15.4.1 什么是错误包装](#15.4.1 什么是错误包装)
      • [15.4.2 多层包装](#15.4.2 多层包装)
      • [15.4.3 %w vs %v](#15.4.3 %w vs %v)
    • [15.5 errors.Is 解包判断](#15.5 errors.Is 解包判断)
      • [15.5.1 基本用法](#15.5.1 基本用法)
      • [15.5.2 工作原理](#15.5.2 工作原理)
      • [15.5.3 更多示例](#15.5.3 更多示例)
    • [15.6 errors.As 类型提取](#15.6 errors.As 类型提取)
    • [15.7 自定义错误类型](#15.7 自定义错误类型)
      • [15.7.1 基础自定义错误](#15.7.1 基础自定义错误)
      • [15.7.2 带分类的错误](#15.7.2 带分类的错误)
    • [15.8 Sentinel Error 模式](#15.8 Sentinel Error 模式)
      • [15.8.1 定义哨兵错误](#15.8.1 定义哨兵错误)
      • [15.8.2 使用哨兵错误](#15.8.2 使用哨兵错误)
      • [15.8.3 Sentinel Error 的优缺点](#15.8.3 Sentinel Error 的优缺点)
    • [15.9 panic 与 recover](#15.9 panic 与 recover)
      • [15.9.1 panic 是什么](#15.9.1 panic 是什么)
      • [15.9.2 何时使用 panic](#15.9.2 何时使用 panic)
      • [15.9.3 recover 是什么](#15.9.3 recover 是什么)
    • [15.10 defer 中的 recover](#15.10 defer 中的 recover)
      • [15.10.1 标准模式](#15.10.1 标准模式)
      • [15.10.2 在goroutine中recover](#15.10.2 在goroutine中recover)
      • [15.10.3 recover 的重要限制](#15.10.3 recover 的重要限制)
    • [15.11 错误处理最佳实践](#15.11 错误处理最佳实践)
      • [15.11.1 十大黄金法则](#15.11.1 十大黄金法则)
      • [15.11.2 更多法则](#15.11.2 更多法则)
      • [15.11.3 早返回 vs 深层嵌套](#15.11.3 早返回 vs 深层嵌套)
    • [15.12 综合案例:文件处理服务](#15.12 综合案例:文件处理服务)
    • [15.13 本章总结](#15.13 本章总结)

🔵 Go 入门到精通:错误处理

📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处

🧑‍💻 作者:布朗克168 | 系列:Go入门到精通 2026 · 第三阶段 · 核心进阶



15.1 Go的错误处理哲学

Go语言的错误处理方式与其他主流语言截然不同。它没有 try-catch ,也没有异常抛出机制(panic 并非异常替代品),而是将错误作为普通的返回值来处理。

15.1.1 核心理念

go 复制代码
// Go的错误处理:显式、透明
f, err := os.Open("file.txt")
if err != nil {
    // 处理错误
    return fmt.Errorf("打开文件失败: %w", err)
}
defer f.Close()

🎯 Go哲学:"错误是值(Errors are values)。" 错误不是异常,不是特殊情况------它是正常的、预期的返回值之一。

15.1.2 与其他语言的对比

特性 Go Java Python Rust
错误机制 返回值 try-catch异常 try-except异常 Result枚举
错误传递 显式逐层返回 异常冒泡 异常冒泡 ? 传播
性能开销 低(值传递) 高(栈展开) 高(栈展开) 低(枚举)
强制处理 ✅ 编译器不强制但lint可查 ❌ 不检查异常 ❌ 不检查异常 ✅ 编译器强制
可读性 显式,有时啰嗦 隐式,可能吞异常 隐式 显式+语法糖

15.1.3 为什么Go选择这种方式

复制代码
1. 显式 > 隐式:错误处理路径一目了然
2. 简单的错误处理不应比"正常"代码更复杂
3. 强制程序员面对每个可能的错误
4. 性能:异常机制在Go中代价太高(panic需要栈展开)

15.2 error 接口

Go的 error 是一个内置接口,简单得令人惊讶:

go 复制代码
// 来自 builtin 包
type error interface {
    Error() string
}

只有一个方法 Error() string 。任何实现了 Error() string 的类型都是一个错误。

15.2.1 error 的零值

go 复制代码
var err error
fmt.Println(err == nil) // true(零值是 nil)

15.2.2 基本使用模式

go 复制代码
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("除数不能为零")
    }
    return a / b, nil  // nil 表示没有错误
}

// 调用方必须检查错误
result, err := divide(10, 0)
if err != nil {
    fmt.Println("计算出错:", err)
    return
}
fmt.Println("结果:", result)

15.2.3 标准模式

go 复制代码
// Go代码中最常见的模式
func DoSomething() error {
    result, err := step1()
    if err != nil {
        return err
    }

    err = step2(result)
    if err != nil {
        return fmt.Errorf("step2失败: %w", err)
    }

    return nil
}

15.3 创建错误

Go提供了多种创建错误的方式,适用于不同场景。

15.3.1 errors.New

go 复制代码
import "errors"

// 创建简单错误
var ErrNotFound = errors.New("记录未找到")
var ErrTimeout = errors.New("操作超时")

注意errors.New 创建的错误是不可变的,适合作为包级别的哨兵错误(sentinel error)。

15.3.2 fmt.Errorf

go 复制代码
import "fmt"

// 格式化错误信息(支持参数)
err := fmt.Errorf("用户 %s 不存在 (ID: %d)", userName, userID)

// 带 %w 包裹另一个错误(Go 1.13+)
err := fmt.Errorf("查询用户失败: %w", originalErr)

15.3.3 错误创建方式对比

方式 适用场景 示例
errors.New("msg") 简单固定错误 ErrNotFound
fmt.Errorf("fmt %s", arg) 带动态信息的错误 参数化错误信息
fmt.Errorf("... %w", err) 包裹(wrap)底层错误 添加上下文+保留原错误
自定义错误类型 需要携带额外信息 错误码、分类等

15.4 错误包装(Error Wrapping)

Go 1.13 引入的错误包装机制是错误处理的重要演进。

15.4.1 什么是错误包装

go 复制代码
// 原始错误
err := sql.ErrNoRows

// 包装错误(添加更多上下文)
wrappedErr := fmt.Errorf("查询用户失败: %w", err)

fmt.Println(wrappedErr.Error())
// "查询用户失败: sql: no rows in result set"

关键%w 动词在 fmt.Errorf 中用来包装另一个错误,保留其"身份"。

15.4.2 多层包装

go 复制代码
// 错误链:底层 → 中层 → 顶层
err1 := errors.New("数据库连接被拒绝")              // 底层
err2 := fmt.Errorf("连接主库失败: %w", err1)       // 中层
err3 := fmt.Errorf("初始化服务失败: %w", err2)      // 顶层

fmt.Println(err3.Error())
// "初始化服务失败: 连接主库失败: 数据库连接被拒绝"

错误链示意图

复制代码
初始化服务失败  →  连接主库失败  →  数据库连接被拒绝
   (err3)            (err2)           (err1)
     ↑ 外层             ↑ 中层           ↑ 底层(根因)

15.4.3 %w vs %v

动词 作用 能否用 errors.Is/errors.As 解包
%w 包装错误,保留错误链 ✅ 可以
%v 仅格式化错误信息为字符串 ❌ 不可以(断链)
go 复制代码
// ✅ 用 %w:保留错误链
wrapped := fmt.Errorf("失败: %w", originalErr)
errors.Is(wrapped, originalErr)  // true

// ❌ 用 %v:断链
broken := fmt.Errorf("失败: %v", originalErr)
errors.Is(broken, originalErr)    // false!

15.5 errors.Is 解包判断

errors.Is 用于判断错误链中是否包含某个特定错误

15.5.1 基本用法

go 复制代码
import "errors"

// 哨兵错误
var ErrNotFound = errors.New("not found")

func queryUser(id int) error {
    return fmt.Errorf("查询用户 %d: %w", id, ErrNotFound)
}

func main() {
    err := queryUser(42)

    // ✅ errors.Is 沿着错误链查找
    if errors.Is(err, ErrNotFound) {
        fmt.Println("用户不存在")
    }

    // ❌ 不要直接用 == (只能判断最外层)
    if err == ErrNotFound {  // false!因为被包装了
        // 不会执行
    }
}

15.5.2 工作原理

复制代码
errors.Is(err, target) 沿着错误链逐个比对:
  1. err 自己 == target?
  2. err.Unwrap() == target?(如果 err 实现了 Unwrap())
  3. err.Unwrap().Unwrap() == target?
  4. ... 直到链结束或找到匹配

15.5.3 更多示例

go 复制代码
// 标准库的错误
_, err := os.Open("notexist.txt")
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("文件不存在,创建新文件")
}

// 对比多个错误
if errors.Is(err, io.EOF) {
    fmt.Println("读取完毕")
} else if errors.Is(err, os.ErrPermission) {
    fmt.Println("没有权限")
}

15.6 errors.As 类型提取

errors.As 用于从错误链中提取特定类型的错误

15.6.1 基本用法

go 复制代码
import "errors"

// 自定义错误类型
type ValidationError struct {
    Field string
    Value any
    Msg   string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("字段 %s 校验失败: %s", e.Field, e.Msg)
}

// 使用
func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field: "age",
            Value: age,
            Msg:   "年龄不在有效范围内",
        }
    }
    return nil
}

func process() error {
    err := validateAge(200)
    if err != nil {
        return fmt.Errorf("处理失败: %w", err)
    }
    return nil
}

func main() {
    err := process()

    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("校验失败!字段=%s, 值=%v, 原因=%s\n",
            valErr.Field, valErr.Value, valErr.Msg)
    }
}

15.6.2 errors.Is vs errors.As

维度 errors.Is errors.As
比较目标 具体值(sentinel error) 类型
参数 errors.Is(err, target) errors.As(err, &target)
用途 "是这个错误吗?" "是这个类型的错误吗?"
返回值 bool bool
提取信息 ❌ 只能判断 ✅ 可提取错误字段
示例 errors.Is(err, io.EOF) errors.As(err, &pathErr)

🔑 记忆口诀:"Is判断是不是这个错误,As提取是不是这种错误。"


15.7 自定义错误类型

当需要携带比字符串更多的错误信息时,自定义错误类型是最佳选择。

15.7.1 基础自定义错误

go 复制代码
// 带错误码的错误
type AppError struct {
    Code    int
    Message string
    Cause   error  // 底层原因
}

func (e *AppError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Cause)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// 实现 Unwrap 方法,支持 errors.Is / errors.As
func (e *AppError) Unwrap() error {
    return e.Cause
}

// 使用
func NewNotFoundError(msg string) *AppError {
    return &AppError{Code: 404, Message: msg}
}

15.7.2 带分类的错误

go 复制代码
type ErrorCategory int

const (
    CategoryValidation ErrorCategory = iota + 1  // 校验错误
    CategoryBusiness                               // 业务错误
    CategorySystem                                 // 系统错误
    CategoryExternal                               // 外部依赖错误
)

type CategorizedError struct {
    Category ErrorCategory
    Message  string
    Cause    error
}

func (e *CategorizedError) Error() string { return e.Message }
func (e *CategorizedError) Unwrap() error { return e.Cause }

// 根据分类做不同处理
func handleError(err error) {
    var catErr *CategorizedError
    if errors.As(err, &catErr) {
        switch catErr.Category {
        case CategoryValidation:
            fmt.Println("⚠️ 客户端输入有误")
        case CategorySystem:
            fmt.Println("🔴 系统故障,触发告警")
        case CategoryExternal:
            fmt.Println("🟡 外部服务异常,启用降级")
        }
    }
}

15.8 Sentinel Error 模式

**Sentinel Error(哨兵错误)**是预定义的、包级别的错误值,调用方可以用 errors.Is 来判断。

15.8.1 定义哨兵错误

go 复制代码
package userservice

import "errors"

// 包级别的哨兵错误
var (
    ErrNotFound      = errors.New("用户未找到")
    ErrAlreadyExists = errors.New("用户已存在")
    ErrInvalidEmail  = errors.New("邮箱格式无效")
    ErrUnauthorized  = errors.New("未授权操作")
)

15.8.2 使用哨兵错误

go 复制代码
// 服务层返回哨兵错误
func GetUser(id int) (*User, error) {
    user, err := db.QueryUser(id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, ErrNotFound  // 转换为业务哨兵错误
    }
    if err != nil {
        return nil, fmt.Errorf("查询用户: %w", err)
    }
    return user, nil
}

// 调用方判断
user, err := GetUser(42)
if errors.Is(err, ErrNotFound) {
    // 返回404
}

15.8.3 Sentinel Error 的优缺点

优点 缺点
简单直观 调用方产生对特定包的依赖
易于判断 无法携带动态信息
生态广泛使用 暴露实现细节
errors.Is 原生支持 错误信息固定

💡 建议 :哨兵错误适用于稳定的、为数不多的错误类型。动态信息用自定义错误类型。


15.9 panic 与 recover

panicrecover 是Go的"紧急机制",不是异常处理工具

15.9.1 panic 是什么

go 复制代码
func divide(a, b int) int {
    if b == 0 {
        panic("除数不能为零!")  // 程序崩溃
    }
    return a / b
}

// 调用会导致:
// panic: 除数不能为零!
// goroutine 1 [running]:
// main.divide(...)
// main.main()

panic 会:

  1. 立即停止当前函数执行
  2. 执行所有已注册的 defer
  3. 向调用栈上方传播
  4. 最终程序退出(如果没有 recover

15.9.2 何时使用 panic

✅ 可以使用 panic ❌ 不应用 panic
程序无法继续运行的致命错误 正常的业务错误
编程错误(如数组越界) 用户输入无效
初始化失败(init函数中) 文件不存在
不变量被破坏 网络超时
go 复制代码
// ✅ panic 的合理使用
func MustCompile(pattern string) *regexp.Regexp {
    re, err := regexp.Compile(pattern)
    if err != nil {
        panic(err)  // 编译时就应该发现的错误
    }
    return re
}

// ❌ panic 的滥用
func GetUser(id int) *User {
    user, err := db.Find(id)
    if err != nil {
        panic(err)  // 不要把业务错误变成 panic!
    }
    return user
}

15.9.3 recover 是什么

go 复制代码
func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获到 panic:", r)
        }
    }()

    panic("出错了!")
    fmt.Println("这行不会执行")
}

func main() {
    safeCall()
    fmt.Println("程序继续运行")  // ✅ 会执行
}

recover 只能在 defer 函数中生效,它截获 panic,让程序恢复运行。


15.10 defer 中的 recover

15.10.1 标准模式

go 复制代码
// HTTP服务器中防止单个请求panic导致整个服务崩溃
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("PANIC: %v\n%s", err, debug.Stack())
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

15.10.2 在goroutine中recover

go 复制代码
// ⚠️ 每个goroutine必须有自己的recover!
go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Println("goroutine panic:", r)
        }
    }()

    // 可能panic的代码
    riskyOperation()
}()

// ❌ 外层recover无法捕获goroutine中的panic
go func() {
    riskyOperation()  // panic会导致整个程序崩溃!
}()

15.10.3 recover 的重要限制

go 复制代码
// ❌ recover 不能在 defer 外面生效
func wrong() {
    r := recover()  // 永远返回 nil!
    fmt.Println(r)
}

// ❌ recover 不能跨goroutine
// ❌ recover 不能捕获 os.Exit 导致的退出
// ❌ recover 不能捕获 fatal error(如栈溢出)

15.11 错误处理最佳实践

15.11.1 十大黄金法则

1. 永远不要忽略错误

go 复制代码
// ❌ 忽略错误
json.Unmarshal(data, &v)

// ✅ 处理错误
if err := json.Unmarshal(data, &v); err != nil {
    return fmt.Errorf("解析JSON失败: %w", err)
}

2. 错误只处理一次

go 复制代码
// ❌ 打印后又返回------重复处理
if err != nil {
    log.Println("错误:", err)  // 处理了
    return err                 // 又传递了
}

// ✅ 要么处理(吞掉),要么传递(返回),二选一
if err != nil {
    return fmt.Errorf("操作失败: %w", err)  // 添加上下文后传递
}

3. 添加上下文信息

go 复制代码
// ❌ 仅返回底层错误
return err

// ✅ 添加有意义的上下文
return fmt.Errorf("用户 %d 查询订单失败: %w", userID, err)

4. 使用 %w 保持错误链

go 复制代码
return fmt.Errorf("读取配置: %w", err)   // ✅ 保留
return fmt.Errorf("读取配置: %v", err)   // ❌ 断链

5. 错误信息小写开头、无标点

go 复制代码
// ✅ Go惯例:小写开头,无句号
errors.New("用户未找到")
fmt.Errorf("id=%d: %w", id, err)

// ❌ 违反惯例
errors.New("User not found.")
errors.New("错误:用户未找到!")

15.11.2 更多法则

# 法则 说明
6 尽早返回错误 减少嵌套 if err == nil { ... }
7 定义哨兵错误 可预期的固定错误用 var ErrXxx
8 错误判断用 errors.Is/As 不用 == 判断包装后的错误
9 panic只用于不可恢复的错误 业务错误永远是 error 返回值
10 每个goroutine都要recover 一个goroutine的panic不会自动被另一个捕获

15.11.3 早返回 vs 深层嵌套

go 复制代码
// ❌ 深层嵌套
func process() error {
    f, err := os.Open("file.txt")
    if err == nil {
        defer f.Close()
        data, err := io.ReadAll(f)
        if err == nil {
            result, err := parse(data)
            if err == nil {
                return save(result)
            } else {
                return err
            }
        } else {
            return err
        }
    } else {
        return err
    }
}

// ✅ 早返回(Happy Path左对齐)
func process() error {
    f, err := os.Open("file.txt")
    if err != nil {
        return fmt.Errorf("打开文件失败: %w", err)
    }
    defer f.Close()

    data, err := io.ReadAll(f)
    if err != nil {
        return fmt.Errorf("读取文件失败: %w", err)
    }

    result, err := parse(data)
    if err != nil {
        return fmt.Errorf("解析失败: %w", err)
    }

    return save(result)
}

15.12 综合案例:文件处理服务

go 复制代码
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "os"
)

// ── 哨兵错误 ──
var (
    ErrFileNotFound = errors.New("文件未找到")
    ErrInvalidJSON  = errors.New("无效的JSON格式")
    ErrEmptyContent = errors.New("内容为空")
)

// ── 自定义错误类型 ──
type FileError struct {
    Op   string // 操作:open/read/parse
    Path string // 文件路径
    Err  error  // 底层错误
}

func (e *FileError) Error() string {
    return fmt.Sprintf("文件操作[%s] %s 失败: %v", e.Op, e.Path, e.Err)
}

func (e *FileError) Unwrap() error { return e.Err }

// ── 服务层 ──
type Config struct {
    Port    int    `json:"port"`
    Host    string `json:"host"`
    Debug   bool   `json:"debug"`
}

func LoadConfig(path string) (*Config, error) {
    // 1. 打开文件
    data, err := os.ReadFile(path)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            return nil, fmt.Errorf("%w: %s", ErrFileNotFound, path)
        }
        return nil, &FileError{Op: "read", Path: path, Err: err}
    }

    // 2. 验证非空
    if len(data) == 0 {
        return nil, ErrEmptyContent
    }

    // 3. 解析JSON
    var config Config
    if err := json.Unmarshal(data, &config); err != nil {
        return nil, fmt.Errorf("%w: %v", ErrInvalidJSON, err)
    }

    // 4. 业务校验
    if config.Port <= 0 || config.Port > 65535 {
        return nil, fmt.Errorf("端口号无效: %d", config.Port)
    }

    return &config, nil
}

// ── 调用方 ──
func main() {
    config, err := LoadConfig("config.json")
    if err != nil {
        // 分类处理
        switch {
        case errors.Is(err, ErrFileNotFound):
            fmt.Println("📁 配置文件不存在,使用默认配置")
            // 使用默认配置

        case errors.Is(err, ErrInvalidJSON):
            fmt.Println("📝 配置文件格式错误:", err)
            return

        case errors.Is(err, ErrEmptyContent):
            fmt.Println("📄 配置文件为空")
            return

        default:
            // 尝试提取文件错误详情
            var fileErr *FileError
            if errors.As(err, &fileErr) {
                fmt.Printf("🔴 文件操作异常 [%s]: %s\n", fileErr.Op, fileErr.Path)
            }
            fmt.Println("❌ 加载配置失败:", err)
            return
        }
    }

    fmt.Printf("✅ 配置加载成功: %+v\n", config)
}

15.13 本章总结

知识点 要点回顾
Go错误哲学 错误是值,显式处理,不依赖异常
error接口 Error() string 一个方法
创建错误 errors.Newfmt.Errorf(用 %w 包装)
错误包装 %w 保留错误链;%v 断链
errors.Is 沿链判断是否包含特定值(sentinel)
errors.As 沿链提取特定类型的错误
自定义错误 实现 Error() + Unwrap()
Sentinel Error 包级 var ErrXxx = errors.New(...)
panic/recover 紧急机制,不是异常处理;defer中recover
最佳实践 不忽略、不重复处理、添加上下文、早返回

💬 思考与互动 :你在项目中是如何组织错误处理的?用了自定义错误类型还是哨兵错误?遇到过 errors.Iserrors.As 的混淆吗?欢迎评论区分享你的Go错误处理心法!


本文是「Go入门到精通 2026」系列第15篇。上一篇:14-Go 入门到精通-包与模块管理 | 下一篇:敬请期待...

相关推荐
冯汉栩11 分钟前
Swift Control RollingCountdownView(动画倒计时)
开发语言·ios·swift
lisanmengmeng14 分钟前
搭建elk环境并接入frostmourne,实现监控报警效果(五)
开发语言·elk·日志·日志监控
Groundwork Explorer42 分钟前
W5500 网卡编程初始化与使用指南
python·单片机
oh,huoyuyan1 小时前
JS 动态网页抓不到?试试火车采集器网页抓取工具
开发语言·javascript·ecmascript
风流 少年1 小时前
hutool
java·服务器·开发语言
逸Y 仙X1 小时前
MCP(模型控制协议)完全指南:从核心概念到实战开发
python·大模型·llm·ai编程·mcp
一木 之林2 小时前
四、STL容器与数据结构
开发语言·数据结构·c++
QX_hao2 小时前
【Go】--Cobra-cli的用法
开发语言·后端·golang
艾醒(AiXing-w)2 小时前
LangChain 1.0 入门(三):稳定性双核心——重试机制+速率限速器参数详解与实战
开发语言·langchain·php
WILLF2 小时前
前端视角:Python requests vs JS axios
前端·python