Structs & JSON
π― 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.
cd ~/dev/learn/go-lang/lessons/0003-structs-json
go mod init structs-jsonCreate 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 fieldsjson:"id"β struct tags that map fields to JSON keysjson.NewEncoder(w).Encode(users)β writes Go data as JSON to the responsejson.NewDecoder(r.Body).Decode(&newUser)β parses JSON from the request into a Go structswitch r.Method { ... }β route by HTTP method on the same pathappend(users, newUser)β adds an element to a slicetime.Now()β current timestamp, fits in theCreatedAtfield
Start the server:
go run main.goIn 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/usersPress 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?