Go – Json Serialization

Json là một trong những định dạng serialization phổ biến nhất. Nó giúp chúng ta dễ dàng đọc được, ngắn gọn hợp lý, và dễ dàng được phân tích cú pháp bởi bất kỳ ứng dụng web nào sử dụng Javascript. Go là một ngôn ngữ lập trình hiện đại có fist-class hỗ trợ cho việc Json Serialization trong thư viện chuẩn của nó.

Json Package

Go hỗ trợ một vài định dạng serialization trong package mã hóa của thư viện chuẩn của nó. Một trong những định dạng này phổ biến nhất là Json. Bạn có thể serialize giá trị sử dụng hàm Marshal() bên trong một slice của byte. Bạn có thể deserialize một slice của byte thành các giá trị của Go sử dụng hàm Unmarshal().

Những chỉ định sau là tương đương trong bài viết này:

  • Serialization/Encoding/Marshalling
  • Deserialization/Decoding/Unmarshalling

Marshal

Hàm Marshal() có thể nhận bất cứ thứ gì, trong Go có nghĩa là một interface trống và trả về một slice của byte hoặc lỗi. Đây là cú pháp của hàm

func Marshal(v interface{}) ([]byte, error)

Nếu hàm Marshal() lỗi khi serialize giá trị đầu vào, nó sẽ trả về môt lỗi non-nil.

Marshal() có một vài giới hạn nghiêm ngặt:

  • Key của Map phải là string
  • Giá trị của Map phải là kiểu có khả năng serialization bằng json package
  • Các kiểu sau là không được hỗ trợ: Channel, complex và hàm
  • Các cấu trúc dữ liệu chu kỳ không được hỗ trợ.
  • Các con trỏ là được mã hóa (và sau đó được giải mã) dưới dạng các giá trị mà chúng đang trỏ đến (hoặc ‘null’ nếu con trỏ là nil)

Unmarshal

Hàm Unmarshal() nhận một slice của byte đại diện cho một Json hợp lệ và một interface đích, mà thường là một con trỏ đến một struct hoặc kiểu cơ bản. Để giải mã ngược (deserialize) Json thành interface theo cách chung. Nếu việc giải mã ngược không thành công, nó sẽ trả về một lỗi theo cú pháp sau:

func Unmarshal(data []byte, v interface{}) error

Serialize kiểu đơn giản

Bạn có thể serialize các kiểu đơn giản sử dụng Json package, kết quả sẽ không phải là một object json đầy đủ, mà là một string đơn giản. Ví dụ chúng ta có một số int 5 được serialize thành một mảng byte [53], tương ứng là string “5”

// Serialize int
var x = 5
bytes, err := json.Marshal(x)
if err != nil {
  fmt.Println("Can't serislize", x)
}
 
fmt.Printf("%v => %v, '%v'\n", x, bytes, string(bytes))
 
// Deserialize int
var r int
err = json.Unmarshal(bytes, &r)
if err != nil {
  fmt.Println("Can't deserislize", bytes)
}
 
fmt.Printf("%v => %v\n", bytes, r)

Kết quả

5 => [53], '5'
[53] => 5

Nếu bạn thử serialize những kiểu không được hỗ trợ như là các hàm thì, bạn sẽ nhận một lỗi

// Trying to serialize a function
foo := func() {
  fmt.Println("foo() here")
}
 
bytes, err = json.Marshal(foo)
if err != nil {
  fmt.Println(err)
}

Kết quả

json: unsupported type: func()

Serialize dữ liệu bất kỳ với Map

Điểm mạnh của Json là nó có thể diễn tả rất tốt dữ liệu cấp bậc bất kỳ. Package Json hỗ trợ và tận dụng kiểu interface trống (interface {}) để diễn tả cấp bậc cho Json bất kỳ. Sau đây là một ví dụ về deserialize và sau đó là serialize cây nhị phân mà mỗi node có một giá trị int và 2 nhánh, trái và phải, mà có thể chứa các node khác hoặc null.

Json null là tương đương với giá trị nil trong Go. Như bạn đã thấy kết quả, hàm json.Unmarshal() chuyển đổi thành công JSON blob thành struct dữ liệu Go bao gồm các map lồng nhau  của các interface và lưu trữ kiểu dữ liệu là int. Hàm json.Marshal() serialize thành công kết quả các object lồng nhau thành một Json tương ứng

// Json được lồng vào nhau
dd := `
    {
        "value": 3,
        "left": {
            "value": 1,
            "left": null,
            "right": {
                "value": 2,
                "left": null,
                "right": null
            }
        },
        "right": {
            "value": 4,
            "left": null,
            "right": null
         }
    }`
 
var obj interface{}
err = json.Unmarshal([]byte(dd), &obj)
if err != nil {
  fmt.Println(err)
} else {
  fmt.Println("--------\n", obj)
}
 
 
data, err = json.Marshal(obj)
if err != nil {
  fmt.Println(err)
} else {
  fmt.Println("--------\n", string(data))
}

Kết quả

--------
 map[right:map[value:4 
               left:<nil> 
               right:<nil>] 
     value:3 
     left:map[left:<nil> 
              right:map[value:2 
                        left:<nil> 
                        right:<nil>] 
              value:1]]
--------
 {"left":{
      "left":null,
      "right":{"left":null,"right":null,"value":2},
      "value":1},
  "right":{"left":null,
           "right":null,
           "value":4},
  "value":3}

Để duyệt qua các map của interface chung, chúng ta cần sử dụng xác nhận kiểu. Ví dụ

func dump(obj interface{}) error {
    if obj == nil {
        fmt.Println("nil")
        return nil
    }
    switch obj.(type) {
    case bool:
        fmt.Println(obj.(bool))
    case int:
        fmt.Println(obj.(int))
    case float64:
        fmt.Println(obj.(float64))
    case string:
        fmt.Println(obj.(string))
    case map[string]interface{}:
        for k, v := range(obj.(map[string]interface{})) {
            fmt.Printf("%s: ", k)
            err := dump(v)
            if err != nil {
                return err
            }
        }
    default:
        return errors.New(
            fmt.Sprintf("Unsupported type: %v", obj))
    }
 
    return nil
}

Serialize dữ liệu có cấu trúc

Làm việc với dữ liệu có cấu trúc thường là lựa chọn tốt hơn. Go cung cấp hoàn toàn hỗ trợ cho serialize Json cho hoặc từ struct thông qua các thẻ struct của nó. Chúng ta hãy tạo một struct tương ứng với cây Json và hàm Dump() in ra

type Tree struct {
    value int
    left *Tree
    right *Tree
}
 
func (t *Tree) Dump(indent string) {
    fmt.Println(indent + "value:", t.value)
    fmt.Print(indent + "left: ")
    if t.left == nil {
        fmt.Println(nil)
    } else {
        fmt.Println()
        t.left.Dump(indent + "  ")
    }
 
    fmt.Print(indent + "right: ")
    if t.right == nil {
        fmt.Println(nil)
    } else {
        fmt.Println()
        t.right.Dump(indent + "  ")
    }
}

Đoạn code bên trên hoạt động không? Không hoàn toàn. Không có lỗi xảy ra, nhưng object tree không được điền bằng Json

jsonTree := `
    {
        "value": 3,
        "left": {
            "value": 1,
            "left": null,
            "right": {
                "value": 2,
                "left": null,
                "right": null
            }
        },
        "right": {
            "value": 4,
            "left": null,
            "right": null
         }
    }`
 
var tree Tree
err = json.Unmarshal([]byte(dd), &tree)
if err != nil {
    fmt.Printf("- Can't deserislize tree, error: %v\n", err)
} else {
    tree.Dump("")
}

Kết qủa

value: 0
left: <nil>
right: <nil>

Vấn đề ở đây, các trường Tree là private. Json serialization chỉ hoạt động trên các trường public. Nên chúng ta có thể tạo các trường struct public. Json package đủ thông minh để chuyển đổi hoàn toàn các key viết thường value, left, và right thành tên các trường viết hoa tương ứng.

type Tree struct {
    Value int   `json:"value"`
    Left  *Tree `json:"left"`
    Right *Tree `json:"right"`
}

Kết quả

value: 3
left: 
  value: 1
  left: <nil>
  right: 
    value: 2
    left: <nil>
    right: <nil>
right: 
  value: 4
  left: <nil>
  right: <nil>

Package Json sẽ âm thầm bỏ qua các trường không được map, cũng như các trường private trong struct. Nhưng thỉnh thoảng bạn muốn map các key cụ thể trong Json vào một trường với một tên khác trong struct. Bạn có thể sử dụng các thẻ struct. Ví dụ giả sử sử chúng ta thêm một trường khác tên là label cho Json, chúng ta cần map nó cho một trường gọi là Tag trong struct.

type Tree struct {
    Value int
    Tag string    `json:"label"`
    Left  *Tree
    Right *Tree
}
 
func (t *Tree) Dump(indent string) {
    fmt.Println(indent + "value:", t.Value)
    if t.Tag != "" {
        fmt.Println(indent + "tag:", t.Tag)
    }
    fmt.Print(indent + "left: ")
    if t.Left == nil {
        fmt.Println(nil)
    } else {
        fmt.Println()
        t.Left.Dump(indent + "  ")
    }
 
    fmt.Print(indent + "right: ")
    if t.Right == nil {
        fmt.Println(nil)
    } else {
        fmt.Println()
        t.Right.Dump(indent + "  ")
    }
}

Đây là một Json mới với node gốc  (root) của cây (tree) dán nhãn là root, serialize tên riêng thành trường Tag và in kết quả

dd := `
{
    "label": "root",
    "value": 3,
    "left": {
        "value": 1,
        "left": null,
        "right": {
            "value": 2,
            "left": null,
            "right": null
        }
    },
    "right": {
        "value": 4,
        "left": null,
        "right": null
     }
}`


var tree Tree
err = json.Unmarshal([]byte(dd), &tree)
if err != nil {
    fmt.Printf("- Can't deserislize tree, error: %v\n", err)
} else {
    tree.Dump("")
}

Kết quả

value: 3
tag: root
left: 
  value: 1
  left: <nil>
  right: 
    value: 2
    left: <nil>
    right: <nil>
right: 
  value: 4
  left: <nil>
  right: <nil>

Viết tuỳ chỉnh Marshaller

Bạn sẽ thường xuyên mong muốn serialize các object mà không phải tuân theo các quy tắc nghiêm ngặt của hàm Marshal(). Ví dụ bạn có lẽ muốn serialize một map với các key int. Trong trường hợp này, bạn có thể viết một marshaller/unmarshaller tuỳ chỉnh bằng cách thực thi các interface Marshaler và Unmarshaler

Chú ý lỗi chính tả: Trong Go, quy ước để đặt tên cho một interface với một phương thức đơn bằng cách thêm hậu tố “er” cho tên phương thức. Nên mặc dù cách đánh vần phổ biến hơn là Marshaller (với L nhân đôi), tên của interface chỉ là Marshaler ( L đơn).

Đây là các interface Marshaler và Unmarshaler

type Marshaler interface {
        MarshalJSON() ([]byte, error)
}
 
type Unmarshaler interface {
        UnmarshalJSON([]byte) error
}

Bạn phải tạo một kiểu khi thực hiện tuỳ chỉnh serialization, thậm trí nếu bạn muốn tuỳ chỉnh các kiểu được tính hợp sẵn, hay các thành phần được tích hợp bên trong như là map[int]string. Ở đây chúng ta định nghĩa một kiểu tên là IntStringMap và thực thi các interface MarshalerUnmarshaler cho kiểu này.

Phương thức MarshalJSON() tạo một map[string]string, chuyển đỗi mỗi key int của nó thành một string, và serialize map với các key string sử dụng hàm json.Marshal() tiêu chuẩn.

type IntStringMap map[int]string
 
func (m *IntStringMap) MarshalJSON() ([]byte, error) {
    ss := map[string]string{}
    for k, v := range *m {
        i := strconv.Itoa(k)
        ss[i] = v
    }
    return json.Marshal(ss)
}

Phương thức UnmarshalJSON() thực hiện ngược lại. Nó deserialize mảng byte dữ liệu thành map[string]string và sau đó chuyển đổi mỗi key string thành một int

func (m *IntStringMap) UnmarshalJSON(data []byte ) error {
    ss := map[string]string{}
    err := json.Unmarshal(data, &ss)
    if err != nil {
        return err
    }
    for k, v := range ss {
        i, err := strconv.Atoi(k)
        if err != nil {
            return err
        }
        (*m)[i] = v
    }
    return nil
}

Sử dụng hàm cho chuong trình như sau

m := IntStringMap{4: "four", 5: "five"}
data, err := m.MarshalJSON()
if err != nil {
    fmt.Println(err)
}
fmt.Println("IntStringMap to JSON: ", string(data))


m = IntStringMap{}

jsonString := []byte("{\"1\": \"one\", \"2\": \"two\"}")
m.UnmarshalJSON(jsonString)

fmt.Printf("IntStringMap from JSON: %v\n", m)
fmt.Println("m[1]:", m[1], "m[2]:", m[2])

Kết quả

IntStringMap to JSON:  {"4":"four","5":"five"}
IntStringMap from JSON: map[2:two 1:one]
m[1]: one m[2]: two

Serialize Enum

Go Enum có thể rất phức tạp để serialize. Tạo các const Zero và One là tương đương với các int 0 và 1.

type EnumType int
 
const (
    Zero     EnumType = iota
    One
)

Khi bạn nghĩ nó là một int và bạn sẽ không thể serialize nó trực tiếp. Bạn phải viết một tuỳ chỉnh marshaler/unmarshaler.MarshalJSON() và UnmarshalJSON() sau đây sẽ serialize hoặc deserialize các hằng số Zero và One đến hoặc từ  string Zero và One.

func (e *EnumType) UnmarshalJSON(data []byte) error {
    var s string
    err := json.Unmarshal(data, &s)
    if err != nil {
        return err
    }
 
    value, ok := map[string]EnumType{"Zero": Zero, "One": One}[s]
    if !ok {
        return errors.New("Invalid EnumType value")
    }
    *e = value
    return nil
}
 
func (e *EnumType) MarshalJSON() ([]byte, error) {
    value, ok := map[EnumType]string{Zero: "Zero", One:"One"}[*e]
    if !ok {
        return nil, errors.New("Invalid EnumType value")
    }
    return json.Marshal(value)
}

Tiếp theo, hãy thử nhúng EnumType vào trong một struct và serialize nó. Hàm main tạo một EnumContainer và khởi tạo nó với tên Uno và một giá trị của One hằng số enum, bằng 1.

type EnumContainer struct {
    Name                string
    Value               EnumType
}
 
 
func main() {
    x := One
    ec := EnumContainer{
        "Uno",
        x,
    }
    s, err := json.Marshal(ec)
    if err != nil {
        fmt.Printf("fail!")
    }
 
    var ec2 EnumContainer
    err = json.Unmarshal(s, &ec2)
 
    fmt.Println(ec2.Name, ":", ec2.Value)
}

Kết quả

Uno : 0

Kết quả mong muốn là Uno : 1 nhưng thay vào đó là Uno : 0. Không có bug trong marshal/unmarshal code. Hoá ra bạn không thể gắn các enum bằng giá trị nếu bạn muốn serialize chúng. Bạn phải gắn vào một con trỏ cho enum. Dưới đây là đoạn code chỉnh sửa để nó chạy như mong muốn

type EnumContainer struct {
    Name                string
    Value               *EnumType
}
 
func main() {
    x := One
    ec := EnumContainer{
        "Uno",
        &x,
    }
    s, err := json.Marshal(ec)
    if err != nil {
        fmt.Printf("fail!")
    }
 
    var ec2 EnumContainer
    err = json.Unmarshal(s, &ec2)
 
    fmt.Println(ec2.Name, ":", *ec2.Value)
}

Kết quả

Uno : 1

 

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.