Skip to content

✅ 变量详解:validFields

🏷️ 类别:变量(包级 var) · 🔠 符号:ipapi.validFields · 📦 所在文件:api.go

本页详解 SDK 内部用于字段名白名单校验的 validFields 变量,涵盖其定义、设计意图、用法示例与相关 API。


📐 定义

validFields 是定义在 pkg/ipapi/api.go 顶部的包级私有变量,用 map[string]struct{} 实现一个集合(set):

go
package ipapi

var validFields = map[string]struct{}{
	"ip": {}, "network": {}, "version": {}, "city": {}, "region": {},
	"region_code": {}, "country": {}, "country_name": {}, "country_code": {},
	"country_code_iso3": {}, "country_capital": {}, "country_tld": {},
	"continent_code": {}, "in_eu": {}, "postal": {},
	"latitude": {}, "longitude": {}, "latlong": {}, "timezone": {},
	"utc_offset": {}, "languages": {}, "country_calling_code": {},
	"currency": {}, "currency_name": {}, "country_area": {},
	"country_population": {}, "asn": {}, "org": {}, "hostname": {},
}
属性
🔖 符号ipapi.validFields(小写开头,未导出
📦 所在文件api.go(第 15–24 行)
🧩 类型map[string]struct{}
📊 条目数28 个合法字段名
🔎 查找复杂度O(1)(基于哈希表的 map 查找)
🌐 可见性包内可见,外部无法直接访问

💡 小知识: Go 没有“集合(set)”这种内建类型。用 map[string]struct{} 是社区公认的轻量做法 —— struct零宽度类型,不占额外内存,仅用 map 的 key 来表达“存在 / 不存在”。


📖 说明

🎯 它的作用是什么?

validFields 是 ipapi.co 单字段查询接口所接受的字段名白名单。当调用 GetFieldGetClientField 时,SDK 会先用这张表校验传入的 field 参数:

  • ✅ 字段在表中 → 继续构造并发出 HTTP 请求。
  • ❌ 字段不在表中 → 立即在本地返回 ErrInvalidField,请求根本不会发往 ipapi.co 服务端。

这是典型的**“快速失败”(fail fast)**设计:把无效参数挡在客户端,避免一次毫无意义的网络往返,也避免把无效字段名拼进 URL。

🎨 一图抵千言

下方流程图直观展示 GetField 的校验分支:命中 validFields 才会构造并发送 HTTP 请求,否则本地立即返回 ErrInvalidField,请求根本不会发往 ipapi.co。

🧩 设计要点

要点说明
🚫 未导出小写 validFields 仅包内可用,外部使用者无法直接读取或篡改这张表,白名单的边界由 SDK 自己掌控。
🧪 与结构体对齐表中的 28 个 key 与 IPInfo 结构体的 JSON tag 一一对应RetrievedAt 除外,它带 json:"-" 不会出现在响应里)。SDK 用 TestValidFields_Completeness 这条测试守护这种对齐关系。
O(1) 校验_, ok := validFields[field] 一次 map 查找即可判定合法性,开销极低。
🧱 零值即空struct{}{} 作为 value 不携带任何信息,仅靠 key 的存在与否表达语义。

🧪 白名单的 28 个字段一览

下表列出全部 28 个合法字段,与 IPInfo 的对应关系一目了然:

#字段名所属分组#字段名所属分组
1ip网络15postal地理
2network网络16latitude坐标
3version网络17longitude坐标
4city地理18latlong坐标
5region地理19timezone时区
6region_code地理20utc_offset时区
7country国家21languages语言
8country_name国家22country_calling_code电话
9country_code国家23currency货币
10country_code_iso3国家24currency_name货币
11country_capital国家25country_area统计
12country_tld国家26country_population统计
13continent_code27asnASN
14in_eu欧盟28org / hostnameASN

📌 关于 hostname:它是否返回取决于服务端配置与 IP 类型,详见 ASN / org / hostname 字段

🚫 为什么不直接发请求让服务端校验?

  • 💸 省一次往返:无效字段名是客户端拼写错误,没必要消耗一次网络请求。
  • 🛡️ 避免脏 URL:未校验直接拼接到 URL(如 https://ipapi.co/任意字符串/)可能得到语义不符的 200 响应,反而更难排查。
  • 🎯 错误更早、更明确:本地立刻抛出 ErrInvalidField,调用方可在第一时间定位是字段名写错,而不是去解析服务端返回的错误体。

🛠 用法 / 示例

validFields 本身未导出,使用者无需、也不应直接访问它。正确做法是通过 GetField / GetClientField 间接受益于这张白名单。下面给出真实可运行的 Go 代码。

✅ 合法字段 —— 正常查询

go
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/cyberspacesec/ipapi.co-skills/pkg/ipapi"
)

func main() {
	client, err := ipapi.NewClient()
	if err != nil {
		log.Fatalf("创建客户端失败: %v", err)
	}

	ctx := context.Background()

	// "city" 在 validFields 白名单内,请求会正常发出
	city, err := client.GetField(ctx, "8.8.8.8", "city")
	if err != nil {
		log.Fatalf("查询 city 失败: %v", err)
	}
	fmt.Printf("8.8.8.8 所在城市: %s\n", city)

	// "asn" 同样合法,且对应 GetClientField 查询本机出口 IP 的 ASN
	asn, err := client.GetClientField(ctx, "asn")
	if err != nil {
		log.Fatalf("查询本机 asn 失败: %v", err)
	}
	fmt.Printf("本机出口 ASN: %s\n", asn)
}

预期输出(示例值):

text
8.8.8.8 所在城市: Mountain View
本机出口 ASN: AS15169

❌ 非法字段 —— 本地立即失败

go
// "foo_bar" 不在 validFields 中,请求不会发往服务端
val, err := client.GetField(ctx, "8.8.8.8", "foo_bar")
if err != nil {
	// err 是 errors.Is(err, ipapi.ErrInvalidField) == true 的包装错误
	fmt.Println("被白名单拦截:", err)
	// 输出:被白名单拦截: invalid field name
	return
}
_ = val

🧪 自己枚举全部合法字段(用于批量校验)

如果你需要在外部维护一份与 SDK 对齐的字段清单(例如生成报表、做表单校验),推荐直接以 IPInfo 的 JSON tag 为准,而非去猜白名单内容。下面是一个与 validFields 完全等价的字段清单:

go
// 与 pkg/ipapi/api.go 中的 validFields 一一对应
var myFieldList = []string{
	"ip", "network", "version", "city", "region", "region_code",
	"country", "country_name", "country_code", "country_code_iso3",
	"country_capital", "country_tld", "continent_code", "in_eu",
	"postal", "latitude", "longitude", "latlong", "timezone",
	"utc_offset", "languages", "country_calling_code",
	"currency", "currency_name", "country_area",
	"country_population", "asn", "org", "hostname",
}

func isValidField(field string) bool {
	for _, f := range myFieldList {
		if f == field {
			return true
		}
	}
	return false
}

⚠️ 维护提示: 这份清单是“复制一份”的快照。SDK 自身用 TestValidFields_Completeness 守护它与 IPInfo 结构体的一致性 —— 任何新增字段都会让测试失败。因此升级 SDK 后,请同步更新你本地的 myFieldList


🔗 相关


➡️ 下一步

  • 📖 阅读 字段查询 Field 概念,理解单字段查询在整体设计中的位置。
  • 🚀 跑通 单字段查询示例,在真实代码里体验白名单校验。
  • 🛡️ 深入 错误处理概念,了解 ErrInvalidField 与其他哨兵错误的协作方式。
  • 🧪 查阅 api_test.go 中的 TestValidFields_Completeness,看 SDK 如何用测试守护白名单与结构体的对齐。

基于 MIT 许可证发布