在 Golang 中检查 IP 地址切片中的 IP 的有效方法

Efficient way to check IP in slice of IP addresses in Golang

我正在用 Golang 开发网络应用程序。我有一段 IP 地址。每次收到请求时,我都会使用 net.LookupIP(host) 找出 returns net.IP 的主机的 IP 地址。比较这些的最佳方法是什么?

顺便说一句,在 Python 中,我们有一个 set 数据结构,这使得上述问题很容易解决,但是 Go 呢?

您可以使用 net 包中的 func (ip IP) Equal(x IP) bool

Equal reports whether ip and x are the same IP address. An IPv4 address and that same address in IPv6 form are considered to be equal.

喜欢这个工作示例:

package main

import (
    "fmt"
    "net"
)

func main() {
    ip := net.ParseIP("127.0.0.1")
    ips, err := net.LookupIP("localhost")
    if err != nil {
        panic(err)
    }
    for _, v := range ips {
        if v.Equal(ip) {
            fmt.Println(v)
        }
    }
}

有了"set"

构建我们的场景

Go 中没有内置的 Set 类型,但您可以优雅地使用 map[Type]bool 作为集合,例如:

// Create a set with 2 values in it: [1, 2]
m := map[int]bool{1: true, 2: true}

// Test an element:
fmt.Println(m[1]) // true
fmt.Println(m[3]) // false

// Set an element:
m[3] = true
fmt.Println(m[3]) // true

// Delete an element:
delete(m, 1)
fmt.Println(m[1]) // false

注意:我们利用了这样一个事实,即如果键不在地图中,索引地图会导致值类型为 zero value,在 [=20 的情况下为 false =],正确地告诉元素不在地图(集合)中。

Go Playground 上试试。

注意#2:有一些技巧可以使处理地图作为集合的代码更短,您可以在这个答案中查看它们: .

在集合中使用net.IP

现在我们只需要一个表示 net.IP which can be used as the key type in a map (see this question about what constitutes a map key type: ) 的类型。

不幸的是net.IP本身不符合条件,因为它是一个切片:

type IP []byte

和切片没有可比性。有关详细信息,请参阅此问题: and this:

一种简单的方法是将其转换为规范的 string 值,我们就完成了。为此,我们可以简单地将 IP 的字节转换为十六进制 string。但是 IPv4 地址可能显示为 IPv6,所以我们应该先将其转换为 IPv6:

func Key(ip net.IP) string {
    return hex.EncodeToString(ip.To16())
}

注意:IP 地址的字节可能不是有效的 UTF-8 编码 string(这是 Go 在内存中存储 strings 的方式),但 string 中的值Go 表示任意字节序列,所以下面的也可以工作,更简单也更高效:

func Key(ip net.IP) string {
    return string(ip.To16())  // Simple []byte => string conversion
}

我们可以使用这样的IP字符串作为key。使用要检查的 IP 填充您的地图:

// Populate forbidden IPs:
forbIPs := map[string]bool{
    Key(ip1): true,
    Key(ip2): true,
}

// Now check a single IP:
ipToCheck := ...
if forbIPs[Key(ipToCheck)] {
    fmt.Println("Forbidden!")
} else {
    fmt.Println("Allowed.")
}

如果您有多个 IP 要检查(由 net.LookupIP() 返回),它是一个 for 循环:

ips, err := net.LookupIP(host)
// Check err
for _, ip := range ips {
    if forbIPs[Key(ip)] {
        // FORBIDDEN!
    }
}

替代键类型

请注意——如上所述——切片不可比较,但数组可以。所以我们也可以使用数组作为键。这就是它的样子:

func Key(ip net.IP) (a [16]byte) {
    copy(a[:], ip)
    return
}

// And the IP set:
forbIPs := map[[16]byte]bool{
    // ...
}

备选方案

排序切片

或者,我们可以简单地将禁止的 IP 存储在一个切片中 []net.IP,然后 保持排序 。如果是排序好的,我们可以用二分查找在里面找一个IP(标准库sort.Search())。

是的,与上述(散列)映射解决方案的 O(1) 复杂性相比,二分查找具有 O(log2(n)) 复杂性。但是这个替代方案还有另一个好处:

枚举单个 IP 并不总是可行的。有时(通常)列出 IP 范围更容易。第一个解决方案对于处理 IP 范围是不可行的,但这个解决方案可能是:您也可以在 O(log2(n)) 时间内找到涵盖 IP 地址的范围。