Làm việc với JSON trong Golang


  • Trùm cuối

    Như các bạn đã biết, việc giao tiếp giữa các dịch vụ và hệ thống khác nhau thường sử dụng JSON như là một format chuẩn cho việc giao tiếp dữ liệu với nhau. Nếu bạn biết đến và đã lập trình với REST API rồi thì sẽ thấy là tất cả ouput data đều là JSON. Trong bài này mình sẽ hướng dẫn các bạn những phương pháp làm việc với JSON.

    b2ebbef5-65fd-46ed-abf4-fbc530cf3af7-image.png

    Tạo JSON

    Chúng ta có thể sử dụng package encoding/json để dễ dàng tạo một JSON từ cấu trúc dữ liệu của chúng ta.

    Từ Structs

    Structs rất thuận tiện để sử dụng và thường nhẹ hơn map hay các cấu trúc dữ liệu phức tạp khác. Chúng ta có thể truyền vào bất cứ thể hiện nào của struct vào hàm json.Marshal và nhận JSON trả về với kiểu dữ liệu là một slice of bytes ([] byte).

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    )
    
    type Person struct {
    	Name   string
    	Age    int
    	Emails []string
    }
    
    func main() {
    	masnun := Person{
    		Name:   "Abu Ashraf Masnun",
    		Age:    27,
    		Emails: []string{"masnun@gmail.com", "masnun@localstaffingllc.com"},
    	}
    
    	json_bytes, _ := json.Marshal(masnun)
    	fmt.Printf("%s",json_bytes)
    }
    

    JSON trả về:

    {"Name":"Abu Ashraf Masnun","Age":27,"Emails":["masnun@gmail.com","masnun@localstaffingllc.com"]}
    

    Những điều cần lưu ý:

    • các trường trong struct phải được bắt đầu bằng chữ viết hoa, điều này rất quan trọng, chúng ta sẽ thảo luận sau
    • Bạn có thể lồng các struct khác, ví dụ ở đây trường Emails chứa một danh sách các strings
    • Hàm json.Marshal trả về JSON và error, đừng quên xử lý lỗi nếu có. Thường thì bạn sẽ không gặp lỗi từ hàm Marshal nhưng trong một số trường hợp khi và một số kiểu không thể convert được sang JSON.
    • JSON trả về là một list of bytes. Vì vậy nếu bạn muốn sử dụng nó như là một chuỗi, bạn cần phải convert nó.

    Đặt tên trường

    Cán bạn hãy xem lại output và để ý là những key/trường trong JSON được tạo tất cả đều bắt đầu với chữ cái đầu viết hoa (trường trong struct của chúng ta cũng tương tự). Nếu bạn vẫn tò mò, thắc mắc tại sao lại như vậy, các bạn có thể thử convert trường trong struct thành chữ thường... và xem nó có hoạt động không. Xem kết quả ở đây: https://play.golang.org/p/93eDoFSjnW.

    Trường name không bắt đầu với chữ cái viết hoa, nó sẽ không được xuất ra. Đây là lý do tại sao các trường nameage không được tạo ra trong JSON.

    Nhưng trong json thì key/trường thường được viết thường, vậy thì chúng ta phải làm sao?... các bạn đừng lo lắng bởi có một cách đơn giản để định danh một trường trong struct như ví dụ dưới đây:

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

    Áp dụng với ví dụ lúc nãy chúng ta sẽ có kết quả như sau: https://play.golang.org/p/xlcjU1_VSE

    Bỏ qua các trường trống

    Sẽ nếu nào nếu như có các trường trống? Hãy thử ví dụ dưới đây:

    func main() {
    	masnun := Person{}
    
    	json_bytes, _ := json.Marshal(masnun)
    	fmt.Printf("%s", json_bytes)
    }
    

    Kết quả thu được như sau:

    {"name":"","age":0,"emails":null}
    

    Nếu một trường là rỗng, chúng ta có thể muốn bỏ qua các trường đó trong chuỗi JSON. Chúng ta sử dụng cờ omitempty trong định danh của json.

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    )
    
    type Person struct {
    	Name   string      `json:"name,omitempty"`
    	Age    int         `json:"age,omitempty"`
    	Emails []string    `json:"emails,omitempty"`
    }
    
    func main() {
    	masnun := Person{
    		Name: "Abu Ashraf Masnun",
    	}
    
    	json_bytes, _ := json.Marshal(masnun)
    	fmt.Printf("%s", json_bytes)
    }
    

    Chúng ta thử kiểm tra lại kết quả:

    {"name":"Abu Ashraf Masnun"}
    

    Tiện quá phải hông? 😁

    Bỏ qua các trường

    Trong struct của chúng ta, chẳng hạn chúng ta muốn giữ trường Age nhưng ko muốn nó xuất hiện trong json tạo ra. Chúng ta sử dụng cờ - trong định danh json cho trường đó.

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    )
    
    type Person struct {
    	Name   string      `json:"name,omitempty"`
    	Age    int         `json:"-"`
    	Emails []string    `json:"emails,omitempty"`
    }
    
    func main() {
    	masnun := Person{
    		Name: "Abu Ashraf Masnun",
    		Age:  27,
    	}
    
    	json_bytes, _ := json.Marshal(masnun)
    	fmt.Printf("%s", json_bytes)
    }
    

    Kết quả xuất ra:

    {"name":"Abu Ashraf Masnun"}
    

    Mặc dù struct có trường Age được khởi tạo nhưng nó không được xuất ra trong json. Tính năng này rất hữu dụng trong một số trường hợp, chẳng hạn khi một User struct có trường Password nhưng chắc chắn là chúng ta không hề muốn nó xuất hiện trong output JSON rồi 😂

    Sử dụng Maps và Slices

    Tới đây thì chúng ta đã biết cách tạo một JSON từ struct rùi. Vậy còn cách nào nữa không?, vâng đó là maps và slices. Sau đây là ví dụ mẫu:

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    )
    
    func main() {
    	masnun := map[string]interface{}{
    
    		"name": "Abu Asharf Masnun",
    		"age":  27,
    	}
    
    	json_bytes, _ := json.Marshal(masnun)
    	fmt.Printf("%s", json_bytes)
    }
    

    và sử dụng slices:

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    )
    
    func main() {
    	emails := []string{"masnun@gmail.com", "masnun@localstaffingllc.com"}
    	json_bytes, _ := json.Marshal(emails)
    	fmt.Printf("%s", json_bytes)
    }
    

    Kết hợp cả hai với nested slices:

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    )
    
    
    func main() {
    	masnun := map[string]interface{}{
    
    		"name": "Abu Asharf Masnun",
    		"age":  27,
    		"emails": []string{"masnun@gmail.com", "masnun@localstaffingllc.com"},
    		"payload": map[string]interface{}{
    			"country": "Viet Nam",
    		},
    	}
    
    	json_bytes, _ := json.Marshal(masnun)
    	fmt.Printf("%s", json_bytes)
    }
    

    Trong tất cả các phương pháp trên thì structs được sử dụng rộng rãi nhất.

    Parse JSON

    Ở trên chúng ta đã biết được cách để tạo một JSON string với Golang. Bây giờ sẽ là ngược lại. Chung ta sẽ parse JSON thành Go structures.

    Parse sang structs

    Đầu tiên chúng ta sẽ tìm hiểu cách parse một JSON string thành structs. Chúng ta sử dụng hàm Unmarshal để lấy bytes và contror đến mất kỳ một interface{}, Nó sẽ duyệt qua chuỗi JSON và lưu trữ dữ liệu trong struct ở tham số thứ 2. Hãy xem qua ví dụ dưới đây:

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    )
    
    type Person struct {
    	Name   string   `json:"name"`
    	Age    int      `json:"age"`
    	Emails []string `json:"emails"`
    }
    
    func main() {
    
    	json_bytes := []byte(`
    		{
    			"Name":"Abu Ashraf Masnun",
    			"Age":27,
    			"Emails":["masnun@gmail.com","masnun@localstaffingllc.com"]
    		}
    	`)
    
    	masnun := Person{}
    	err := json.Unmarshal(json_bytes, &masnun)
    	if err != nil {
    		panic(err)
    	}
    
    	fmt.Println(masnun.Name, masnun.Age, masnun.Emails)
    
    }
    

    Ở đay biến json_bytes sẽ chứa JSON mà chúng ta muốn xử lý. Chúng ta đã có Person struct với các định danh cho các trường tương ứng. Chúng ta chỉ cần truyền json_bytes và con trỏ đến thể hiện của Person vào hàm Unmarshal. Xin chú ý là con trỏ chứ không phải giá trị. Sử dụng con trỏ chúng ta mới có thể gán ngược lại được thể hiện của Person struct.

    Nếu struct không có một số trường mà trong JSON lại có và ngược lại, những trường đó sẽ được bỏ qua.

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    )
    
    type Person struct {
    	Name    string   `json:"name"`
    	Age     int      `json:"age"`
    	Emails  []string `json:"emails"`
    	Address string
    }
    
    func main() {
    
    	json_bytes := []byte(`
    		{
    			"Name":"Abu Ashraf Masnun",
    			"Age":27,
    			"Emails":["masnun@gmail.com","masnun@localstaffingllc.com"],
    			"Score":97
    		}
    	`)
    
    	var masnun Person
    	err := json.Unmarshal(json_bytes, &masnun)
    	if err != nil {
    		panic(err)
    	}
    
    	fmt.Println(masnun)
    
    }
    

    Trong ví dụ trên, struct có trường Address nhưng JSON không có, và JSON có trường Score nhưnng struct lại không có. Trong trường hợp này masnun.Address sẽ là chuỗi rỗng, còn Score sẽ bị bỏ qua.

    Parse sang Maps / Slices

    Mình đã đề cập tới việc sử dụng structs sẽ nhẹ hơn và được sử dụng rộng rãi hơn maps. Nhưng trong một số trương hợp như chúng ta không chắc chắc được về cấu trúc của JSON data mà chúng ta muốn parse. Trương hợp này maps sẽ rất hữu dụng.

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    )
    
    func main() {
    
    	json_bytes := []byte(`
    		{
    			"Name":"Abu Ashraf Masnun",
    			"Age":27,
    			"Emails":["masnun@gmail.com","masnun@localstaffingllc.com"],
    			"Score":97,
    			"Payload": {
    				"Country": "Viet Nam"
    			}
    		}
    	`)
    
    	var masnun map[string]interface{}
    	err := json.Unmarshal(json_bytes, &masnun)
    	if err != nil {
    		panic(err)
    	}
    	
    	fmt.Println(masnun["Name"], masnun["Age"], masnun["Emails"], masnun["Score"], masnun["Payload"].(map[string]interface{})["Country"])
    }
    

    Okay, chúng ta đã truyền map[string]interface{} và nhận tất cả dữ liệu từ JSON thành công rùi. Nhưng hãy nhớ là giá trị của mỗi key trong map sẽ là một interface. Nếu muốn tách một phần dữ liệu chẳng hạn emails chúng ta có thể convert nó sang string.

    Chẳng hạn ví dụ sau sẽ báo lỗi:

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"strings"
    )
    
    func main() {
    
    	json_bytes := []byte(`
    		{
    			"Name":"Abu Ashraf Masnun",
    			"Age":27,
    			"Emails":["masnun@gmail.com","masnun@localstaffingllc.com"],
    			"Score":97
    		}
    	`)
    
    	var masnun map[string]interface{}
    	err := json.Unmarshal(json_bytes, &masnun)
    	if err != nil {
    		panic(err)
    	}
    
    	var firstEmail string
    	firstEmail = masnun["Emails"][0]
    
    	fmt.Println(strings.ToUpper(firstEmail))
    
    }
    

    Chúng ta sẽ gặp lỗi

    # command-line-arguments
    ./main.go:27: invalid operation: masnun["Emails"][0] (type interface {} does not support indexing)
    

    Trường Email sẽ có giá trị là một interface, vì thế chúng ta không thể truy cập được vào phần tử của nó. Hãy ép kiểu nó thành một list interface{} trước tiên. Sau đó lại tiếp tục ép kiểu của phần tử trong đó thành string.

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"strings"
    )
    
    func main() {
    
    	json_bytes := []byte(`
    		{
    			"Name":"Abu Ashraf Masnun",
    			"Age":27,
    			"Emails":["masnun@gmail.com","masnun@localstaffingllc.com"],
    			"Score":97
    		}
    	`)
    
    	var masnun map[string]interface{}
    	err := json.Unmarshal(json_bytes, &masnun)
    	if err != nil {
    		panic(err)
    	}
    
    	var emails []interface{}
    	var firstEmail string
    	emails = masnun["Emails"].([]interface{})
    	firstEmail = emails[0].(string)
    
    	fmt.Println(strings.ToUpper(firstEmail))
    }
    

    Bạn có thắc mắc tại sao chúng ta không thể lấy Emails bằng []string cho nó nhanh gọn lẹ? Bởi vì Go không thể biết được kiểu giá trị trong chuỗi JSON của bạn là gì. Bởi vì vậy chúng ta dùng interface{}. 😎

    Mã hoá và giải mã JSON stream

    Package json chung cấp các hàm NewEncoderNewDecoder để giúp chúng ta lấy được kiểu EncoderDecoder. Những kiểu này có thể làm việc với đối tượng khác mà hỗ trợ io.Readerio.Writer để streaming.

    Stream JSON từ file

    Chúng ta có thể mở một file JSON sử dụng hàm os.Open và stream nó sử dụng hàm json.NewDecoder. Đây là ví dụ mẫu:

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"os"
    )
    
    func main() {
    	fileReader, _ := os.Open("masnun.json")
    	var masnun map[string]interface{}
    	json.NewDecoder(fileReader).Decode(&masnun)
    	fmt.Println(masnun)
    }
    

    Chúng ta sẽ mở một file sử dụng interface io.Reader. Sau đó chúng ta có thể sử dụng nó với kiểu Decoder.

    Stream JSON vào file

    Ghi một JSON vào file, chúng ta cần mở file đó ở write mode, sử dụng io.Writer và truyền nó vào json.NewEncoder. Sau đó chúng ta có thể truyền dữ liêụ vào phương thức Encode để stream json vào file.

    package main
    
    import (
    	"encoding/json"
    	"os"
    )
    
    type Person struct {
    	Name   string
    	Age    int
    	Emails []string
    }
    
    func main() {
    	masnun := Person{
    		Name:   "Abu Ashraf Masnun",
    		Age:    27,
    		Emails: []string{"masnun@gmail.com", "masnun@localstaffingllc.com"},
    	}
    
    	fileWriter, _ := os.Create("output.json")
    	json.NewEncoder(fileWriter).Encode(masnun)
    }
    

    Tuỳ chỉnh Marshal / Unmarshal

    Nếu chúng ta muốn thay đổi cách mà kiểu dữ liệu của chúng ta được marshalled hay unmarshalled, chúng ta có thể sử dụng interface json.Marshalerjson.Unmarshaler. Chúng ta cần phải định nghĩa phương thức MarshalJSONUnmarshalJSON trong structs và thế là xong. Dưới đây là ví dụ cho các bạn dễ hình dung:

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"log"
    	"strings"
    )
    
    type Animal int
    
    const (
    	Unknown Animal = iota
    	Gopher
    	Zebra
    )
    
    func (a *Animal) UnmarshalJSON(b []byte) error {
    	var s string
    	if err := json.Unmarshal(b, &s); err != nil {
    		return err
    	}
    	switch strings.ToLower(s) {
    	default:
    		*a = Unknown
    	case "gopher":
    		*a = Gopher
    	case "zebra":
    		*a = Zebra
    	}
    
    	return nil
    }
    
    func (a Animal) MarshalJSON() ([]byte, error) {
    	var s string
    	switch a {
    	default:
    		s = "unknown"
    	case Gopher:
    		s = "gopher"
    	case Zebra:
    		s = "zebra"
    	}
    
    	return json.Marshal(s)
    }
    
    func main() {
    	blob := `["gopher","armadillo","zebra","unknown","gopher","bee","gopher","zebra"]`
    	var zoo []Animal
    	if err := json.Unmarshal([]byte(blob), &zoo); err != nil {
    		log.Fatal(err)
    	}
    
    	census := make(map[Animal]int)
    	for _, animal := range zoo {
    		census[animal] += 1
    	}
    
    	fmt.Printf("Zoo Census:\n* Gophers: %d\n* Zebras:  %d\n* Unknown: %d\n",
    		census[Gopher], census[Zebra], census[Unknown])
    
    }
    

    Khá bảnh đúng hem? 😂

    Bài này đã cung cấp cho bạn gần như đầy đủ kiến thức để làm việc với JSON rồi, việc còn lại của các bạn là lựa chọn phương thức phù hợp với mình và sử dụng thôi, chúc các bạn có một ngày code vui vẻ, ko trễ deadline! 😹



Có thể bạn cũng quan tâm

.
DMCA.com Protection Status