☰ Mục lục series
Tài liệu Go tiếng Việt
  1. Bắt đầu (Getting started)
    1. Tổng quan
    2. Tải về và cài đặt
    3. Bắt đầu với Go
    4. Quản lý các bản cài đặt Go
    5. Plugin trình soạn thảo và IDE
  2. Dạo quanh Go (A Tour of Go)
    1. Chào mừng
    2. Package, biến và hàm
    3. Điều khiển luồng
    4. Struct, slice và map
    5. Method và interface
    6. Generics
    7. Xử lý đồng thời
  3. Hướng dẫn (Tutorials)
    1. Danh sách hướng dẫn
    2. Tạo một Go module
    3. Gọi mã của bạn từ một module khác
    4. Trả về và xử lý lỗi
    5. Trả về lời chào ngẫu nhiên
    6. Trả về lời chào cho nhiều người
    7. Thêm một test
    8. Biên dịch và cài đặt ứng dụng
    9. Kết luận
    10. Bắt đầu với workspace nhiều module
    11. Phát triển RESTful API với Go và Gin
    12. Truy cập cơ sở dữ liệu quan hệ
    13. Bắt đầu với generics
    14. Bắt đầu với fuzzing
    15. Làm việc với JSON
    16. Tìm và sửa các dependency có lỗ hổng với govulncheck
    17. Tìm và khắc phục phụ thuộc có lỗ hổng với VS Code Go
    18. Viết ứng dụng web
  4. Viết code Go
    1. Cách viết mã Go
    2. Effective Go
    3. Doc Comment trong Go
  5. Module
    1. Phát triển và publish module
    2. Quản lý phụ thuộc
    3. Tổ chức một module Go
    4. Quản lý mã nguồn của module
    5. Cách đánh số phiên bản module
    6. Quy trình phát hành và đánh phiên bản module
    7. Publish một module
    8. Phát triển một bản cập nhật major version
    9. Tham chiếu tệp go.mod
    10. Ngừng hỗ trợ 'go get' cho việc cài đặt tệp thực thi
  6. Cơ sở dữ liệu (Database)
    1. Truy cập cơ sở dữ liệu quan hệ
    2. Mở một handle cơ sở dữ liệu
    3. Thực thi câu lệnh SQL không trả về dữ liệu
    4. Truy vấn dữ liệu
    5. Dùng prepared statement
    6. Thực thi transaction
    7. Huỷ các thao tác đang chạy
    8. Quản lý kết nối
    9. Tránh rủi ro SQL injection
  7. Chẩn đoán và hiệu năng
    1. Chẩn đoán
    2. Hướng dẫn về Go Garbage Collector
    3. Tối ưu hoá dựa trên profile
    4. Data Race Detector
    5. Gỡ lỗi mã Go bằng GDB
    6. Hỗ trợ coverage profiling cho integration test
  8. Bảo mật (Security)
    1. Tổng quan bảo mật
    2. Thực hành tốt về bảo mật cho lập trình viên Go
    3. Quản lý lỗ hổng trong Go
    4. Cơ sở dữ liệu lỗ hổng của Go
    5. Quét lỗ hổng trong IDE
    6. Chính sách CNA của Go
    7. Go Fuzzing
    8. Chi tiết kỹ thuật của Go Fuzzing
    9. Chính sách bảo mật của Go
    10. Threat Model của Go
    11. Các quyết định về bảo mật của Go
    12. Tuân thủ FIPS 140-3
  9. Công cụ và toolchain
    1. Tài liệu về các lệnh
    2. Về lệnh go
    3. Go Toolchains
    4. Telemetry của Go
    5. Cài đặt Go từ mã nguồn
    6. Cài đặt và sử dụng gccgo
  10. Tham khảo
    1. Câu hỏi thường gặp (FAQ)
    2. Go 1 và tương lai của các chương trình Go
    3. Hướng dẫn chuyển đổi sang encoding/json/v2
    4. Hướng dẫn đóng góp
    5. Đóng góp cho frontend gccgo
  11. Phụ lục
    1. Bảng thuật ngữ
Hướng dẫn (Tutorials)

Hướng dẫn: Làm việc với JSON

Bản dịch từ go.dev/doc/tutorial/json

JSON (JavaScript Object Notation) là một định dạng trao đổi dữ liệu đơn giản. Về mặt cú pháp, nó giống với các object và list của JavaScript. JSON thường được dùng để giao tiếp với các dịch vụ API qua mạng, nhưng nó cũng được dùng ở rất nhiều nơi khác. Trang chủ của nó, json.org, cung cấp một định nghĩa cực kỳ rõ ràng và súc tích về chuẩn này.

Với package encoding/json/v2, việc đọc và ghi dữ liệu JSON từ chương trình Go của bạn trở nên dễ như trở bàn tay. Package này cung cấp API gọn gàng hơn và các giá trị mặc định hợp lý hơn so với package encoding/json cũ.

Encoding

Để encode (mã hoá) dữ liệu JSON, chúng tôi dùng hàm Marshal.

func Marshal(in any, opts ...Options) (out []byte, err error)

Cho cấu trúc dữ liệu Go sau, Message,

type Message struct {
	Name string
	Body string
	Time time.Time
}

và một giá trị cụ thể của Message

m := Message{"Alice", "Hello", time.Date(2011, 1, 25, 0, 0, 0, 0, time.UTC)}

chúng tôi có thể marshal (chuyển giá trị Go thành JSON) m bằng json.Marshal:

b, err := json.Marshal(m)

Nếu mọi thứ ổn thoả, err sẽ là nil và b sẽ là một []byte chứa dữ liệu JSON này:

b == []byte(`{"Name":"Alice","Body":"Hello","Time":"2011-01-25T00:00:00Z"}`)

Chỉ những cấu trúc dữ liệu có thể biểu diễn dưới dạng JSON hợp lệ mới được encode:

  • Con trỏ sẽ được encode thành giá trị mà chúng trỏ tới (hoặc null nếu con trỏ là nil).

  • JSON object chỉ hỗ trợ chuỗi làm khoá. Do đó, để encode một map của Go, map đó phải có kiểu khoá encode được thành một chuỗi JSON, chẳng hạn map[string]T (với T là kiểu Go bất kỳ mà package json hỗ trợ).

  • Các kiểu channel, complex và hàm không thể encode được.

  • Các cấu trúc dữ liệu có chu trình không được hỗ trợ.

  • Marshal mô tả đầy đủ ngữ nghĩa encoding.

Package json chỉ truy cập các field được export của kiểu struct (những field bắt đầu bằng chữ in hoa). Vì vậy chỉ các field được export của một struct mới xuất hiện trong đầu ra JSON.

Decoding

Để decode (giải mã) dữ liệu JSON, chúng tôi dùng hàm Unmarshal.

func Unmarshal(in []byte, out any, opts ...Options) (err error)

Trước hết chúng tôi phải tạo một nơi để chứa dữ liệu sau khi decode

var m Message

rồi gọi json.Unmarshal, truyền cho nó một []byte chứa dữ liệu JSON và một con trỏ tới m

err := json.Unmarshal(b, &m)

Nếu b chứa JSON hợp lệ và vừa khớp với m, thì sau lời gọi đó err sẽ là nil và dữ liệu từ b sẽ được lưu vào struct m, như thể được gán bằng câu lệnh:

m = Message{
	Name: "Alice",
	Body: "Hello",
	Time: time.Date(2011, 1, 25, 0, 0, 0, 0, time.UTC),
}

Làm sao Unmarshal xác định được field nào để lưu dữ liệu đã decode? Với một khoá JSON cho trước "Foo", Unmarshal sẽ duyệt qua các field của struct đích để tìm (theo thứ tự ưu tiên):

  • Một field được export có struct tag là json:"Foo" (xem đặc tả Go để biết thêm về struct tag), hoặc

  • Một field được export có tên là Foo.

Điều gì xảy ra khi cấu trúc của dữ liệu JSON không khớp chính xác với kiểu Go?

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

Unmarshal chỉ decode những field mà nó tìm thấy trong kiểu đích. Trong trường hợp này, chỉ field Name của m được điền giá trị, còn field Food sẽ bị bỏ qua. Hành vi này đặc biệt hữu ích khi bạn chỉ muốn lấy ra một vài field cụ thể từ một khối JSON lớn. Nó cũng có nghĩa là mọi field không được export trong struct đích sẽ không bị Unmarshal tác động.

Nhưng nếu bạn không biết trước cấu trúc dữ liệu JSON của mình thì sao?

JSON tổng quát với any

Kiểu any mô tả mọi kiểu dữ liệu Go. any được định nghĩa là interface{} (interface rỗng), tức một interface không có method nào. Mọi kiểu Go đều hiện thực ít nhất là không method nào, nên đều thoả mãn interface rỗng.

any đóng vai trò như một kiểu chứa tổng quát:

var a any
a = "a string"
a = 2011
a = 2.777

Một type assertion truy cập được kiểu cụ thể nằm bên dưới:

r := a.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

Hoặc, nếu chưa biết kiểu bên dưới là gì, một type switch sẽ xác định kiểu đó:

switch v := a.(type) {
case int:
    fmt.Println("twice a is", v*2)
case float64:
    fmt.Println("the reciprocal of a is", 1/v)
case string:
    h := len(v) / 2
    fmt.Println("a swapped by halves is", v[h:]+v[:h])
default:
    // a isn't one of the types above
}

Package json dùng các giá trị map[string]any và []any để lưu các JSON object và mảng tuỳ ý; nó sẵn sàng unmarshal (chuyển JSON thành giá trị Go) bất kỳ khối JSON hợp lệ nào vào một giá trị any thuần tuý. Các kiểu Go cụ thể mặc định là:

  • bool cho giá trị boolean của JSON,

  • float64 cho số của JSON,

  • string cho chuỗi của JSON, và

  • nil cho null của JSON.

Decode dữ liệu bất kỳ

Hãy xét dữ liệu JSON sau, được lưu trong biến b:

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

Dù không biết cấu trúc của dữ liệu này, chúng tôi vẫn có thể decode nó vào một giá trị any bằng Unmarshal:

var f any
err := json.Unmarshal(b, &f)

Lúc này giá trị Go trong f sẽ là một map có khoá là chuỗi và giá trị cũng được lưu dưới dạng any:

f = map[string]any{
	"Name": "Wednesday",
	"Age":  6,
	"Parents": []any{
		"Gomez",
		"Morticia",
	},
}

Để truy cập dữ liệu này, chúng tôi có thể dùng type assertion để lấy ra map[string]any nằm bên dưới của f:

m := f.(map[string]any)

Sau đó chúng tôi có thể duyệt map bằng câu lệnh range và dùng type switch để truy cập các giá trị theo đúng kiểu cụ thể của chúng:

for k, v := range m {
	switch vv := v.(type) {
	case string:
		fmt.Println(k, "is string", vv)
	case float64:
		fmt.Println(k, "is float64", vv)
	case []any:
		fmt.Println(k, "is an array:")
		for i, u := range vv {
			fmt.Println(i, u)
		}
	default:
		fmt.Println(k, "is of a type I don't know how to handle")
	}
}

Bằng cách này bạn có thể làm việc với dữ liệu JSON chưa biết trước mà vẫn tận dụng được lợi ích của an toàn kiểu.

Kiểu tham chiếu

Hãy định nghĩa một kiểu Go để chứa dữ liệu từ ví dụ trước:

type FamilyMember struct {
	Name    string
	Age     int
	Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

Việc unmarshal dữ liệu đó vào một giá trị FamilyMember hoạt động đúng như mong đợi, nhưng nếu nhìn kỹ, bạn sẽ thấy một điều đáng chú ý đã xảy ra. Với câu lệnh var, chúng tôi đã cấp phát một struct FamilyMember, rồi truyền con trỏ tới giá trị đó cho Unmarshal, nhưng tại thời điểm ấy field Parents còn là một slice có giá trị nil. Để điền dữ liệu cho field Parents, Unmarshal đã âm thầm cấp phát một slice mới. Đây là cách Unmarshal thường làm việc với các kiểu tham chiếu được hỗ trợ (pointer, slice và map).

Hãy xét việc unmarshal vào cấu trúc dữ liệu sau:

type Foo struct {
	Bar *Bar
}

Nếu trong JSON object có field Bar, Unmarshal sẽ cấp phát một Bar mới và điền dữ liệu vào đó. Nếu không, Bar sẽ được giữ nguyên là con trỏ nil.

Từ đây nảy sinh một mẫu hình hữu ích: nếu bạn có một ứng dụng nhận một vài loại thông điệp khác nhau, bạn có thể định nghĩa một cấu trúc “receiver” như sau

type IncomingMessage struct {
	Cmd *Command
	Msg *Message
}

và bên gửi có thể điền field Cmd và/hoặc field Msg của JSON object ở mức cao nhất, tuỳ theo loại thông điệp mà họ muốn truyền đạt. Khi decode JSON vào một struct IncomingMessage, Unmarshal sẽ chỉ cấp phát những cấu trúc dữ liệu có mặt trong dữ liệu JSON. Để biết cần xử lý thông điệp nào, lập trình viên chỉ cần kiểm tra xem Cmd hay Msg khác nil.

Marshal và Unmarshal theo stream

Các interface io.Reader và io.Writer có mặt ở khắp nơi trong Go, cung cấp quyền truy cập theo stream tới các tài nguyên như kết nối HTTP, WebSocket hay tệp. Các hàm MarshalWrite và UnmarshalRead cho phép marshal và unmarshal trực tiếp tới và từ các stream này mà không cần một []byte trung gian chứa toàn bộ thông điệp.

func MarshalWrite(out io.Writer, in any, opts ...Options) (err error)
func UnmarshalRead(in io.Reader, out any, opts ...Options) (err error)

Ví dụ, chúng tôi có thể ghi kết quả marshal thẳng ra đầu ra chuẩn:

err := json.MarshalWrite(os.Stdout, m)

Marshal và Unmarshal tuỳ biến

Đôi khi hành vi marshal mặc định không hợp lý với kiểu dữ liệu của bạn.

Ví dụ, giả sử chúng tôi có một kiểu mô tả phiên bản của một phần mềm.

type Version struct {
	Major, Minor, Patch int64
}

v := Version{1, 2, 3}
b, err := json.Marshal(s)

Kiểu này marshal thành {"Major":1,"Minor":2,"Patch":3}, đúng như mong đợi dựa trên khai báo struct, nhưng một chuỗi phiên bản như “1.2.3” hẳn sẽ là cách biểu diễn tốt hơn cho bên tiêu thụ JSON.

Bằng cách hiện thực interface MarshalerTo, chúng tôi có thể cung cấp một cách biểu diễn JSON tuỳ biến. UnmarshalerFrom cung cấp việc unmarshal cho cách biểu diễn JSON tuỳ biến đó.

type MarshalerTo interface {
	MarshalJSONTo(*jsontext.Encoder) error
}

type UnmarshalerFrom interface {
	UnmarshalJSONFrom(*jsontext.Decoder) error
}

Chúng tôi có thể hiện thực các method này để chuyển đổi qua lại với cách biểu diễn dạng chuỗi.

func (v Version) MarshalJSONTo(enc *jsontext.Encoder) error {
	return json.MarshalEncode(enc, fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch))
}

Với marshal tuỳ biến, giá trị Version{1, 2, 3} giờ đây marshal thành "1.2.3".

func (v *Version) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
	if k := dec.PeekKind(); k != jsontext.KindString {
		// Value must be a string.
		return &json.SemanticError{JSONKind: k}
	}

	var s string
	if err := json.UnmarshalDecode(dec, &s); err != nil {
		return err
	}

	_, err := fmt.Sscanf(s, "%d.%d.%d", &v.Major, &v.Minor, &v.Patch)
	return err
}

Với unmarshal tuỳ biến, "1.2.3" giờ đây unmarshal thành Version{1, 2, 3}. Ví dụ đơn giản này đòi hỏi đúng định dạng phiên bản "major.minor.patch", nhưng một UnmarshalJSONFrom phức tạp hơn có thể linh hoạt hơn, chẳng hạn cho phép bỏ qua phần phiên bản minor và patch.

Tài liệu tham khảo

Để biết thêm thông tin, xem tài liệu của package encoding/json/v2 và encoding/json/jsontext.

Nội dung gốc: Go Documentation © The Go Authors, giấy phép CC BY 3.0. Bản dịch và ghi chú: Lê Văn Dũng.