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
GoJSON
booltrue / false
numeric typesnumber
stringstring
[]bytebase64 string
struct, map[string]Tobject
slice, arrayarray
nil slice or mapnull
empty slice []T{}[]
pointerthe value, or null
chan, func, complexerror, 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".

Struct Tags

type Post struct {
    ID       int       `json:"id"`
    Title    string    `json:"title"`
    Body     string    `json:"body,omitempty"`     // skip "" , 0, nil, len 0
    Views    int       `json:"views,omitzero"`     // 1.24+, skip the zero value
    Draft    bool      `json:"-"`                  // never encoded or decoded
    Dash     string    `json:"-,"`                 // a field literally named "-"
    BigID    int64     `json:"big_id,string"`      // "9007199254740993"
    Meta     any       `json:"meta,omitempty"`
}
TagEffect
json:"name"rename the key
json:"name,omitempty"omit false, 0, nil, and any empty string/slice/map
json:"name,omitzero"omit the type's zero value (Go 1.24+)
json:"-"drop the field entirely
json:"-,"key is the literal -
json:",omitempty"keep the Go field name, add the option
json:"n,string"encode a number or bool as a JSON string

omitempty does not omit an empty struct or a zero time.Time, which is the classic surprise. That is exactly what omitzero fixed in Go 1.24: it uses an IsZero() bool method when the type has one, so a zero time.Time finally disappears. Both options together omit a value that is either empty or zero. The ,string option applies to numbers, bools and strings only, and is the usual fix for int64 IDs that JavaScript would round.

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
JSONGo type in any
objectmap[string]any
array[]any
numberfloat64
stringstring
true / falsebool
nullnil

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.