net/go.book
All Parts Marketing

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 net and net/http packages 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 has no timeout by default
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"

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.

Always close the response body
Every *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"

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 #.

Don't build URLs with fmt.Sprintf
Concatenating query parameters with 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"

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 is intentionally minimal
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"

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 defaults to a Go-only encoding
By default 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"

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.

Never ship InsecureSkipVerify: true
Setting 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.Scanner has a default 64KB line-size limit
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's r.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.


  • 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.

Exercise: Simple HTTP Server


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:

  1. Run the server, then connect with nc localhost 9000 (or telnet localhost 9000) from another terminal.
  2. Type a line and press enter — the server echoes it back uppercased.
  3. 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.
  4. Modify handleConn to 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.