Go Networking Packages Overview
"Go's networking packages are like a box of high-tech LEGO bricks—snap them together and you can build anything from a simple chat app to a global-scale web server!"
What Go's Standard Library Gives You
Go's standard library is unusually complete for a language this size: TCP, UDP, HTTP, DNS, and TLS all ship ready to use, with no third-party package required to get started.
- Compared to most languages: where networking means picking a framework or a request library before you write your first line, Go's own
netandnet/httppackages are already production-grade. - In practice: several large infrastructure projects — Docker and Kubernetes among them — are written in Go partly for this reason; their networking code leans directly on the standard library rather than a third-party stack.
Go ships a production-grade networking stack in its standard library, so "downloading a framework" is the exception, not the starting point.
Core Networking Packages in Go
1. net
The foundation for all things networking in Go. Sockets, TCP, UDP, IP, and more.
import "net"
- Example: Creating a TCP server or client, resolving DNS, working with IP addresses.
- Exercise: TCP Server
The net package is deliberately low-level: it exposes Dial, Listen,
and Conn almost one-to-one with the underlying BSD socket calls, just
with Go's error handling and garbage collection instead of manual
file-descriptor bookkeeping. Every higher-level package in this chapter —
net/http, net/smtp, net/rpc — is built on top of net, not
instead of it. That is why learning net first pays off: once you
understand net.Conn, you already understand what an http.Client or a
smtp.Client is doing underneath.
net.Dial("tcp", addr) blocks forever if the remote host exists but never responds (a firewall silently dropping packets, for example). Prefer net.DialTimeout for anything user-facing, or build a net.Dialer{Timeout: ...} and call its DialContext method so the call also respects a context.Context deadline.2. net/http
The go-to package for building web servers and clients. REST APIs, static sites, and more.
import "net/http"
- Example: Serving web pages, building RESTful APIs, making HTTP requests.
- Exercise: HTTP Server
net/http gives you a production-ready HTTP/1.1 and HTTP/2 client and
server in a handful of function calls, but that convenience hides real
machinery underneath: http.ListenAndServe spins up a net.Listener
internally (so it's still just net, wrapped), and http.Client reuses a
pool of connections (a Transport) so repeated requests to the same host
don't pay the TCP and TLS handshake cost every single time.
*http.Response returned by a successful http.Client call carries an open io.ReadCloser in resp.Body. Forgetting defer resp.Body.Close() means the underlying TCP connection can never be returned to the client's connection pool, and a long-running program will slowly leak sockets until it hits the OS file-descriptor limit.3. net/url
Parse and build URLs without hand-rolling the escaping rules yourself.
import "net/url"
- Example: Parsing query parameters, building URLs for API calls.
- Exercise: URL Parsing
net/url exists because URLs have surprisingly strict rules about which
characters need percent-encoding, and getting query strings right by hand
is easy to get wrong the moment a value contains an &, a space, or a #.
fmt.Sprintf("%s?q=%s", base, input) silently breaks the moment input contains a character like &, =, or %. Build a url.URL (or start from url.Parse), set values through its Query()/url.Values, and call .String() — the package handles escaping for you.4. net/smtp, net/mail
Send and receive emails programmatically.
import "net/smtp"
import "net/mail"
- Example: Sending automated emails, reading email headers.
- Exercise: Send Email
net/smtp speaks raw SMTP: it opens a connection and runs the
EHLO/AUTH/MAIL FROM/RCPT TO/DATA sequence, exposing a Client
that controls each step. net/mail only parses the RFC 5322 headers
and address lists of a message you already have — the two packages solve
different halves of "working with email" and are frequently used
together.
net/smtp has no built-in retry logic or connection pooling, and its PlainAuth helper assumes the connection is already encrypted (usually via STARTTLS). For anything beyond a simple notification email, most Go programs reach for a maintained third-party mail client library rather than driving net/smtp directly against a real-world provider.5. net/rpc
Remote Procedure Calls: one Go program invoking exported methods on another, over the network, with the socket and encoding handled for you.
import "net/rpc"
- Example: Building distributed systems, microservices.
- Exercise: RPC Example
net/rpc lets one Go program call exported methods on another Go program
as though they were local function calls, hiding the socket, the request
framing, and the encoding behind a single Client.Call.
net/rpc encodes requests and responses with encoding/gob, a format only Go programs can read. If a non-Go client needs to talk to your RPC service, swap in the net/rpc/jsonrpc codec, or reach for gRPC (grpc/grpc-go), which speaks Protocol Buffers over HTTP/2 and has first-class support in many languages.6. crypto/tls
Secure your connections with TLS/SSL—because privacy matters.
import "crypto/tls"
- Example: HTTPS servers, encrypted TCP connections.
- Exercise: TLS Server
crypto/tls wraps an existing net.Conn and layers the TLS handshake,
certificate verification, and symmetric encryption on top of it, which is
why you'll often see it used as tls.Client(conn, config) or
tls.Listen("tcp", addr, config) — TLS is a layer over TCP, not a
replacement for it.
tls.Config{InsecureSkipVerify: true} disables certificate verification entirely, so your program will happily connect to an attacker performing a man-in-the-middle attack. It's occasionally useful for local testing against a self-signed certificate, but it should never reach a production build — add the test certificate to a custom RootCAs pool instead.7. bufio
Buffered, line-oriented reading and writing on top of any io.Reader or
io.Writer — including a raw net.Conn.
import "bufio"
- Example: Reading newline-delimited messages from a TCP connection one line at a time instead of juggling raw byte slices.
- Exercise: TCP Server
A raw net.Conn.Read call returns whatever bytes happened to arrive at
that instant — it might be half a message, or three messages stuck
together. Wrapping the connection in a bufio.Scanner or bufio.Reader
lets you ask for "the next line" or "the next delimited message" and have
Go handle the buffering and reassembly for you.
bufio.NewScanner(conn) uses a fixed-size internal buffer and returns a bufio.ErrTooLong error (and simply stops scanning) if a single line exceeds about 64KB. If your protocol allows longer lines or unbounded messages, call scanner.Buffer with a larger maximum size, or switch to bufio.NewReader(conn).ReadString('\n'), which grows its buffer as needed.8. encoding/json
Turn Go structs into JSON bytes on the wire, and JSON bytes back into Go structs — the backbone of nearly every REST API written in Go.
import "encoding/json"
- Example: Encoding an HTTP handler's response, or decoding a JSON
request body straight from
net/http'sr.Body. - Exercise: HTTP Server
encoding/json uses reflection to walk a struct's fields and their
json:"..." tags, so net/http and encoding/json are almost always
used together: json.NewEncoder(w).Encode(v) writes straight to an
http.ResponseWriter, and json.NewDecoder(r.Body).Decode(&v) reads
straight from an incoming request without ever materializing the whole
body as a []byte in memory.
Popular Third-Party Networking Packages
- gorilla/websocket: Real-time, bidirectional communication for web apps.
- gin-gonic/gin: An HTTP web framework built for low overhead.
- go-redis/redis: A client for talking to Redis.
- grpc/grpc-go: Google's RPC framework, built for cross-language services.
Almost every third-party networking library in this list is a thin layer
over the same standard-library interfaces: gin-gonic/gin still runs on
top of net/http.Handler, and grpc-go still opens its connections
through net.Dial underneath. Learning the standard library first means
third-party frameworks read as "the same primitives, with more sugar,"
rather than a completely new mental model.
In Go, third-party networking frameworks rarely replace the standard library — they wrap it.
Real-World Example: Simple HTTP Server
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go HTTP server!")
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
status := map[string]string{"status": "ok"}
json.NewEncoder(w).Encode(status)
}
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/health", healthHandler)
fmt.Println("Server running at http://localhost:8080/")
http.ListenAndServe(":8080", nil)
}
The original handler still serves the plain-text greeting at /. The
new healthHandler shows how net/http and encoding/json combine in
practice: it sets the Content-Type header so clients know to expect
JSON, then streams the response straight through json.NewEncoder
instead of building a []byte first.
Try It Yourself: A Buffered Echo Server
Combine net and bufio to build a tiny TCP server that reads one line
at a time and echoes it back in upper case. This exercise is a good way to
see why bufio sits directly on top of net.Conn rather than replacing
it.
package main
import (
"bufio"
"fmt"
"net"
"strings"
)
func handleConn(conn net.Conn) {
defer conn.Close()
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
line := scanner.Text()
fmt.Fprintln(conn, strings.ToUpper(line))
}
}
func main() {
ln, err := net.Listen("tcp", ":9000")
if err != nil {
fmt.Println("Error:", err)
return
}
defer ln.Close()
fmt.Println("Echo server listening on :9000")
for {
conn, err := ln.Accept()
if err != nil {
fmt.Println("Accept error:", err)
continue
}
go handleConn(conn)
}
}
Try it:
- Run the server, then connect with
nc localhost 9000(ortelnet localhost 9000) from another terminal. - Type a line and press enter — the server echoes it back uppercased.
- Open a second connection at the same time and confirm both are served independently; that's the "one goroutine per connection" model from the DeepDive above, in action.
- Modify
handleConnto also print each received line to the server's own stdout, so you can watch multiple clients being served concurrently from a single process.
Visual Summary
[net] <--- [net/http] <--- [crypto/tls]
| | |
[TCP] [Web] [Security]
| | |
[Your App] [API/Server] [HTTPS]
|
[bufio] --- line framing
|
[encoding/json] --- structured payloads
Frequently Asked Questions
Do I need to learn a framework like Gin before I can build real servers in Go?
No—this chapter's whole point is the opposite. net/http already gives you a production-ready HTTP/1.1 and HTTP/2 server and client, and third-party frameworks like gin-gonic/gin are thin layers on top of http.Handler, not replacements for it. Learning net, net/http, and friends first means frameworks read as "the same primitives with more sugar" rather than an entirely new mental model.
Why does a Go server handle thousands of connections with plain blocking calls like conn.Read(), when other languages need callbacks or an event loop to do the same?
Because the blocking is only blocking from the goroutine's point of view. When a goroutine calls conn.Read() or http.Get(), the Go runtime scheduler parks that goroutine and frees up the underlying OS thread to run other goroutines in the meantime. You get to write plain synchronous-looking code while the runtime gives you the throughput of an asynchronous event loop underneath.
My program that calls net.Dial hangs forever when the remote host is silently dropping packets—why doesn't Dial just time out on its own?
Because net.Dial has no timeout by default; it will wait as long as the OS lets it, which for a firewall silently dropping packets can be effectively forever. Use net.DialTimeout instead, or build a net.Dialer{Timeout: ...} and call its DialContext method so the connection attempt also respects a context.Context deadline.
Why did my bufio.Scanner just stop reading in the middle of a long line instead of erroring loudly?
It very likely hit the scanner's default 64KB line-size limit and returned bufio.ErrTooLong, quietly halting instead of continuing. If your protocol allows longer lines, call scanner.Buffer with a larger maximum, or switch to bufio.NewReader(conn).ReadString('\n'), which grows its buffer as needed instead of capping it.
Should I use json.Marshal or json.NewEncoder when writing a JSON response from an HTTP handler?
For the tiny healthHandler example in this chapter it barely matters, but json.NewEncoder(w).Encode(v) is the better habit: it streams directly to the http.ResponseWriter without ever building the whole payload as a []byte in memory first. json.Marshal is simplest for small, one-off payloads, but once responses reach megabytes, streaming through an encoder is the difference between steady memory use and a memory spike per request.