Go netip 入门:更清楚地处理 IP 地址和网段

本文讲解 Go 1.18 中 net/netip 包的基本用法,包括解析 IP、前缀、包含判断和 HTTP 客户端 IP 白名单场景。

IP 地址不只是字符串

很多 Web 服务都会遇到 IP 处理:记录客户端 IP、判断内网地址、做白名单、解析 CIDR 网段、限制管理后台来源。初学时你可能会把 IP 当成字符串处理,比如 strings.HasPrefix(ip, "192.168.")。这种写法很脆弱,遇到 IPv6、前导格式、网段判断时很容易错。

Go 1.18 引入了 net/netip 包,提供了更现代的 IP 地址和前缀类型。它比早期 net.IP 在值语义、可比较性和清晰度上更适合很多场景。

这篇文章用几个小例子讲 netip.Addrnetip.Prefix 的基本用法。

解析 IP 地址

addr, err := netip.ParseAddr("192.168.1.10")
if err != nil {
	return err
}

fmt.Println(addr.String())
fmt.Println(addr.Is4())
fmt.Println(addr.Is6())

IPv6:

addr, err := netip.ParseAddr("2001:db8::1")
if err != nil {
	return err
}
fmt.Println(addr.Is6())

netip.Addr 是值类型,可以比较,也可以作为 map key:

seen := map[netip.Addr]bool{}
seen[addr] = true

这比 net.IP 的切片表示更方便。net.IP 不能直接作为 map key,因为切片不可比较。

解析网段

prefix, err := netip.ParsePrefix("192.168.1.0/24")
if err != nil {
	return err
}

addr, _ := netip.ParseAddr("192.168.1.42")
fmt.Println(prefix.Contains(addr)) // true

IPv6 网段也一样:

prefix, err := netip.ParsePrefix("2001:db8::/32")

白名单可以这样表示:

type IPAllowList struct {
	prefixes []netip.Prefix
}

func NewIPAllowList(values []string) (IPAllowList, error) {
	prefixes := make([]netip.Prefix, 0, len(values))
	for _, value := range values {
		prefix, err := netip.ParsePrefix(value)
		if err != nil {
			return IPAllowList{}, fmt.Errorf("parse prefix %s: %w", value, err)
		}
		prefixes = append(prefixes, prefix)
	}
	return IPAllowList{prefixes: prefixes}, nil
}

func (l IPAllowList) Allows(addr netip.Addr) bool {
	for _, prefix := range l.prefixes {
		if prefix.Contains(addr) {
			return true
		}
	}
	return false
}

调用:

allowList, err := NewIPAllowList([]string{
	"127.0.0.1/32",
	"10.0.0.0/8",
})
if err != nil {
	return err
}

addr, _ := netip.ParseAddr("10.1.2.3")
fmt.Println(allowList.Allows(addr))

从 HTTP 请求里取客户端 IP

最直接的来源是:

host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
	return netip.Addr{}, err
}

addr, err := netip.ParseAddr(host)
if err != nil {
	return netip.Addr{}, err
}

如果服务部署在反向代理后面,真实客户端 IP 可能在 X-Forwarded-ForX-Real-IP。但这些头不能无条件信任,因为客户端也能伪造。只有当请求来自可信代理时,才应该读取这些头。

一个保守函数:

func ClientAddr(r *http.Request) (netip.Addr, error) {
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		return netip.Addr{}, err
	}
	return netip.ParseAddr(host)
}

真实生产环境可以在网关层统一处理真实 IP,并把规则写清楚。业务服务不要各自猜。

管理后台 IP 限制中间件

func RequireIP(allowList IPAllowList, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		addr, err := ClientAddr(r)
		if err != nil || !allowList.Allows(addr) {
			http.Error(w, "forbidden", http.StatusForbidden)
			return
		}
		next.ServeHTTP(w, r)
	})
}

使用:

allowList, err := NewIPAllowList([]string{"10.0.0.0/8"})
if err != nil {
	log.Fatal(err)
}

mux.Handle("/admin", RequireIP(allowList, adminHandler))

这只是入门版本。真实管理后台还需要登录、权限、审计和更完整的代理信任规则。IP 白名单是补充,不是唯一安全机制。

小结

net/netip 让 Go 处理 IP 地址和网段更清楚。netip.Addr 可以表示 IPv4 和 IPv6,netip.Prefix 可以判断网段包含关系,二者都是值语义,适合 map key 和配置解析。

不要把 IP 当普通字符串判断。只要涉及网段、IPv6、白名单和安全边界,就应该使用专门类型。netip 是 2022 年学习 Go 网络相关代码时非常值得掌握的标准库能力。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页