maxminddb.Open 必须全局复用,仅在 init() 或启动时调用一次并赋值给包级变量,路径需为绝对路径或基于二进制目录拼接,shutdown 时统一 Close,文件权限设为 644。

maxminddb.Open 必须全局复用,别在 handler 里反复调
每次 maxminddb.Open() 都会打开文件句柄并建立 mmap 映射,高并发下很快触发 too many open files。微服务生命周期长,必须只初始化一次。
- 在
init()或启动时调用maxminddb.Open("GeoLite2-City.mmdb"),赋值给包级变量*maxminddb.Reader - 确保路径是绝对路径(如
/etc/geoip/GeoLite2-City.mmdb),或基于二进制所在目录拼接,避免 daemon 模式下工作目录不可控 - 务必调用
defer reader.Close()—— 不是在每个请求里,而是在服务 shutdown 阶段统一关闭 - 数据库文件权限要设为
644,否则非 root 用户进程可能因权限不足静默失败
真实客户端 IP 提取不能信 X-Forwarded-For
直接 r.Header.Get("X-Forwarded-For") 取第一个值等于把防火墙交给攻击者。微服务通常部署在 Nginx / ALB / Cloudflare 后,IP 提取逻辑必须和上游配置对齐。
- 若上游是 Nginx,且配置了
set_real_ip_from 10.0.0.0/8;和real_ip_recursive on;,则优先用r.RemoteAddr(它已被 Nginx 替换为真实源 IP) - 若走 Cloudflare,必须校验
CF-Connecting-IPheader,并确认该请求的 ASN 属于 Cloudflare(防止伪造) - 无论哪种方式,最终拿到字符串后,必须过一遍
net.ParseIP(),再用ip.IsGlobalUnicast()过滤掉私有地址、回环、文档地址等无效 IP - IPv6 地址如
::ffff:192.0.2.1要先ip.To16()统一格式,否则Lookup()可能返回空结果
结构体字段 tag 写错就查不到数据,不是库的问题
maxminddb 不自动映射字段,全靠 struct tag 显式声明路径。写错一个字母,比如把 "city" 写成 "City",字段就永远为空——没报错,但数据丢了。
- 国家代码字段路径是
Country.IsoCode,对应 tag:Country struct { IsoCode string `maxminddb:"iso_code"` } `maxminddb:"country"` - 城市中文名在
City.Names["zh"],不是"zh-CN"或"cn";英文名才是Names["en"] - 经纬度是
Location.Latitude和Location.Longitude,不是Lat/Lng或latitude/longitude - 只解你需要的字段:如果只要国家,就定义只含
Country的结构体;加City会让内存占用翻倍、查询变慢
地理围栏拦截要在中间件里做超时和 fallback
查库操作一旦卡住(比如磁盘故障、mmdb 文件损坏),整个请求就 hang 死。微服务不能因单个 IP 查询拖垮所有流量。
立即学习“go语言免费学习笔记(深入)”;
- 用
context.WithTimeout(ctx, 100*time.Millisecond)包裹reader.Lookup(),超时直接 fallback 到默认策略(如放行或拒绝) - 查不到结果(
err == maxminddb.ErrInvalidDatabase或err == maxminddb.ErrNotFound)时,别 panic,按业务规则处理:白名单优先?还是黑名单优先? - 拦截逻辑别写在业务 handler 里,统一抽成中间件,比如 Gin 的
func(c *gin.Context)或 net/http 的http.Handlerwrapper - 日志里至少记录 IP、国家码、是否命中围栏,但别打
record.City.Names["zh"]这种可能含敏感信息的字段
os.Rename() 替换,得用原子 swap + reader.Close() + 新 Open(),中间还要加锁防并发访问旧 reader。这事不做成自动化,迟早出线上事故。


















