Web Analytics

JSON & HTTP

Intermediate ~35 min read

JSON and HTTP are fundamental for modern web applications. Go's standard library provides excellent support for both. In this lesson, you'll learn to work with JSON data and build HTTP clients and servers.

JSON Encoding (Marshal)

Output
Click Run to execute your code
JSON Struct Tags:
  • json:"name" - Custom field name
  • json:"name,omitempty" - Omit if zero value
  • json:"-" - Skip this field
  • json:",string" - Encode as string

JSON Decoding (Unmarshal)

Output
Click Run to execute your code

Working with Unknown JSON

// Use map for unknown structure
var data map[string]interface{}
err := json.Unmarshal(jsonData, &data)

// Access fields
name := data["name"].(string)
age := data["age"].(float64)  // JSON numbers are float64

// Or use json.RawMessage for partial parsing
type Response struct {
    Status string          `json:"status"`
    Data   json.RawMessage `json:"data"`  // Parse later
}

HTTP Client

GET Request

resp, err := http.Get("https://api.example.com/users")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
    log.Fatal(err)
}

fmt.Println(string(body))

POST Request with JSON

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

user := User{Name: "Alice", Email: "[email protected]"}
jsonData, _ := json.Marshal(user)

resp, err := http.Post(
    "https://api.example.com/users",
    "application/json",
    bytes.NewBuffer(jsonData),
)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

fmt.Println("Status:", resp.Status)

Custom HTTP Request

client := &http.Client{
    Timeout: 10 * time.Second,
}

req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
if err != nil {
    log.Fatal(err)
}

// Add headers
req.Header.Add("Authorization", "Bearer token123")
req.Header.Add("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

// Parse JSON response
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)

HTTP Server

Basic Server

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

JSON API Server

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

var users = []User{
    {ID: 1, Name: "Alice", Email: "[email protected]"},
    {ID: 2, Name: "Bob", Email: "[email protected]"},
}

func getUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    user.ID = len(users) + 1
    users = append(users, user)
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case "GET":
            getUsers(w, r)
        case "POST":
            createUser(w, r)
        default:
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        }
    })
    
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

REST API Patterns

Router with Mux

import "github.com/gorilla/mux"

func main() {
    r := mux.NewRouter()
    
    // Routes
    r.HandleFunc("/users", getUsers).Methods("GET")
    r.HandleFunc("/users", createUser).Methods("POST")
    r.HandleFunc("/users/{id}", getUser).Methods("GET")
    r.HandleFunc("/users/{id}", updateUser).Methods("PUT")
    r.HandleFunc("/users/{id}", deleteUser).Methods("DELETE")
    
    log.Fatal(http.ListenAndServe(":8080", r))
}

func getUser(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]
    // ... find user by id
}

Middleware

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "Bearer secret" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Use middleware
r.Use(loggingMiddleware)
r.Use(authMiddleware)

Common Mistakes

1. Not closing response body

// ❌ Wrong - response body leak
resp, _ := http.Get(url)
body, _ := io.ReadAll(resp.Body)

// ✅ Correct - always close
resp, err := http.Get(url)
if err != nil {
    return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

2. Unexported struct fields

// ❌ Wrong - fields won't be marshaled
type User struct {
    name  string  // lowercase = unexported
    email string
}

// ✅ Correct - capitalize for export
type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

3. Not checking HTTP status

// ❌ Wrong - assuming success
resp, _ := http.Get(url)
json.NewDecoder(resp.Body).Decode(&data)

// ✅ Correct - check status
resp, err := http.Get(url)
if err != nil {
    return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
    return fmt.Errorf("bad status: %s", resp.Status)
}
json.NewDecoder(resp.Body).Decode(&data)

Exercise: Weather API Client

Task: Create a weather API client.

Requirements:

  • Fetch weather data from a public API
  • Parse JSON response into struct
  • Display temperature and conditions
  • Handle errors properly
Show Solution
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
)

type WeatherResponse struct {
    Main struct {
        Temp     float64 `json:"temp"`
        Humidity int     `json:"humidity"`
    } `json:"main"`
    Weather []struct {
        Description string `json:"description"`
    } `json:"weather"`
    Name string `json:"name"`
}

func getWeather(city, apiKey string) (*WeatherResponse, error) {
    url := fmt.Sprintf(
        "https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=metric",
        city, apiKey,
    )
    
    resp, err := http.Get(url)
    if err != nil {
        return nil, fmt.Errorf("request failed: %w", err)
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(resp.Body)
        return nil, fmt.Errorf("API error: %s - %s", resp.Status, body)
    }
    
    var weather WeatherResponse
    err = json.NewDecoder(resp.Body).Decode(&weather)
    if err != nil {
        return nil, fmt.Errorf("decode failed: %w", err)
    }
    
    return &weather, nil
}

func main() {
    apiKey := "your-api-key"  // Get from openweathermap.org
    city := "London"
    
    weather, err := getWeather(city, apiKey)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Weather in %s:\n", weather.Name)
    fmt.Printf("Temperature: %.1f°C\n", weather.Main.Temp)
    fmt.Printf("Humidity: %d%%\n", weather.Main.Humidity)
    if len(weather.Weather) > 0 {
        fmt.Printf("Conditions: %s\n", weather.Weather[0].Description)
    }
}

Summary

  • json.Marshal() converts Go → JSON
  • json.Unmarshal() converts JSON → Go
  • Struct tags control JSON field names
  • http.Get/Post() for simple requests
  • http.Client for custom requests
  • http.HandleFunc() creates server routes
  • Always close response bodies
  • Check HTTP status codes

What's Next?

Congratulations on completing the Packages & Standard Library module! You now know how to organize code, work with files, and build web services. Next, you'll learn about Testing—how to write tests, benchmarks, and ensure code quality!