When working with Go slices, you might assume that a nil slice and an empty slice are completely interchangeable. After all, both have zero length, zero capacity, and support operations like append(). However, there’s one important case where they behave differently: JSON marshaling.

The Difference

Let’s look at a simple example:

package main

import (
    "encoding/json"
    "fmt"
)

type Example struct {
    NilSlice   []int
    EmptySlice []int
}

func main() {
    e := Example{
        EmptySlice: make([]int, 0),
        // NilSlice left as nil
    }
    
    data, _ := json.Marshal(e)
    fmt.Println(string(data))
}

This outputs:

{"NilSlice":null,"EmptySlice":[]}

Why This Matters

This distinction becomes important when:

  1. You’re building APIs where clients expect consistent JSON structure
  2. You’re integrating with systems that handle null and [] differently
  3. You need to maintain backward compatibility with existing JSON parsers

Solutions

You have several ways to handle this:

1. Initialize with make()

vv := make([]int, 0)

2. Explicit nil check

if vv == nil {
    vv = []int{}
}

3. Use omitempty tag

type MyStruct struct {
    Versions []int `json:"versions,omitempty"`
}

Best Practices

Choose your approach based on your requirements:

  • Use omitempty when the field’s presence is optional
  • Initialize with make() when you always want [] in your JSON
  • Use explicit nil checks when you need to convert nil slices to empty ones at specific points

Remember: while nil and empty slices behave the same in most Go operations, JSON marshaling is one of the few cases where the difference becomes visible and potentially significant.