golang实现通过ip地址解析城市信息

 更新时间:2026年03月04日 08:55:00   作者:子玖  
这篇文章主要为大家详细介绍了如果使用golang实现通过ip地址解析城市信息,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

通过ip解析城市

环境:

  • Go 1.24.4+
  • ip2region 数据库文件

功能特性

  • 自动从 IP 解析城市信息
  • 支持 IPv4 和 IPv6 地址
  • 内网 IP 自动跳过(不报错)
  • 数据库文件缺失时优雅降级(服务可正常启动)
  • 请求中可手动指定 city,优先级高于 IP 解析

准备

下载ip2region数据库文件

将下载的 ip2region_v4.xdb 文件放到项目目录:

your-project/
├── data/
│   └── ip2region_v4.xdb    <-- 放在这里
├── config/
├── internal/
└── ...

配置说明

config/config.yaml:

ip:
  resolver:
    enabled: true              # 是否启用 IP 解析
    db_path: "data/ip2region_v4.xdb"  # 数据文件路径(服务器要在根目录下例如:/f服务器根目录/your-project/data/ip2region_v4.xdb)
    mode: "memory"             # 查询模式:memory(内存缓存) / file(文件读取)

核心组件设计

整体架构

┌─────────────────────────────────────────────────────────────┐
│                        API Layer                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  POST /api/v1/test(需要解析ip为city的接口)                      │   │
│  │  Handler: CreateLog                                 │   │
│  │  - 从 gin.Context 获取 IP                           │   │
│  │  - 调用 Service.CreateDeviceLog                     │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────┬───────────────────────────────────┘
                          │
┌─────────────────────────▼───────────────────────────────────┐
│                      Service Layer                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  UserDeviceLogService.CreateDeviceLog               │   │
│  │  - 接收 IP 字符串参数                               │   │
│  │  - 调用 IPResolver.Resolve(ip) 获取城市           │   │
│  │  - 组装数据,调用 Repository 保存                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                          │                                  │
│  ┌───────────────────────▼─────────────────────────────┐   │
│  │  IPResolver (internal/pkg/ip/resolver.go)           │   │
│  │  - 加载 ip2region.xdb 数据文件                      │   │
│  │  - 提供 Resolve(ip string) (city string, err error) │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────┬───────────────────────────────────┘
                          │
┌─────────────────────────▼───────────────────────────────────┐
│                    Repository Layer                         │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  UserDeviceLogRepository.Create                     │   │
│  │  - 保存设备日志到数据库                             │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

模块划分

internal/
├── infra/
│   └── ip/
│       ├── resolver.go                 # IP 解析器接口和实现
│       ├── util.go                     # IP 工具函数
│       └── provider.go                 # Wire Provider
└── modules/
    └── test/
        ├── api/v1/
        │   └── test.go      # Handler 层
        ├── service/
        │   └── test_service.go  # Service 层
        └── repository/
            └── test_repository.go # Repository 层

核心代码设计

调用时序图

infra/ip 模块

// internal\infra\ip\provider.go
package ip

import "github.com/google/wire"

// ProviderSet IP解析器依赖注入集合
var ProviderSet = wire.NewSet(
	NewResolver,
)

// internal\infra\ip\resolver.go

// Package ip 提供IP地址解析功能
package ip

import (
	"context"
	"net"
	"strings"

	"go-api/config"
	"go-api/pkg/logx"

	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
)

// Resolver IP解析器接口
type Resolver interface {
	// Resolve 根据IP地址解析城市信息
	// 返回空字符串表示无法解析(内网IP、IPv6、查询失败)
	Resolve(ctx context.Context, ip string) string

	// Close 关闭解析器,释放资源
	Close()
}

// ip2regionResolver 基于ip2region的实现
type ip2regionResolver struct {
	searcher *xdb.Searcher
}

// NewResolver 创建IP解析器
// 如果配置文件未启用或数据文件不存在,返回nil(不报错,服务可正常启动)
func NewResolver(cfg *config.Config) (Resolver, error) {
	if !cfg.IP.Resolver.Enabled {
		logx.Default.Info("IP解析器已禁用")
		return nil, nil
	}

	dbPath := cfg.IP.Resolver.DBPath
	if dbPath == "" {
		logx.Default.Warn("IP解析器数据文件路径未配置,IP解析功能将不可用")
		return nil, nil
	}

	// 从数据文件加载头部信息,获取版本
	header, err := xdb.LoadHeaderFromFile(dbPath)
	if err != nil {
		logx.Default.Warn("加载IP数据库头部失败,IP解析功能将不可用",
			"db_path", dbPath,
			"error", err,
		)
		return nil, nil
	}

	// 从头部获取版本信息
	version, err := xdb.VersionFromHeader(header)
	if err != nil {
		logx.Default.Warn("获取IP数据库版本失败,IP解析功能将不可用",
			"db_path", dbPath,
			"error", err,
		)
		return nil, nil
	}

	// 创建 searcher(使用文件模式,根据数据文件版本)
	searcher, err := xdb.NewWithFileOnly(version, dbPath)
	if err != nil {
		logx.Default.Warn("加载IP数据库失败,IP解析功能将不可用",
			"db_path", dbPath,
			"error", err,
		)
		return nil, nil
	}

	logx.Default.Info("IP解析器初始化成功",
		"db_path", dbPath,
		"mode", cfg.IP.Resolver.Mode,
		"version", version.Name,
	)

	return &ip2regionResolver{
		searcher: searcher,
	}, nil
}

// Resolve 解析IP获取城市信息
func (r *ip2regionResolver) Resolve(ctx context.Context, ip string) string {
	if r == nil || r.searcher == nil {
		return ""
	}

	// 清洗IP地址
	ip = cleanIP(ip)
	if ip == "" {
		return ""
	}

	// 检查是否为内网IP
	if isPrivateIP(ip) {
		logx.G(ctx).Debug("内网IP,跳过解析", "ip", ip)
		return ""
	}

	// 将IP转换为字节数组
	ipBytes, err := xdb.ParseIP(ip)
	if err != nil {
		logx.G(ctx).Warn("IP地址解析失败", "ip", ip, "error", err)
		return ""
	}

	// 查询ip2region
	region, err := r.searcher.Search(ipBytes)
	if err != nil {
		logx.G(ctx).Warn("IP解析失败", "ip", ip, "error", err)
		return ""
	}

	// 解析结果格式:国家|区域|省份|城市|ISP
	city := extractCity(region)
	logx.G(ctx).Debug("IP解析成功", "ip", ip, "city", city, "region", region)

	return city
}

// Close 关闭解析器
func (r *ip2regionResolver) Close() {
	if r != nil && r.searcher != nil {
		r.searcher.Close()
	}
}

// cleanIP 清洗IP地址
// - 去除端口信息 [::1]:8080 -> ::1, 192.168.1.1:8080 -> 192.168.1.1
// - 处理IPv6映射地址 ::ffff:192.168.1.1 -> 192.168.1.1
func cleanIP(ip string) string {
	if ip == "" {
		return ""
	}

	// 使用 net.SplitHostPort 分离 IP 和端口
	host, _, err := net.SplitHostPort(ip)
	if err == nil {
		// 成功分离,使用 host 部分
		ip = host
	}
	// 如果分离失败(没有端口),保持原样

	// 处理IPv6映射的IPv4地址 ::ffff:192.168.1.1
	if strings.HasPrefix(ip, "::ffff:") {
		ip = ip[7:]
	}

	return ip
}

// isPrivateIP 检查是否为内网IP
func isPrivateIP(ip string) bool {
	parsedIP := net.ParseIP(ip)
	if parsedIP == nil {
		return false
	}

	// 检查IPv4私有地址段
	privateRanges := []string{
		"10.0.0.0/8",     // 10.0.0.0 - 10.255.255.255
		"172.16.0.0/12",  // 172.16.0.0 - 172.31.255.255
		"192.168.0.0/16", // 192.168.0.0 - 192.168.255.255
		"127.0.0.0/8",    // 127.0.0.0 - 127.255.255.255
		"169.254.0.0/16", // 链路本地地址
	}

	for _, cidr := range privateRanges {
		_, ipNet, err := net.ParseCIDR(cidr)
		if err != nil {
			continue
		}
		if ipNet.Contains(parsedIP) {
			return true
		}
	}

	return false
}

// extractCity 从ip2region结果中提取省份
// 格式:国家|省份|城市|ISP|国家代码
// 示例:中国|广东省|深圳市|电信|CN
func extractCity(region string) string {
	logx.Default.Info("ip2region返回的region", "region", region)
	if region == "" {
		return ""
	}

	parts := strings.Split(region, "|")
	if len(parts) < 2 {
		return ""
	}

	// 省份是第2个字段(索引1)
	province := parts[1]

	return province
}

// internal\infra\ip\util.go

package ip

import (
	"strings"

	"github.com/gin-gonic/gin"
)

// GetClientIP 从gin.Context获取客户端真实IP
// 优先级:X-Forwarded-For → X-Real-IP → RemoteAddr
func GetClientIP(c *gin.Context) string {
	// 1. 尝试从 X-Forwarded-For 获取
	if xff := c.GetHeader("X-Forwarded-For"); xff != "" {
		if ip := parseXForwardedFor(xff); ip != "" {
			return ip
		}
	}

	// 2. 尝试从 X-Real-IP 获取
	if xri := c.GetHeader("X-Real-IP"); xri != "" {
		if ip := cleanIP(xri); ip != "" {
			return ip
		}
	}

	// 3. 从 RemoteAddr 获取
	if ip := cleanIP(c.Request.RemoteAddr); ip != "" {
		return ip
	}

	return ""
}

// parseXForwardedFor 解析X-Forwarded-For头
// 格式:client, proxy1, proxy2,取第一个有效IP
func parseXForwardedFor(header string) string {
	if header == "" {
		return ""
	}

	// 按逗号分割,取第一个IP
	parts := strings.Split(header, ",")
	for _, part := range parts {
		ip := strings.TrimSpace(part)
		if ip = cleanIP(ip); ip != "" {
			return ip
		}
	}

	return ""
}

handler和service中使用 获得ip

// api\test\v1\test_handler.go
package v1

import (
	"go-api/internal/infra/ip"
)

// 从gin.Context获取客户端真实IP
func (h *UserDeviceLogHandler) GetClientIP(c *gin.Context) string {
	// 获取客户端真实IP
	clientIP := ip.GetClientIP(c)
	resp, err := h.logService.CreateLog(c, userID, clientIP, &req)
	if err != nil {
		logx.G(c).Error("创建设备日志失败", "error", err)
		return nil
	}
	return resp
}
// internal\modules\test\service\test_service.go
func (s *userDeviceLogService) CreateLog(ctx context.Context, userID int64, clientIP string, req *v1.CreateUserDeviceLogReq) (*v1.UserDeviceLogResp, error) {

    // 解析IP获取城市
    city := s.ipResolver.Resolve(clientIP)
    if city == "" {
        // 如果解析失败,保持数据库字段为空
        city = nil
    }
    // 后面的处理我省略了
}

本地测试

  • 下载 ip2region_v4.xdb 文件到 data/ 目录
  • 确保配置 enabled: true
  • 启动服务:go run main.go
  • 使用 Postman 测试,注意:
    • 本地 ::1127.0.0.1 无法解析城市(内网 IP)
    • 需要公网 IP 才能解析出城市

到此这篇关于golang实现通过ip地址解析城市信息的文章就介绍到这了,更多相关golang解析ip地址内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Golang 类型转换的实现(断言、强制、显式类型)

    Golang 类型转换的实现(断言、强制、显式类型)

    将一个值从一种类型转换到另一种类型,便发生了类型转换,在go可以分为断言、强制、显式类型转换,本文就详细的介绍一下这就几种转换方式,具有一定的参考价值,感兴趣的可以了解一下
    2023-09-09
  • golang实现java uuid的序列化方法

    golang实现java uuid的序列化方法

    这篇文章主要介绍了golang实现java uuid的序列化方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • 如何在VScode 中编译多个Go文件

    如何在VScode 中编译多个Go文件

    这篇文章主要介绍了VScode 中编译多个Go文件的实现方法,本文通过实例图文并茂的形式给大家介绍的非常详细,需要的朋友可以参考下
    2021-08-08
  • golang中struct和[]byte的相互转换示例

    golang中struct和[]byte的相互转换示例

    这篇文章主要介绍了golang中struct和[]byte的相互转换示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-07-07
  • GoLang sync.Pool简介与用法

    GoLang sync.Pool简介与用法

    这篇文章主要介绍了GoLang sync.Pool简介与用法,Pool是可伸缩、并发安全的临时对象池,用来存放已经分配但暂时不用的临时对象,通过对象重用机制,缓解GC压力,提高程序性能
    2023-01-01
  • GO语言实现TCP服务器的示例代码

    GO语言实现TCP服务器的示例代码

    这篇文章主要为大家详细介绍了如何通过GO语言实现TCP服务器,文中的示例代码讲解详细,对我们深入了解Go语言有一定的帮助,需要的可以参考一下
    2023-03-03
  • golang优化目录遍历的实现方法

    golang优化目录遍历的实现方法

    对于go1.16的新变化,大家印象最深的可能是io包的大规模重构,但这个重构实际上还引进了一个优化,这篇文章要说的就是这个优化,所以本将给大家介绍golang是如何优化目录遍历的,需要的朋友可以参考下
    2024-08-08
  • Go语言中defer语句的用法

    Go语言中defer语句的用法

    这篇文章介绍了Go语言中defer语句的用法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • Go和RabbitMQ构建高效的消息队列系统

    Go和RabbitMQ构建高效的消息队列系统

    本文主要介绍了使用Go语言和RabbitMQ搭建一个简单的消息队列系统,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-01-01
  • Go语言编译程序从后台运行,不出现dos窗口的操作

    Go语言编译程序从后台运行,不出现dos窗口的操作

    这篇文章主要介绍了Go语言编译程序从后台运行,不出现dos窗口的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04

最新评论