Hey there! In our previous article, we explored Addr methods in detail. Now let's dive deep into AddrPort methods. AddrPort is a crucial type when working with network services since it combines an IP address with a port number. We'll explore every method with practical examples and real-world use cases.
Core Method Exploration
First, let's look at all the ways to work with AddrPort.
Creation and Parsing
package main
import (
"fmt"
"net/netip"
)
func demoAddrPortCreation() {
// From string
ap1, _ := netip.ParseAddrPort("192.168.1.1:8080")
// From Addr and port
addr := netip.MustParseAddr("192.168.1.1")
ap2 := netip.AddrPortFrom(addr, 8080)
// From raw IP and port
ap3 := netip.AddrPortFrom(
netip.AddrFrom4([4]byte{192, 168, 1, 1}),
8080,
)
fmt.Printf("From string: %v\n", ap1)
fmt.Printf("From components: %v\n", ap2)
fmt.Printf("From raw: %v\n", ap3)
}
Component Access Methods
func exploreComponents(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)
// Check validity
fmt.Printf("Is valid: %v\n", ap.IsValid())
// String representation
str := ap.String()
fmt.Printf("String form: %s\n", str)
}
Practical Applications
1. Service Discovery System
Here's a robust service discovery implementation using AddrPort:
type ServiceType string
const (
ServiceHTTP ServiceType = "http"
ServiceHTTPS ServiceType = "https"
ServiceGRPC ServiceType = "grpc"
)
type ServiceInstance struct {
ID string
Type ServiceType
Endpoint netip.AddrPort
Metadata map[string]string
LastSeen time.Time
}
type ServiceRegistry struct {
services map[ServiceType]map[string]*ServiceInstance
mu sync.RWMutex
}
func NewServiceRegistry() *ServiceRegistry {
return &ServiceRegistry{
services: make(map[ServiceType]map[string]*ServiceInstance),
}
}
func (sr *ServiceRegistry) Register(instance *ServiceInstance) error {
sr.mu.Lock()
defer sr.mu.Unlock()
if !instance.Endpoint.IsValid() {
return fmt.Errorf("invalid endpoint")
}
// Initialize type map if needed
if sr.services[instance.Type] == nil {
sr.services[instance.Type] = make(map[string]*ServiceInstance)
}
// Update or add instance
instance.LastSeen = time.Now()
sr.services[instance.Type][instance.ID] = instance
return nil
}
func (sr *ServiceRegistry) Discover(stype ServiceType) []*ServiceInstance {
sr.mu.RLock()
defer sr.mu.RUnlock()
var instances []*ServiceInstance
for _, instance := range sr.services[stype] {
instances = append(instances, instance)
}
return instances
}
func (sr *ServiceRegistry) Cleanup(maxAge time.Duration) {
sr.mu.Lock()
defer sr.mu.Unlock()
now := time.Now()
for stype, typeMap := range sr.services {
for id, instance := range typeMap {
if now.Sub(instance.LastSeen) > maxAge {
delete(typeMap, id)
}
}
if len(typeMap) == 0 {
delete(sr.services, stype)
}
}
}
2. Connection Pool Manager
A connection pool that uses AddrPort for endpoint tracking:
type ConnState int
const (
ConnIdle ConnState = iota
ConnInUse
ConnBroken
)
type PooledConn struct {
Conn net.Conn
State ConnState
LastUsed time.Time
UseCount int
}
type ConnectionPool struct {
endpoints map[netip.AddrPort][]*PooledConn
mu sync.RWMutex
maxIdle time.Duration
maxUses int
}
func NewConnectionPool(maxIdle time.Duration, maxUses int) *ConnectionPool {
return &ConnectionPool{
endpoints: make(map[netip.AddrPort][]*PooledConn),
maxIdle: maxIdle,
maxUses: maxUses,
}
}
func (cp *ConnectionPool) GetConnection(endpoint netip.AddrPort) (net.Conn, error) {
cp.mu.Lock()
defer cp.mu.Unlock()
// Look for available connection
conns := cp.endpoints[endpoint]
for _, pc := range conns {
if pc.State == ConnIdle {
if time.Since(pc.LastUsed) > cp.maxIdle || pc.UseCount >= cp.maxUses {
// Connection too old or overused - close and remove
pc.Conn.Close()
continue
}
pc.State = ConnInUse
pc.LastUsed = time.Now()
pc.UseCount++
return pc.Conn, nil
}
}
// Create new connection
conn, err := net.Dial("tcp", endpoint.String())
if err != nil {
return nil, fmt.Errorf("failed to connect to %v: %w", endpoint, err)
}
pc := &PooledConn{
Conn: conn,
State: ConnInUse,
LastUsed: time.Now(),
UseCount: 1,
}
cp.endpoints[endpoint] = append(cp.endpoints[endpoint], pc)
return conn, nil
}
func (cp *ConnectionPool) ReleaseConnection(endpoint netip.AddrPort, conn net.Conn) {
cp.mu.Lock()
defer cp.mu.Unlock()
for _, pc := range cp.endpoints[endpoint] {
if pc.Conn == conn {
pc.State = ConnIdle
pc.LastUsed = time.Now()
return
}
}
}
func (cp *ConnectionPool) Cleanup() {
cp.mu.Lock()
defer cp.mu.Unlock()
now := time.Now()
for endpoint, conns := range cp.endpoints {
var active []*PooledConn
for _, pc := range conns {
if pc.State == ConnIdle &&
(now.Sub(pc.LastUsed) > cp.maxIdle || pc.UseCount >= cp.maxUses) {
pc.Conn.Close()
continue
}
active = append(active, pc)
}
if len(active) == 0 {
delete(cp.endpoints, endpoint)
} else {
cp.endpoints[endpoint] = active
}
}
}
3. Load Balancer Implementation
A load balancer using AddrPort for backend management:
type Backend struct {
Endpoint netip.AddrPort
Weight int
Connections int64
LastChecked time.Time
Healthy bool
}
type LoadBalancer struct {
backends []*Backend
mu sync.RWMutex
}
func NewLoadBalancer() *LoadBalancer {
return &LoadBalancer{}
}
func (lb *LoadBalancer) AddBackend(endpoint netip.AddrPort, weight int) error {
if !endpoint.IsValid() {
return fmt.Errorf("invalid endpoint")
}
lb.mu.Lock()
defer lb.mu.Unlock()
// Check for duplicate
for _, b := range lb.backends {
if b.Endpoint == endpoint {
return fmt.Errorf("backend already exists")
}
}
lb.backends = append(lb.backends, &Backend{
Endpoint: endpoint,
Weight: weight,
LastChecked: time.Now(),
Healthy: true,
})
return nil
}
func (lb *LoadBalancer) RemoveBackend(endpoint netip.AddrPort) {
lb.mu.Lock()
defer lb.mu.Unlock()
for i, b := range lb.backends {
if b.Endpoint == endpoint {
// Remove backend
lb.backends = append(lb.backends[:i], lb.backends[i+1:]...)
return
}
}
}
func (lb *LoadBalancer) GetBackend() (*Backend, error) {
lb.mu.RLock()
defer lb.mu.RUnlock()
var totalWeight int
var candidates []*Backend
for _, b := range lb.backends {
if b.Healthy {
candidates = append(candidates, b)
totalWeight += b.Weight
}
}
if len(candidates) == 0 {
return nil, fmt.Errorf("no healthy backends available")
}
// Weighted random selection
target := rand.Intn(totalWeight)
currentWeight := 0
for _, b := range candidates {
currentWeight += b.Weight
if target < currentWeight {
atomic.AddInt64(&b.Connections, 1)
return b, nil
}
}
// Shouldn't reach here, but just in case
b := candidates[len(candidates)-1]
atomic.AddInt64(&b.Connections, 1)
return b, nil
}
Best Practices
- Validation Always validate AddrPort before use:
func validateEndpoint(ep netip.AddrPort) error {
if !ep.IsValid() {
return fmt.Errorf("invalid endpoint")
}
if ep.Addr().IsUnspecified() {
return fmt.Errorf("unspecified address not allowed")
}
return nil
}
- String Handling Be careful with string conversions:
func formatEndpoint(ep netip.AddrPort) string {
if ep.Addr().Is6() {
// IPv6 addresses need brackets
return fmt.Sprintf("[%s]:%d", ep.Addr(), ep.Port())
}
return ep.String()
}
- Port Range Validation Check port numbers when needed:
func isUserPort(ep netip.AddrPort) bool {
port := ep.Port()
return port >= 1024 && port <= 49151
}
Common Patterns
- Service Configuration
type ServiceConfig struct {
Listen netip.AddrPort
Upstream []netip.AddrPort
}
func parseConfig(config map[string]string) (ServiceConfig, error) {
var cfg ServiceConfig
if listen, err := netip.ParseAddrPort(config["listen"]); err != nil {
return cfg, fmt.Errorf("invalid listen address: %w", err)
} else {
cfg.Listen = listen
}
for _, up := range strings.Split(config["upstream"], ",") {
if endpoint, err := netip.ParseAddrPort(up); err != nil {
return cfg, fmt.Errorf("invalid upstream %q: %w", up, err)
} else {
cfg.Upstream = append(cfg.Upstream, endpoint)
}
}
return cfg, nil
}
- Address Family Handling
func getDialNetwork(ep netip.AddrPort) string {
if ep.Addr().Is6() {
return "tcp6"
}
return "tcp4"
}
Performance Tips
- Avoid String Parsing
// Bad
ep, _ := netip.ParseAddrPort(fmt.Sprintf("%s:%d", addr, port))
// Good
ep := netip.AddrPortFrom(addr, port)
- Efficient Storage
// Bad
map[string]string // Using string representations
// Good
map[netip.AddrPort]interface{} // Using AddrPort directly
- Batch Operations
func batchConnect(endpoints []netip.AddrPort) []net.Conn {
var wg sync.WaitGroup
conns := make([]net.Conn, len(endpoints))
for i, ep := range endpoints {
wg.Add(1)
go func(i int, ep netip.AddrPort) {
defer wg.Done()
if conn, err := net.Dial("tcp", ep.String()); err == nil {
conns[i] = conn
}
}(i, ep)
}
wg.Wait()
return conns
}
What's Next?
In our next article, we'll explore Prefix methods in depth, completing our detailed examination of the core types in net/netip. We'll see how to work effectively with CIDR notations and subnet operations.
Until then, keep experimenting with AddrPort! It's a fundamental building block for network services in Go.
Top comments (0)