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