What is Marshal in Go?

Introduction

When handling data transfer, it’s easy to forget the step of “serializing and deserializing data” because libraries like Axios🔗 abstract it away. Recently while working on backend code I refreshed this knowledge, continuing from my previous post: Go Struct Tag and reflect to explore how Go handles serialized data.

fetch("x", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
})
// Axios 自動序列化資料
axios.post("x", {
email: "user@example.com",
project_id: "<project_id>"
})

Serialization and Marshalling

What is Serialization?

Programming languages have their own data structures. For example, Go’s map and JavaScript’s object may look similar but are implemented very differently under the hood. The most common way for them to communicate is via the JSON data format.

Converting an in-memory data structure into a storable or transmittable format is called serialization. Converting it back to the original data structure is called deserialization.

What is Marshalling

The original meaning of “marshal” is to “assemble”, “arrange”, or “organize”.

  • Military: “To marshal troops” — assembling scattered soldiers into an orderly formation for action.
  • Legal: “To marshal facts” — organizing chaotic information into a logical structure.
  • Computer science: Borrowed to mean arranging complex in-memory or pointer-based data structures (e.g., a struct containing pointers) into a flat sequence of bytes suitable for transmission.

Subtle differences between Marshal and Serialization

Although they are often used interchangeably, in computer science terminology there are subtle differences:

CharacteristicSerializationMarshalling
Core intentConvert data into a persistent format (saving, storing in DB)Convert data into a transmittable format (inter-process or network communication)
Included contentUsually focuses only on the data values themselvesBesides data, may include metadata and even handling for remote procedure calls (RPC)
Structural complexityEmphasizes flattening dataEmphasizes arranging complex, non-contiguous objects into a transmittable form

Go Marshal

The Go standard library already includes the encoding/json package.

  • Struct field names become JSON keys.
  • Only fields that start with an uppercase letter can be serialized (exported).

encoding/json struct tag basics

You usually control the output format via struct tags:

import (
"encoding/json"
"fmt"
)
func main() {
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
u := User{
Name: "Riceball",
Email: "rice@example.com",
}
b, _ := json.Marshal(u)
// Convert to JSON: {"name":"Riceball","email":"rice@example.com"}
fmt.Println(string(b))
}

Common tag options

type User struct {
ID int `json:"id"` // Rename the field
Name string `json:"name,omitempty"` // Do not output if the value is zero
Token string `json:"-"` // Ignore this field completely
}

Unmarshal (Deserialization) example

type User struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
Email string `json:"email"`
Token string `json:"-"`
}
var u User
// Note: Extra fields in the JSON will be ignored.
err := json.Unmarshal([]byte(`{"id":1,"name":"Riceball","email":"rice@example.com","extra":"ignored"}`), &u)
if err != nil {
fmt.Println("出錯了:", err)
}
// result: {ID:1 Name:Riceball Email:rice@example.com Token:}
fmt.Printf("解析結果: %+v\n", u)

JSON Struct Tag advanced usage

string option: force values to strings

When you need to present numbers or booleans as strings in JSON, you can use the string option:

  • Useful when interfacing with some legacy systems or APIs that expect all values as strings.
  • Prevents precision loss when JavaScript handles very large numbers by sending them as strings.
type Product struct {
ID int `json:"id,string"` // output is "123" instead of 123
Price float64 `json:"price,string"` // output is "99.99" instead of 99.99
InStock bool `json:"in_stock,string"` // output is "true" instead of true
}
p := Product{
ID: 123,
Price: 99.99,
InStock: true,
}
b, _ := json.Marshal(p)
fmt.Println(string(b))
// Output: {"id":"123","price":"99.99","in_stock":"true"}

Anonymous embedding: flatten nested structures

type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type User struct {
Name string `json:"name"`
Address // Anonymous embedding will "elevate" the field to the User level
}
u := User{
Name: "Riceball",
Address: Address{
City: "Taipei",
Country: "Taiwan",
},
}
b, _ := json.Marshal(u)
fmt.Println(string(b))
// Output: {"name":"Riceball","city":"Taipei","country":"Taiwan"}
// Note: The Address field appears directly at the outermost level, not nested under "address".

If you want a nested structure, name it explicitly:

type User struct {
Name string `json:"name"`
Address Address `json:"address"`
}
// Output: {"name":"Riceball","address":{"city":"Taipei","country":"Taiwan"}}

Mongo Driver

import "go.mongodb.org/mongo-driver/bson"
type Address struct {
City string `bson:"city"`
Country string `bson:"country"`
}
type User struct {
Name string `bson:"name"`
Address Address `bson:",inline"` // MongoDB's inline syntax
}
// File structure after storing in MongoDB:
// {
// "name": "Riceball",
// "city": "Taipei", // Note: Not a nested structure under address
// "country": "Taiwan"
// }

Combining multiple options

type APIResponse struct {
Code int `json:"code,string,omitempty"`
Message string `json:"message,omitempty"`
Data any `json:"data,omitempty"`
}

Further reading