Go Web Development with Standard Library
Learning Objectives: By the end of this lesson, you will be able to:
- Build HTTP servers using Go’s standard library
- Implement routing and middleware patterns
- Handle templates for dynamic web content
- Create RESTful APIs with proper HTTP methods
- Manage sessions, cookies, and authentication
- Deploy and test web applications
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Think about your favorite web application. Behind every click, form submission, or page load is a web server processing requests and generating responses. Today we’ll learn how to build these servers using Go’s powerful standard library - no external frameworks needed!
Lesson Overview
- Duration: 4-5 days (12-15 hours)
- Prerequisites: Basic Go syntax, HTTP fundamentals, file I/O
- Materials Needed: Go development environment, web browser, API testing tool
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- HTTP Server Basics - Creating and configuring servers
- Routing and Handlers - Managing different endpoints
- Middleware - Cross-cutting concerns and request processing
- Templates - Dynamic HTML generation
- Static Files - Serving CSS, JS, and images
- RESTful APIs - Building proper REST endpoints
- Advanced Topics - Sessions, cookies, authentication
Success Criteria:
By the end of this session, you should be able to build a complete web application with both API endpoints and a web interface.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
HTTP in Go
Go’s net/http package provides everything needed to build production-ready web servers. It follows a simple pattern: handlers process requests and write responses.
Real-World Analogy: Think of a web server like a restaurant. Customers (clients) make requests (orders), waiters (handlers) process them, and the kitchen (your code) prepares responses (meals).
Why This Matters: Understanding HTTP fundamentals is crucial for building any web service, from simple APIs to complex web applications.
Request/Response Cycle
Every HTTP interaction follows the same pattern:
- Client sends a request
- Server routes the request to appropriate handler
- Handler processes the request
- Handler writes a response
- Server sends response back to client
Key Terminology
- Handler: A function that processes HTTP requests
- Router: A component that directs requests to appropriate handlers
- Middleware: Code that runs before/after handlers
- Template: A pattern for generating dynamic content
- Session: Server-side storage for user state
4. Detailed Explanation and Examples (25-30 minutes)
Understanding HTTP Servers in Go
Go’s HTTP server is built around the http.Handler interface, which has a single method: ServeHTTP(ResponseWriter, *Request). This simple interface enables powerful composition and middleware patterns.
Basic HTTP Server
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// Basic handler function
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the Home Page!")
}
// Handler with request information
func infoHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Method: %s\n", r.Method)
fmt.Fprintf(w, "URL: %s\n", r.URL.Path)
fmt.Fprintf(w, "User-Agent: %s\n", r.UserAgent())
fmt.Fprintf(w, "Time: %s\n", time.Now().Format("2006-01-02 15:04:05"))
}
func basicServerExample() {
// Register handlers
http.HandleFunc("/", homeHandler)
http.HandleFunc("/info", infoHandler)
fmt.Println("Server starting on :8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Advanced Server Configuration
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"
)
func createConfiguredServer() *http.Server {
mux := http.NewServeMux()
// Register routes
mux.HandleFunc("/health", healthCheckHandler)
mux.HandleFunc("/api/users", usersHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20, // 1 MB
}
return server
}
func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status": "healthy", "timestamp": "%s"}`, time.Now().Format(time.RFC3339))
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// Get users
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"users": ["alice", "bob", "charlie"]}`)
case http.MethodPost:
// Create user
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `{"message": "User created"}`)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintf(w, `{"error": "Method not allowed"}`)
}
}
func gracefulShutdownExample() {
server := createConfiguredServer()
// Start server in a goroutine
go func() {
fmt.Println("Server starting on :8080...")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
fmt.Println("Shutting down server...")
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("Server forced to shutdown: %v", err)
}
fmt.Println("Server exited")
}
Middleware Implementation
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// Middleware type
type Middleware func(http.Handler) http.Handler
// Logging middleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Create a response writer that captures status code
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
log.Printf("%s %s %d %v", r.Method, r.URL.Path, wrapped.statusCode, time.Since(start))
})
}
// Response writer wrapper to capture status code
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// CORS middleware
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// Authentication middleware
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// Simple token validation (in real apps, validate JWT or session)
if token != "Bearer valid-token" {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Chain multiple middlewares
func chainMiddleware(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
func middlewareExample() {
mux := http.NewServeMux()
// Public endpoint
mux.HandleFunc("/public", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is a public endpoint")
})
// Protected endpoint
protectedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is a protected endpoint")
})
mux.Handle("/protected", authMiddleware(protectedHandler))
// Apply global middleware
handler := chainMiddleware(mux, loggingMiddleware, corsMiddleware)
fmt.Println("Server with middleware starting on :8080...")
log.Fatal(http.ListenAndServe(":8080", handler))
}
5. Live Coding Demonstration (20-25 minutes)
Example 1: Blog API with CRUD Operations
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
type Post struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type BlogAPI struct {
posts map[int]*Post
nextID int
mu sync.RWMutex
}
func NewBlogAPI() *BlogAPI {
return &BlogAPI{
posts: make(map[int]*Post),
nextID: 1,
}
}
func (api *BlogAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Parse URL path
path := strings.TrimPrefix(r.URL.Path, "/api/posts")
switch {
case path == "" || path == "/":
api.handlePosts(w, r)
case strings.HasPrefix(path, "/"):
api.handlePost(w, r, path[1:]) // Remove leading slash
default:
http.NotFound(w, r)
}
}
func (api *BlogAPI) handlePosts(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
api.getAllPosts(w, r)
case http.MethodPost:
api.createPost(w, r)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (api *BlogAPI) handlePost(w http.ResponseWriter, r *http.Request, idStr string) {
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid post ID", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
api.getPost(w, r, id)
case http.MethodPut:
api.updatePost(w, r, id)
case http.MethodDelete:
api.deletePost(w, r, id)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (api *BlogAPI) getAllPosts(w http.ResponseWriter, r *http.Request) {
api.mu.RLock()
defer api.mu.RUnlock()
posts := make([]*Post, 0, len(api.posts))
for _, post := range api.posts {
posts = append(posts, post)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string][]*Post{"posts": posts})
}
func (api *BlogAPI) createPost(w http.ResponseWriter, r *http.Request) {
var post Post
if err := json.NewDecoder(r.Body).Decode(&post); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
api.mu.Lock()
defer api.mu.Unlock()
post.ID = api.nextID
api.nextID++
post.CreatedAt = time.Now()
post.UpdatedAt = time.Now()
api.posts[post.ID] = &post
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(&post)
}
func (api *BlogAPI) getPost(w http.ResponseWriter, r *http.Request, id int) {
api.mu.RLock()
defer api.mu.RUnlock()
post, exists := api.posts[id]
if !exists {
http.Error(w, "Post not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(post)
}
func (api *BlogAPI) updatePost(w http.ResponseWriter, r *http.Request, id int) {
api.mu.Lock()
defer api.mu.Unlock()
post, exists := api.posts[id]
if !exists {
http.Error(w, "Post not found", http.StatusNotFound)
return
}
var updates Post
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Update fields
if updates.Title != "" {
post.Title = updates.Title
}
if updates.Content != "" {
post.Content = updates.Content
}
if updates.Author != "" {
post.Author = updates.Author
}
post.UpdatedAt = time.Now()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(post)
}
func (api *BlogAPI) deletePost(w http.ResponseWriter, r *http.Request, id int) {
api.mu.Lock()
defer api.mu.Unlock()
_, exists := api.posts[id]
if !exists {
http.Error(w, "Post not found", http.StatusNotFound)
return
}
delete(api.posts, id)
w.WriteHeader(http.StatusNoContent)
}
func blogAPIExample() {
api := NewBlogAPI()
// Add some sample data
api.posts[1] = &Post{
ID: 1,
Title: "Welcome to Go",
Content: "Go is a great language for web development",
Author: "gopher",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
api.nextID = 2
mux := http.NewServeMux()
mux.Handle("/api/posts", api)
mux.Handle("/api/posts/", api)
fmt.Println("Blog API server starting on :8080...")
fmt.Println("Try: curl http://localhost:8080/api/posts")
log.Fatal(http.ListenAndServe(":8080", mux))
}
Example 2: Template-Based Web Application
package main
import (
"html/template"
"net/http"
"strconv"
"time"
)
type User struct {
ID int
Name string
Email string
JoinDate time.Time
}
type PageData struct {
Title string
Users []User
User *User
}
var templates = template.Must(template.ParseGlob("templates/*.html"))
// Sample data
var users = []User{
{1, "Alice Johnson", "alice@example.com", time.Now().AddDate(0, -6, 0)},
{2, "Bob Smith", "bob@example.com", time.Now().AddDate(0, -3, 0)},
{3, "Charlie Brown", "charlie@example.com", time.Now().AddDate(0, -1, 0)},
}
func homePageHandler(w http.ResponseWriter, r *http.Request) {
data := PageData{
Title: "User Management System",
Users: users,
}
err := templates.ExecuteTemplate(w, "index.html", data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func userPageHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Query().Get("id")
if idStr == "" {
http.Error(w, "User ID required", http.StatusBadRequest)
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
var user *User
for _, u := range users {
if u.ID == id {
user = &u
break
}
}
if user == nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
data := PageData{
Title: "User Details",
User: user,
}
err = templates.ExecuteTemplate(w, "user.html", data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func createTemplateFiles() {
// Create templates directory and files
os.MkdirAll("templates", 0755)
// Base template
baseTemplate := `<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.user-card { border: 1px solid #ddd; padding: 20px; margin: 10px 0; border-radius: 5px; }
.btn { background: #007bff; color: white; padding: 10px 15px; text-decoration: none; border-radius: 3px; }
.btn:hover { background: #0056b3; }
</style>
</head>
<body>
<h1>{{.Title}}</h1>
{{template "content" .}}
</body>
</html>`
// Index template
indexTemplate := `{{define "content"}}
<h2>Users</h2>
{{range .Users}}
<div class="user-card">
<h3>{{.Name}}</h3>
<p>Email: {{.Email}}</p>
<p>Joined: {{.JoinDate.Format "2006-01-02"}}</p>
<a href="/user?id={{.ID}}" class="btn">View Details</a>
</div>
{{end}}
{{end}}`
// User template
userTemplate := `{{define "content"}}
<div class="user-card">
<h2>{{.User.Name}}</h2>
<p><strong>ID:</strong> {{.User.ID}}</p>
<p><strong>Email:</strong> {{.User.Email}}</p>
<p><strong>Join Date:</strong> {{.User.JoinDate.Format "January 2, 2006"}}</p>
<a href="/" class="btn">Back to Users</a>
</div>
{{end}}`
// Write template files
os.WriteFile("templates/base.html", []byte(baseTemplate), 0644)
os.WriteFile("templates/index.html", []byte(baseTemplate+indexTemplate), 0644)
os.WriteFile("templates/user.html", []byte(baseTemplate+userTemplate), 0644)
}
func templateExample() {
createTemplateFiles()
mux := http.NewServeMux()
mux.HandleFunc("/", homePageHandler)
mux.HandleFunc("/user", userPageHandler)
// Serve static files
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
fmt.Println("Template server starting on :8080...")
fmt.Println("Visit: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
Example 3: File Upload Handler
package main
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
)
const maxUploadSize = 10 << 20 // 10 MB
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Limit upload size
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
// Parse multipart form
err := r.ParseMultipartForm(maxUploadSize)
if err != nil {
http.Error(w, "File too large", http.StatusBadRequest)
return
}
// Get file from form
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No file provided", http.StatusBadRequest)
return
}
defer file.Close()
// Validate file type
contentType := header.Header.Get("Content-Type")
if !isValidFileType(contentType) {
http.Error(w, "Invalid file type", http.StatusBadRequest)
return
}
// Create uploads directory
uploadDir := "uploads"
os.MkdirAll(uploadDir, 0755)
// Create destination file
filename := header.Filename
if filename == "" {
http.Error(w, "Invalid filename", http.StatusBadRequest)
return
}
dst, err := os.Create(filepath.Join(uploadDir, filename))
if err != nil {
http.Error(w, "Failed to create file", http.StatusInternalServerError)
return
}
defer dst.Close()
// Copy uploaded file to destination
_, err = io.Copy(dst, file)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"message": "File uploaded successfully", "filename": "%s"}`, filename)
}
func isValidFileType(contentType string) bool {
allowedTypes := []string{
"image/jpeg",
"image/png",
"image/gif",
"text/plain",
"application/pdf",
}
for _, allowed := range allowedTypes {
if strings.HasPrefix(contentType, allowed) {
return true
}
}
return false
}
func uploadFormHandler(w http.ResponseWriter, r *http.Request) {
html := `
<!DOCTYPE html>
<html>
<head>
<title>File Upload</title>
</head>
<body>
<h1>Upload File</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" required>
<br><br>
<input type="submit" value="Upload">
</form>
</body>
</html>`
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, html)
}
func fileUploadExample() {
mux := http.NewServeMux()
mux.HandleFunc("/", uploadFormHandler)
mux.HandleFunc("/upload", uploadHandler)
// Serve uploaded files
mux.Handle("/uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir("uploads"))))
fmt.Println("File upload server starting on :8080...")
fmt.Println("Visit: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
6. Hands-On Practice (30-40 minutes)
Exercise 1: Todo API (10 minutes)
Objective: Create a RESTful API for managing todo items
Instructions:
- Create a Todo struct with ID, Title, Description, Completed fields
- Implement CRUD operations (GET, POST, PUT, DELETE)
- Use proper HTTP status codes
- Return JSON responses
Starter Code:
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Completed bool `json:"completed"`
}
func main() {
// TODO: Implement todo API
}
Exercise 2: Authentication Middleware (15 minutes)
Objective: Implement session-based authentication
Requirements:
- Create login/logout endpoints
- Implement session middleware
- Protect certain routes with authentication
- Store sessions in memory (for simplicity)
Hints:
- Use cookies to store session IDs
- Create a session store to manage user sessions
- Implement middleware to check authentication status
Exercise 3: Static Website with Dynamic Content (15 minutes)
Objective: Build a personal portfolio website
Challenge: Create a website that:
- Serves static CSS and JavaScript files
- Uses templates for dynamic content
- Has multiple pages (home, about, projects)
- Includes a contact form that processes submissions
Extension Ideas:
- Add a simple blog functionality
- Implement a guest book feature
- Add file upload for project images
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
- Problem: Not setting proper Content-Type headers Cause: Browsers can’t interpret responses correctly Solution: Always set appropriate Content-Type headers
// JSON response
w.Header().Set("Content-Type", "application/json")
// HTML response
w.Header().Set("Content-Type", "text/html; charset=utf-8")
- Problem: Memory leaks from not closing request bodies Cause: Request bodies are not automatically closed Solution: Always close request bodies when reading them
func handler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() // Important for long-running servers
body, err := io.ReadAll(r.Body)
// ... handle body
}
Debugging Tips
- Use browser developer tools to inspect requests/responses
- Test APIs with curl or Postman
- Add logging middleware to see request details
- Use Go’s built-in race detector:
go run -race main.go
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- RESTful Design: Use proper HTTP methods and status codes
- Error Handling: Return consistent error responses
- Security: Validate inputs, use HTTPS, implement CSRF protection
- Performance: Use appropriate caching headers, optimize static file serving
Security Considerations
// Input validation
func validateInput(input string) error {
if len(input) > 1000 {
return errors.New("input too long")
}
// Add more validations as needed
return nil
}
// CSRF protection (simplified)
func csrfMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
token := r.FormValue("csrf_token")
if !validateCSRFToken(token) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
}
next.ServeHTTP(w, r)
})
}
Next Steps in Your Learning Journey
- Learn popular frameworks like Gin or Echo
- Study database integration patterns
- Explore WebSocket implementation
- Practice with container deployment
9. Summary and Review (5 minutes)
Key Takeaways
- Go’s standard library provides everything needed for web development
- Middleware patterns enable clean separation of concerns
- Templates allow for dynamic content generation
- Proper error handling and status codes improve API usability
Quick Review Questions
- What’s the difference between http.HandleFunc and http.Handle?
- How do you implement middleware in Go?
- When should you use templates vs JSON responses?
For Instructors
Teaching Tips
- Start with simple examples before introducing complex patterns
- Use browser developer tools to show HTTP requests/responses
- Demonstrate both API and web UI development
- Emphasize the importance of proper error handling
Assessment Ideas
- Build a complete web application with both frontend and API
- Implement authentication and authorization
- Create a simple e-commerce or blog platform
Adaptation Notes
- For beginners: Focus on basic handlers and simple templates
- For advanced students: Include performance optimization and security considerations
- Online delivery: Use shared development environments or provide pre-configured setups