A Deep Dive into Go’s net Package: Networking from First Principles

The net package is the foundation of all network programming in Go. Everything — from HTTP servers to gRPC, Redis clients, DNS resolvers, and low-level TCP/UDP …

The net package is the foundation of all network programming in Go.
Everything — from HTTP servers to gRPC, Redis clients, DNS resolvers, and low-level TCP/UDP tools — ultimately relies on Go’s networking stack built around the net package.

This article provides a deep, practical, and complete exploration of net with clear explanations and runnable examples.

1. Why the net Package Matters

Go’s networking model is:

  • Simple – Uses familiar Unix-style sockets and file descriptors.
  • Cross-platform – Same code works on Linux, macOS, Windows.
  • Concurrent by design – Each connection can be handled by a goroutine.
  • Powerful – TCP, UDP, Unix domain sockets, DNS, interfaces, IPs, CIDR tools, etc.

Higher-level packages rely on it:

  • net/http
  • crypto/tls
  • net/rpc
  • golang.org/x/net

Understanding net gives you the ability to build:

  • custom servers
  • proxies and gateways
  • TCP tunnelers
  • UDP discovery tools
  • performance-critical networking software

2. The Key Interfaces in net

The most important interfaces are:

net.Conn — a bidirectional stream

type Conn interface {
    Read(b []byte) (n int, err error)
    Write(b []byte) (n int, err error)
    Close() error
    LocalAddr() Addr
    RemoteAddr() Addr
    SetDeadline(t time.Time) error
    SetReadDeadline(t time.Time) error
    SetWriteDeadline(t time.Time) error
}

This is the foundation for TCP connections (and some UDP behavior when wrapped with net.PacketConn).

net.Listener — accepts incoming connections

type Listener interface {
    Accept() (Conn, error)
    Close() error
    Addr() Addr
}

3. Building a Simple TCP Server

This is the “Hello TCP” example.

package main

import (
    "bufio"
    "fmt"
    "net"
)

func main() {
    ln, err := net.Listen("tcp", ":9000")
    if err != nil {
        panic(err)
    }
    fmt.Println("Server running on :9000")

    for {
        conn, err := ln.Accept()
        if err != nil {
            fmt.Println("Error:", err)
            continue
        }

        go handle(conn)
    }
}

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

    reader := bufio.NewReader(conn)
    for {
        msg, err := reader.ReadString('\n')
        if err != nil {
            fmt.Println("Client disconnected")
            return
        }
        fmt.Println("Received:", msg)
        conn.Write([]byte("Echo: " + msg))
    }
}

Run:

go run server.go

Connect using nc or telnet:

nc localhost 9000

Type:

hello

Server responds:

Echo: hello

4. TCP Client Example

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
)

func main() {
    conn, err := net.Dial("tcp", "localhost:9000")
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    fmt.Println("Connected to server")

    for {
        fmt.Print("Enter message: ")
        text, _ := bufio.NewReader(os.Stdin).ReadString('\n')

        conn.Write([]byte(text))

        resp, _ := bufio.NewReader(conn).ReadString('\n')
        fmt.Println("Server:", resp)
    }
}

5. Understanding net.Dial, net.Listen, and Address Formats

net.Dial(network, address) supports:

  • "tcp" / "tcp4" / "tcp6"
  • "udp" / "udp4" / "udp6"
  • "unix" (Unix domain sockets)
  • "ip" (raw sockets — root privileges needed)

Address formats:

host:port
example: 127.0.0.1:9000
example: [2001:db8::1]:8080

6. TCP Timeouts Using Deadlines

You can control network reliability using deadlines.

conn.SetDeadline(time.Now().Add(3 * time.Second))

or specific parts:

conn.SetReadDeadline(time.Now().Add(time.Second))
conn.SetWriteDeadline(time.Now().Add(time.Second))

If the deadline expires:

i/o timeout

7. UDP Basics with PacketConn

UDP is connectionless.
No net.Conn — instead use net.PacketConn.

UDP Server

package main

import (
    "fmt"
    "net"
)

func main() {
    addr, _ := net.ResolveUDPAddr("udp", ":9001")
    conn, _ := net.ListenUDP("udp", addr)
    defer conn.Close()

    fmt.Println("UDP server on :9001")

    buffer := make([]byte, 1024)
    for {
        n, clientAddr, _ := conn.ReadFromUDP(buffer)
        fmt.Printf("Received %s from %s\n", string(buffer[:n]), clientAddr)

        conn.WriteToUDP([]byte("pong"), clientAddr)
    }
}

UDP Client

package main

import (
    "fmt"
    "net"
)

func main() {
    serverAddr, _ := net.ResolveUDPAddr("udp", "localhost:9001")
    conn, _ := net.DialUDP("udp", nil, serverAddr)
    defer conn.Close()

    conn.Write([]byte("ping"))

    buf := make([]byte, 1024)
    n, _, _ := conn.ReadFromUDP(buf)
    fmt.Println("Server:", string(buf[:n]))
}

8. Working with IPs and CIDR Blocks

Go’s net package includes powerful IP tools.

Validate IP

ip := net.ParseIP("192.168.1.1")
fmt.Println(ip)

Parse CIDR

ip, ipnet, _ := net.ParseCIDR("192.168.1.0/24")
fmt.Println(ip, ipnet)

Check if an IP is inside a subnet

if ipnet.Contains(net.ParseIP("192.168.1.50")) {
    fmt.Println("inside network")
}

9. Working with DNS

Lookup hostnames

ips, _ := net.LookupHost("google.com")
fmt.Println(ips)

Lookup CNAME

cname, _ := net.LookupCNAME("www.example.com")
fmt.Println(cname)

Lookup MX records

mx, _ := net.LookupMX("gmail.com")
fmt.Println(mx)

Lookup SRV

_, addrs, _ := net.LookupSRV("xmpp-server", "tcp", "google.com")
fmt.Println(addrs)

10. Inspecting Network Interfaces

ifs, _ := net.Interfaces()
for _, iface := range ifs {
    fmt.Printf("Name: %s, MTU: %d, Flags: %v\n", iface.Name, iface.MTU, iface.Flags)
}

Interface addresses:

addrs, _ := iface.Addrs()
for _, addr := range addrs {
    fmt.Println("Address:", addr.String())
}

11. Building a Concurrent TCP Server

Each connection in its own goroutine — Go’s biggest networking strength.

func main() {
    ln, _ := net.Listen("tcp", ":9000")

    for {
        conn, _ := ln.Accept()
        go func(c net.Conn) {
            defer c.Close()
            buf := make([]byte, 1024)

            for {
                n, err := c.Read(buf)
                if err != nil {
                    return
                }
                c.Write([]byte("OK\n"))
                fmt.Println(string(buf[:n]))
            }
        }(conn)
    }
}

Avoids callback hell.
Simple, clear, high performance.

12. Implementing a TCP Proxy (Forwarder)

A real-world example: a simple TCP proxy.

package main

import (
    "io"
    "net"
)

func forward(src net.Conn, dst net.Conn) {
    defer src.Close()
    defer dst.Close()
    io.Copy(dst, src)
}

func main() {
    ln, _ := net.Listen("tcp", ":9000")

    for {
        client, _ := ln.Accept()
        server, _ := net.Dial("tcp", "example.com:80")

        go forward(client, server)
        go forward(server, client)
    }
}

This is the basis for:

  • Proxies
  • Gateways
  • Load balancers
  • Tunneling
  • MITM tools

13. Implementing a Mini HTTP Server Using Only net

Without net/http.

package main

import (
    "bufio"
    "fmt"
    "net"
    "strings"
)

func main() {
    ln, _ := net.Listen("tcp", ":8080")
    fmt.Println("Mini HTTP server on 8080")

    for {
        conn, _ := ln.Accept()
        go func(c net.Conn) {
            defer c.Close()

            reader := bufio.NewReader(c)
            line, _ := reader.ReadString('\n')
            method := strings.Fields(line)[0]

            fmt.Println("HTTP request:", method)

            response := "HTTP/1.1 200 OK\r\n" +
                "Content-Type: text/plain\r\n\r\n" +
                "Hello from raw net!\n"

            c.Write([]byte(response))
        }(conn)
    }
}

You now understand how HTTP works behind the scenes.

14. Deadlines, Keepalives, ReusePort (Performance Topics)

Set keepalive

tcpConn := conn.(*net.TCPConn)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)

SO_REUSEADDR / SO_REUSEPORT

Go exposes them via net.ListenConfig:

lc := net.ListenConfig{
    Control: func(network, address string, c syscall.RawConn) error {
        return c.Control(func(fd uintptr) {
            syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
        })
    },
}
ln, _ := lc.Listen(context.Background(), "tcp", ":9000")

Useful for:

  • high-performance servers
  • fast restarts
  • load balancers

15. Summary

You now have a deep understanding of Go’s net package, including:

  • Low-level TCP/UDP servers and clients
  • How net.Conn and net.Listener work
  • DNS lookups
  • Working with interfaces, IPs, CIDRs
  • Deadlines, keepalives, socket options
  • Writing your own lightweight HTTP server
  • Implementing a TCP proxy
  • Building high-performance concurrent servers using goroutines

With this knowledge, you can build anything from:

  • network tools
  • custom protocols
  • game servers
  • proxies
  • distributed systems
  • microservices

Keep Reading

Follow the engineering thread

Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.

Join newsletter Browse articles