优先选github.com/oschwald/geoip2-golang,因其自动处理names["zh"]映射、空切片及IPv4-mapped IPv6转换;maxminddb-golang需手动解析易出错,且中文名键实为"zh"非"zh-CN"。

选错库直接查不到中文名
用 github.com/oschwald/geoip2-golang,别碰 maxminddb-golang 做业务查询。前者自动展开 names["zh"]、处理空切片和嵌套结构;后者只返回 raw map,你得自己 decode struct、手动判空、硬写 Names["zh-CN"] 还容易错——实际键是 "zh",不是 "zh-CN",查出来永远 nil。
安装命令必须是:go get github.com/oschwald/geoip2-golang(不是 fork 或旧版 maxmind/geoip-api-golang)
常见错误现象:
-
record.Country.Names["zh-CN"]返回nil,但record.Country.Names["zh"]才有值 -
record.Subdivisions是空切片,直接取[0]panic - 没调
ip.To4()处理:::ffff:192.0.2.1这类 IPv4-mapped IPv6,查库返回空结构体
真实客户端 IP 怎么取才不全是“未知”
微服务跑在 Nginx / Traefik / ALB 后面时,r.RemoteAddr 是代理地址,不是用户真实出口 IP。硬解析它,线上 99% 请求都定位成内网段或回环地址。
立即学习“go语言免费学习笔记(深入)”;
实操建议按顺序取值并校验:
- 先读
X-Real-IP头,存在且net.ParseIP().IsGlobalUnicast()为 true 则直接用 - 否则取
X-Forwarded-For,按逗号分割后从左到右遍历,对每个ipStr调用net.ParseIP(strings.TrimSpace(ipStr))→ 检查ip != nil && ip.IsGlobalUnicast() - 最后 fallback 到
strings.Split(r.RemoteAddr, ":")[0],再做一次IsGlobalUnicast()
特别注意:IPv4-mapped IPv6 必须先 ip.To4() 再传给 db.City(),否则匹配失败。
数据库加载失败的三个典型原因
maxminddb.Open 报 invalid database,大概率不是代码问题,而是:
- 下载的是
.tar.gz压缩包,没解压就直接指向目录或压缩包路径——必须解压,且路径指向GeoLite2-City.mmdb文件本身(不是GeoLite2-City_20240401/GeoLite2-City.mmdb这种嵌套路径) - Docker 容器里文件不可读:host 上 root 写的文件,容器以非 root 用户运行,静默失败或报
permission denied - 文件损坏:用
file GeoLite2-City.mmdb检查输出是否含MaxMind DB字样
修复建议:
- Dockerfile 加
RUN chmod 644 /app/GeoLite2-City.mmdb - 全局复用一个
*geoip2.Reader实例(它线程安全),别每次请求都Open+Close
City 和 Country 查询性能差一倍
如果只是做国家级路由(语言自动切换、区域限流、内容灰度),用 db.Country(ip) 就够了。它比 db.City(ip) 快 30%~50%,内存占用低一半,对应 .mmdb 文件体积更小——在容器内存受限或 P99 延迟敏感场景下,这点差异会直接暴露。
另外,db.City(ip) 返回的 record.Country.IsoCode 为空不是 bug,是数据精度问题:City 数据库对某些 IP(如运营商 NAT 地址、新分配 IPv6)只定位到国家或更粗粒度,City、Subdivisions、Location 都可能为 nil。
别在灰度开关逻辑里无脑查 City;也别假设所有字段一定有值——加 if record.Country != nil 和 if len(record.Subdivisions) > 0 判空才是常态。


















