Hey there! Today we're going to dig into Go's net/netip package, specifically focusing on the Addr type. If you've been working with Go's networking code, you've probably encountered the older net.IP type. While it served us well, it had some quirks that made it less than ideal for modern networking code. The net/netip package, introduced in Go 1.18, gives us a much more robust and efficient way to handle IP addresses.
Why net/netip.Addr?
Before we dive into the details, let's understand why this type exists. The traditional net.IP type is basically a byte slice ([]byte
), which means:
- It's mutable
- It requires heap allocations
- It can contain invalid states
- It's not comparable using == operator
The new Addr type fixes all these issues. It's a value type (struct internally), immutable, and always represents a valid IP address. No more defensive programming needed!
Getting Started with Addr
Let's look at the basic ways to create and work with Addr:
package main
import (
"fmt"
"net/netip"
)
func main() {
// Creating an Addr from string
addr, err := netip.ParseAddr("192.168.1.1")
if err != nil {
panic(err)
}
// If you're absolutely sure about the input
addr2 := netip.MustParseAddr("2001:db8::1")
fmt.Printf("IPv4: %v\nIPv6: %v\n", addr, addr2)
}
One thing I really like about ParseAddr is that it's strict. It won't accept weird formats or invalid addresses. For example:
// These will fail
_, err1 := netip.ParseAddr("256.1.2.3") // Invalid IPv4 octet
_, err2 := netip.ParseAddr("2001:db8::1::2") // Invalid IPv6 (double ::)
_, err3 := netip.ParseAddr("192.168.1.1/24") // CIDR notation not allowed for Addr
Deep Dive into Addr Methods
Let's explore the key methods you'll use with Addr. I'll share some real-world examples where each comes in handy.
Is This IPv4 or IPv6?
func checkAddressType(addr netip.Addr) {
if addr.Is4() {
fmt.Println("This is IPv4")
// You can safely use As4() here
bytes := addr.As4()
fmt.Printf("As bytes: %v\n", bytes)
} else if addr.Is6() {
fmt.Println("This is IPv6")
// You can safely use As16() here
bytes := addr.As16()
fmt.Printf("As bytes: %v\n", bytes)
}
}
Pro tip: When dealing with IPv4-mapped IPv6 addresses (like ::ffff:192.0.2.1), use Is4In6()
to detect them. This is particularly useful when writing protocol-agnostic code.
Address Classification Methods
The Addr type provides several methods to classify IP addresses. Here's a comprehensive example:
func classifyAddress(addr netip.Addr) {
checks := []struct {
name string
fn func() bool
}{
{"IsGlobalUnicast", addr.IsGlobalUnicast},
{"IsPrivate", addr.IsPrivate},
{"IsLoopback", addr.IsLoopback},
{"IsMulticast", addr.IsMulticast},
{"IsLinkLocalUnicast", addr.IsLinkLocalUnicast},
{"IsLinkLocalMulticast", addr.IsLinkLocalMulticast},
{"IsInterfaceLocalMulticast", addr.IsInterfaceLocalMulticast},
{"IsUnspecified", addr.IsUnspecified},
}
for _, check := range checks {
if check.fn() {
fmt.Printf("Address is %s\n", check.name)
}
}
}
Real-world example: Let's say you're writing a service that needs to bind to all interfaces except loopback:
func getBindableAddresses(addrs []netip.Addr) []netip.Addr {
var bindable []netip.Addr
for _, addr := range addrs {
if !addr.IsLoopback() && !addr.IsLinkLocalUnicast() {
bindable = append(bindable, addr)
}
}
return bindable
}
Working with Zones (IPv6 Scope IDs)
If you're working with IPv6, you'll eventually run into zones. They're used primarily with link-local addresses to specify which network interface to use:
func handleZones() {
// Create an address with a zone
addr := netip.MustParseAddr("fe80::1%eth0")
// Get the zone
zone := addr.Zone()
fmt.Printf("Zone: %s\n", zone)
// Compare addresses with zones
addr1 := netip.MustParseAddr("fe80::1%eth0")
addr2 := netip.MustParseAddr("fe80::1%eth1")
// These are different addresses because of different zones
fmt.Printf("Same address? %v\n", addr1 == addr2) // false
// WithZone creates a new address with a different zone
addr3 := addr1.WithZone("eth2")
fmt.Printf("New zone: %s\n", addr3.Zone())
}
Real-World Application: IP Address Filter
Let's put this all together in a practical example. Here's a simple IP filter that could be used in a web service:
type IPFilter struct {
allowed []netip.Addr
denied []netip.Addr
}
func NewIPFilter(allowed, denied []string) (*IPFilter, error) {
f := &IPFilter{}
// Parse allowed addresses
for _, a := range allowed {
addr, err := netip.ParseAddr(a)
if err != nil {
return nil, fmt.Errorf("invalid allowed address %s: %w", a, err)
}
f.allowed = append(f.allowed, addr)
}
// Parse denied addresses
for _, d := range denied {
addr, err := netip.ParseAddr(d)
if err != nil {
return nil, fmt.Errorf("invalid denied address %s: %w", d, err)
}
f.denied = append(f.denied, addr)
}
return f, nil
}
func (f *IPFilter) IsAllowed(ip string) bool {
addr, err := netip.ParseAddr(ip)
if err != nil {
return false
}
// Check denied list first
for _, denied := range f.denied {
if addr == denied {
return false
}
}
// If no allowed addresses specified, allow all non-denied
if len(f.allowed) == 0 {
return true
}
// Check allowed list
for _, allowed := range f.allowed {
if addr == allowed {
return true
}
}
return false
}
Usage example:
func main() {
filter, err := NewIPFilter(
[]string{"192.168.1.100", "10.0.0.1"},
[]string{"192.168.1.50"},
)
if err != nil {
panic(err)
}
tests := []string{
"192.168.1.100", // allowed
"192.168.1.50", // denied
"192.168.1.200", // not in either list
}
for _, ip := range tests {
fmt.Printf("%s allowed? %v\n", ip, filter.IsAllowed(ip))
}
}
Performance Considerations
One of the great things about net/netip.Addr is its performance characteristics. Since it's a value type:
- No heap allocations for basic operations
- Efficient comparison operations
- Zero-value is invalid (unlike net.IP where zero-value could be valid)
Here's a quick benchmark comparison with the old net.IP:
func BenchmarkNetIP(b *testing.B) {
for i := 0; i < b.N; i++ {
ip := net.ParseIP("192.168.1.1")
_ = ip.To4()
}
}
func BenchmarkNetipAddr(b *testing.B) {
for i := 0; i < b.N; i++ {
addr, _ := netip.ParseAddr("192.168.1.1")
_ = addr.As4()
}
}
The netip version typically performs better and generates less garbage for the GC to handle.
Common Gotchas and Tips
Don't mix net.IP and netip.Addr carelessly
While you can convert between them, try to stick to netip.Addr throughout your codebase for consistency.Watch out for zones in comparisons
Two addresses that are identical except for their zones are considered different.Use MustParseAddr carefully
While convenient in tests or initialization code, prefer ParseAddr in production code handling user input.Remember it's immutable
All methods that seem to modify the address (like WithZone) actually return a new address.
What's Next?
This covers the basics and some advanced usage of the Addr type, but there's more to explore in the net/netip package. In the next article, we'll look at AddrPort, which combines an IP address with a port number - super useful for network programming.
Until then, happy coding! Feel free to reach out if you have questions about using net/netip.Addr in your projects.
Top comments (0)