应使用 maxminddb + geolite2 离线数据库:官方免费 api 限流严(15 qps/1000 日)、需鉴权,易返回 429/401;而 geolite2-city.mmdb 可免费下载、本地加载、毫秒级查询。

Go 语言里查 IP 归属,别直接写正则或 HTTP 请求调第三方 API——用 maxminddb + 官方 GeoLite2 数据库才是稳定、离线、高性能的正解。
为什么不能用 HTTP 调 MaxMind 免费 API?
MaxMind 官方免费 Web API(geoip.maxmind.com/geoip/v2.1/city/{ip})有严格限流(每秒 15 次、每天 1000 次),且需注册账号、加请求头 X-Request-ID 和授权 token。生产环境一压就崩,调试时还常返回 429 Too Many Requests 或 401 Unauthorized。
真正靠谱的做法是:下载 GeoLite2-City.mmdb 二进制数据库文件,本地加载,用 Go 原生解析。
- 数据库每月更新一次,官网注册邮箱即可免费下载(无需付费订阅)
- Go 生态已有成熟封装:
github.com/oschwald/maxminddb-golang - 单次查询耗时通常
如何正确加载并查询 GeoLite2-City.mmdb?
关键不是“能不能读”,而是“读得对不对”——常见错误是忽略数据库结构嵌套和字段空值。
MaxMind 的 .mmdb 是树状结构,Go 解析后返回的是 map[string]interface{} 或自定义 struct。直接用 result["country"]["names"]["zh-CN"] 会 panic:字段可能不存在,或 names 是 nil,或 zh-CN 根本没收录。
实操建议:
- 用
maxminddb.Open()加载文件,检查返回 error;不要用os.Open+ 手动解析 - 查询必须用
reader.City(ip)(查城市)或reader.Country(ip)(查国家),不能用reader.Lookup(ip)—— 后者返回原始 map,字段路径不统一 - 所有字段访问前必须做类型断言和非空判断,例如:
if city, ok := result.City; ok && city.Names != nil - 中文名字段推荐 fallback:优先取
city.Names["zh-CN"],不存在则取city.Names["en"]
示例片段:
db, err := maxminddb.Open("GeoLite2-City.mmdb")
if err != nil {
log.Fatal(err)
}
defer db.Close()
ip := net.ParseIP("202.102.110.200")
var record maxminddb.City
err = db.City(ip, &record)
if err != nil {
// 注意:不是所有 IP 都有 City 记录(如某些 ASN 或私有地址)
log.Printf("no city record for %v: %v", ip, err)
return
}
if name, ok := record.City.Names["zh-CN"]; ok && name != "" {
fmt.Println("城市:", name)
}
常见报错和兼容性陷阱
实际部署时最常卡在三个地方:路径、版本、结构体字段。
-
open GeoLite2-City.mmdb: no such file or directory:确保路径是运行时工作目录相对路径,或用filepath.Abs()转绝对路径;Docker 中注意 COPY 文件到容器内正确位置 -
invalid database format:下载的是.tar.gz,必须解压出.mmdb文件,不能直接传压缩包给Open() -
panic: interface conversion: interface {} is nil, not string:没判空就直接取record.Country.IsoCode,实际该字段在部分 IP(如局域网地址)下为 nil - GeoLite2-Country.mmdb 和 GeoLite2-City.mmdb 结构不同:前者没有
City字段,后者才有;混用会导致 struct 解析失败
另外注意:Go 1.16+ 默认启用 GO111MODULE=on,go get github.com/oschwald/maxminddb-golang 会拉取最新版(v1.10+),它已支持 Go 1.21,但若项目锁了旧版(如 v1.7),可能无法解析新版 mmdb 格式(2023 年后发布的库)。
要不要自己缓存查询结果?
不需要为单个 IP 做 LRU 缓存——maxminddb-golang 内部已对数据库内存映射做了优化,重复查同一 IP 不会显著变快;但如果你高频查少量固定 IP(比如 CDN 回源地址池),可以建 map[string]City 做应用层缓存。
更值得做的是:预热常用 IP 段。例如把国内主要 ISP 的网段(如 202.96.0.0/16)提前查一遍,触发 mmap 页面加载,避免首查延迟毛刺。
真正容易被忽略的一点:GeoLite2 数据库不含精确经纬度(只给城市中心粗略坐标),也不含运营商信息(ASN 需用 GeoLite2-ASN.mmdb 单独查)。一个 IP 要拿全量归属,得同时打开两个 .mmdb 文件,分别查 City 和 ASN,再 merge 结果——这步多数人直接漏掉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











