A Deep Dive into Go’s net Package: Networking from First Principles
Leeting Yan
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/httpcrypto/tlsnet/rpcgolang.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.Connandnet.Listenerwork - 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