GoLang Networking & Security: Building Robust HTTP Services & Crypto
Go is the language of the cloud. This guide explores the `net` and `crypto` packages, teaching you to build performant HTTP/2 servers, manage connection pooling, and secure data with proper encryption standards.
Networking
net package
The net package provides portable low-level networking primitives for TCP, UDP, Unix sockets, and name resolution. It's the foundation for all network communication in Go, implementing Dial/Listen patterns.
import "net" // Common network types // "tcp", "tcp4", "tcp6" - TCP // "udp", "udp4", "udp6" - UDP // "unix", "unixpacket" - Unix domain sockets // "ip", "ip4", "ip6" - Raw IP (requires root) // Address types addr, _ := net.ResolveTCPAddr("tcp", "localhost:8080") addr, _ := net.ResolveUDPAddr("udp", "localhost:8080") // Interface enumeration ifaces, _ := net.Interfaces() for _, iface := range ifaces { addrs, _ := iface.Addrs() fmt.Println(iface.Name, addrs) }
TCP connections (net.Dial, net.Listen)
net.Dial creates outbound connections while net.Listen accepts inbound connections. Both return interfaces that abstract the network type, making code reusable across protocols.
// Client: Connect to server conn, err := net.Dial("tcp", "example.com:80") if err != nil { log.Fatal(err) } defer conn.Close() conn.Write([]byte("GET / HTTP/1.0\r\n\r\n")) io.Copy(os.Stdout, conn) // Server: Accept connections ln, err := net.Listen("tcp", ":8080") if err != nil { log.Fatal(err) } defer ln.Close() for { conn, err := ln.Accept() if err != nil { continue } go handleConnection(conn) // Handle in goroutine }
UDP connections
UDP uses net.DialUDP/net.ListenUDP for connected sockets or net.ListenPacket for unconnected. UDP is connectionless—no guarantees on delivery, order, or duplication. Use for real-time, low-latency scenarios.
// UDP Client conn, _ := net.Dial("udp", "server:1234") defer conn.Close() conn.Write([]byte("Hello UDP")) buf := make([]byte, 1024) n, _ := conn.Read(buf) fmt.Println(string(buf[:n])) // UDP Server addr, _ := net.ResolveUDPAddr("udp", ":1234") conn, _ := net.ListenUDP("udp", addr) defer conn.Close() buf := make([]byte, 1024) for { n, remoteAddr, _ := conn.ReadFromUDP(buf) fmt.Printf("Received from %s: %s\n", remoteAddr, buf[:n]) conn.WriteToUDP([]byte("ACK"), remoteAddr) }
net.Conn interface
net.Conn is the interface for all network connections, providing Read/Write/Close methods plus address info and deadline control. Write code against this interface for protocol-agnostic handling.
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 } func handleConnection(conn net.Conn) { defer conn.Close() log.Printf("Client: %s", conn.RemoteAddr()) conn.SetDeadline(time.Now().Add(30 * time.Second)) buf := make([]byte, 4096) n, err := conn.Read(buf) // ... }
net.Listener interface
net.Listener is the interface for network servers, abstracting the accept loop. It works with TCP, Unix sockets, and any other connection-oriented protocol. Always close listeners when shutting down.
type Listener interface { Accept() (Conn, error) Close() error Addr() Addr } func serve(ln net.Listener) { defer ln.Close() for { conn, err := ln.Accept() if err != nil { if ne, ok := err.(net.Error); ok && ne.Temporary() { time.Sleep(time.Millisecond * 100) continue } return // Listener closed } go handle(conn) } }
Connection deadlines
Deadlines prevent connections from blocking forever, essential for robust network code. Set deadlines before each operation; they're absolute times, not durations. Reset with zero time.Time to disable.
conn.SetDeadline(time.Now().Add(30 * time.Second)) // All ops conn.SetReadDeadline(time.Now().Add(10 * time.Second)) // Read only conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) // Write only // Typical server pattern for { conn.SetReadDeadline(time.Now().Add(30 * time.Second)) n, err := conn.Read(buf) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { // Handle timeout continue } return err } // Process data... conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) conn.Write(response) } // Remove deadline conn.SetDeadline(time.Time{})
IP address parsing
The net package provides types and functions for parsing and manipulating IP addresses. net.IP represents addresses, net.IPNet represents CIDR networks, and parsing functions validate input.
// Parse IP address ip := net.ParseIP("192.168.1.1") // Returns nil if invalid ip := net.ParseIP("::1") // IPv6 loopback // Check IP properties ip.IsLoopback() ip.IsPrivate() // RFC 1918 ip.IsGlobalUnicast() ip.To4() // Returns nil if IPv6 ip.To16() // Always works // CIDR parsing ip, network, _ := net.ParseCIDR("192.168.1.0/24") network.Contains(net.ParseIP("192.168.1.100")) // true // IP from string (host:port) host, port, _ := net.SplitHostPort("192.168.1.1:8080") addr := net.JoinHostPort("::1", "8080") // "[::1]:8080"
URL parsing (net/url)
The net/url package parses URLs per RFC 3986 and builds/encodes query strings. Use url.Parse for parsing, url.Values for query parameters, and url.PathEscape/QueryEscape for encoding.
import "net/url" u, err := url.Parse("https://user:pass@example.com:8080/path?q=1#frag") u.Scheme // "https" u.User // *Userinfo (user:pass) u.Host // "example.com:8080" u.Hostname()// "example.com" u.Port() // "8080" u.Path // "/path" u.RawQuery // "q=1" u.Fragment // "frag" // Query parameters q := u.Query() // url.Values (map[string][]string) q.Get("q") // "1" q.Add("key", "value") u.RawQuery = q.Encode() // "key=value&q=1" // Build URL u := &url.URL{ Scheme: "https", Host: "example.com", Path: "/search", } u.String() // "https://example.com/search"
DNS lookup
Go provides DNS resolution through net.Lookup* functions. These use the system resolver by default but can be configured. Results are cached internally, and IPv4/IPv6 can be preferred.
// Lookup IP addresses ips, err := net.LookupIP("google.com") for _, ip := range ips { fmt.Println(ip) } // Lookup specific record types addrs, _ := net.LookupHost("example.com") // []string cname, _ := net.LookupCNAME("www.example.com") // string mxs, _ := net.LookupMX("example.com") // []*MX txts, _ := net.LookupTXT("example.com") // []string nss, _ := net.LookupNS("example.com") // []*NS // Reverse lookup names, _ := net.LookupAddr("8. 8.8.8") // ["dns.google."] // Custom resolver with timeout resolver := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { d := net.Dialer{Timeout: 5 * time.Second} return d.DialContext(ctx, "udp", "8.8.8.8:53") }, } ips, _ := resolver.LookupIP(context.Background(), "ip4", "example.com")
HTTP Client
net/http package
The net/http package provides HTTP client and server implementations. It's production-ready out of the box with connection pooling, TLS, timeouts, and HTTP/2 support. Most Go web applications use it directly or through frameworks built on it.
import "net/http" // Simple GET (uses default client) resp, err := http.Get("https://api.example.com/data") if err != nil { log.Fatal(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) // Simple POST resp, err := http.Post(url, "application/json", strings.NewReader(`{"key":"value"}`)) // Form POST resp, err := http.PostForm(url, url.Values{ "username": {"alice"}, "password": {"secret"}, })
http.Client
http.Client manages HTTP connections, cookies, redirects, and timeouts. Always create a custom client with timeouts for production use—the default client has no timeout!
// ⚠️ Default client - no timeout, dangerous! http.DefaultClient.Get(url) // ✅ Custom client with timeouts client := &http.Client{ Timeout: 30 * time.Second, // Total request timeout CheckRedirect: func(req *http.Request, via []*http.Request) error { if len(via) >= 10 { return errors.New("too many redirects") } return nil }, } resp, err := client.Get(url) // Reuse clients - they're goroutine-safe and pool connections var httpClient = &http.Client{Timeout: 30 * time.Second}
http.Get, http.Post
These convenience functions use the default client for simple requests. Fine for scripts and examples, but for production code, use a custom http.Client with proper timeouts.
// GET request resp, err := http.Get("https://api.example.com/users") if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("status: %d", resp.StatusCode) } var users []User json.NewDecoder(resp.Body).Decode(&users) // POST with JSON data := bytes.NewBuffer(jsonBytes) resp, err := http.Post(url, "application/json", data) // POST form data resp, err := http.PostForm(url, url.Values{ "email": {"user@example.com"}, })
http.NewRequest
http.NewRequest creates a request for full control over method, headers, and body. Use with Client.Do() for custom HTTP methods, headers, or context support.
// Create request req, err := http.NewRequest("GET", "https://api.example.com/users", nil) if err != nil { return err } // Add headers req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept", "application/json") // Execute request client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) // With context (for cancellation/timeout) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := client.Do(req)
Request headers
HTTP headers are accessed via http.Header, a map[string][]string. Use Set for single values, Add for multiple values, and Get for reading. Header names are canonicalized automatically.
req, _ := http.NewRequest("POST", url, body) // Set headers (overwrites) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("User-Agent", "MyApp/1.0") // Add headers (appends) req.Header.Add("Accept", "application/json") req.Header.Add("Accept", "text/plain") // Multiple values // Get header ct := req.Header.Get("Content-Type") // Delete header req.Header.Del("User-Agent") // Iterate all headers for name, values := range req.Header { fmt.Printf("%s: %v\n", name, values) }
Request body
The request body is an io.Reader that provides data for POST, PUT, and PATCH requests. Common sources are bytes.Buffer, strings.Reader, and bytes.NewReader. The body is consumed once—you can't read it twice.
// JSON body jsonData := []byte(`{"name":"Alice","age":30}`) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") // From struct user := User{Name: "Alice", Age: 30} body, _ := json.Marshal(user) req, _ := http.NewRequest("POST", url, bytes.NewReader(body)) // Streaming large body file, _ := os.Open("large.bin") defer file.Close() req, _ := http.NewRequest("PUT", url, file) req.ContentLength = fileSize // Optional but helpful // io.Pipe for generated content pr, pw := io.Pipe() go func() { json.NewEncoder(pw).Encode(largeData) pw.Close() }() req, _ := http.NewRequest("POST", url, pr)
Response handling
The http.Response contains status, headers, and body. Always close the body (even on error responses), check status codes, and handle the body before checking errors from body operations.
resp, err := client.Do(req) if err != nil { return err // Network error } defer resp.Body.Close() // ALWAYS close! // Check status if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("status %d: %s", resp.StatusCode, body) } // Read body body, err := io.ReadAll(resp.Body) // Or decode directly var result Result err = json.NewDecoder(resp.Body).Decode(&result) // Response properties resp.Status // "200 OK" resp.StatusCode // 200 resp.Header // http.Header resp.ContentLength // -1 if unknown resp.Cookies() // []*Cookie
Custom transport
http.Transport controls low-level connection behavior: TLS, proxies, connection pooling, and timeouts. Customize it for specific requirements like custom CAs, proxy settings, or connection limits.
transport := &http.Transport{ // Connection pooling MaxIdleConns: 100, MaxIdleConnsPerHost: 10, MaxConnsPerHost: 100, IdleConnTimeout: 90 * time.Second, // Timeouts TLSHandshakeTimeout: 10 * time.Second, ResponseHeaderTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, // TLS config TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS12, }, // Proxy Proxy: http.ProxyFromEnvironment, // Proxy: http.ProxyURL(proxyURL), } client := &http.Client{ Transport: transport, Timeout: 30 * time.Second, }
Connection pooling
http.Transport maintains a pool of idle connections for reuse. Configure pool sizes based on your workload—too small causes connection churn, too large wastes resources. One client should be shared across goroutines.
transport := &http.Transport{ MaxIdleConns: 100, // Total idle connections MaxIdleConnsPerHost: 10, // Per-host idle connections MaxConnsPerHost: 0, // 0 = unlimited IdleConnTimeout: 90 * time.Second, // Close idle after this } // ✅ Share one client - connections are pooled var client = &http.Client{Transport: transport} // ❌ Don't create client per request for i := 0; i < 1000; i++ { client := &http.Client{} // Bad! client.Get(url) } // Check pool stats (for debugging) // transport.CloseIdleConnections() // Force close all idle
Timeouts
Multiple timeouts control different phases of a request. Set Client.Timeout for total request timeout, or use Transport settings and context for fine-grained control.
┌────────────────────────────────────────────────────────────────────────┐ │ Request Timeline │ ├────────────────────────────────────────────────────────────────────────┤ │ │ │ [Dial] → [TLS] → [Send Request] → [Wait Headers] → [Read Body] │ │ └─────────────────────────────────────────────────────────────┘ │ │ Client.Timeout (total) │ │ │ │ Transport.DialTimeout (legacy, use DialContext) │ │ Transport.TLSHandshakeTimeout │ │ Transport.ResponseHeaderTimeout │ │ │ └────────────────────────────────────────────────────────────────────────┘
client := &http.Client{ Timeout: 30 * time.Second, // Total timeout Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 5 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, TLSHandshakeTimeout: 5 * time.Second, ResponseHeaderTimeout: 10 * time.Second, }, }
Redirects
By default, http.Client follows up to 10 redirects. Customize with CheckRedirect to limit, prevent, or log redirects. Return http.ErrUseLastResponse to stop but not error.
client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { // Limit redirects if len(via) >= 5 { return errors.New("too many redirects") } // Don't follow cross-domain redirects if req.URL.Host != via[0].URL.Host { return http.ErrUseLastResponse } // Copy auth header to redirect if auth := via[0].Header.Get("Authorization"); auth != "" { req.Header.Set("Authorization", auth) } return nil }, } // Disable redirects completely client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, }
Cookies
http.Client uses a CookieJar to store and send cookies automatically. Use cookiejar.New() for a standard jar, or implement http.CookieJar for custom storage.
import "net/http/cookiejar" jar, _ := cookiejar.New(nil) client := &http.Client{ Jar: jar, Timeout: 30 * time.Second, } // Cookies are automatically stored and sent client.Get("https://example.com/login") client.Get("https://example.com/dashboard") // Sends cookies // Manual cookie handling req, _ := http.NewRequest("GET", url, nil) req.AddCookie(&http.Cookie{Name: "session", Value: "abc123"}) // Read cookies from response for _, cookie := range resp.Cookies() { fmt.Printf("%s = %s\n", cookie.Name, cookie.Value) }
Client authentication
HTTP authentication can be Basic, Bearer token, API key, or OAuth. Set the appropriate Authorization header or implement custom authentication in a RoundTripper.
// Basic Auth req, _ := http.NewRequest("GET", url, nil) req.SetBasicAuth("username", "password") // Sets: Authorization: Basic base64(username:password) // Bearer Token req.Header.Set("Authorization", "Bearer "+accessToken) // API Key (header) req.Header.Set("X-API-Key", apiKey) // API Key (query parameter) u, _ := url.Parse(baseURL) q := u.Query() q.Set("api_key", apiKey) u.RawQuery = q.Encode() // Custom RoundTripper for automatic auth type authTransport struct { token string base http.RoundTripper } func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) { req.Header.Set("Authorization", "Bearer "+t.token) return t.base.RoundTrip(req) }
Multipart requests
Multipart form data is used for file uploads and mixed content. Use multipart.Writer to create the body, then set the correct Content-Type header with the boundary.
import "mime/multipart" var body bytes.Buffer writer := multipart.NewWriter(&body) // Add form field writer.WriteField("name", "Alice") writer.WriteField("email", "alice@example.com") // Add file file, _ := os.Open("photo.jpg") defer file.Close() part, _ := writer.CreateFormFile("avatar", "photo.jpg") io.Copy(part, file) // Add file from memory part, _ := writer.CreateFormFile("document", "data.json") part.Write(jsonData) writer.Close() // Must close before sending! req, _ := http.NewRequest("POST", url, &body) req.Header.Set("Content-Type", writer.FormDataContentType()) client.Do(req)
HTTP Server
http.Server
http.Server is the core HTTP server type providing full control over address, timeouts, TLS, and shutdown behavior. For production, always configure timeouts to prevent resource exhaustion.
server := &http.Server{ Addr: ":8080", Handler: mux, // or nil for DefaultServeMux // Timeouts ReadTimeout: 5 * time.Second, ReadHeaderTimeout: 2 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, // Limits MaxHeaderBytes: 1 << 20, // 1 MB // TLS TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}, // Logging ErrorLog: log.New(os.Stderr, "http: ", log.LstdFlags), } // Start server log.Fatal(server.ListenAndServe()) // Or with TLS: log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
http.Handler interface
http.Handler is the core interface for HTTP request handling. Any type implementing ServeHTTP(ResponseWriter, *Request) can handle HTTP requests, enabling composition and middleware.
type Handler interface { ServeHTTP(ResponseWriter, *Request) } // Custom handler type type helloHandler struct { greeting string } func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%s, World!", h.greeting) } mux := http.NewServeMux() mux.Handle("/hello", &helloHandler{greeting: "Hello"}) // Handlers are composable type loggingHandler struct { next http.Handler } func (h *loggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL.Path) h.next.ServeHTTP(w, r) }
http.HandlerFunc
http.HandlerFunc is a function adapter that allows using ordinary functions as http.Handler. Just cast a function with the right signature—it's the most common way to create handlers.
// HandlerFunc is a type adapter type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } // Use a plain function as handler func helloHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, World!") } mux := http.NewServeMux() mux.HandleFunc("/hello", helloHandler) // Or explicitly convert mux.Handle("/hello", http.HandlerFunc(helloHandler)) // Inline handler mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello")) })
http.ListenAndServe
http.ListenAndServe starts an HTTP server with default settings. For production, create an http.Server with timeouts instead. It blocks until the server shuts down.
// Quick start (defaults, no timeouts - avoid in production) http.HandleFunc("/", homeHandler) log.Fatal(http.ListenAndServe(":8080", nil)) // nil = DefaultServeMux // With custom mux mux := http.NewServeMux() mux.HandleFunc("/", homeHandler) log.Fatal(http.ListenAndServe(":8080", mux)) // HTTPS log.Fatal(http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux)) // ⚠️ For production, use http.Server with timeouts! server := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } log.Fatal(server.ListenAndServe())
ServeMux (http.NewServeMux)
ServeMux is Go's HTTP request router (multiplexer). It matches URL patterns to handlers, supporting exact matches and subtree patterns (ending with /). Go 1.22+ adds method matching and path parameters.
mux := http.NewServeMux() // Exact match mux.HandleFunc("/about", aboutHandler) // Only /about // Subtree match (trailing /) mux.HandleFunc("/api/", apiHandler) // /api/* // Root mux.HandleFunc("/", homeHandler) // Catch-all // Go 1.22+: Method matching mux.HandleFunc("GET /users", listUsers) mux.HandleFunc("POST /users", createUser) // Go 1.22+: Path parameters mux.HandleFunc("GET /users/{id}", getUser) mux.HandleFunc("DELETE /users/{id}", deleteUser) // Pattern priority: most specific wins // /users/new matches before /users/{id} http.ListenAndServe(":8080", mux)
Handler patterns
URL patterns determine routing priority. Longer patterns take precedence, exact matches beat subtrees, and method-specific patterns (Go 1.22+) beat generic ones. Host-specific patterns are also supported.
mux := http.NewServeMux() // Pattern matching order (most to least specific): // 1. Exact method + host + path // 2. Exact method + path // 3. Host + path // 4. Path only // 5. Longer paths before shorter mux.HandleFunc("GET /users", listUsers) // Exact mux.HandleFunc("/users/", usersHandler) // Subtree mux.HandleFunc("/users/profile", profileHandler) // More specific // Host-specific patterns mux.HandleFunc("api.example.com/", apiHandler) mux.HandleFunc("www.example.com/", webHandler) // Go 1.22+ wildcards mux.HandleFunc("/files/{path...}", serveFiles) // Matches rest of path mux.HandleFunc("/users/{id}/posts/{pid}", getPost)
Middleware pattern
Middleware wraps handlers to add cross-cutting concerns like logging, authentication, and recovery. Chain them by nesting: middleware1(middleware2(handler)). Return http.Handler for composability.
// Middleware signature func middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Before next.ServeHTTP(w, r) // After }) } // Logging middleware func logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) }) } // Recovery middleware func recovery(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { http.Error(w, "Internal Server Error", 500) } }() next.ServeHTTP(w, r) }) } // Chain middleware handler := recovery(logging(mux))
Request routing
Beyond ServeMux, extract routing info from requests using path matching, method checking, and path parameters (Go 1.22+). Many use third-party routers for complex routing needs.
func handler(w http.ResponseWriter, r *http.Request) { // Method switch r.Method { case http.MethodGet: handleGet(w, r) case http.MethodPost: handlePost(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } // Go 1.22+: Path parameters mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") // ... }) // Manual path parsing (older Go) func userHandler(w http.ResponseWriter, r *http.Request) { parts := strings.Split(r.URL.Path, "/") // /users/123 → ["", "users", "123"] if len(parts) >= 3 { id := parts[2] } }
Path parameters
Go 1.22+ added built-in path parameters using {name} syntax in patterns. Access values with r.PathValue("name"). Use {name...} to capture the rest of the path.
// Go 1.22+ path parameters mux := http.NewServeMux() mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") fmt.Fprintf(w, "User ID: %s", id) }) mux.HandleFunc("GET /posts/{postID}/comments/{commentID}", func(w http.ResponseWriter, r *http.Request) { postID := r.PathValue("postID") commentID := r.PathValue("commentID") }) // Wildcard - captures rest of path mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) { path := r.PathValue("path") // "subdir/file.txt" }) // Before Go 1.22: use gorilla/mux, chi, or parse manually
Query parameters
Query parameters are accessed via r.URL.Query(), returning a url.Values (map of string slices). Use Get for single values, iterate for multiple values of the same key.
func handler(w http.ResponseWriter, r *http.Request) { // /search?q=golang&page=2&tag=web&tag=api query := r.URL.Query() // Single value q := query.Get("q") // "golang" page := query.Get("page") // "2" missing := query.Get("foo") // "" (empty if not present) // Check existence if _, ok := query["page"]; ok { // page parameter exists } // Multiple values tags := query["tag"] // []string{"web", "api"} // Parse to int pageNum, err := strconv.Atoi(query.Get("page")) if err != nil { pageNum = 1 // Default } }
Form parsing
Form data from POST requests is parsed with r.ParseForm() or r.ParseMultipartForm(). Access values via r.Form (all sources) or r.PostForm (body only).
func handler(w http.ResponseWriter, r *http.Request) { // Parse URL-encoded form (application/x-www-form-urlencoded) if err := r.ParseForm(); err != nil { http.Error(w, "Bad request", 400) return } // r.Form includes both URL query and POST body name := r.FormValue("name") // Shortcut (auto-parses) email := r.PostFormValue("email") // POST body only // r.PostForm - POST body values only // r.Form - merged query + POST values // For multipart/form-data (file uploads) r.ParseMultipartForm(10 << 20) // 10 MB max name := r.FormValue("name") }
File uploads
File uploads use multipart/form-data encoding. Parse with ParseMultipartForm, then access files via r.FormFile() or r.MultipartForm.File. Always limit upload size.
func uploadHandler(w http.ResponseWriter, r *http.Request) { // Limit size (before parsing) r.Body = http.MaxBytesReader(w, r.Body, 10<<20) // 10 MB // Parse multipart form if err := r.ParseMultipartForm(10 << 20); err != nil { http.Error(w, "File too large", 400) return } // Get file file, header, err := r.FormFile("upload") if err != nil { http.Error(w, "No file", 400) return } defer file.Close() fmt.Printf("Filename: %s, Size: %d\n", header.Filename, header.Size) // Save file dst, _ := os.Create("/uploads/" + header.Filename) defer dst.Close() io.Copy(dst, file) // Multiple files files := r.MultipartForm.File["uploads"] for _, fh := range files { f, _ := fh.Open() defer f.Close() // Process each file } }
Response writing
http.ResponseWriter writes the response. Write headers first, then status code, then body. Once you write body data, headers and status are implicitly sent.
func handler(w http.ResponseWriter, r *http.Request) { // Set headers BEFORE WriteHeader or Write w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Custom-Header", "value") // Set status code w.WriteHeader(http.StatusCreated) // 201 // Write body w.Write([]byte(`{"status":"created"}`)) // Or use helpers fmt.Fprintf(w, "Hello, %s!", name) io.WriteString(w, "Hello!") // JSON response pattern w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) // ⚠️ Order matters: // 1. Set headers // 2. WriteHeader (optional, defaults to 200) // 3. Write body }
Status codes
Use http.Status* constants for readable, self-documenting code. Call WriteHeader before writing body; if omitted, 200 is used automatically.
// Common status codes http.StatusOK // 200 http.StatusCreated // 201 http.StatusNoContent // 204 http.StatusMovedPermanently // 301 http.StatusFound // 302 http.StatusBadRequest // 400 http.StatusUnauthorized // 401 http.StatusForbidden // 403 http.StatusNotFound // 404 http.StatusMethodNotAllowed // 405 http.StatusInternalServerError // 500 http.StatusServiceUnavailable // 503 // Usage w.WriteHeader(http.StatusNotFound) w.Write([]byte("Not found")) // Helper for errors http.Error(w, "Not found", http.StatusNotFound) // Redirect http.Redirect(w, r, "/new-location", http.StatusMovedPermanently)
Headers
Response headers are set via w.Header() before calling WriteHeader or Write. Common headers include Content-Type, Cache-Control, and custom headers for APIs.
func handler(w http.ResponseWriter, r *http.Request) { h := w.Header() // Content type h.Set("Content-Type", "application/json; charset=utf-8") // Caching h.Set("Cache-Control", "max-age=3600") h.Set("ETag", `"abc123"`) // CORS h.Set("Access-Control-Allow-Origin", "*") h.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") // Security headers h.Set("X-Content-Type-Options", "nosniff") h.Set("X-Frame-Options", "DENY") h.Set("Strict-Transport-Security", "max-age=31536000") // Custom headers h.Set("X-Request-ID", requestID) w.WriteHeader(http.StatusOK) w.Write(body) }
Cookies
Set cookies via http.SetCookie and read via r.Cookie or r.Cookies. Always set security attributes like HttpOnly, Secure, and SameSite for sensitive cookies.
// Set cookie func loginHandler(w http.ResponseWriter, r *http.Request) { cookie := &http.Cookie{ Name: "session", Value: sessionToken, Path: "/", HttpOnly: true, // Not accessible via JavaScript Secure: true, // HTTPS only SameSite: http.SameSiteStrictMode, MaxAge: 3600, // 1 hour (0 = session cookie) // Expires: time.Now().Add(time.Hour), // Alternative to MaxAge } http.SetCookie(w, cookie) } // Read cookie func handler(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err == http.ErrNoCookie { // No cookie return } sessionToken := cookie.Value } // Delete cookie (set MaxAge to negative) http.SetCookie(w, &http.Cookie{Name: "session", MaxAge: -1})
Sessions
Go's standard library doesn't include sessions—use cookies for simple cases or a session library like gorilla/sessions for encrypted, server-side sessions with various storage backends.
// Simple: encrypted cookie (client-side session) import "github.com/gorilla/sessions" var store = sessions.NewCookieStore([]byte("secret-key-32-bytes-long!!!!!!!!")) func handler(w http.ResponseWriter, r *http.Request) { session, _ := store.Get(r, "session-name") // Read userID := session.Values["user_id"] // Write session.Values["user_id"] = 123 session.Options.MaxAge = 3600 session.Save(r, w) } // Server-side sessions (Redis, database, etc.) // Store only session ID in cookie, data in backend import "github.com/gorilla/sessions" import "github.com/rbcervilla/redisstore/v9" store, _ := redisstore.NewRedisStore(context.Background(), redisClient)
Graceful shutdown
Graceful shutdown lets the server finish handling in-flight requests before stopping. Use server.Shutdown(ctx) with a context timeout, triggered by OS signals.
func main() { server := &http.Server{ Addr: ":8080", Handler: mux, } // Run server in goroutine go func() { if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatal(err) } }() // Wait for interrupt quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit // Graceful shutdown with timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Printf("Shutdown error: %v", err) } log.Println("Server stopped") }
HTTP/2
Go automatically uses HTTP/2 for HTTPS connections with compatible clients. For explicit HTTP/2 (including h2c - HTTP/2 over cleartext), use golang.org/x/net/http2.
// Automatic HTTP/2 with TLS server := &http.Server{ Addr: ":443", Handler: mux, } server.ListenAndServeTLS("cert.pem", "key.pem") // HTTP/2 works automatically! // Explicit HTTP/2 configuration import "golang.org/x/net/http2" server := &http.Server{Addr: ":443", Handler: mux} http2.ConfigureServer(server, &http2.Server{ MaxConcurrentStreams: 250, }) // HTTP/2 cleartext (h2c) - without TLS import "golang.org/x/net/http2/h2c" h2s := &http2.Server{} server := &http.Server{ Addr: ":8080", Handler: h2c.NewHandler(mux, h2s), }
HTTPS/TLS
Configure TLS with tls.Config for security settings. Use Let's Encrypt with autocert for free, automatic certificates. Always set minimum TLS version to 1.2+.
import "crypto/tls" tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, }, PreferServerCipherSuites: true, } server := &http.Server{ Addr: ":443", Handler: mux, TLSConfig: tlsConfig, } server.ListenAndServeTLS("cert.pem", "key.pem") // Auto TLS with Let's Encrypt import "golang.org/x/crypto/acme/autocert" m := &autocert.Manager{ Cache: autocert.DirCache("/var/www/.cache"), Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist("example.com"), } server := &http.Server{ Addr: ":443", Handler: mux, TLSConfig: m.TLSConfig(), }
Cryptography
crypto package overview
Go's crypto package provides cryptographic primitives: hashing, encryption, random numbers, and TLS. Use high-level packages (crypto/tls, crypto/rand) over low-level ones, and never implement crypto yourself.
┌──────────────────────────────────────────────────────────┐
│ crypto packages │
├──────────────────────────────────────────────────────────┤
│ Hashing: crypto/sha256, sha512, md5, sha1 │
│ Symmetric: crypto/aes, cipher │
│ Asymmetric: crypto/rsa, ecdsa, ed25519 │
│ Random: crypto/rand │
│ TLS: crypto/tls, crypto/x509 │
│ MAC: crypto/hmac │
│ Extended: golang.org/x/crypto (bcrypt, argon2...) │
└──────────────────────────────────────────────────────────┘
// Use crypto/rand for secure random, NOT math/rand! import "crypto/rand" // Avoid deprecated/weak algorithms: // ❌ md5, sha1, des, rc4 // ✅ sha256, sha512, aes-gcm, chacha20-poly1305
crypto/md5 (avoid for security)
MD5 produces a 128-bit hash but is cryptographically broken—collisions can be generated quickly. Use only for checksums and legacy compatibility, never for security (passwords, signatures).
import "crypto/md5" // ⚠️ MD5 is NOT secure for cryptographic purposes! // Only use for checksums, cache keys, etc. data := []byte("hello world") hash := md5.Sum(data) // [16]byte fmt.Printf("%x\n", hash) // Streaming h := md5.New() h.Write([]byte("hello ")) h.Write([]byte("world")) hash := h.Sum(nil) // []byte // File checksum (non-security use case) f, _ := os.Open("file.bin") h := md5.New() io.Copy(h, f) fmt.Printf("%x\n", h.Sum(nil))
crypto/sha1, crypto/sha256, crypto/sha512
SHA-2 family (sha256, sha512) are secure hash functions for integrity verification and as building blocks for other crypto. SHA-1 is deprecated for security but still used in legacy systems.
import ( "crypto/sha1" // 160-bit, deprecated for security "crypto/sha256" // 256-bit, recommended "crypto/sha512" // 512-bit, extra security margin ) data := []byte("hello world") // SHA-256 (most common) hash := sha256.Sum256(data) // [32]byte fmt.Printf("%x\n", hash) // SHA-512 hash512 := sha512.Sum512(data) // [64]byte // SHA-512/256 (truncated, same speed as SHA-512) hash := sha512.Sum512_256(data) // [32]byte // Streaming h := sha256.New() io.Copy(h, file) hash := h.Sum(nil)
crypto/rand
crypto/rand provides cryptographically secure random numbers using OS entropy sources. Use it for keys, tokens, nonces—anything security-related. Never use math/rand for crypto!
import "crypto/rand" // Read random bytes buf := make([]byte, 32) _, err := rand.Read(buf) // Random big integer import "crypto/rand" import "math/big" max := big.NewInt(1000) n, _ := rand.Int(rand.Reader, max) // 0 <= n < 1000 // Random token (URL-safe) import "encoding/base64" token := make([]byte, 32) rand.Read(token) encoded := base64.URLEncoding.EncodeToString(token) // ⚠️ Common mistake: // import "math/rand" // rand.Intn(100) // NOT secure! Predictable!
crypto/aes
AES is the standard symmetric encryption algorithm. Use AES-GCM (Galois/Counter Mode) for authenticated encryption—it provides both confidentiality and integrity. Never use ECB mode.
import ( "crypto/aes" "crypto/cipher" "crypto/rand" ) // AES-GCM encryption (recommended) func encrypt(plaintext, key []byte) ([]byte, error) { block, _ := aes.NewCipher(key) // Key: 16, 24, or 32 bytes gcm, _ := cipher.NewGCM(block) nonce := make([]byte, gcm.NonceSize()) // 12 bytes rand.Read(nonce) // Seal appends encrypted data to nonce ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) return ciphertext, nil } func decrypt(ciphertext, key []byte) ([]byte, error) { block, _ := aes.NewCipher(key) gcm, _ := cipher.NewGCM(block) nonceSize := gcm.NonceSize() nonce, encrypted := ciphertext[:nonceSize], ciphertext[nonceSize:] return gcm.Open(nil, nonce, encrypted, nil) }
crypto/rsa
RSA provides asymmetric encryption and digital signatures. Use 2048+ bit keys (4096 for long-term), OAEP for encryption, and PSS for signatures. Consider ECDSA/Ed25519 for new applications.
import "crypto/rsa" // Generate key pair privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) publicKey := &privateKey.PublicKey // Encrypt with OAEP (recommended) ciphertext, _ := rsa.EncryptOAEP( sha256.New(), rand.Reader, publicKey, plaintext, nil, // label ) // Decrypt plaintext, _ := rsa.DecryptOAEP( sha256.New(), rand.Reader, privateKey, ciphertext, nil, ) // Sign with PSS (recommended) hash := sha256.Sum256(message) signature, _ := rsa.SignPSS(rand.Reader, privateKey, crypto.SHA256, hash[:], nil) // Verify err := rsa.VerifyPSS(publicKey, crypto.SHA256, hash[:], signature, nil)
crypto/ecdsa
ECDSA provides asymmetric signatures with smaller keys than RSA. Use P-256 (secp256r1) for compatibility or P-384/P-521 for higher security. Consider Ed25519 for simpler, faster signatures.
import ( "crypto/ecdsa" "crypto/elliptic" ) // Generate key privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) publicKey := &privateKey.PublicKey // Sign hash := sha256.Sum256(message) signature, _ := ecdsa.SignASN1(rand.Reader, privateKey, hash[:]) // Verify valid := ecdsa.VerifyASN1(publicKey, hash[:], signature) // Available curves elliptic.P224() // Rarely used elliptic.P256() // Most common, NIST recommended elliptic.P384() // Higher security elliptic.P521() // Maximum security // For simpler API, consider Ed25519: import "crypto/ed25519" pub, priv, _ := ed25519.GenerateKey(rand.Reader) sig := ed25519.Sign(priv, message) valid := ed25519.Verify(pub, message, sig)
crypto/tls
The crypto/tls package implements TLS 1.2 and 1.3 for secure connections. Configure cipher suites, certificate verification, and client authentication. Use with http.Server or net.Dial.
import "crypto/tls" // Server TLS config serverConfig := &tls.Config{ MinVersion: tls.VersionTLS12, CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, }, Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAndVerifyClientCert, // mTLS ClientCAs: clientCertPool, } // Client TLS config clientConfig := &tls.Config{ MinVersion: tls.VersionTLS12, InsecureSkipVerify: false, // Never set true in production! RootCAs: certPool, ServerName: "example.com", // For virtual hosting } // TLS connection conn, _ := tls.Dial("tcp", "example.com:443", clientConfig)
TLS configuration
Proper TLS configuration balances security and compatibility. Always set minimum version, prefer modern cipher suites, and configure timeouts. Use Mozilla's recommendations as a baseline.
// Modern TLS configuration config := &tls.Config{ MinVersion: tls.VersionTLS12, // TLS 1.3 cipher suites (automatic in TLS 1.3) // TLS 1.2 suites - ordered by preference CipherSuites: []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, }, // Curve preferences CurvePreferences: []tls.CurveID{ tls.X25519, tls.CurveP256, }, // Session tickets for resumption SessionTicketsDisabled: false, }
Certificate handling
Load certificates from PEM files, verify chains, and parse X.509 certificates. Use x509.CertPool for trusted CAs. Let's Encrypt certificates work out of the box with system CAs.
import "crypto/x509" // Load certificate and key cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") // Parse PEM certificate block, _ := pem.Decode(certPEM) cert, _ := x509.ParseCertificate(block.Bytes) // Certificate info cert.Subject.CommonName cert.DNSNames cert.NotBefore cert.NotAfter cert.Issuer // Create cert pool pool := x509.NewCertPool() pool.AppendCertsFromPEM(caCertPEM) // Use system CAs systemPool, _ := x509.SystemCertPool() // Verify certificate opts := x509.VerifyOptions{ Roots: pool, Intermediates: x509.NewCertPool(), } chains, err := cert.Verify(opts)
crypto/x509
The crypto/x509 package parses X.509 certificates, CSRs, and CRLs. Use it for certificate verification, extracting metadata, and building certificate chains.
import "crypto/x509" // Parse DER-encoded certificate cert, err := x509.ParseCertificate(derBytes) // Parse PEM-encoded certificate block, _ := pem.Decode(pemBytes) cert, err := x509.ParseCertificate(block.Bytes) // Certificate properties cert.Subject.CommonName // "example.com" cert.Subject.Organization // []string{"Company Inc"} cert.DNSNames // []string{"example.com", "www.example.com"} cert.IPAddresses // []net.IP cert.NotBefore, cert.NotAfter // Validity period cert.IsCA // Is CA certificate cert.KeyUsage // Key usage bits cert.ExtKeyUsage // Extended key usage // Create self-signed certificate template := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "test"}, NotBefore: time.Now(), NotAfter: time.Now().AddDate(1, 0, 0), KeyUsage: x509.KeyUsageDigitalSignature, } derBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
HMAC (crypto/hmac)
HMAC provides message authentication using a hash function and secret key. Use it to verify data integrity and authenticity. Always use constant-time comparison to prevent timing attacks.
import "crypto/hmac" // Create HMAC func sign(message, key []byte) []byte { h := hmac.New(sha256.New, key) h.Write(message) return h.Sum(nil) } // Verify HMAC (constant-time comparison!) func verify(message, key, expectedMAC []byte) bool { h := hmac.New(sha256.New, key) h.Write(message) actualMAC := h.Sum(nil) return hmac.Equal(actualMAC, expectedMAC) } // ⚠️ Never use bytes.Equal for MAC verification! // hmac.Equal prevents timing attacks // Common use: API authentication signature := sign(payload, apiSecret) signatureHex := hex.EncodeToString(signature) req.Header.Set("X-Signature", signatureHex)
bcrypt (golang.org/x/crypto/bcrypt)
bcrypt is a password hashing algorithm designed to be slow, preventing brute-force attacks. Use it for storing user passwords with appropriate cost factor (10-12 for most applications).
import "golang.org/x/crypto/bcrypt" // Hash password func hashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword( []byte(password), bcrypt.DefaultCost, // 10, adjust higher for more security ) return string(bytes), err } // Verify password func checkPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } // Usage hash, _ := hashPassword("user-password") // Store hash in database // Later, verify if checkPassword("user-password", hash) { // Password correct } // Cost factors: 10=~100ms, 12=~400ms, 14=~1.5s // Higher = more secure but slower
Password hashing
For passwords, use bcrypt, scrypt, or Argon2 (preferred for new applications). Never use plain hashes (SHA-256, MD5) for passwords—they're too fast for this use case.
// Argon2id - recommended for new applications import "golang.org/x/crypto/argon2" func hashPasswordArgon2(password string) (string, error) { salt := make([]byte, 16) rand.Read(salt) // Argon2id parameters hash := argon2.IDKey( []byte(password), salt, 1, // Time (iterations) 64*1024, // Memory in KB (64 MB) 4, // Parallelism 32, // Key length ) // Encode for storage return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, 64*1024, 1, 4, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(hash), ), nil } // ❌ Never use for passwords: // sha256.Sum256([]byte(password)) // Too fast! // md5.Sum([]byte(password)) // Too fast and broken!