Go Web Development: Building Modern Web Applications
Go’s standard library provides everything you need to build production-quality web applications. The net/http package includes a robust HTTP server, request routing, static file serving, and cookie management. This guide covers the full spectrum of Go web development, from basic serving to advanced patterns.
The HTTP Server
Go’s http.Server is production-ready out of the box, but requires explicit configuration for production use.
Basic Server
package main
import (
"fmt"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
---
func main() {
http.HandleFunc("/", helloHandler)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
---Production Server Configuration
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", helloHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20, // 1 MB
}
// Graceful shutdown
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("shutdown error: %v", err)
}
---The ReadTimeout and WriteTimeout are critical for production. Without them, slow clients can hold connections open indefinitely, exhausting server resources. Graceful shutdown ensures in-flight requests complete before the process exits.
Routing
Go 1.22 introduced method-based routing in the standard ServeMux:
mux := http.NewServeMux()
// Method-specific handlers
mux.HandleFunc("GET /api/items", listItems)
mux.HandleFunc("POST /api/items", createItem)
mux.HandleFunc("PUT /api/items/{id}", updateItem)
mux.HandleFunc("DELETE /api/items/{id}", deleteItem)
// Path value extraction
func updateItem(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// ...
---For more complex routing requirements (subdomain routing, regex patterns, middleware chaining), third-party routers like chi or gorilla/mux remain popular. Chi is particularly well-regarded for its lightweight nature and compatibility with net/http.
Templating
Go’s html/template package generates HTML safely with automatic escaping.
Template Files
Create templates/base.html:
<!DOCTYPE html>
<html>
<head>
<title>{{block "title" .}}Default Title{{end}}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/items">Items</a>
</nav>
</header>
<main>
{{block "content" .}}{{end}}
</main>
<footer>
<p>© 2025 ExcellentWiki</p>
</footer>
</body>
</html>Create templates/items.html:
{{define "title"}}Items{{end}}
{{define "content"}}
<h1>Items</h1>
<ul>
{{range .Items}}
<li>{{.Name}} - ${{.Price}}</li>
{{else}}
<li>No items found</li>
{{end}}
</ul>
{{end}}Template Rendering
type Item struct {
Name string
Price float64
---
type PageData struct {
Items []Item
---
func renderTemplate(w http.ResponseWriter, tmpl string, data any) {
templates := template.Must(template.ParseGlob("templates/*.html"))
err := templates.ExecuteTemplate(w, tmpl, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
---
func itemsHandler(w http.ResponseWriter, r *http.Request) {
data := PageData{
Items: []Item{
{Name: "Widget", Price: 9.99},
{Name: "Gadget", Price: 24.99},
},
}
renderTemplate(w, "items.html", data)
---Template Functions
Add custom functions to templates:
func formatCurrency(amount float64) string {
return fmt.Sprintf("$%.2f", amount)
---
func myFuncs() template.FuncMap {
return template.FuncMap{
"currency": formatCurrency,
}
---
func init() {
templates = template.New("").Funcs(myFuncs()).ParseGlob("templates/*.html")
---Static Files
func main() {
mux := http.NewServeMux()
// Serve static files with caching
fileServer := http.FileServer(http.Dir("./static"))
mux.Handle("GET /static/", cacheMiddleware(fileServer))
// Specific static file
mux.Handle("GET /favicon.ico", http.FileServer(http.Dir("./static")))
log.Fatal(http.ListenAndServe(":8080", mux))
---
func cacheMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=86400")
w.Header().Set("ETag", `"v1"`)
next.ServeHTTP(w, r)
})
---Session Management
Use secure, server-side sessions:
import "github.com/gorilla/sessions"
var store = sessions.NewCookieStore([]byte("super-secret-key-32-bytes!"))
func loginHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
session.Values["user_id"] = 123
session.Values["role"] = "admin"
session.Save(r, w)
---
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session-name")
if err != nil || session.Values["user_id"] == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
---Sessions are stored in a signed cookie on the client. For distributed deployments, use a shared session store backed by Redis or PostgreSQL.
Middleware Patterns
Composable middleware via http.Handler wrapping:
func Chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
for i := len(middleware) - 1; i >= 0; i-- {
h = middleware[i](h)
}
return h
---
// Usage
handler := Chain(
itemsHandler,
middleware.Recovery,
middleware.Logging,
middleware.Auth,
)WebSockets and Real-Time Features
import "github.com/gorilla/websocket"
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // restrict in production
},
---
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
return
}
if err := conn.WriteMessage(messageType, p); err != nil {
return
}
}
---Deployment
Behind a Reverse Proxy
Go HTTP servers are designed to run behind Nginx, Caddy, or a cloud load balancer:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
---Health Checks
Expose a health endpoint for load balancers:
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
})
---Database Integration
Connect to databases for dynamic web applications:
import (
"database/sql"
_ "github.com/lib/pq"
"net/http"
)
type App struct {
db *sql.DB
---
func (app *App) userHandler(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var name string
err := app.db.QueryRow("SELECT name FROM users WHERE id = $1", id).Scan(&name)
if err == sql.ErrNoRows {
http.Error(w, "User not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "User: %s", name)
---
func main() {
db, _ := sql.Open("postgres", "postgres://user:pass@localhost/dbname")
app := &App{db: db}
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", app.userHandler)
http.ListenAndServe(":8080", mux)
---Connection Pool Configuration
db.SetMaxOpenConns(25) // max open connections
db.SetMaxIdleConns(10) // max idle connections
db.SetConnMaxLifetime(5 * time.Minute) // recycle connectionsForm Handling
func formHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
name := r.FormValue("name")
email := r.FormValue("email")
// Validate
if name == "" {
fmt.Fprintf(w, "Name is required")
return
}
fmt.Fprintf(w, "Received: %s (%s)", name, email)
---File Uploads
func uploadHandler(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 10<<20) // 10 MB limit
if err := r.ParseMultipartForm(5 << 20); err != nil {
http.Error(w, "File too large", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No file provided", http.StatusBadRequest)
return
}
defer file.Close()
dst, _ := os.Create("/uploads/" + header.Filename)
defer dst.Close()
io.Copy(dst, file)
fmt.Fprintf(w, "Uploaded: %s (%d bytes)", header.Filename, header.Size)
---Advanced Patterns in Go
Context Propagation
Go’s context package is essential for managing timeouts, cancellations, and request-scoped values across API boundaries and goroutines:
func handler(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result, err := queryDatabase(ctx)
if err != nil {
return fmt.Errorf("query failed: %w", err)
}
return result
---Memory Management
Go’s garbage collector has been optimized significantly since version 1.5. The GC is concurrent and generational, with typical pause times under 1ms. For latency-critical applications, you can tune GC behavior with GOGC (target heap growth percentage) and GOMEMLIMIT (soft memory cap).
# Aggressive GC for latency-sensitive apps
export GOGC=50 # trigger GC when heap grows 50%
export GOMEMLIMIT=2GiB # soft memory limitBuild Tags and Conditional Compilation
Build tags let you compile platform-specific or feature-flag code:
//go:build linux
// +build linux
package main
func getOS() string {
return "linux"
---go build -tags=debug,linux .Pprof Profiling
import _ "net/http/pprof"
// Access profiles at /debug/pprof/
go tool pprof http://localhost:8080/debug/pprof/heap
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30FAQ
Q: Should I use a web framework in Go? A: The standard library suffices for most applications. Frameworks like Gin, Echo, or Fiber add convenience but are unnecessary for straightforward APIs.
Q: How do I handle file uploads?
A: Use r.ParseMultipartForm and r.FormFile. Set MaxBytesReader on the request body to limit upload size.
Q: How do I serve Single Page Applications (SPAs)?
A: Serve the static build directory and add a catch-all route that returns index.html for non-API paths.
Q: How do I run HTTPS in development?
A: Use http.ListenAndServeTLS with self-signed certificates or tools like mkcert. For production, use a reverse proxy like Caddy that handles TLS automatically.
Q: How do I add request-scoped context?
A: Use context.WithValue in middleware and pass it through the request’s r.Context(). Access it inside handlers with r.Context().Value(key).
Q: What ORM should I use?
A: Go prefers SQL directly. Use database/sql with pgx or sqlx for convenience. Popular ORM-like tools include gorm and ent. Consider the trade-off: ORMs add abstraction but reduce control over query performance.
Q: How do I structure a larger web application? A: Follow the standard layout: separate handlers, models, services, middleware, and configuration into packages. Use dependency injection through constructor functions to wire them together.
For a comprehensive overview, read our article on Getting Started With Go.
For a comprehensive overview, read our article on Go Concurrency Guide.
Related Concepts and Further Reading
Understanding go web development requires familiarity with several interconnected ideas and principles that together form a complete picture. Exploring these related concepts deepens your knowledge and provides context that makes the core material more meaningful and applicable. Each concept builds on the others, creating a web of understanding that supports deeper learning and practical application. Taking time to explore how these elements connect reveals patterns that accelerate comprehension and retention of new information.
The relationship between go web development and adjacent fields is worth particular attention. Many of the most important insights emerge at the boundaries between disciplines, where ideas from different areas combine to create new approaches and solutions that neither field could produce alone. Exploring these connections pays dividends in both breadth and depth of understanding, revealing patterns and principles that might otherwise remain hidden from view. Cross-disciplinary knowledge is increasingly valued as problems become more complex and interconnected.
For those looking to go beyond introductory material, several excellent resources provide deeper treatment of specific aspects of go web development. Academic journals, industry publications, authoritative reference works, and online courses each offer different perspectives and levels of detail. The key is to match your reading to your current learning goals and build knowledge progressively, focusing on quality over quantity in your study materials. A well-chosen resource that matches your current level is worth more than dozens of resources that are too basic or too advanced.