net: Dial, Listen, and UDP

Updated

September 8, 2026

net: Dial, Listen, and UDP

Overview

Package net is the foundation under net/http: TCP/UDP addresses, listeners, dialers, and resolution. When HTTP is too high-level—or you need a custom protocol—you work here.

TCP services deep dive: 131 TCP services.

Addresses and resolution

// TCPAddr, UDPAddr, UnixAddr
tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:9000")
ips, err := net.LookupIP(ctx, "example.com") // prefer context variants where available

Prefer net.Dialer over bare net.Dial so you can set timeouts and keep-alives.

d := net.Dialer{
    Timeout:   3 * time.Second,
    KeepAlive: 30 * time.Second,
}
conn, err := d.DialContext(ctx, "tcp", "127.0.0.1:9000")

TCP server (line protocol sketch)

ln, err := net.Listen("tcp", "127.0.0.1:9000")
if err != nil {
    log.Fatal(err)
}
defer ln.Close()

for {
    conn, err := ln.Accept()
    if err != nil {
        log.Println("accept:", err)
        continue
    }
    go handle(conn)
}

func handle(c net.Conn) {
    defer c.Close()
    _ = c.SetDeadline(time.Now().Add(30 * time.Second))
    br := bufio.NewReader(c)
    line, err := br.ReadString('\n')
    if err != nil {
        return
    }
    _, _ = io.WriteString(c, "echo: "+line)
}
Habit Why
Deadline per connection Avoid stuck clients forever
One goroutine per conn (or limited pool) Bound resource use
Always Close Prevent FD leaks
bufio for line protocols Fewer syscalls

TCP client

conn, err := d.DialContext(ctx, "tcp", addr)
if err != nil {
    return err
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
_, err = io.WriteString(conn, "ping\n")
// read response...

UDP

// listen
pc, err := net.ListenPacket("udp", "127.0.0.1:9001")
// or DialUDP for connected UDP

buf := make([]byte, 1500)
n, addr, err := pc.ReadFrom(buf)
_, err = pc.WriteTo(buf[:n], addr) // echo

UDP is message-oriented: no streams, no automatic retransmit. Size buffers for your MTU/use case.

Unix domain sockets

ln, err := net.Listen("unix", "/tmp/app.sock")
// client: Dial("unix", "/tmp/app.sock")

Useful for local IPC (sidecar, same-host tools). Remember file permissions on the socket path.

net.Conn as io.Reader / Writer

// stream copy with limit
n, err := io.Copy(dst, io.LimitReader(conn, 1<<20))

Composition with io is the Go style: your protocol parsers should take io.Reader/io.Writer when possible, not only net.Conn.

Errors and temporary failures

var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
    // retry or return 504-style
}

DNS and dial failures are often wrapped—log with %+v / slog attributes; branch with errors.As.

ip and ipnet helpers

ip := net.ParseIP("2001:db8::1")
_, cidr, _ := net.ParseCIDR("10.0.0.0/8")
ok := cidr.Contains(net.ParseIP("10.1.2.3"))

Handy for allowlists and basic network policy in tools.

Rules of thumb

Do Don’t
Dial/Listen with timeouts/deadlines Block forever on Accept/Read
Propagate context on Dial Ignore cancel when user hits Ctrl-C
Limit concurrent handlers Accept unbounded go handle under load without a cap
Close connections Leak FDs on error paths

Try next

  1. Build a TCP echo server and client with 2s deadlines.
  2. Send a UDP packet to yourself and print the peer address.
  3. Wrap handle with a semaphore channel of size 100 and load-test Accept.