Skip to content

🛡 错误类型

pkg/ipapi/errors.go + client.go 中的哨兵错误。

哨兵错误值

定义在 client.go

错误消息触发场景
ErrInvalidIPinvalid IP addressIP 格式非法
ErrInvalidFieldinvalid field name字段名不在白名单
ErrInvalidFormatinvalid response format格式非 5 种之一
ErrRateLimitedAPI rate limit exceeded触发 429 / 限流
ErrReservedIPreserved IP address私有/回环等保留地址
ErrNotFoundresource not found404
ErrServerErrorserver error5xx / 400
ErrUnexpectedDataunexpected response dataJSON 解码失败
ErrMethodNotAllowedmethod not allowed405
ErrInvalidKeyinvalid API key403 / Invalid Key

错误处理函数

handleError

go
func (c *Client) handleError(err error) error

统一错误出口:

  1. 若设了 errorHandler,先调它
  2. 否则 errors.As 解包 APIError,按 Reason 映射哨兵

Reason → 哨兵映射:

APIError.Reason哨兵
"RateLimited"ErrRateLimited
"Reserved IP Address"ErrReservedIP
"Invalid IP Address"ErrInvalidIP
"Invalid Key"ErrInvalidKey

IsRetryableError

go
func IsRetryableError(err error) bool

判断错误是否值得重试,返回 true 的:

  • ErrRateLimited
  • ErrServerError
  • ErrNotFound

重试决策表

哨兵错误可重试原因
ErrInvalidIP客户端输入错误,重试无意义
ErrInvalidField字段名固定,重试不变
ErrInvalidFormat客户端格式错误
ErrRateLimited限流是临时的,稍后可能恢复
ErrReservedIPIP 性质固定
ErrNotFound资源可能瞬时缺失
ErrServerError5xx 可能是临时故障
ErrUnexpectedData响应体结构问题
ErrMethodNotAllowedHTTP 方法固定
ErrInvalidKey鉴权问题,重试无用

⚠️ 429 不重试

虽然 ErrRateLimited 满足 IsRetryableError=true,但 SDK 内部重试逻辑不重试 4xx(含 429)IsRetryableError 是给调用方判断是否值得自己重试的语义提示,与 SDK 内部重试策略是两套独立机制。

详见 IsRetryableError

WrapError

go
func WrapError(op string, err error) error

给错误加操作名,保留 %w 链:

go
err := ipapi.WrapError("lookup", originalErr)
// "lookup failed: ..."

详见 WrapError

状态码映射

mapStatusCodeToError(在 api.go):

HTTP错误
400ErrServerError
403ErrInvalidKey
404ErrNotFound
405ErrMethodNotAllowed
429ErrRateLimited
500ErrServerError

🎨 一图抵千言

10 个哨兵错误的生命周期与可重试性流转。

💡 优先级

当响应体含 APIErrorHasError=true)时,doRequest 直接返回它,handleError 再按 Reason 映射。状态码映射仅在没有可用 APIError 时兜底。

用法示例

go
info, err := client.GetIPInfo(ctx, "invalid.ip", "json")
if err != nil {
	switch {
	case errors.Is(err, ipapi.ErrInvalidIP):
		fmt.Println("→ 无效 IP")
	case errors.Is(err, ipapi.ErrReservedIP):
		fmt.Println("→ 保留地址")
	case errors.Is(err, ipapi.ErrRateLimited):
		time.Sleep(time.Minute)
	default:
		log.Println(err)
	}
}

💡 errors.Is 而非 ==

哨兵错误可能被 WrapError 包裹,直接 == 比较会漏判。务必用 errors.Is(err, ipapi.ErrXxx) 顺着 %w 链查找,兼容包装后的错误。

🔍 错误处理决策树

按错误来源分流处理:

  • 客户端错误ErrInvalidIP/ErrInvalidField/ErrInvalidFormat):修正输入,不重试
  • 鉴权错误ErrInvalidKey):检查 Key 配置/额度
  • 限流错误ErrRateLimited):退避后重试,考虑加缓存
  • 服务端错误ErrServerError):指数退避重试,告警
  • 资源错误ErrNotFound):核对 IP/字段是否存在
  • 数据错误ErrUnexpectedData):报告解析异常,可能版本不兼容

取错误细节

go
var apiErr *ipapi.APIError
if errors.As(err, &apiErr) {
	log.Printf("reason=%s ip=%s reserved=%v msg=%s",
		apiErr.Reason, apiErr.IP, apiErr.Reserved, apiErr.Message)
}

下一步

基于 MIT 许可证发布