sql.ErrNoRows 是因误将 MaxMind GeoLite2-City.mmdb(非 SQLite)当数据库直连;正确做法是使用导出的 SQLite 文件,建表含整数型 ip_start/ip_end 字段,查询时转 IP 为 uint32 并用 BETWEEN 或显式不等式配合联合索引。

用 database/sql 读取 GeoIP 数据库时,为什么总是 sql.ErrNoRows?
不是数据没查到,而是表结构或查询条件和你手里的 GeoIP 数据库不匹配。常见于直接拿 MaxMind 的 GeoLite2-City.mmdb 当 SQLite 用——它根本不是关系型数据库,不能用 database/sql 直接打开。
真正能用 database/sql 读的,是你自己导出的 CSV 或生成的 SQLite 文件(比如把 IP 段转成 ip_start/ip_end 整数列)。否则一调 db.QueryRow 就报 sql.ErrNoRows,其实是驱动压根没连上有效表。
- 确认你的数据库文件是 SQLite3 格式:
file geoip.db输出含SQLite 3.x database - 建表必须带整数型 IP 范围字段,别存点分十进制字符串:
CREATE TABLE ip_location (ip_start INTEGER, ip_end INTEGER, country TEXT) - 查 IP 时先转成 uint32:
ip4 := net.ParseIP(ipStr).To4(); binary.BigEndian.Uint32(ip4),再塞进 WHERE 条件
查 IP 时用 BETWEEN 还是 ip_start = ??
效果一样,但后者更安全。SQLite 的 BETWEEN 是闭区间,看着简洁,可一旦字段类型是 TEXT(比如误存了 "192.168.1.1"),比较就变成字典序,"2" > "192" 这种错会静默发生。
用显式不等式 + 整数字段,还能让 SQLite 利用索引。只要给 (ip_start, ip_end) 建联合索引,查询速度能从秒级降到毫秒级。
立即学习“go语言免费学习笔记(深入)”;
- 建索引别漏掉顺序:
CREATE INDEX idx_ip_range ON ip_location (ip_start, ip_end) - WHERE 条件必须同时用上两个字段,只写
ip_start 索引会失效 - 别用
SELECT *,只取需要字段,减少内存拷贝,尤其当城市名字段很长时
为什么用 maxminddb 库比自己读 SQLite 更常见?
因为 MMDB 文件是专为 IP 查询设计的内存映射二叉树结构,查一个 IP 平均只要 2~3 次内存跳转,不用 IO、不占连接、无锁。而 SQLite 要开文件、解析页、走 B-tree,还可能被其他 goroutine 竞争。
如果你只是做简单查询,maxminddb 几行代码搞定:reader, _ := maxminddb.Open("GeoLite2-City.mmdb"); defer reader.Close(); reader.City(net.ParseIP("8.8.8.8"))。它返回的结构体里字段名和官网文档一致,不用自己映射。
- 注意:Go 的
maxminddb不支持自定义解码器,想省内存就别取Location.TimeZone这种大字段 - MMDB 文件必须从 MaxMind 官网下载或用
geoipupdate自动更新,别用网上来路不明的“精简版” - 如果非要 SQLite 方案,至少把 MMDB 用官方工具
mmdblookup导出再建库,别手动切 CSV
本地测试时 net.ParseIP 返回 nil 怎么办?
输入 IP 字符串带空格、换行、中文引号,或者用了 IPv6 地址却没加方括号(如 http://[::1]/api?ip=::1),net.ParseIP 都会静默失败。
最稳妥的做法是先 strings.TrimSpace,再检查长度是否在 7~15(IPv4)或符合 IPv6 格式(用 strings.Contains 看有没有冒号),最后才 parse。别依赖错误日志——HTTP handler 里没处理 panic,整个服务就挂了。
- 别用
fmt.Sprintf("%v", ip)打日志,IPv6 会被转成十六进制,看不出原始输入 - 测试用例至少覆盖:
" 1.2.3.4 "、"::1"、"127.0.0.1:8080"(端口要提前截掉) - 生产环境建议加个
if ip == nil { return fmt.Errorf("invalid ip: %q", rawIP) },别让错误穿透到 DB 层










