0

Go Zero to Hero - Bài 74: Parsing JSON (Marshal và Unmarshal)

1. Hai nguyên tắc sinh tử của JSON trong Go

Trước khi gọi hàm, bạn phải khắc cốt ghi tâm 2 quy tắc thiết kế Struct này, nếu không dữ liệu của bạn sẽ bốc hơi một cách vô lý:

Quy tắc 1: BẮT BUỘC phải viết hoa chữ cái đầu (Exported Fields)

Package encoding/json của Go nằm ở bên ngoài package của bạn. Để nó có thể "nhìn thấy" và truy cập vào các biến bên trong Struct của bạn, các biến đó phải được viết hoa chữ cái đầu. Nếu bạn viết thường, thư viện JSON sẽ coi đó là biến private và làm lơ nó luôn!

Quy tắc 2: Phép thuật của Struct Tags

Mặc dù biến trong Go phải viết hoa (ví dụ TransactionID), nhưng chuẩn API RESTful thế giới lại yêu cầu JSON phải dùng snake_case (ví dụ transaction_id). Để map (ánh xạ) hai cái tên này với nhau, Go dùng Struct Tags.

Hãy thiết kế một Struct chuẩn mực cho giao dịch vé:

type AFCTransaction struct {
    // 1. Map chuẩn sang tên JSON
    TransactionID string `json:"transaction_id"`
    Station       string `json:"station"`
    
    // 2. omitempty: Bỏ qua trường này (không in ra JSON) nếu nó mang giá trị Zero-value (0, "", false)
    Amount        int    `json:"amount,omitempty"`
    
    // 3. Biến private (viết thường): Sẽ BỊ BỎ QUA HOÀN TOÀN khi đóng gói/giải mã JSON
    secretToken   string 
}

2. json.Marshal: Biến Struct thành chuỗi JSON

Quá trình dịch từ Go Struct sang mảng byte []byte chuẩn JSON được gọi là Marshal (Đóng gói).

package main

import (
    "encoding/json"
    "fmt"
)

type AFCTransaction struct {
    TransactionID string `json:"transaction_id"`
    Station       string `json:"station"`
    Amount        int    `json:"amount,omitempty"`
    IsVIP         bool   `json:"is_vip"`
}

func main() {
    // Tạo một đối tượng giao dịch mới
    txn := AFCTransaction{
        TransactionID: "TXN-998877",
        Station:       "Bến Thành",
        Amount:        0, // Chú ý: Vì có tag `omitempty`, trường này sẽ biến mất khỏi JSON!
        IsVIP:         true,
    }

    // Đóng gói thành JSON
    // Hàm này trả về một mảng []byte và một error
    jsonData, err := json.Marshal(txn)
    if err != nil {
        fmt.Println("Lỗi đóng gói JSON:", err)
        return
    }

    // Ép kiểu mảng byte sang string để in ra màn hình
    fmt.Println(string(jsonData))
}

Kết quả in ra: {"transaction_id":"TXN-998877","station":"Bến Thành","is_vip":true}
(Trường amount đã bị ẩn đi một cách thanh lịch nhờ omitempty, giúp tối ưu băng thông mạng).


3. json.Unmarshal: Giải mã JSON về lại Struct

Khi bạn gọi API đối soát hoặc nhận message từ RabbitMQ/Kafka, cái bạn cầm trên tay là một mảng byte thô chứa JSON. Để nhồi nó ngược lại vào Struct, ta dùng Unmarshal (Giải nén).

🚨 Cái bẫy kinh điển của Gopher mới:

Hàm Unmarshal yêu cầu tham số thứ 2 BẮT BUỘC phải là một Con trỏ (Pointer). Nếu bạn truyền tham trị (Pass-by-value), Go sẽ nhồi dữ liệu vào một bản copy ảo, và Struct gốc của bạn vẫn trống rỗng!

func main() {
    // Dữ liệu JSON thô gửi từ máy quét thẻ lên
    rawJSON := []byte(`{
        "transaction_id": "TXN-112233",
        "station": "Suối Tiên",
        "amount": 15000,
        "is_vip": false
    }`)

    // 1. Tạo một Struct rỗng để hứng dữ liệu
    var receivedTxn AFCTransaction

    // 2. GIẢI MÃ: Nhớ truyền ĐỊA CHỈ (&receivedTxn)
    err := json.Unmarshal(rawJSON, &receivedTxn)
    if err != nil {
        fmt.Println("Lỗi giải mã JSON:", err)
        return
    }

    // 3. Sử dụng Struct đã có dữ liệu như một Object bình thường
    fmt.Println("Giao dịch tại trạm:", receivedTxn.Station)
    fmt.Println("Số tiền thu được:", receivedTxn.Amount)
}

4. Tuyệt kỹ đối phó với JSON "Không có hình thù" (Dynamic JSON)

Đôi khi, hệ thống đối tác trả về cho bạn một cái JSON rất quái thai: Mỗi lần một cấu trúc khác nhau, hoặc nó có quá nhiều trường rác mà bạn lười không muốn định nghĩa một cái Struct khổng lồ chỉ để lấy đúng 1 giá trị.

Lúc này, chúng ta sẽ giải mã JSON thẳng vào một cấu trúc Map đa năng: map[string]any (ở các bản Go cũ là map[string]interface{}).

func main() {
    weirdJSON := []byte(`{
        "status": "success",
        "dynamic_data": {
            "battery_level": 98,
            "door_sensor": "OPEN"
        }
    }`)

    // Khai báo một Map mà Key là string, Value là bất kỳ kiểu gì (any)
    var result map[string]any

    json.Unmarshal(weirdJSON, &result)

    // Lấy dữ liệu ra (Phải cẩn thận ép kiểu bằng Type Assertion)
    status := result["status"].(string)
    fmt.Println("Trạng thái:", status)

    // Thâm nhập vào object con bên trong
    dynamicData := result["dynamic_data"].(map[string]any)
    battery := dynamicData["battery_level"].(float64) // Lưu ý: JSON số mặc định parse ra float64
    fmt.Printf("Mức pin thiết bị: %.0f%%\n", battery)
}

Cảnh báo Kiến trúc: Chỉ dùng map[string]any như một lối thoát hiểm cuối cùng. Nó làm mất đi tính Type-Safe (An toàn kiểu dữ liệu) cực kỳ quý giá của Go. 95% thời gian, hãy kiên nhẫn định nghĩa Struct rõ ràng.


💡 Tổng kết

Bạn đã nắm được ngôn ngữ giao tiếp chuẩn mực của Microservices:

  • Marshal: Biến Go Struct ➔ []byte JSON (Để gửi đi).
  • Unmarshal: Biến []byte JSON ➔ Phục hồi lại Go Struct (Nhớ truyền Con trỏ).
  • Tags & Export: Biến Struct BẮT BUỘC viết hoa chữ cái đầu và dùng `json:"tên_trường"` để ánh xạ cho chuẩn. Thêm ,omitempty để ẩn giá trị rỗng.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí