目录

Go 实现 IP 地址与十进制互转:IPv4/IPv6 位运算与 net 包两种方案详解

Go 实现 IP 地址与十进制互转:位运算与 net 包两种方案

做 Go 网络开发时,你大概率遇到过这种场景:线上日志里的 IP 字段突然变成一串纯数字(比如 3232235777),排查问题时需要手动还原成 192.168.1.1 这样的可读格式;又或者反过来,某些存储方案要求把 IP 地址压缩成数字存进数据库,查询时再转回来。

这类需求的本质其实很简单——IP 地址和十进制数字之间的相互转换。这篇文章会从底层原理讲起,再用 Go 给出两种完整的实现方案(位运算 + 标准库),覆盖 IPv4 和 IPv6 两种协议,所有代码都可以直接复制运行。

转换原理:IP 地址的数字本质

在动手写代码之前,先搞清楚一个前提:IP 地址的本质就是一个二进制整数

IPv4 是一个 32 位的二进制数,IPv6 是一个 128 位的二进制数。我们平时看到的 192.168.1.1(点分十进制)和 2001:0db8::1(冒分十六进制)只是为了方便人类阅读而设计的展示格式,底层传输和存储用的都是二进制。

所以,“IP 地址转十进制"这件事,核心就两步:把各段数值按权重合并成一个完整的二进制数,再转成十进制。反过来也是同样的道理。

IPv4 转换原理

IPv4 的 32 位被拆成了 4 个 8 位段(每段取值范围 0-255),用点号分隔。转十进制时,每段按位置乘以对应权重再求和:

192.168.1.1 为例:

十进制值 二进制表示 权重(位移量)
第 1 段 192 11000000 左移 24 位
第 2 段 168 10101000 左移 16 位
第 3 段 1 00000001 左移 8 位
第 4 段 1 00000001 左移 0 位

拼接后的 32 位二进制:11000000 10101000 00000001 00000001,对应十进制就是 3232235777

用公式表达:192 × 2^24 + 168 × 2^16 + 1 × 2^8 + 1 × 2^0 = 3232235777

反向转换就是拆解过程:对十进制数依次右移并取低 8 位,还原出每个段的值。

IPv6 转换原理

IPv6 的逻辑和 IPv4 完全一致,区别在于位数更长——128 位被拆成 8 个 16 位段,每段用十六进制表示后用冒号分隔(如 2001:0db8:85a3:0000:0000:8a2e:0370:7334)。

由于 128 位对应的十进制数可以达到 39 位(最大值约 3.4 × 10^38),远远超出了 uint64 能表示的范围,所以必须借助 Go 的 math/big 包来处理。

方案一:IPv4 与十进制互转

这部分给出两种实现思路:net 标准库的原生方法用位运算手动实现。生产环境推荐前者,学习原理适合看后者。

方法 1:net 标准库实现

net.ParseIP 可以把字符串解析成 net.IP 类型(本质是字节切片),拿到字节后逐字节拼接就能得到十进制数值。这里用 big.Int 来承接结果,主要是为了避免边界值溢出。

package main

import (
    "fmt"
    "math/big"
    "net"
)

// IPv4ToDecimal 将 IPv4 地址字符串转换为十进制字符串
func IPv4ToDecimal(ipStr string) (string, error) {
    ip := net.ParseIP(ipStr)
    if ip == nil {
        return "", fmt.Errorf("无效的 IP 地址: %s", ipStr)
    }
    ip4 := ip.To4()
    if ip4 == nil {
        return "", fmt.Errorf("不是 IPv4 地址: %s", ipStr)
    }
    decimal := big.NewInt(0)
    for _, b := range ip4 {
        decimal.Lsh(decimal, 8).Add(decimal, big.NewInt(int64(b)))
    }
    return decimal.String(), nil
}

// DecimalToIPv4 将十进制字符串转换为 IPv4 地址
func DecimalToIPv4(decimalStr string) (string, error) {
    decimal := new(big.Int)
    _, ok := decimal.SetString(decimalStr, 10)
    if !ok {
        return "", fmt.Errorf("无效的十进制数字: %s", decimalStr)
    }
    maxIPv4 := new(big.Int).Sub(
        new(big.Int).Lsh(big.NewInt(1), 32),
        big.NewInt(1),
    )
    if decimal.Sign() < 0 || decimal.Cmp(maxIPv4) > 0 {
        return "", fmt.Errorf("数值超出 IPv4 范围 (0 ~ 4294967295): %s", decimalStr)
    }
    ip4Bytes := make([]byte, 4)
    for i := 3; i >= 0; i-- {
        ip4Bytes[i] = byte(decimal.Uint64() & 0xFF)
        decimal.Rsh(decimal, 8)
    }
    return net.IPv4(ip4Bytes[0], ip4Bytes[1], ip4Bytes[2], ip4Bytes[3]).String(), nil
}

func main() {
    ipStr := "192.168.1.1"
    dec, _ := IPv4ToDecimal(ipStr)
    fmt.Printf("%s -> %s\n", ipStr, dec) // 192.168.1.1 -> 3232235777

    restored, _ := DecimalToIPv4(dec)
    fmt.Printf("%s -> %s\n", dec, restored) // 3232235777 -> 192.168.1.1
}

这段代码的核心逻辑在 for 循环里:每次把当前结果左移 8 位,再加上新的字节值——这和我们前面讲的"按权重求和"本质上是一回事。

方法 2:位运算手动实现

如果你想彻底搞懂转换过程,可以不依赖 net 包,直接用字符串拆分和位运算来实现。这种方式代码量多一点,但每一步都看得见。

package main

import (
    "fmt"
    "strconv"
    "strings"
)

// CustomIPv4ToDecimal 用位运算将 IPv4 字符串转为十进制
func CustomIPv4ToDecimal(ipStr string) (uint32, error) {
    segments := strings.Split(ipStr, ".")
    if len(segments) != 4 {
        return 0, fmt.Errorf("IPv4 格式错误,需要 4 段: %s", ipStr)
    }
    var result uint32
    for i, seg := range segments {
        val, err := strconv.Atoi(seg)
        if err != nil || val < 0 || val > 255 {
            return 0, fmt.Errorf("第 %d 段数值无效 (应为 0-255): %s", i+1, seg)
        }
        // 第 1 段左移 24 位,第 2 段左移 16 位,第 3 段左移 8 位,第 4 段不移
        result += uint32(val) << ((3 - i) * 8)
    }
    return result, nil
}

// CustomDecimalToIPv4 用位运算将十进制转为 IPv4 字符串
func CustomDecimalToIPv4(decimal uint32) string {
    return fmt.Sprintf("%d.%d.%d.%d",
        (decimal>>24)&0xFF,
        (decimal>>16)&0xFF,
        (decimal>>8)&0xFF,
        decimal&0xFF,
    )
}

func main() {
    ipStr := "192.168.1.1"
    dec, _ := CustomIPv4ToDecimal(ipStr)
    fmt.Printf("%s -> %d\n", ipStr, dec) // 192.168.1.1 -> 3232235777

    restored := CustomDecimalToIPv4(dec)
    fmt.Printf("%d -> %s\n", dec, restored) // 3232235777 -> 192.168.1.1
}

这里有个细节值得注意:result 的类型选择了 uint32 而不是 int。原因是 IPv4 的最大十进制值为 2^32 - 1 = 4294967295,而 Go 中 int32 的范围只到 2147483647,存不下。uint32 是无符号 32 位整数,范围刚好覆盖 0 ~ 4294967295

方案二:IPv6 与十进制互转

IPv6 的十进制数可以长达 39 位,常规整数类型完全装不下,因此只推荐用 net 包配合 big.Int 来处理。手写位运算在这个场景下既复杂又容易出 bug,性价比不高。

package main

import (
    "fmt"
    "math/big"
    "net"
)

// IPv6ToDecimal 将 IPv6 地址转换为十进制字符串
func IPv6ToDecimal(ipStr string) (string, error) {
    ip := net.ParseIP(ipStr)
    if ip == nil {
        return "", fmt.Errorf("无效的 IPv6 地址: %s", ipStr)
    }
    ip6 := ip.To16()
    if ip6 == nil {
        return "", fmt.Errorf("无法转换为 IPv6: %s", ipStr)
    }
    decimal := big.NewInt(0)
    for _, b := range ip6 {
        decimal.Lsh(decimal, 8).Add(decimal, big.NewInt(int64(b)))
    }
    return decimal.String(), nil
}

// DecimalToIPv6 将十进制字符串转换为 IPv6 地址
func DecimalToIPv6(decimalStr string) (string, error) {
    decimal := new(big.Int)
    _, ok := decimal.SetString(decimalStr, 10)
    if !ok {
        return "", fmt.Errorf("无效的十进制数字: %s", decimalStr)
    }
    maxIPv6 := new(big.Int).Sub(
        new(big.Int).Lsh(big.NewInt(1), 128),
        big.NewInt(1),
    )
    if decimal.Sign() < 0 || decimal.Cmp(maxIPv6) > 0 {
        return "", fmt.Errorf("数值超出 IPv6 范围: %s", decimalStr)
    }
    ip6Bytes := make([]byte, 16)
    for i := 15; i >= 0; i-- {
        ip6Bytes[i] = byte(decimal.Uint64() & 0xFF)
        decimal.Rsh(decimal, 8)
    }
    return net.IP(ip6Bytes).String(), nil
}

func main() {
    ipv6Str := "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
    dec, _ := IPv6ToDecimal(ipv6Str)
    fmt.Printf("%s -> %s\n", ipv6Str, dec)

    restored, _ := DecimalToIPv6(dec)
    fmt.Printf("%s -> %s\n", dec, restored)
    // 输出: 2001:db8:85a3::8a2e:370:7334(Go 会自动压缩连续零段为 ::)
}

转换后你可能会发现输出的 IPv6 地址和输入不太一样,比如中间的 0000:0000 变成了 ::。这不是 bug,而是 IPv6 的标准压缩格式——连续的全零段可以用 :: 替代,Go 的 net.IP.String() 方法会自动做这个处理。

实战技巧:自动识别 IP 版本再转换

实际项目中,输入的 IP 地址可能是 IPv4 也可能是 IPv6,不会提前告诉你版本。一个比较稳妥的做法是先判断版本,再走对应的转换逻辑:

func ConvertIPToDecimal(ipStr string) (string, error) {
    ip := net.ParseIP(ipStr)
    if ip == nil {
        return "", fmt.Errorf("无效的 IP 地址: %s", ipStr)
    }
    if ip.To4() != nil {
        return IPv4ToDecimal(ipStr)
    }
    return IPv6ToDecimal(ipStr)
}

To4() 会尝试把地址转成 4 字节的 IPv4 格式,如果返回 nil 说明不是 IPv4,就走 IPv6 的逻辑。这个判断顺序很重要——因为 IPv4 映射地址(如 ::ffff:192.168.1.1)在 To16() 里也能通过,所以要先判断 To4()

常见问题

为什么自定义方法用 uint32 而不是 int?

IPv4 的最大值是 4294967295(即 2^32 - 1),Go 中 int32 的最大值只有 2147483647,远远不够。uint32 是无符号 32 位整型,取值范围 0 ~ 4294967295,正好能完整覆盖 IPv4 的地址空间。如果误用 int32,处理大于 2147483647 的 IP(比如 128.0.0.0 以上的地址)就会发生溢出。

IPv6 的十进制数字那么长,怎么存储和传输?

只能用字符串来存储和传输。Go 中 uint64 最大也只有 64 位(约 1.8 × 10^19),而 IPv6 地址对应的十进制数最多有 39 位,常规整数类型根本放不下。在代码中用 big.Int 做计算,转换完成后调用 .String() 输出为字符串即可。数据库存储时建议用 VARCHAR(39)TEXT 类型。

转换时报 “无效 IP” 错误,一般是什么原因?

根据我的经验,最常见的几个原因:

  1. 段数不对:IPv4 必须是 4 段用点分隔,IPv6 必须是 8 段用冒号分隔(压缩格式除外);
  2. 段值越界:IPv4 每段必须在 0-255 之间,IPv6 每段必须在 0-FFFF 之间;
  3. 非法字符:比如 IP 字符串里混入了空格、中文字符,或者 IPv4 里出现了字母;
  4. 前导零问题:部分严格的解析器会拒绝 192.168.01.1 这样带前导零的写法(Go 的 net.ParseIP 可以正常处理)。

遇到这类错误,先打印原始字符串检查一下格式,基本都能快速定位。

net.ParseIP 返回的切片长度为什么有时是 4 有时是 16?

net.ParseIP 内部的处理逻辑是这样的:如果输入是标准的 IPv4 格式(如 192.168.1.1),它返回的 net.IP 底层是一个 16 字节的切片(IPv4-mapped IPv6 格式)。调用 To4() 后会截取最后 4 个字节返回。调用 To16() 则始终返回 16 字节。所以在代码里用 To4() 判断 IPv4 会更精确。

两种方案怎么选?

一句话总结:生产环境用 net 包方案,学习原理看位运算方案。

net 包方案的优势在于:格式校验由标准库完成(覆盖了各种边界情况),代码更简洁,而且同时支持 IPv4 和 IPv6。位运算方案的优势在于不依赖外部包,执行效率更高(省去了字节解析的开销),但只适用于 IPv4 场景。

总结

IP 地址和十进制之间的转换,归根结底就是字节拆分/合并 + 进制转换这么一件事。掌握了原理,不管用什么语言实现都是换汤不换药。

回顾一下这篇文章的要点:

  1. IPv4 场景有两种做法——想要快速实现就用 net 标准库,想搞懂底层细节就用位运算手动拆解,两种方案的转换结果完全一致;
  2. IPv6 场景由于数值范围太大,只推荐用 net 包搭配 big.Int 来处理,不建议手写位运算;
  3. 实际使用时,记得先校验 IP 格式和版本,十进制结果用字符串存储(尤其是 IPv6),就能避免绝大部分踩坑。

这套转换逻辑在日志分析(把数字 IP 还原成可读格式)、数据库存储(用整数存 IP 节省空间)、网络安全分析(IP 段批量运算)等场景中都很常见。如果你的项目有类似的需求,直接把上面的代码拿去用就行。

如果大家在 Go 网络编程中遇到过其他关于 IP 地址处理的棘手问题,或者对文章中的代码有任何疑问,欢迎在评论区交流讨论,我们一起学习进步~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-ip-address-decimal-conversion/

备用原文链接: https://blog.fiveyoboy.com/articles/go-ip-address-decimal-conversion/