Working with AddrPort in net/netip: Complete Guide 2/7

Hey again! In our last article, we explored the Addr type for handling IP addresses. Today, we're diving into AddrPort, which combines an IP address with a port number. If you've ever worked with network services, you know how common this combination is - think web servers, database connections, or any network service really. Why AddrPort? Before net/netip came along, we'd typically handle IP:port combinations using strings or by keeping the IP and port separate. This led to a lot of string parsing, validation, and potential errors. AddrPort gives us a clean, type-safe way to handle these pairs together. Getting Started with AddrPort Let's look at the basics first: package main import ( "fmt" "net/netip" ) func main() { // Create from string ap1, err := netip.ParseAddrPort("192.168.1.1:8080") if err != nil { panic(err) } // Create from Addr and port addr := netip.MustParseAddr("192.168.1.1") ap2 := netip.AddrPortFrom(addr, 8080) fmt.Printf("From string: %v\nFrom components: %v\n", ap1, ap2) } Some things to note about the port number: It must be between 0 and 65535 It's stored as a uint16 Leading zeros in the port are fine when parsing ("8080" and "08080" are equivalent) Deep Dive into AddrPort Methods Let's explore all the methods available on AddrPort and when to use them. Getting Address and Port Components func examineAddrPort(ap netip.AddrPort) { // Get the address part addr := ap.Addr() fmt.Printf("Address: %v\n", addr) // Get the port number port := ap.Port() fmt.Printf("Port: %d\n", port) // Get it as a string (format: ":") str := ap.String() fmt.Printf("String representation: %s\n", str) } Working with IPv4 and IPv6 AddrPort handles both IPv4 and IPv6 addresses naturally. Here's how to work with them: func handleBothIPVersions() { // IPv4 with port ap4 := netip.MustParseAddrPort("192.168.1.1:80") // IPv6 with port ap6 := netip.MustParseAddrPort("[2001:db8::1]:80") // Note: IPv6 addresses must be enclosed in brackets // This would fail: "2001:db8::1:80" // IPv6 with zone and port apZone := netip.MustParseAddrPort("[fe80::1%eth0]:80") fmt.Printf("IPv4: %v\n", ap4) fmt.Printf("IPv6: %v\n", ap6) fmt.Printf("IPv6 with zone: %v\n", apZone) } Real-World Applications Let's look at some practical examples where AddrPort shines. 1. Simple TCP Server func runServer(ap netip.AddrPort) error { listener, err := net.Listen("tcp", ap.String()) if err != nil { return fmt.Errorf("failed to start server: %w", err) } defer listener.Close() fmt.Printf("Server listening on %v\n", ap) for { conn, err := listener.Accept() if err != nil { return fmt.Errorf("accept failed: %w", err) } go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() // Handle the connection... } 2. Service Registry Here's a more complex example - a simple service registry that keeps track of services and their endpoints: type ServiceType string const ( ServiceHTTP ServiceType = "http" ServiceHTTPS ServiceType = "https" ServiceGRPC ServiceType = "grpc" ) type ServiceRegistry struct { services map[ServiceType][]netip.AddrPort mu sync.RWMutex } func NewServiceRegistry() *ServiceRegistry { return &ServiceRegistry{ services: make(map[ServiceType][]netip.AddrPort), } } func (sr *ServiceRegistry) Register(stype ServiceType, endpoint netip.AddrPort) { sr.mu.Lock() defer sr.mu.Unlock() // Check if endpoint already exists endpoints := sr.services[stype] for _, ep := range endpoints { if ep == endpoint { return // Already registered } } sr.services[stype] = append(sr.services[stype], endpoint) } func (sr *ServiceRegistry) Unregister(stype ServiceType, endpoint netip.AddrPort) { sr.mu.Lock() defer sr.mu.Unlock() endpoints := sr.services[stype] for i, ep := range endpoints { if ep == endpoint { // Remove the endpoint sr.services[stype] = append(endpoints[:i], endpoints[i+1:]...) return } } } func (sr *ServiceRegistry) GetEndpoints(stype ServiceType) []netip.AddrPort { sr.mu.RLock() defer sr.mu.RUnlock() // Return a copy to prevent modifications endpoints := make([]netip.AddrPort, len(sr.services[stype])) copy(endpoints, sr.services[stype]) return endpoints } Usage example: func main() { registry := NewServiceRegistry() // Register some services registry.Register(ServiceHTTP, netip.MustParseAddrPort("192.168.1.10:80")) registry.Register(ServiceHTTP, netip.MustParseAddrPort("192.168.1.11:80")) registry.Register(ServiceGRPC, netip.MustParseAddr

Jan 17, 2025 - 16:06
Working with AddrPort in net/netip: Complete Guide 2/7

Hey again! In our last article, we explored the Addr type for handling IP addresses. Today, we're diving into AddrPort, which combines an IP address with a port number. If you've ever worked with network services, you know how common this combination is - think web servers, database connections, or any network service really.

Why AddrPort?

Before net/netip came along, we'd typically handle IP:port combinations using strings or by keeping the IP and port separate. This led to a lot of string parsing, validation, and potential errors. AddrPort gives us a clean, type-safe way to handle these pairs together.

Getting Started with AddrPort

Let's look at the basics first:

package main

import (
    "fmt"
    "net/netip"
)

func main() {
    // Create from string
    ap1, err := netip.ParseAddrPort("192.168.1.1:8080")
    if err != nil {
        panic(err)
    }

    // Create from Addr and port
    addr := netip.MustParseAddr("192.168.1.1")
    ap2 := netip.AddrPortFrom(addr, 8080)

    fmt.Printf("From string: %v\nFrom components: %v\n", ap1, ap2)
}

Some things to note about the port number:

  • It must be between 0 and 65535
  • It's stored as a uint16
  • Leading zeros in the port are fine when parsing ("8080" and "08080" are equivalent)

Deep Dive into AddrPort Methods

Let's explore all the methods available on AddrPort and when to use them.

Getting Address and Port Components

func examineAddrPort(ap netip.AddrPort) {
    // Get the address part
    addr := ap.Addr()
    fmt.Printf("Address: %v\n", addr)

    // Get the port number
    port := ap.Port()
    fmt.Printf("Port: %d\n", port)

    // Get it as a string (format: ":")
    str := ap.String()
    fmt.Printf("String representation: %s\n", str)
}

Working with IPv4 and IPv6

AddrPort handles both IPv4 and IPv6 addresses naturally. Here's how to work with them:

func handleBothIPVersions() {
    // IPv4 with port
    ap4 := netip.MustParseAddrPort("192.168.1.1:80")

    // IPv6 with port
    ap6 := netip.MustParseAddrPort("[2001:db8::1]:80")

    // Note: IPv6 addresses must be enclosed in brackets
    // This would fail: "2001:db8::1:80"

    // IPv6 with zone and port
    apZone := netip.MustParseAddrPort("[fe80::1%eth0]:80")

    fmt.Printf("IPv4: %v\n", ap4)
    fmt.Printf("IPv6: %v\n", ap6)
    fmt.Printf("IPv6 with zone: %v\n", apZone)
}

Real-World Applications

Let's look at some practical examples where AddrPort shines.

1. Simple TCP Server

func runServer(ap netip.AddrPort) error {
    listener, err := net.Listen("tcp", ap.String())
    if err != nil {
        return fmt.Errorf("failed to start server: %w", err)
    }
    defer listener.Close()

    fmt.Printf("Server listening on %v\n", ap)

    for {
        conn, err := listener.Accept()
        if err != nil {
            return fmt.Errorf("accept failed: %w", err)
        }

        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()

    // Handle the connection...
}

2. Service Registry

Here's a more complex example - a simple service registry that keeps track of services and their endpoints:

type ServiceType string

const (
    ServiceHTTP  ServiceType = "http"
    ServiceHTTPS ServiceType = "https"
    ServiceGRPC  ServiceType = "grpc"
)

type ServiceRegistry struct {
    services map[ServiceType][]netip.AddrPort
    mu       sync.RWMutex
}

func NewServiceRegistry() *ServiceRegistry {
    return &ServiceRegistry{
        services: make(map[ServiceType][]netip.AddrPort),
    }
}

func (sr *ServiceRegistry) Register(stype ServiceType, endpoint netip.AddrPort) {
    sr.mu.Lock()
    defer sr.mu.Unlock()

    // Check if endpoint already exists
    endpoints := sr.services[stype]
    for _, ep := range endpoints {
        if ep == endpoint {
            return // Already registered
        }
    }

    sr.services[stype] = append(sr.services[stype], endpoint)
}

func (sr *ServiceRegistry) Unregister(stype ServiceType, endpoint netip.AddrPort) {
    sr.mu.Lock()
    defer sr.mu.Unlock()

    endpoints := sr.services[stype]
    for i, ep := range endpoints {
        if ep == endpoint {
            // Remove the endpoint
            sr.services[stype] = append(endpoints[:i], endpoints[i+1:]...)
            return
        }
    }
}

func (sr *ServiceRegistry) GetEndpoints(stype ServiceType) []netip.AddrPort {
    sr.mu.RLock()
    defer sr.mu.RUnlock()

    // Return a copy to prevent modifications
    endpoints := make([]netip.AddrPort, len(sr.services[stype]))
    copy(endpoints, sr.services[stype])
    return endpoints
}

Usage example:

func main() {
    registry := NewServiceRegistry()

    // Register some services
    registry.Register(ServiceHTTP, netip.MustParseAddrPort("192.168.1.10:80"))
    registry.Register(ServiceHTTP, netip.MustParseAddrPort("192.168.1.11:80"))
    registry.Register(ServiceGRPC, netip.MustParseAddrPort("192.168.1.20:9000"))

    // Get HTTP endpoints
    httpEndpoints := registry.GetEndpoints(ServiceHTTP)
    fmt.Println("HTTP endpoints:", httpEndpoints)
}

3. Load Balancer Configuration

Here's how you might use AddrPort in a simple load balancer configuration:

type Backend struct {
    Endpoint  netip.AddrPort
    Healthy   bool
    LastCheck time.Time
}

type LoadBalancer struct {
    backends []Backend
    mu       sync.RWMutex
}

func (lb *LoadBalancer) AddBackend(endpoint string) error {
    ap, err := netip.ParseAddrPort(endpoint)
    if err != nil {
        return fmt.Errorf("invalid endpoint %q: %w", endpoint, err)
    }

    lb.mu.Lock()
    defer lb.mu.Unlock()

    lb.backends = append(lb.backends, Backend{
        Endpoint:  ap,
        Healthy:   true,
        LastCheck: time.Now(),
    })

    return nil
}

func (lb *LoadBalancer) GetNextHealthyBackend() (netip.AddrPort, error) {
    lb.mu.RLock()
    defer lb.mu.RUnlock()

    // Simple round-robin among healthy backends
    for _, backend := range lb.backends {
        if backend.Healthy {
            return backend.Endpoint, nil
        }
    }

    return netip.AddrPort{}, fmt.Errorf("no healthy backends available")
}

Common Patterns and Best Practices

  1. Validation Always validate user input:
   func validateEndpoint(input string) error {
       _, err := netip.ParseAddrPort(input)
       if err != nil {
           return fmt.Errorf("invalid endpoint %q: %w", input, err)
       }
       return nil
   }
  1. Zero Value Handling The zero value of AddrPort is invalid:
   func isValidEndpoint(ap netip.AddrPort) bool {
       return ap.IsValid()
   }
  1. String Representation When storing AddrPort as strings (e.g., in config files):
   func saveConfig(endpoints []netip.AddrPort) map[string]string {
       config := make(map[string]string)
       for i, ep := range endpoints {
           key := fmt.Sprintf("endpoint_%d", i)
           config[key] = ep.String()
       }
       return config
   }

Integration with Standard Library

AddrPort works seamlessly with the standard library:

func dialService(endpoint netip.AddrPort) (net.Conn, error) {
    return net.Dial("tcp", endpoint.String())
}

func listenAndServe(endpoint netip.AddrPort, handler http.Handler) error {
    return http.ListenAndServe(endpoint.String(), handler)
}

Performance Tips

  1. Use AddrPortFrom when possible If you already have a valid Addr, use AddrPortFrom instead of parsing a string:
   addr := netip.MustParseAddr("192.168.1.1")
   ap := netip.AddrPortFrom(addr, 8080)  // More efficient than parsing "192.168.1.1:8080"
  1. Avoid unnecessary string conversions Keep addresses in AddrPort form as long as possible, only converting to strings when needed.

What's Next?

In our next article, we'll explore the Prefix type, which is used for working with CIDR notation and subnet operations. This will complete our journey through the core types in net/netip.

Until then, enjoy working with AddrPort! It's one of those types that once you start using it, you'll wonder how you lived without it.