IP 地址不只是字符串
很多 Web 服务都会遇到 IP 处理:记录客户端 IP、判断内网地址、做白名单、解析 CIDR 网段、限制管理后台来源。初学时你可能会把 IP 当成字符串处理,比如 strings.HasPrefix(ip, "192.168.")。这种写法很脆弱,遇到 IPv6、前导格式、网段判断时很容易错。
Go 1.18 引入了 net/netip 包,提供了更现代的 IP 地址和前缀类型。它比早期 net.IP 在值语义、可比较性和清晰度上更适合很多场景。
这篇文章用几个小例子讲 netip.Addr 和 netip.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-For 或 X-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 网络相关代码时非常值得掌握的标准库能力。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。