Logo
Logo

Atharva Pandey/Lesson 21: JSON and HTTP — Reading and writing the web's language

Created Sun, 21 Apr 2024 00:00:00 +0000 Modified Sun, 21 Apr 2024 00:00:00 +0000

Almost every program I write these days talks to something over HTTP — a third-party API, a database service, a webhook. And almost everything speaks JSON. So learning how Go handles JSON and HTTP isn’t an advanced topic; it’s one of the most practical skills you can pick up early.

In this lesson I’ll walk you through converting Go structs to JSON and back, decoding JSON from an API response, and making real HTTP requests — all using only Go’s standard library. No third-party packages needed.


The Basics

Structs and struct tags

Go’s encoding/json package works by looking at your struct fields. You control how they appear in JSON using struct tags — little annotations that sit next to each field.

package main

import (
    "encoding/json"
    "fmt"
)

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

func main() {
    u := User{Name: "Atharva", Email: "hi@example.com", Age: 28}

    data, err := json.Marshal(u)
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    fmt.Println(string(data))
    // Output: {"name":"Atharva","email":"hi@example.com","age":28}
}

json.Marshal takes any value and returns []byte — a slice of bytes representing the JSON. We call string(data) to print it as readable text.

The struct tags json:"name" tell the encoder to use "name" as the key instead of the Go field name Name. If you omit the tag, Go uses the field name as-is, including the capital letter. Tags keep your JSON looking clean and lowercase.

There’s a handy extra option: json:"age,omitempty" will leave the field out of the JSON entirely if its value is the zero value (0 for int, "" for string, nil for a pointer). Very useful when you don’t want to send empty fields to an API.

Unmarshalling JSON into a struct

Going the other way — turning JSON bytes into a Go struct — uses json.Unmarshal:

package main

import (
    "encoding/json"
    "fmt"
)

type Product struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Price float64 `json:"price"`
}

func main() {
    raw := `{"id":42,"title":"Go Programming Book","price":29.99}`

    var p Product
    err := json.Unmarshal([]byte(raw), &p)
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    fmt.Printf("Product: %s costs $%.2f\n", p.Title, p.Price)
    // Output: Product: Go Programming Book costs $29.99
}

Notice two things: we pass a pointer &p to Unmarshal (because it needs to modify p), and we convert the string to []byte using []byte(raw). If the JSON has extra fields your struct doesn’t know about, Go quietly ignores them — no error.

Streaming with json.NewEncoder and json.NewDecoder

Marshal and Unmarshal work on byte slices held in memory. When you’re writing to an http.ResponseWriter or reading from an http.Response.Body, it’s more efficient to stream using an encoder or decoder directly:

// Writing JSON to a response writer (in an HTTP handler)
json.NewEncoder(w).Encode(myStruct)

// Reading JSON from an HTTP response body
var result MyStruct
json.NewDecoder(resp.Body).Decode(&result)

The encoder writes directly to any io.Writer without buffering the whole thing in memory first. The decoder reads from any io.Reader the same way. For large payloads this matters; for small ones either approach works fine.

Making HTTP requests with net/http

Go’s net/http package includes a ready-to-use HTTP client. A simple GET request looks like this:

package main

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

type Post struct {
    UserID int    `json:"userId"`
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Body   string `json:"body"`
}

func main() {
    resp, err := http.Get("https://jsonplaceholder.typicode.com/posts/1")
    if err != nil {
        fmt.Println("request failed:", err)
        return
    }
    defer resp.Body.Close()

    var post Post
    if err := json.NewDecoder(resp.Body).Decode(&post); err != nil {
        fmt.Println("decode failed:", err)
        return
    }

    fmt.Printf("Title: %s\n", post.Title)
}

The pattern is always: make the request → check the error → defer closing the body → decode the body. The defer resp.Body.Close() is important — it ensures the connection is released back to the pool even if something goes wrong later.


Try It Yourself

Exercise 1: Define a City struct with fields Name, Country, and Population. Create an instance, marshal it to JSON with lowercase keys, and print the result.

Exercise 2: Take this JSON string and unmarshal it into a matching struct:

{"username":"gopher","score":1500,"active":true}

Then print each field individually.

Exercise 3: Hit the free API at https://jsonplaceholder.typicode.com/users/1, decode the response into a struct that captures at least id, name, and email, and print a greeting like “Hello, Leanne Graham! Your email is Sincere@april.biz.”


Common Mistakes

Forgetting the pointer in Unmarshal

json.Unmarshal(data, p) will not work — you must pass &p. Without the pointer, Unmarshal can’t modify your struct. Go won’t crash; it just won’t change anything, and you’ll be left wondering why your struct is empty.

Not closing the response body

resp.Body is an open network connection. If you never close it, you leak that connection. Always defer resp.Body.Close() immediately after checking the error from http.Get or http.Do.

Unexported fields are invisible to the JSON package

The encoding/json package can only see exported (capitalised) fields. A field named name (lowercase) will always be ignored, even if you give it a tag. If your struct field is lowercase, your JSON will silently be missing that data.

Ignoring the HTTP status code

http.Get returns nil for err even when the server responds with a 404 or 500. Always check resp.StatusCode after checking the error:

if resp.StatusCode != http.StatusOK {
    fmt.Println("unexpected status:", resp.Status)
    return
}

Key Takeaway

Go’s encoding/json and net/http packages handle the two most common tasks in modern programs — serialising data and talking to the outside world — without any extra dependencies. The core pattern to memorise is: define a struct, add json:"..." tags, marshal to send, unmarshal to receive, and always close the response body. Once this pattern is in your fingers, you can consume or produce any JSON API Go encounters.


← Previous: Lesson 20 — Testing | Course Index: Go from Scratch | Next: Lesson 22 — File I/O →