Go Routing
Building Web Servers with Go
Explanation
Web Routing in Go
Go's standard library includes everything you need to build web servers. The net/http package is powerful enough for production use, and frameworks like Gin and Echo add convenience without sacrificing performance.
Key Concepts
- Handler: A function that processes HTTP requests
- Mux/Router: Routes requests to the right handler
- Middleware: Functions that wrap handlers for cross-cutting concerns
- Context: Carries request-scoped values and cancellation
Standard Library vs Frameworks
| Feature | net/http | Gin | Echo | |---------|----------|-----|------| | Routing | Basic | Advanced | Advanced | | Parameters | Manual | Built-in | Built-in | | Middleware | Manual | Built-in | Built-in | | Performance | Excellent | Excellent | Excellent | | Learning | Medium | Easy | Easy |
Demonstration
Example 1: Standard Library (net/http)
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
)
// Response helper
func jsonResponse(w http.ResponseWriter, data interface{}, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// User handlers
func handleUsers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
users := []map[string]string{
{"id": "1", "name": "Arthur"},
{"id": "2", "name": "Sarah"},
}
jsonResponse(w, users, http.StatusOK)
case http.MethodPost:
var user map[string]string
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
jsonResponse(w, map[string]string{"error": "Invalid JSON"}, http.StatusBadRequest)
return
}
user["id"] = "3"
jsonResponse(w, user, http.StatusCreated)
default:
jsonResponse(w, map[string]string{"error": "Method not allowed"}, http.StatusMethodNotAllowed)
}
}
// Extract path parameter manually
func handleUser(w http.ResponseWriter, r *http.Request) {
// URL: /users/123
path := strings.TrimPrefix(r.URL.Path, "/users/")
if path == "" {
jsonResponse(w, map[string]string{"error": "ID required"}, http.StatusBadRequest)
return
}
user := map[string]string{
"id": path,
"name": fmt.Sprintf("User %s", path),
}
jsonResponse(w, user, http.StatusOK)
}
// Query parameters
func handleSearch(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
q := query.Get("q")
limit := query.Get("limit")
if q == "" {
q = "*"
}
if limit == "" {
limit = "10"
}
jsonResponse(w, map[string]string{
"query": q,
"limit": limit,
}, http.StatusOK)
}
func main() {
http.HandleFunc("/users", handleUsers)
http.HandleFunc("/users/", handleUser)
http.HandleFunc("/search", handleSearch)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Example 2: Gin Framework
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type User struct {
ID int `json:"id"`
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
var users = []User{
{ID: 1, Name: "Arthur", Email: "art@bpc.com"},
{ID: 2, Name: "Sarah", Email: "sarah@example.com"},
}
var nextID = 3
func main() {
r := gin.Default()
// GET /users - List all
r.GET("/users", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": users})
})
// GET /users/:id - Get one
r.GET("/users/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
for _, user := range users {
if user.ID == id {
c.JSON(http.StatusOK, gin.H{"data": user})
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
})
// POST /users - Create
r.POST("/users", func(c *gin.Context) {
var input User
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
input.ID = nextID
nextID++
users = append(users, input)
c.JSON(http.StatusCreated, gin.H{"data": input})
})
// PUT /users/:id - Update
r.PUT("/users/:id", func(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var input User
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for i, user := range users {
if user.ID == id {
users[i].Name = input.Name
users[i].Email = input.Email
c.JSON(http.StatusOK, gin.H{"data": users[i]})
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
})
// DELETE /users/:id - Delete
r.DELETE("/users/:id", func(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
for i, user := range users {
if user.ID == id {
users = append(users[:i], users[i+1:]...)
c.Status(http.StatusNoContent)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
})
// Query parameters
r.GET("/search", func(c *gin.Context) {
q := c.DefaultQuery("q", "*")
limit := c.DefaultQuery("limit", "10")
c.JSON(http.StatusOK, gin.H{
"query": q,
"limit": limit,
})
})
r.Run(":8080")
}
Example 3: Middleware
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// Custom logging middleware
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
// Process request
c.Next()
// Log after response
duration := time.Since(start)
status := c.Writer.Status()
log.Printf("[%d] %s %s - %v", status, c.Request.Method, path, duration)
}
}
// Auth middleware
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Authorization required",
})
return
}
// Validate token (simplified)
if token != "Bearer valid-token" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid token",
})
return
}
// Set user in context
c.Set("userID", 1)
c.Next()
}
}
// CORS middleware
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
func main() {
r := gin.New()
// Global middleware
r.Use(Logger())
r.Use(CORS())
r.Use(gin.Recovery())
// Public routes
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// Protected routes
api := r.Group("/api")
api.Use(AuthRequired())
{
api.GET("/profile", func(c *gin.Context) {
userID := c.GetInt("userID")
c.JSON(http.StatusOK, gin.H{
"userID": userID,
"name": "Arthur",
})
})
api.GET("/settings", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"theme": "dark",
"lang": "en",
})
})
}
r.Run(":8080")
}
Key Takeaways:
- Standard library is production-ready
- Gin adds convenience (routing, binding, middleware)
- Use middleware for cross-cutting concerns
- Group routes for organization and shared middleware
Imitation
Challenge 1: Add Pagination
Task: Add pagination to the list users endpoint.
Solution
r.GET("/users", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "10"))
start := (page - 1) * perPage
end := start + perPage
if start > len(users) {
start = len(users)
}
if end > len(users) {
end = len(users)
}
c.JSON(http.StatusOK, gin.H{
"data": users[start:end],
"page": page,
"per_page": perPage,
"total": len(users),
"total_pages": (len(users) + perPage - 1) / perPage,
})
})
Challenge 2: Rate Limiting Middleware
Task: Create middleware that limits requests to 100 per minute per IP.
Solution
import (
"sync"
"time"
)
type RateLimiter struct {
requests map[string][]time.Time
mu sync.Mutex
limit int
window time.Duration
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
return &RateLimiter{
requests: make(map[string][]time.Time),
limit: limit,
window: window,
}
}
func (rl *RateLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
rl.mu.Lock()
now := time.Now()
cutoff := now.Add(-rl.window)
// Clean old requests
var valid []time.Time
for _, t := range rl.requests[ip] {
if t.After(cutoff) {
valid = append(valid, t)
}
}
rl.requests[ip] = valid
if len(rl.requests[ip]) >= rl.limit {
rl.mu.Unlock()
c.AbortWithStatusJSON(429, gin.H{"error": "Rate limit exceeded"})
return
}
rl.requests[ip] = append(rl.requests[ip], now)
rl.mu.Unlock()
c.Next()
}
}
// Usage
limiter := NewRateLimiter(100, time.Minute)
r.Use(limiter.Middleware())
Practice
Exercise 1: RESTful API
Difficulty: Intermediate
Build a complete REST API for a blog:
- Posts (CRUD operations)
- Comments on posts
- Tags with many-to-many relationship
- Pagination and filtering
Exercise 2: File Upload API
Difficulty: Advanced
Create an API for file uploads:
- Upload single/multiple files
- File type validation
- Size limits
- Generate thumbnails for images
Summary
What you learned:
- HTTP routing with standard library
- Gin framework for web APIs
- Request binding and validation
- Middleware patterns
- Route grouping
Next Steps:
- Read: Go OOP
- Practice: Build a REST API
- Deploy: Containerize with Docker
