Skip to content

🗃 数据模型

pkg/ipapi/models.goIPInfoAPIError 结构体及辅助方法。

IPInfo

完整的 IP 地理信息结构体,JSON 解码目标:

go
type IPInfo struct {
	IP                 string    `json:"ip"`
	Network            string    `json:"network"`
	Version            string    `json:"version"`
	City               string    `json:"city"`
	Region             string    `json:"region"`
	RegionCode         string    `json:"region_code"`
	Country            string    `json:"country"`
	CountryName        string    `json:"country_name"`
	CountryCode        string    `json:"country_code"`
	CountryCodeISO3    string    `json:"country_code_iso3"`
	CountryCapital     string    `json:"country_capital"`
	CountryTLD         string    `json:"country_tld"`
	ContinentCode      string    `json:"continent_code"`
	InEU               bool      `json:"in_eu"`
	Postal             *string   `json:"postal"`
	Latitude           float64   `json:"latitude"`
	Longitude          float64   `json:"longitude"`
	LatLong            string    `json:"latlong"`
	Timezone           string    `json:"timezone"`
	UTCOffset          string    `json:"utc_offset"`
	CountryCallingCode string    `json:"country_calling_code"`
	Currency           string    `json:"currency"`
	CurrencyName       string    `json:"currency_name"`
	Languages          string    `json:"languages"`
	CountryArea        float64   `json:"country_area"`
	CountryPopulation  int       `json:"country_population"`
	ASN                string    `json:"asn"`
	Org                string    `json:"org"`
	Hostname           string    `json:"hostname,omitempty"`
	RetrievedAt        time.Time `json:"-"`
}

字段含义按类别见:

🎨 一图抵千言

IPInfo 共 28 个字段,按语义分 8 组。下图展示分组归属与字段类型。

🌐 视角二:Format 与结构体的类型关系

这张图展示 5 种 Format 如何映射到不同的解码目标——json/jsonp/xml/yaml 解码进 IPInfoAPIError,而 csv 是纯文本无法解码。

🧭 视角三:从 APIError 到哨兵错误的决策树

这张图展示 *Client 内部 mapStatusCodeToErrorAPIError 的优先级:先看 HTTP 状态码(4xx 不重试、5xx 可重试),再看响应体 error=true,最后落到具体哨兵错误。

📐 三张图的分工
视角回答的问题
上方 classDiagram结构IPInfo 28 字段怎么分组?类型是什么?
视角二 flowchart-LR类型5 种 Format 各解码到哪个结构体?
视角三 flowchart-TD行为一个响应如何决定成正常结果还是某个哨兵错误?能否重试?

关键设计

  • Postal *string:用指针,因为部分国家无邮政编码,需区分「空」与「无」。
  • RetrievedAt time.Timejson:"-",不参与序列化,由 SDK 填入查询时刻。
  • Hostnameomitempty,可选 add-on 字段。

⚠️ Postal 必须用 GetPostal 访问

Postal*string,直接 .Postal 解引用在 nil 时会 panic。务必用 GetPostal 安全访问,或先判 nil。

go
// ❌ 危险:nil 时 panic
fmt.Println(*info.Postal)

// ✅ 安全
fmt.Println(info.GetPostal())
字段类型特殊标记设计原因
Postal*string区分「空字符串」与「无邮政编码」
RetrievedAttime.Timejson:"-"SDK 填查询时刻,不参与序列化
Hostnamestringomitempty可选 add-on,无值则省略
InEUbool布尔直接零值即「不在 EU」,语义明确

IPInfo 方法

ParseLatLong

go
func (info *IPInfo) ParseLatLong() (float64, float64, error)

解析 LatLong 字符串 "lat,lon" 为两个 float64

go
lat, lon, err := info.ParseLatLong()
// lat=37.4056, lon=-122.0775

详见 坐标字段 / 示例

GetPostal

go
func (info *IPInfo) GetPostal() string

安全获取 Postalnil 时返回空字符串,避免空指针。

go
fmt.Println(info.GetPostal()) // 不用判 nil

APIError

服务端错误结构体:

go
type APIError struct {
	HasError bool   `json:"error"`
	Reason   string `json:"reason"`
	Message  string `json:"message"`
	IP       string `json:"ip"`
	Reserved bool   `json:"reserved"`
	Version  string `json:"version"`
}

Error 方法

go
func (e *APIError) Error() string

实现 error 接口,区分保留 IP:

go
// 普通错误
"ipapi error: <Message> (reason: <Reason>)"
// 保留 IP
"ipapi error: <Message> (reason: <Reason>, ip: <IP>, reserved: true)"
🔍 APIError 字段速查
字段含义示例
HasError是否有错true
Reason错误原因短码"RateLimited"
Message人类可读说明"You exceeded the limit..."
IP出错的 IP"127.0.0.1"
Reserved是否保留 IPtrue
VersionAPI 版本"1.0"

Reserved=trueError() 输出会附带 ipreserved 字段,便于排查保留 IP(如 127.0.0.110.x)的查询。

ToError

go
func (e *APIError) ToError() error

返回 e 自身(保留兼容性)。

ValidateIP

go
func ValidateIP(ip string) error

net.ParseIP 校验 IP 格式,非法返回 ErrInvalidIP。详见 ValidateIP 文档

下一步

基于 MIT 许可证发布