1. Gambaran umum tentang pustaka standar encoding/json

Bahasa Go menyediakan pustaka encoding/json yang kuat untuk menangani format data JSON. Dengan pustaka ini, Anda dapat dengan mudah mengonversi tipe data Go ke format JSON (serialisasi) atau mengonversi data JSON ke tipe data Go (deserialisasi). Pustaka ini menyediakan banyak fungsionalitas seperti encoding, decoding, streaming IO, dan dukungan untuk logika parsing JSON kustom.

Tipe data dan fungsi yang paling penting dalam pustaka ini meliputi:

  • Marshal dan MarshalIndent: digunakan untuk membuat serialisasi tipe data Go menjadi string JSON.
  • Unmarshal: digunakan untuk deserialisasi string JSON menjadi tipe data Go.
  • Encoder dan Decoder: digunakan untuk streaming IO data JSON.
  • Valid: digunakan untuk memeriksa apakah string yang diberikan adalah format JSON yang valid.

Kita akan belajar secara khusus mengenai penggunaan fungsi dan tipe data ini di bab-bab berikutnya.

2. Mengonversi Struktur Data Go ke JSON

2.1 Menggunakan json.Marshal

json.Marshal adalah fungsi yang mengonversi tipe data Go menjadi string JSON. Fungsi ini mengambil tipe data dari bahasa Go sebagai input, mengonversinya ke format JSON, dan mengembalikan potongan byte bersama dengan kemungkinan kesalahan.

Berikut adalah contoh sederhana yang menunjukkan bagaimana mengonversi sebuah struktur data Go ke string JSON:

package main

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

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    person := Person{"Alice", 30}
    jsonData, err := json.Marshal(person)
    if err != nil {
        log.Fatalf("Gagal melakukan marshaling JSON: %s", err)
    }
    fmt.Println(string(jsonData)) // Output: {"name":"Alice","age":30}
}

Selain dari struktur, fungsi json.Marshal juga dapat melakukan serialisasi tipe data lain seperti map dan slice. Berikut adalah contoh menggunakan map[string]interface{} dan slice:

// Mengonversi map ke JSON
myMap := map[string]interface{}{
    "name": "Bob",
    "age":  25,
}
jsonData, err := json.Marshal(myMap)
// ... penanganan kesalahan dan keluaran dihilangkan ...

// Mengonversi slice ke JSON
mySlice := []string{"Apel", "Pisang", "Ceri"}
jsonData, err := json.Marshal(mySlice)
// ... penanganan kesalahan dan keluaran dihilangkan ...

2.2 Tag Struktur

Di dalam Go, tag struktur digunakan untuk memberikan metadata untuk bidang-bidang struktur, mengontrol perilaku serialisasi JSON. Kasus penggunaan paling umum mencakup pengubahan nama bidang, mengabaikan bidang, dan serialisasi bersyarat.

Sebagai contoh, Anda dapat menggunakan tag json:"<nama>" untuk menentukan nama dari bidang JSON:

type Hewan struct {
    NamaSpesies string `json:"spesies"`
    Deskripsi   string `json:"desc,omitempty"`
    Tag         string `json:"-"` // Menambahkan tag "-" menandakan bidang ini tidak akan di-serialisasi
}

Pada contoh di atas, tag json:"-" di depan bidang Tag memberitahu json.Marshal untuk mengabaikan bidang ini. Opsi omitempty untuk bidang Deskripsi menunjukkan bahwa jika bidang tersebut kosong (nilai nol, seperti string kosong), maka tidak akan disertakan dalam JSON yang ter-serialisasi.

Berikut adalah contoh lengkap penggunaan tag struktur:

package main

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

type Hewan struct {
    NamaSpesies string `json:"spesies"`
    Deskripsi   string `json:"desc,omitempty"`
    Tag         string `json:"-"`
}

func main() {
    hewan := Hewan{
        NamaSpesies: "Gajah Afrika",
        Deskripsi: "Mamalia besar dengan belalai dan gading.",
        Tag: "terancam punah", // Bidang ini tidak akan di-serialisasi ke JSON
    }
    jsonData, err := json.Marshal(hewan)
    if err != nil {
        log.Fatalf("Gagal melakukan marshaling JSON: %s", err)
    }
    fmt.Println(string(jsonData)) // Output: {"spesies":"Gajah Afrika","desc":"Mamalia besar dengan belalai dan gading."}
}

Dengan cara ini, Anda dapat memastikan struktur data yang jelas sambil mengontrol representasi JSON, menangani secara fleksibel berbagai kebutuhan serialisasi.

3. Deserialisasi JSON ke Struktur Data Go

3.1 Menggunakan json.Unmarshal

Fungsi json.Unmarshal memungkinkan kita untuk mengurai string JSON ke dalam struktur data Go seperti struk, peta, dll. Untuk menggunakan json.Unmarshal, kita pertama-tama perlu mendefinisikan struktur data Go yang cocok dengan data JSON.

Misalkan kita memiliki data JSON berikut:

{
    "name": "Alice",
    "age": 25,
    "emails": ["[email protected]", "[email protected]"]
}

Untuk mengurai data ini ke dalam struktur Go, kita perlu mendefinisikan struktur yang cocok:

type User struct {
    Name   string   `json:"name"`
    Age    int      `json:"age"`
    Emails []string `json:"emails"`
}

Sekarang kita dapat menggunakan json.Unmarshal untuk deserialisasi:

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonData := `{
        "name": "Alice",
        "age": 25,
        "emails": ["[email protected]", "[email protected]"]
    }`

    var user User
    err := json.Unmarshal([]byte(jsonData), &user)
    if err != nil {
        fmt.Println("Error saat melakukan unmarshalling JSON:", err)
        return
    }

    fmt.Printf("User: %+v\n", user)
}

Pada contoh di atas, kami menggunakan tag seperti json:"name" untuk memberi tahu fungsi json.Unmarshal tentang pemetaan bidang JSON ke bidang struktur.

3.2 Parsing Dinamis

Terkadang, struktur JSON yang perlu kita parsing tidak diketahui sebelumnya, atau struktur data JSON dapat berubah secara dinamis. Dalam kasus seperti itu, kita dapat menggunakan interface{} atau json.RawMessage untuk parsing.

Menggunakan interface{} memungkinkan kita untuk parsing tanpa mengetahui struktur JSON:

func main() {
    jsonData := `{
        "name": "Alice",
        "details": {
            "age": 25,
            "job": "Engineer"
        }
    }`

    var result map[string]interface{}
    json.Unmarshal([]byte(jsonData), &result)

    fmt.Println(result)

    // Asersi tipe, pastikan cocok sebelum digunakan
    name := result["name"].(string)
    fmt.Println("Nama:", name)
    details := result["details"].(map[string]interface{})
    age := details["age"].(float64) // Catatan: angka dalam interface{} diperlakukan sebagai float64
    fmt.Println("Usia:", age)
}

Menggunakan json.RawMessage memungkinkan kita untuk mempertahankan JSON asli sambil secara selektif melakukan parsing bagian-bagiannya:

type UserDynamic struct {
    Name    string          `json:"name"`
    Details json.RawMessage `json:"details"`
}

func main() {
    jsonData := `{
        "name": "Alice",
        "details": {
            "age": 25,
            "job": "Engineer"
        }
    }`

    var user UserDynamic
    json.Unmarshal([]byte(jsonData), &user)

    var details map[string]interface{}
    json.Unmarshal(user.Details, &details)

    fmt.Println("Nama:", user.Name)
    fmt.Println("Usia:", details["age"])
    fmt.Println("Pekerjaan:", details["job"])
}

Pendekatan ini berguna untuk menangani struktur JSON di mana beberapa bidang dapat berisi berbagai jenis data, dan memungkinkan penanganan data yang fleksibel.

4 Mengelola Struktur Bertingkat dan Array

4.1 Objek JSON Bertingkat

Data JSON umumnya tidak datar, tetapi mengandung struktur bertingkat. Dalam Go, kita dapat menangani situasi ini dengan mendefinisikan struktur bertingkat.

Misalkan kita memiliki JSON bertingkat berikut:

{
    "name": "Bob",
    "contact": {
        "email": "[email protected]",
        "address": "123 Main St"
    }
}

Kita dapat mendefinisikan struktur Go sebagai berikut:

type ContactInfo struct {
    Email   string `json:"email"`
    Address string `json:"address"`
}

type UserWithContact struct {
    Name    string      `json:"name"`
    Contact ContactInfo `json:"contact"`
}

Operasi deserialisasi mirip dengan struktur non-bertingkat:

func main() {
    jsonData := `{
        "name": "Bob",
        "contact": {
            "email": "[email protected]",
            "address": "123 Main St"
        }
    }`

    var user UserWithContact
    err := json.Unmarshal([]byte(jsonData), &user)
    if err != nil {
        fmt.Println("Error saat melakukan unmarshalling JSON:", err)
    }

    fmt.Printf("%+v\n", user)
}

4.2 JSON Arrays (Array JSON)

Pada JSON, array adalah struktur data yang umum. Di Go, array tersebut sesuai dengan slice.

Pertimbangkan array JSON berikut:

[
    {"name": "Dave", "age": 34},
    {"name": "Eve", "age": 28}
]

Di Go, kita mendefinisikan struct dan slice yang sesuai sebagai berikut:

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    jsonData := `[
        {"name": "Dave", "age": 34},
        {"name": "Eve", "age": 28}
    ]`

    var people []Person
    json.Unmarshal([]byte(jsonData), &people)
    
    for _, person := range people {
        fmt.Printf("%+v\n", person)
    }
}

Dengan cara ini, kita dapat melakukan deserialisasi setiap elemen dalam array JSON menjadi slice dari struct Go untuk pemrosesan dan akses lebih lanjut.

5 Penanganan Kesalahan (Error Handling)

Ketika menangani data JSON, baik itu proses serialisasi (mengkonversi data terstruktur ke format JSON) atau deserialisasi (mengkonversi JSON kembali ke data terstruktur), kesalahan dapat terjadi. Selanjutnya, kita akan membahas kesalahan umum dan bagaimana cara menanganinya.

5.1 Penanganan Kesalahan Serialisasi (Serialization Error Handling)

Kesalahan serialisasi biasanya terjadi selama proses mengonversi struct atau tipe data lain menjadi string JSON. Sebagai contoh, jika kita mencoba untuk melakukan serialisasi terhadap struct yang mengandung field ilegal (seperti tipe channel atau fungsi yang tidak dapat direpresentasikan dalam JSON), json.Marshal akan mengembalikan sebuah kesalahan.

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

type User struct {
    Name string
    Age  int
    // Diasumsikan ada field di sini yang tidak dapat diserialisasi
    // Data chan struct{} // Channel tidak dapat direpresentasikan dalam JSON
}

func main() {
    u := User{
        Name: "Alice",
        Age:  30,
        // Data: make(chan struct{}),
    }

    bytes, err := json.Marshal(u)
    if err != nil {
        log.Fatalf("Gagal melakukan serialisasi JSON: %v", err)
    }
    
    fmt.Println(string(bytes))
}

Pada contoh di atas, kita dengan sengaja mengomentari bagian field Data. Jika tidak dikomentari, serialisasi akan gagal, dan program akan mencatat kesalahannya dan menghentikan eksekusi. Penanganan kesalahan semacam ini umumnya melibatkan pemeriksaan kesalahan dan implementasi strategi penanganan kesalahan yang sesuai (seperti mencatat kesalahan, mengembalikan data default, dll.).

5.2 Penanganan Kesalahan Deserialisasi (Deserialization Error Handling)

Kesalahan deserialisasi dapat terjadi selama proses mengonversi string JSON kembali menjadi struct Go atau tipe data lainnya. Sebagai contoh, jika format string JSON tidak benar atau tidak kompatibel dengan tipe target, json.Unmarshal akan mengembalikan sebuah kesalahan.

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

func main() {
    var data = []byte(`{"name":"Alice","age":"unknown"}`) // "age" seharusnya berupa bilangan bulat, tetapi di sini disediakan dalam bentuk string
    var u User

    err := json.Unmarshal(data, &u)
    if err != nil {
        log.Fatalf("Gagal melakukan deserialisasi JSON: %v", err)
    }
    
    fmt.Printf("%+v\n", u)
}

Pada contoh kode ini, kita dengan sengaja memberikan tipe data yang salah untuk field age (sebuah string alih-alih bilangan bulat yang diharapkan), menyebabkan json.Unmarshal mengembalikan kesalahan. Oleh karena itu, kita perlu menangani situasi ini secara tepat. Praktik umumnya adalah mencatat pesan kesalahan dan, tergantung pada skenario, mungkin mengembalikan objek kosong, nilai default, atau pesan kesalahan.

6 Fitur Lanjutan dan Optimisasi Kinerja

6.1 Penyesuaian Marshal dan Unmarshal

Secara default, paket encoding/json di Go melakukan serialisasi dan deserialisasi JSON melalui refleksi. Namun, kita dapat menyesuaikan proses-proses ini dengan mengimplementasikan antarmuka json.Marshaler dan json.Unmarshaler.

import (
    "encoding/json"
    "fmt"
)

type Color struct {
    Red   uint8
    Green uint8
    Blue  uint8
}

func (c Color) MarshalJSON() ([]byte, error) {
    hex := fmt.Sprintf("\"#%02x%02x%02x\"", c.Red, c.Green, c.Blue)
    return []byte(hex), nil
}

func (c *Color) UnmarshalJSON(data []byte) error {
    _, err := fmt.Sscanf(string(data), "\"#%02x%02x%02x\"", &c.Red, &c.Green, &c.Blue)
    return err
}

func main() {
    c := Color{Red: 255, Green: 99, Blue: 71}
    
    jsonColor, _ := json.Marshal(c)
    fmt.Println(string(jsonColor))

    var newColor Color
    json.Unmarshal(jsonColor, &newColor)
    fmt.Println(newColor)
}

Di sini, kita telah mendefinisikan tipe Color dan mengimplementasikan metode MarshalJSON dan UnmarshalJSON untuk mengonversi warna RGB ke tipe string heksadesimal dan kemudian kembali ke warna RGB.

6.2 Pengekoder dan Penguraian

Ketika menangani data JSON besar, penggunaan langsung json.Marshal dan json.Unmarshal dapat menyebabkan konsumsi memori berlebih atau operasi input/output yang tidak efisien. Oleh karena itu, paket encoding/json di Go menyediakan tipe Encoder dan Decoder, yang dapat memproses data JSON secara streaming.

6.2.1 Menggunakan json.Encoder

json.Encoder dapat langsung menulis data JSON ke objek apa pun yang mengimplementasikan antarmuka io.Writer, artinya Anda dapat mengkodekan data JSON langsung ke file, koneksi jaringan, dll.

import (
    "encoding/json"
    "os"
)

func main() {
    users := []User{
        {Name: "Alice", Age: 30},
        {Name: "Bob", Age: 25},
    }
    
    file, _ := os.Create("users.json")
    defer file.Close()
    
    encoder := json.NewEncoder(file)
    if err := encoder.Encode(users); err != nil {
        log.Fatalf("Kesalahan Encoding: %v", err)
    }
}

6.2.2 Menggunakan json.Decoder

json.Decoder dapat langsung membaca data JSON dari objek apa pun yang mengimplementasikan antarmuka io.Reader, mencari dan memparsing objek dan larik JSON.

import (
    "encoding/json"
    "os"
)

func main() {
    file, _ := os.Open("users.json")
    defer file.Close()
    
    var users []User
    decoder := json.NewDecoder(file)
    if err := decoder.Decode(&users); err != nil {
        log.Fatalf("Kesalahan Penguraian: %v", err)
    }
    
    for _, u := range users {
        fmt.Printf("%+v\n", u)
    }
}

Dengan memproses data dengan pengode dan pengura, Anda dapat melakukan pemrosesan JSON saat membaca, mengurangi penggunaan memori, dan meningkatkan efisiensi pemrosesan, yang sangat berguna untuk penanganan transfer jaringan atau file besar.