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:

  1. HTTP Server Basics - Creating and configuring servers
  2. Routing and Handlers - Managing different endpoints
  3. Middleware - Cross-cutting concerns and request processing
  4. Templates - Dynamic HTML generation
  5. Static Files - Serving CSS, JS, and images
  6. RESTful APIs - Building proper REST endpoints
  7. 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:

  1. Client sends a request
  2. Server routes the request to appropriate handler
  3. Handler processes the request
  4. Handler writes a response
  5. 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:

  1. Create a Todo struct with ID, Title, Description, Completed fields
  2. Implement CRUD operations (GET, POST, PUT, DELETE)
  3. Use proper HTTP status codes
  4. 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

  1. 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")
  1. 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

  1. Go’s standard library provides everything needed for web development
  2. Middleware patterns enable clean separation of concerns
  3. Templates allow for dynamic content generation
  4. Proper error handling and status codes improve API usability

Quick Review Questions

  1. What’s the difference between http.HandleFunc and http.Handle?
  2. How do you implement middleware in Go?
  3. 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