Skip to main content Skip to sidebar

JSON vs GOB in Golang: Performance and Use Case Comparison

When working with data serialization in Go, developers often face the choice between JSON and GOB encoding. Both have their strengths and ideal use cases. This post explores the differences, performance characteristics, and when to use each format.

What is JSON?

JSON (JavaScript Object Notation) is a text-based, human-readable data interchange format. It’s language-independent and widely supported across different platforms and programming languages.

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type Metric struct {
    Timestamp float64 `json:"timestamp"`
    NameHash  uint64  `json:"name_hash"`
    Value     float64 `json:"value"`
}

type ComplexData struct {
    Users    []User            `json:"users"`
    Metadata map[string]string `json:"metadata"`
    Count    int               `json:"count"`
    Active   bool              `json:"active"`
}

func jsonExample() {
    user := User{ID: 1, Name: "John Doe", Email: "john@example.com"}
    
    // Encoding
    data, err := json.Marshal(user)
    if err != nil {
        log.Fatal(err)
    }
    
    // Decoding
    var decoded User
    err = json.Unmarshal(data, &decoded)
    if err != nil {
        log.Fatal(err)
    }
}

What is GOB?

GOB is Go’s native binary encoding format designed specifically for communication between Go programs. It’s efficient, type-safe, and handles Go-specific types naturally.

import (
    "bytes"
    "encoding/gob"
)

func gobExample() {
    user := User{ID: 1, Name: "John Doe", Email: "john@example.com"}
    
    var buf bytes.Buffer
    encoder := gob.NewEncoder(&buf)
    
    // Encoding
    err := encoder.Encode(user)
    if err != nil {
        log.Fatal(err)
    }
    
    // Decoding
    decoder := gob.NewDecoder(&buf)
    var decoded User
    err = decoder.Decode(&decoded)
    if err != nil {
        log.Fatal(err)
    }
}

Performance Comparison

Speed Benchmarks

Performance varies significantly based on data complexity:

func BenchmarkJSONMarshalSimple(b *testing.B) {
    user := User{ID: 1, Name: "John Doe", Email: "john@example.com"}
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := json.Marshal(user)
        if err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkGOBEncodeSimple(b *testing.B) {
    user := User{ID: 1, Name: "John Doe", Email: "john@example.com"}
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var buf bytes.Buffer
        encoder := gob.NewEncoder(&buf)
        err := encoder.Encode(user)
        if err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkJSONMarshalMetric(b *testing.B) {
    metric := Metric{Timestamp: 1642723200.123, NameHash: 1234567890123456789, Value: 99.99}
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := json.Marshal(metric)
        if err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkGOBEncodeMetric(b *testing.B) {
    metric := Metric{Timestamp: 1642723200.123, NameHash: 1234567890123456789, Value: 99.99}
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var buf bytes.Buffer
        encoder := gob.NewEncoder(&buf)
        err := encoder.Encode(metric)
        if err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkJSONMarshalComplex(b *testing.B) {
    data := ComplexData{
        Users:    make([]User, 100),
        Metadata: make(map[string]string),
        Count:    100,
        Active:   true,
    }
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := json.Marshal(data)
        if err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkGOBEncodeComplex(b *testing.B) {
    data := ComplexData{
        Users:    make([]User, 100),
        Metadata: make(map[string]string),
        Count:    100,
        Active:   true,
    }
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var buf bytes.Buffer
        encoder := gob.NewEncoder(&buf)
        err := encoder.Encode(data)
        if err != nil {
            b.Fatal(err)
        }
    }
}

Here are actual benchmark results from Apple M3 Pro (single-core, Go 1.24.4):

Simple Struct (User with 3 fields)

BenchmarkJSONMarshalSimple      6,461,572    183.0 ns/op    112 B/op    2 allocs/op
BenchmarkJSONUnmarshalSimple    1,542,492    774.5 ns/op    288 B/op    7 allocs/op
BenchmarkGOBEncodeSimple          856,372   1402 ns/op    1376 B/op   19 allocs/op
BenchmarkGOBDecodeSimple          121,036   9910 ns/op    7168 B/op  161 allocs/op

Numeric Struct (Metric with 3 numeric fields)

BenchmarkJSONMarshalMetric      4,449,670    266.9 ns/op    104 B/op    2 allocs/op
BenchmarkJSONUnmarshalMetric    1,238,731    968.3 ns/op    240 B/op    5 allocs/op
BenchmarkGOBEncodeMetric          850,065   1432 ns/op    1480 B/op   20 allocs/op
BenchmarkGOBDecodeMetric          122,762   9760 ns/op    7176 B/op  159 allocs/op

Metrics Slice (100 Metric structs)

BenchmarkJSONMarshalMetricsSlice   54,502   22,000 ns/op   8,216 B/op    2 allocs/op
BenchmarkJSONUnmarshalMetricsSlice 14,090   83,989 ns/op   6,376 B/op   14 allocs/op
BenchmarkGOBEncodeMetricsSlice    109,743   10,877 ns/op  17,032 B/op   29 allocs/op
BenchmarkGOBDecodeMetricsSlice     64,017   18,720 ns/op  13,744 B/op  178 allocs/op

Complex Struct (100 users + metadata)

BenchmarkJSONMarshalComplex       52,438   23,151 ns/op   11,888 B/op   103 allocs/op
BenchmarkJSONUnmarshalComplex     12,192   98,392 ns/op   25,968 B/op   518 allocs/op
BenchmarkGOBEncodeComplex         68,394   17,567 ns/op   26,192 B/op   133 allocs/op
BenchmarkGOBDecodeComplex         40,778   29,417 ns/op   25,192 B/op   516 allocs/op

Key Findings:

  • JSON is faster for simple structs: JSON marshal is 7.6x faster, unmarshal is 12.5x faster
  • JSON performance varies by data type: Numeric fields are slower to marshal (267ns vs 183ns)
  • GOB excels with array-like data: GOB encoding is 2.0x faster, decoding is 4.5x faster for metrics slice
  • GOB wins with complex data: GOB encoding is 1.3x faster, decoding is 3.3x faster
  • GOB uses significantly less space:
    • Complex data: 4,326 vs 7,346 bytes (41% smaller)
    • Metrics slice: 3,179 vs 8,080 bytes (61% smaller)
  • JSON has lower memory overhead for simple cases

Memory Usage

GOB generally uses less memory due to:

  • Binary format efficiency
  • No string parsing overhead
  • Built-in compression for repeated data

Key Differences

AspectJSONGOB
FormatText-basedBinary
Human ReadableYesNo
Language SupportUniversalGo-specific
Performance (Simple)FasterSlower
Performance (Complex)SlowerFaster
SizeLargerSmaller (41% reduction)
Type SafetyLimitedFull Go type support
Memory Usage (Simple)LowerHigher
Memory Usage (Complex)HigherSimilar
DebuggingEasyRequires tools

Advanced GOB Features

Interface Handling

gob.Register(ConcreteType{})

Custom Types

type Point struct {
    X, Y float64
}

func (p Point) GobEncode() ([]byte, error) {
    // Custom encoding logic
    return []byte(fmt.Sprintf("%f,%f", p.X, p.Y)), nil
}

func (p *Point) GobDecode(data []byte) error {
    // Custom decoding logic
    parts := strings.Split(string(data), ",")
    if len(parts) != 2 {
        return errors.New("invalid format")
    }
    var err error
    p.X, err = strconv.ParseFloat(parts[0], 64)
    if err != nil {
        return err
    }
    p.Y, err = strconv.ParseFloat(parts[1], 64)
    return err
}

When to Use Each

Use JSON When:

  • Interoperability with other languages/systems
  • Human-readable output needed
  • Web APIs and REST services
  • Configuration files
  • Data needs to be debugged easily
  • Working with JavaScript frontends
  • Simple data structures (single structs) where JSON’s performance advantage matters
  • Memory usage is a primary concern for simple structs
  • Cross-platform data exchange is required

Use GOB When:

  • Go-to-Go communication only
  • Complex data structures with slices, maps, and nested objects
  • Array-like data (slices of structs) where GOB shows 2-4.5x performance gains
  • Bulk data processing and batch operations
  • Performance is critical for large/complex datasets
  • Working with complex Go types (interfaces, custom types)
  • Internal microservices communication
  • Caching Go objects and data structures
  • Network protocols between Go services
  • Storage space optimization is critical (41-61% size reduction)
  • Full Go type safety and reflection support is required
  • High-throughput data pipelines

Conclusion

The choice between JSON and GOB depends heavily on your data patterns and performance requirements:

Performance Hierarchy:

  • Simple structs: JSON dominates (7.6x faster encoding, 12.5x faster decoding)
  • Numeric-heavy structs: JSON still wins but with reduced advantage (5.4x faster encoding, 10.1x faster decoding)
  • Array/slice data: GOB takes over (2.0x faster encoding, 4.5x faster decoding)
  • Complex nested data: GOB maintains advantage (1.3x faster encoding, 3.3x faster decoding)

Space Efficiency:

  • GOB consistently produces 41-61% smaller output across all data types
  • Critical for network bandwidth and storage optimization

Use Case Guidelines:

  • External APIs & Web Services: Always use JSON for interoperability
  • Internal Go Services: Use GOB for performance-critical operations
  • Data Processing Pipelines: GOB excels with bulk operations on structured data
  • Configuration & Debugging: JSON for human readability
  • Caching & Serialization: GOB for space efficiency and speed with complex data

The crossover point occurs when data complexity increases beyond simple structs - this is where GOB’s binary efficiency and Go-native optimizations shine.