Go Cheatsheet
JSON and Encoding
Use this Go reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Marshal and Unmarshal
encoding/json maps Go values to JSON via reflection. Only exported fields are
encoded or decoded, and an unexported field is silently ignored in both
directions.
type User struct { ID int `json:"id"` Name string `json:"name"` Tags []string `json:"tags"` admin bool // unexported, never touched by encoding/json } b, err := json.Marshal(User{ID: 1, Name: "ada", Tags: []string{"go"}}) // {"id":1,"name":"ada","tags":["go"]} b, err = json.MarshalIndent(u, "", " ") // prefix, indent var u User err = json.Unmarshal(b, &u) // pointer, or nothing is written
| Go | JSON |
|---|---|
bool | true / false |
| numeric types | number |
string | string |
[]byte | base64 string |
struct, map[string]T | object |
| slice, array | array |
nil slice or map | null |
empty slice []T{} | [] |
| pointer | the value, or null |
chan, func, complex | error, unsupported type |
Unmarshal leaves fields absent from the JSON untouched, it does not zero
them. Decoding into a non-empty struct therefore merges rather than replaces.
Field matching is case-insensitive when no tag matches, so "NAME" still lands in
a field tagged json:"name".
Decoding Into map[string]any
When the shape is unknown, decode into any. Every number becomes a
float64.
var m map[string]any json.Unmarshal([]byte(`{"id":9007199254740993,"ok":true}`), &m) id := m["id"].(float64) // 9.007199254740992e+15, precision already lost
| JSON | Go type in any |
|---|---|
| object | map[string]any |
| array | []any |
| number | float64 |
| string | string |
| true / false | bool |
| null | nil |
json.Number keeps the literal text, so large integers survive:
dec := json.NewDecoder(bytes.NewReader(data)) dec.UseNumber() var m map[string]any dec.Decode(&m) n := m["id"].(json.Number) i, err := n.Int64() // exact f, err := n.Float64() s := n.String() // the original digits
Always use the comma-ok form on these assertions. A plain m["id"].(float64)
panics the moment the field is missing or a string.
Encoder and Decoder
Use the streaming forms for an io.Reader or io.Writer, which is every HTTP
body and every file.
// Write enc := json.NewEncoder(w) enc.SetIndent("", " ") enc.SetEscapeHTML(false) // keep <, >, & literal if err := enc.Encode(v); err != nil { /* ... */ }
Encode appends a trailing newline per value, so it writes newline-delimited
JSON when called in a loop.
// Read dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() // error on a key with no matching field dec.UseNumber() var in Request if err := dec.Decode(&in); err != nil { var syn *json.SyntaxError var ute *json.UnmarshalTypeError switch { case errors.As(err, &syn): return fmt.Errorf("malformed json at byte %d", syn.Offset) case errors.As(err, &ute): return fmt.Errorf("field %q wants %s", ute.Field, ute.Type) case errors.Is(err, io.EOF): return errors.New("empty body") } return err } // Reject a second JSON value in the same body if dec.More() { return errors.New("body must contain a single json object") }
Reading a stream of values, or walking it token by token:
for dec.More() { // one object per iteration var item Item if err := dec.Decode(&item); err != nil { return err } } // Token-level, for a huge array you do not want in memory at once dec := json.NewDecoder(f) tok, _ := dec.Token() // json.Delim('[') for dec.More() { var item Item dec.Decode(&item) } dec.Token() // json.Delim(']')
json.Valid(b) reports whether bytes are syntactically valid JSON without
allocating a value.
Embedded Structs
An embedded struct with no tag has its fields promoted into the parent object. Give it a tag and it becomes a nested object.
type Base struct { ID int `json:"id"` CreatedAt time.Time `json:"created_at"` } type Article struct { Base // promoted: {"id":1,"created_at":...,"title":"x"} Title string `json:"title"` } type Wrapped struct { Base Base `json:"base"` // nested: {"base":{...},"title":"x"} Title string `json:"title"` }
A shallower field wins a name conflict against a deeper embedded one. If two fields collide at the same depth, both are dropped with no error. Embedding is also the trick for adding a field to a type you do not own:
type apiUser struct { User Token string `json:"token"` }
Custom Marshaling
Implement json.Marshaler / json.Unmarshaler for full control, or the lighter
encoding.TextMarshaler pair when the value is naturally a string.
type Status int const ( Pending Status = iota Active ) var statusName = map[Status]string{Pending: "pending", Active: "active"} func (s Status) MarshalJSON() ([]byte, error) { name, ok := statusName[s] if !ok { return nil, fmt.Errorf("unknown status %d", int(s)) } return json.Marshal(name) } func (s *Status) UnmarshalJSON(b []byte) error { var name string if err := json.Unmarshal(b, &name); err != nil { return err } for k, v := range statusName { if v == name { *s = k return nil } } return fmt.Errorf("unknown status %q", name) }
- MarshalJSON on a value receiver, UnmarshalJSON on a pointer
receiver. A value-receiver UnmarshalJSON compiles and silently does nothing.
- TextMarshaler/TextUnmarshaler are used automatically by encoding/json and
also work as map keys, which json.Marshaler does not.
- Calling json.Marshal(s) inside s.MarshalJSON recurses forever. Marshal a
different type, or a named alias with no method, instead.
Absent vs Zero, and Time
A pointer field is the standard way to tell "the key was missing" from "the key was sent as the zero value".
type Patch struct { Name *string `json:"name"` // nil = absent, non-nil = explicitly set Views *int `json:"views"` // *0 means "set it to zero" } if p.Name != nil { user.Name = *p.Name }
time.Time marshals to RFC 3339 with nanoseconds and unmarshals from RFC 3339.
Anything else needs a wrapper type.
b, _ := json.Marshal(time.Now()) // "2026-09-15T10:04:05.123456789Z" type Date struct{ time.Time } func (d Date) MarshalJSON() ([]byte, error) { return json.Marshal(d.Format("2006-01-02")) } func (d *Date) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { return err } t, err := time.Parse("2006-01-02", s) if err != nil { return err } d.Time = t return nil }
time.Duration is an int64, so it marshals as a raw nanosecond count, not
"5s". Wrap it if you want the readable form.
RawMessage and Deferred Decoding
json.RawMessage holds bytes verbatim, so you can decode a discriminated union in
two passes.
type Envelope struct { Type string `json:"type"` Payload json.RawMessage `json:"payload"` } var e Envelope if err := json.Unmarshal(data, &e); err != nil { return err } switch e.Type { case "user": var u User if err := json.Unmarshal(e.Payload, &u); err != nil { return err } case "order": var o Order if err := json.Unmarshal(e.Payload, &o); err != nil { return err } default: return fmt.Errorf("unknown payload type %q", e.Type) }
RawMessage also works on the way out, to embed already-encoded JSON without
re-escaping it.
Gotchas
- Marshal escapes <, > and & into \u003c, \u003e and \u0026 by
default. Only Encoder.SetEscapeHTML(false) turns it off, json.Marshal has no
such switch, so a plain Marshal followed by a byte replace is not the fix.
Marshal into an Encoder writing to a bytes.Buffer instead.
- Map keys are sorted, so Marshal output is deterministic for maps.
- Unmarshaling into a non-pointer returns *json.InvalidUnmarshalError.
- A []byte field is base64, which surprises people expecting an array.
- NaN and ±Inf are unsupported and return an error.
- A failed Unmarshal may have already written some fields, so treat the target
as garbage on error.
- Go 1.25 ships an experimental rewrite behind GOEXPERIMENT=jsonv2, adding
encoding/json/v2 and encoding/json/jsontext. It is faster on decode and the
API is still evolving, so keep production on encoding/json for now.