Understanding Go’s net/netip Addr Type: A Deep Dive 1/7

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 th…


This content originally appeared on DEV Community and was authored by Rez Moss

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

  1. 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.

  2. Watch out for zones in comparisons
    Two addresses that are identical except for their zones are considered different.

  3. Use MustParseAddr carefully
    While convenient in tests or initialization code, prefer ParseAddr in production code handling user input.

  4. 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.


This content originally appeared on DEV Community and was authored by Rez Moss


Print Share Comment Cite Upload Translate Updates
APA

Rez Moss | Sciencx (2025-01-11T01:14:08+00:00) Understanding Go’s net/netip Addr Type: A Deep Dive 1/7. Retrieved from https://www.scien.cx/2025/01/11/understanding-gos-net-netip-addr-type-a-deep-dive-1-7/

MLA
" » Understanding Go’s net/netip Addr Type: A Deep Dive 1/7." Rez Moss | Sciencx - Saturday January 11, 2025, https://www.scien.cx/2025/01/11/understanding-gos-net-netip-addr-type-a-deep-dive-1-7/
HARVARD
Rez Moss | Sciencx Saturday January 11, 2025 » Understanding Go’s net/netip Addr Type: A Deep Dive 1/7., viewed ,<https://www.scien.cx/2025/01/11/understanding-gos-net-netip-addr-type-a-deep-dive-1-7/>
VANCOUVER
Rez Moss | Sciencx - » Understanding Go’s net/netip Addr Type: A Deep Dive 1/7. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/01/11/understanding-gos-net-netip-addr-type-a-deep-dive-1-7/
CHICAGO
" » Understanding Go’s net/netip Addr Type: A Deep Dive 1/7." Rez Moss | Sciencx - Accessed . https://www.scien.cx/2025/01/11/understanding-gos-net-netip-addr-type-a-deep-dive-1-7/
IEEE
" » Understanding Go’s net/netip Addr Type: A Deep Dive 1/7." Rez Moss | Sciencx [Online]. Available: https://www.scien.cx/2025/01/11/understanding-gos-net-netip-addr-type-a-deep-dive-1-7/. [Accessed: ]
rf:citation
» Understanding Go’s net/netip Addr Type: A Deep Dive 1/7 | Rez Moss | Sciencx | https://www.scien.cx/2025/01/11/understanding-gos-net-netip-addr-type-a-deep-dive-1-7/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.