Skip to main content

JSON · XML · CSV · base64 · gob — Complete Encoding Guide

Serialization is needed whenever you store or transmit data. Go's standard library supports all the most common formats out of the box.

encoding/json — The Most Commonly Used Encoding

Basic Marshal/Unmarshal

package main

import (
"encoding/json"
"fmt"
)

type Address struct {
City string `json:"city"`
Country string `json:"country"`
ZipCode string `json:"zip_code,omitempty"` // omit field when empty
}

type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age,omitempty"` // omit when zero value
Password string `json:"-"` // always omit
Tags []string `json:"tags"`
Address Address `json:"address"`
IsActive bool `json:"is_active"`
}

func main() {
user := User{
ID: 1,
Name: "Alice",
Email: "alice@example.com",
Age: 30,
Password: "secret123",
Tags: []string{"admin", "developer"},
Address: Address{
City: "Seoul",
Country: "Korea",
},
IsActive: true,
}

// struct → JSON (Marshal)
data, err := json.Marshal(user)
if err != nil {
fmt.Println("marshal error:", err)
return
}
fmt.Println(string(data))
// {"id":1,"name":"Alice","email":"alice@example.com","age":30,"tags":["admin","developer"],"address":{"city":"Seoul","country":"Korea"},"is_active":true}

// pretty-print with indentation (MarshalIndent)
pretty, _ := json.MarshalIndent(user, "", " ")
fmt.Println(string(pretty))

// JSON → struct (Unmarshal)
jsonStr := `{
"id": 2,
"name": "Bob",
"email": "bob@example.com",
"tags": ["user"],
"address": {"city": "London", "country": "UK", "zip_code": "SW1A 1AA"},
"is_active": false
}`

var decoded User
if err := json.Unmarshal([]byte(jsonStr), &decoded); err != nil {
fmt.Println("unmarshal error:", err)
return
}
fmt.Printf("decoded user: %+v\n", decoded)
}

Streaming Encoder/Decoder

package main

import (
"encoding/json"
"fmt"
"os"
"strings"
)

type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}

func main() {
// NewEncoder — write JSON directly to io.Writer
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")

products := []Product{
{1, "Go Textbook", 29.99},
{2, "Go Keyboard", 89.00},
{3, "Go Mug", 15.00},
}

for _, p := range products {
if err := encoder.Encode(p); err != nil {
fmt.Println("encode error:", err)
}
}

// NewDecoder — read JSON from io.Reader
jsonLines := `{"id":1,"name":"Apple","price":1.50}
{"id":2,"name":"Banana","price":0.80}
{"id":3,"name":"Cherry","price":5.00}`

decoder := json.NewDecoder(strings.NewReader(jsonLines))
for decoder.More() { // true while more JSON values remain
var p Product
if err := decoder.Decode(&p); err != nil {
fmt.Println("decode error:", err)
break
}
fmt.Printf("product: %s ($%.2f)\n", p.Name, p.Price)
}
}

Custom MarshalJSON / UnmarshalJSON

package main

import (
"encoding/json"
"fmt"
"time"
)

// CustomTime — custom time type
type CustomTime struct {
time.Time
}

// MarshalJSON — customize JSON serialization
func (ct CustomTime) MarshalJSON() ([]byte, error) {
return json.Marshal(ct.Format("January 2, 2006"))
}

// UnmarshalJSON — customize JSON deserialization
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
t, err := time.Parse("January 2, 2006", s)
if err != nil {
return err
}
ct.Time = t
return nil
}

type Event struct {
Name string `json:"name"`
StartDate CustomTime `json:"start_date"`
}

func main() {
event := Event{
Name: "Go Conference",
StartDate: CustomTime{time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)},
}

data, _ := json.Marshal(event)
fmt.Println(string(data))
// {"name":"Go Conference","start_date":"March 15, 2024"}

var decoded Event
json.Unmarshal(data, &decoded)
fmt.Printf("event: %s, date: %s\n",
decoded.Name, decoded.StartDate.Format("2006-01-02"))
}

json.RawMessage — Deferred Parsing

package main

import (
"encoding/json"
"fmt"
)

// APIResponse — different data structure depending on type
type APIResponse struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"` // defer parsing
}

type UserData struct {
Name string `json:"name"`
Email string `json:"email"`
}

type OrderData struct {
OrderID string `json:"order_id"`
Amount float64 `json:"amount"`
}

func processResponse(resp APIResponse) {
switch resp.Type {
case "user":
var u UserData
json.Unmarshal(resp.Data, &u)
fmt.Printf("user: %s (%s)\n", u.Name, u.Email)
case "order":
var o OrderData
json.Unmarshal(resp.Data, &o)
fmt.Printf("order %s: $%.2f\n", o.OrderID, o.Amount)
default:
fmt.Printf("unknown type: %s\n", resp.Type)
}
}

func main() {
responses := []string{
`{"type":"user","data":{"name":"Alice","email":"alice@example.com"}}`,
`{"type":"order","data":{"order_id":"ORD-001","amount":29.99}}`,
}

for _, r := range responses {
var resp APIResponse
json.Unmarshal([]byte(r), &resp)
processResponse(resp)
}
}

encoding/xml — XML Encoding

package main

import (
"encoding/xml"
"fmt"
)

type Book struct {
XMLName xml.Name `xml:"book"` // XML element name
ID int `xml:"id,attr"` // XML attribute
Title string `xml:"title"`
Author string `xml:"author"`
Price float64 `xml:"price"`
InStock bool `xml:"in_stock"`
Tags []string `xml:"tags>tag"` // nested elements
}

type Library struct {
XMLName xml.Name `xml:"library"`
Books []Book `xml:"book"`
}

func main() {
library := Library{
Books: []Book{
{
ID: 1,
Title: "The Go Programming Language",
Author: "Alan Donovan",
Price: 45.99,
InStock: true,
Tags: []string{"go", "programming", "systems"},
},
{
ID: 2,
Title: "Go in Action",
Author: "William Kennedy",
Price: 39.99,
InStock: false,
Tags: []string{"go", "web"},
},
},
}

// struct → XML
data, err := xml.MarshalIndent(library, "", " ")
if err != nil {
fmt.Println("marshal error:", err)
return
}
// add XML declaration
fmt.Println(xml.Header + string(data))

// XML → struct
xmlStr := `<library>
<book id="3">
<title>Learning Go</title>
<author>Jon Bodner</author>
<price>34.99</price>
<in_stock>true</in_stock>
</book>
</library>`

var decoded Library
if err := xml.Unmarshal([]byte(xmlStr), &decoded); err != nil {
fmt.Println("unmarshal error:", err)
return
}
fmt.Printf("first book: %s ($%.2f)\n", decoded.Books[0].Title, decoded.Books[0].Price)
}

encoding/csv — CSV File Processing

package main

import (
"encoding/csv"
"fmt"
"os"
"strconv"
"strings"
)

type Employee struct {
ID int
Name string
Department string
Salary float64
}

func writeCSV(employees []Employee) error {
file, err := os.Create("employees.csv")
if err != nil {
return err
}
defer file.Close()

writer := csv.NewWriter(file)
defer writer.Flush()

// write header
if err := writer.Write([]string{"ID", "Name", "Department", "Salary"}); err != nil {
return err
}

// write data
for _, e := range employees {
record := []string{
strconv.Itoa(e.ID),
e.Name,
e.Department,
strconv.FormatFloat(e.Salary, 'f', 2, 64),
}
if err := writer.Write(record); err != nil {
return err
}
}

return writer.Error()
}

func readCSV(filename string) ([]Employee, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()

reader := csv.NewReader(file)
reader.TrimLeadingSpace = true // trim leading whitespace

// skip header
if _, err := reader.Read(); err != nil {
return nil, err
}

// read all records
records, err := reader.ReadAll()
if err != nil {
return nil, err
}

var employees []Employee
for _, r := range records {
id, _ := strconv.Atoi(r[0])
salary, _ := strconv.ParseFloat(r[3], 64)
employees = append(employees, Employee{
ID: id, Name: r[1], Department: r[2], Salary: salary,
})
}

return employees, nil
}

func main() {
employees := []Employee{
{1, "Alice", "Engineering", 95000},
{2, "Bob", "Design", 82000},
{3, "Charlie", "Marketing", 74000},
}

// write CSV
if err := writeCSV(employees); err != nil {
fmt.Println("write error:", err)
return
}

// read CSV
loaded, err := readCSV("employees.csv")
if err != nil {
fmt.Println("read error:", err)
return
}

fmt.Println("=== Employee List ===")
for _, e := range loaded {
fmt.Printf("ID: %d, name: %s, dept: %s, salary: $%.2f\n",
e.ID, e.Name, e.Department, e.Salary)
}

// in-memory CSV parsing (without a file)
csvData := `product,price,stock
Apple,1.50,100
Banana,0.80,250
Cherry,5.00,30`

r := csv.NewReader(strings.NewReader(csvData))
r.Read() // skip header
for {
record, err := r.Read()
if err != nil {
break // io.EOF
}
fmt.Printf("product: %s, price: $%s\n", record[0], record[1])
}

// clean up temp file
os.Remove("employees.csv")
}

encoding/base64 — Binary-to-Text Encoding

package main

import (
"encoding/base64"
"fmt"
)

func main() {
data := []byte("Hello, Go World! 🌍")

// StdEncoding — standard Base64 (with '=' padding)
encoded := base64.StdEncoding.EncodeToString(data)
fmt.Println("standard:", encoded)

decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
fmt.Println("decode error:", err)
return
}
fmt.Println("restored:", string(decoded))

// URLEncoding — URL-safe Base64 ('+' → '-', '/' → '_')
urlEncoded := base64.URLEncoding.EncodeToString(data)
fmt.Println("URL-safe:", urlEncoded)

// RawStdEncoding — Base64 without padding
rawEncoded := base64.RawStdEncoding.EncodeToString(data)
fmt.Println("no padding:", rawEncoded)

// RawURLEncoding — URL-safe without padding (used in JWT)
jwtPart := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`))
fmt.Println("JWT header:", jwtPart)

// encoding binary data (images, files, etc.)
binaryData := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} // PNG signature
b64 := base64.StdEncoding.EncodeToString(binaryData)
fmt.Printf("PNG header Base64: %s\n", b64)
}

encoding/gob — Go Native Serialization

package main

import (
"bytes"
"encoding/gob"
"fmt"
)

type Shape interface {
Area() float64
}

type Circle struct {
Radius float64
}

func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}

type Rectangle struct {
Width, Height float64
}

func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

type GameState struct {
Score int
Level int
Player string
Shapes []Shape
}

func main() {
// types containing interfaces must be registered before gob encoding
gob.Register(Circle{})
gob.Register(Rectangle{})

state := GameState{
Score: 12500,
Level: 5,
Player: "Alice",
Shapes: []Shape{
Circle{Radius: 5},
Rectangle{Width: 10, Height: 3},
},
}

// serialize (Encode)
var buf bytes.Buffer
encoder := gob.NewEncoder(&buf)
if err := encoder.Encode(state); err != nil {
fmt.Println("encode error:", err)
return
}
fmt.Printf("serialized size: %d bytes\n", buf.Len())

// deserialize (Decode)
decoder := gob.NewDecoder(&buf)
var loaded GameState
if err := decoder.Decode(&loaded); err != nil {
fmt.Println("decode error:", err)
return
}

fmt.Printf("game state: player=%s, level=%d, score=%d\n",
loaded.Player, loaded.Level, loaded.Score)
for _, s := range loaded.Shapes {
fmt.Printf(" shape area: %.2f\n", s.Area())
}
}

Real-World Example — REST API Response Serialization

package main

import (
"encoding/json"
"fmt"
"net/http"
"time"
)

// APIError — standard error response
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}

func (e APIError) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// APIResponse — standard success response
type APIResponse[T any] struct {
Success bool `json:"success"`
Data T `json:"data,omitempty"`
Error *APIError `json:"error,omitempty"`
Timestamp time.Time `json:"timestamp"`
RequestID string `json:"request_id"`
}

func writeJSON(w http.ResponseWriter, statusCode int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(statusCode)
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
encoder.Encode(v)
}

func successResponse[T any](data T, requestID string) APIResponse[T] {
return APIResponse[T]{
Success: true,
Data: data,
Timestamp: time.Now().UTC(),
RequestID: requestID,
}
}

func errorResponse(code int, message, details, requestID string) APIResponse[any] {
return APIResponse[any]{
Success: false,
Error: &APIError{
Code: code,
Message: message,
Details: details,
},
Timestamp: time.Now().UTC(),
RequestID: requestID,
}
}

type UserProfile struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Roles []string `json:"roles"`
JoinedAt string `json:"joined_at"`
}

func main() {
// success response simulation
user := UserProfile{
ID: 42,
Name: "Alice",
Email: "alice@example.com",
Roles: []string{"user", "admin"},
JoinedAt: "2024-01-15",
}

resp := successResponse(user, "req-abc-123")
data, _ := json.MarshalIndent(resp, "", " ")
fmt.Println("=== Success Response ===")
fmt.Println(string(data))

// error response simulation
errResp := errorResponse(404, "user not found",
"no user with ID 999", "req-def-456")
errData, _ := json.MarshalIndent(errResp, "", " ")
fmt.Println("\n=== Error Response ===")
fmt.Println(string(errData))

// config file parser
configJSON := `{
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp",
"max_connections": 100
},
"cache": {
"ttl_seconds": 300,
"max_size_mb": 512
},
"feature_flags": {
"dark_mode": true,
"new_ui": false
}
}`

var config map[string]json.RawMessage
json.Unmarshal([]byte(configJSON), &config)

var dbConfig map[string]interface{}
json.Unmarshal(config["database"], &dbConfig)
fmt.Printf("\nDB host: %s, port: %.0f\n",
dbConfig["host"], dbConfig["port"])
}

Format Comparison

FormatAdvantagesDisadvantagesBest For
JSONuniversal, readablesize, number precisionREST APIs, config files
XMLschema support, attributesverbose, complex parsinglegacy systems, SOAP
CSVsimple, spreadsheet-compatibleno types, no nestingdata import/export
Base64binary as text33% size increaseJWT, embedding images
GobGo-native, fastGo-onlyinternal caches, IPC

For performance-critical JSON in production, consider github.com/json-iterator/go or github.com/bytedance/sonic. They provide the same API as the standard library but run 3–8x faster.