Structs & JSON


Progress
0/3

🎯 Your mission: Real APIs send and receive structured data β€” not raw strings. This lesson teaches Go structs (typed data containers) and how they pair with JSON (the wire format). By the end you’ll have a /users endpoint that returns JSON and a POST /users that accepts it.

Structs: Go's way of grouping data

A struct collects related fields into one type. Think of it like a row in a spreadsheet or a JSON object. Fields are typed β€” string, int, time.Time, etc. β†’ Tour of Go: Structs

Struct tags: json:"name"

Tags are little metadata labels on struct fields. json:"name" tells the JSON encoder/decoder what key name to use. Without a tag, Go uses the field name as-is (e.g., Name β†’ "Name"). β†’ encoding/json docs

json.NewEncoder vs json.NewDecoder

json.NewEncoder(w).Encode(v) β€” writes Go value as JSON to a writer (response).
json.NewDecoder(r.Body).Decode(&v) β€” reads JSON from a reader (request) into a Go value. Encoder/Decoder work with streams. Marshal/Unmarshal work with bytes β€” we’ll use those later.

& β€” "address of"

&newUser means "give me the memory address of newUser." Decode needs this because it has to modify the variable, not just read it. You already saw this with *http.Request β€” same idea.

// Go struct ──encoding/json──→ JSON string

User struct                  JSON output
─────────────────────        ──────────────────────────
ID: 1                       "id": 1
Name: "Alice"               "name": "Alice"
Email: "alice@example.com"  "email": "alice@example.com"
CreatedAt: 2026-06-13       "created_at": "2026-06-13T..."

// Tag controls the JSON key name
// Name string `json:"name"`  β†’  "name": "Alice"

Follow each step in order. Click Done βœ“ after completing it.

1Create directory and initialize moduleCurrent
cd ~/dev/learn/go-lang/lessons/0003-structs-json
go mod init structs-json
2Write the struct-based serverLocked

Create main.go:

package main

import (
  "encoding/json"
  "log"
  "net/http"
  "time"
)

// User represents a person in our system.
// The json:"..." tags control how fields appear in JSON.
type User struct {
  ID        int       `json:"id"`
  Name      string    `json:"name"`
  Email     string    `json:"email"`
  CreatedAt time.Time `json:"created_at"`
}

// In-memory "database" β€” a slice of users.
var users = []User{
  {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()},
  {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()},
}

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) {
  if r.Method != http.MethodPost {
      http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
      return
  }

  var newUser User
  err := json.NewDecoder(r.Body).Decode(&newUser)
  if err != nil {
      http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
      return
  }
  defer r.Body.Close()

  newUser.ID = len(users) + 1
  newUser.CreatedAt = time.Now()

  users = append(users, newUser)

  w.Header().Set("Content-Type", "application/json")
  w.WriteHeader(http.StatusCreated)
  json.NewEncoder(w).Encode(newUser)
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
      switch r.Method {
      case http.MethodGet:
          getUsers(w, r)
      case http.MethodPost:
          createUser(w, r)
      default:
          http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
      }
  })

  log.Println("Server starting on http://localhost:8080")
  log.Fatal(http.ListenAndServe(":8080", mux))
}
What’s new this lesson?
  • type User struct { ... } β€” declares a new type with typed fields
  • json:"id" β€” struct tags that map fields to JSON keys
  • json.NewEncoder(w).Encode(users) β€” writes Go data as JSON to the response
  • json.NewDecoder(r.Body).Decode(&newUser) β€” parses JSON from the request into a Go struct
  • switch r.Method { ... } β€” route by HTTP method on the same path
  • append(users, newUser) β€” adds an element to a slice
  • time.Now() β€” current timestamp, fits in the CreatedAt field
3Run and test JSON endpointsLocked

Start the server:

go run main.go

In a second terminal, test each endpoint:

# GET /users β€” returns all users as JSON
curl http://localhost:8080/users

# POST /users β€” create a new user
curl -X POST -H "Content-Type: application/json" \
-d '{"name":"Charlie","email":"charlie@example.com"}' \
http://localhost:8080/users

# Verify Charlie was added
curl http://localhost:8080/users

Press Ctrl+C to stop the server when you’re done.

Check your understanding

What would the JSON output look like if we removed all the json:"..." tags from the User struct?

Why do we pass &newUser to Decode instead of just newUser?

What does w.WriteHeader(http.StatusCreated) do before writing the new user JSON?