JSON
JSON is the lingua franca of APIs and config files, and Go's encoding/json
package maps it onto structs. Two functions do almost all the work:
json.Marshal turns a Go value into JSON bytes, and json.Unmarshal parses
JSON bytes into a Go value.
Marshal: Go → JSON
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
data, err := json.Marshal(User{Name: "Ada", Email: "ada@example.com"})
if err != nil {
fmt.Println("marshal failed:", err)
return
}
fmt.Println(string(data)) // {"name":"Ada","email":"ada@example.com"}
}
The backtick strings after the fields are struct tags — metadata the
json package reads via reflection. json:"name" says: in JSON, this field
is called name. Without tags, Go would use the field names as-is
("Name", "Email"), which looks odd next to the lowercase keys the rest
of the world produces.
Only exported fields travel
Here's a rule that bites everyone once: encoding/json can only see
exported fields — the ones starting with a capital letter. A lowercase
field like password string is silently skipped by Marshal and never
filled by Unmarshal, no tag can change that. The reason is honest
mechanics: the json package is outside your package, and unexported fields
are exactly the ones outside packages can't touch. If a field mysteriously
stays empty, check its case first.
Unmarshal: JSON → Go
Unmarshal takes the raw bytes and a pointer to the destination — it
needs the pointer so it can write into your struct:
var u User
err := json.Unmarshal([]byte(`{"name":"Ada","email":"ada@example.com"}`), &u)
if err != nil {
// malformed JSON lands here — always check
}
fmt.Println(u.Name) // Ada
Two behaviors worth knowing:
- Unknown JSON fields are ignored. If the document contains a key your
struct doesn't declare,
Unmarshalskips it without complaint. That's a feature — APIs can add fields without breaking old clients. - Missing fields stay at their zero value. No
"email"key?u.Emailis simply"".Unmarshalonly errors on malformed JSON (or a type mismatch, like a string where a number belongs), not on absent data.
omitempty
Marshal has one more trick. Tag a field json:"email,omitempty" and it's
dropped from the output entirely when it holds its zero value (empty string,
0, false, nil, empty slice/map). Handy for optional fields — the JSON
stays compact and consumers can distinguish "not provided" from "provided as
empty"... mostly. The classic gotcha: omitempty on a bool also drops
false, so a real "explicitly false" setting vanishes. Choose it
deliberately.
Errors are part of the contract
Both directions return an error, and the Unmarshal one matters in
practice: config files get truncated, HTTP bodies get mangled. The idiomatic
shape is a small function that parses and returns (T, error), letting the
caller decide what a broken document means.
That's precisely the exercise: define a tagged Config struct and write
parseConfig(data []byte) (Config, error) that unmarshals into it —
returning the error when the bytes aren't valid JSON.
Practice
1. Parse a Config File
IntermediateA tiny server reads its configuration from a JSON document like this:
{"host": "localhost", "port": 8080, "debug": true}Finish the
Configstruct — addjsontags mapping each field to its lowercase key — and implementparseConfig:type Config struct { Host string `json:"host"` Port int `json:"port"` Debug bool `json:"debug"` } func parseConfig(data []byte) (Config, error)- Parse
datawithjson.Unmarshal(remember: it needs a pointer). - On success, return the populated
Configand anilerror. - If the bytes aren't valid JSON, return the error from
Unmarshal(a zeroConfigalongside it is fine). - Fields missing from the document simply keep their zero values — that's not an error.
- Unknown keys in the document are ignored — that's not an error either.
package main import "fmt" // Config holds the server settings read from a JSON document. // TODO: add json struct tags mapping each field to its lowercase key // ("host", "port", "debug"). type Config struct { Host string Port int Debug bool } // parseConfig unmarshals a JSON document into a Config. It returns the // error from json.Unmarshal when data is not valid JSON. func parseConfig(data []byte) (Config, error) { // TODO: implement return Config{}, nil } func main() { cfg, err := parseConfig([]byte(`{"host": "localhost", "port": 8080, "debug": true}`)) fmt.Println(cfg, err) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Parse