DEV Community

Rez Moss
Rez Moss

Posted on

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)
}
Enter fullscreen mode Exit fullscreen mode

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: "<addr>:<port>")
    str := ap.String()
    fmt.Printf("String representation: %s\n", str)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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
   }
Enter fullscreen mode Exit fullscreen mode
  1. Zero Value Handling The zero value of AddrPort is invalid:
   func isValidEndpoint(ap netip.AddrPort) bool {
       return ap.IsValid()
   }
Enter fullscreen mode Exit fullscreen mode
  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
   }
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
  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.

Top comments (0)