Marshal và Unmarshal
json.Marshal biến một giá trị Go thành các byte JSON. json.Unmarshal điền dữ liệu từ các byte JSON vào một giá trị Go. Struct tag quyết định tên các trường trong JSON.
Email không có trong đầu ra đầu tiên vì có omitempty và chuỗi rỗng. Unmarshal cần một con trỏ (&back); truyền chính giá trị vào sẽ trả về một InvalidUnmarshalError.
Struct tag
Cú pháp tag là json:"name,option,option".
| Tag | Tác dụng |
|---|---|
json:"user_id" | dùng user_id làm khóa |
json:"email,omitempty" | bỏ qua khi là false, 0, "", nil, hoặc slice hay map rỗng |
json:",omitzero" (Go 1.24) | bỏ qua khi giá trị là giá trị zero, hoặc IsZero() của nó trả về true |
json:"-" | không bao giờ mã hóa hay giải mã trường này |
json:"-," | dùng đúng khóa - |
json:"count,string" | mã hóa số hoặc bool thành chuỗi JSON ("42") |
| không có tag | khóa là tên trường trong Go, UserID |
Mật khẩu không bao giờ rời khỏi struct, số dư được đặt trong dấu ngoặc kép, và deleted_at biến mất trong khi created_at in ra thời điểm zero 0001-01-01T00:00:00Z. Sự khác biệt đó là lý do omitzero ra đời: omitempty chưa bao giờ hoạt động với struct, và từ lâu đã là điều gây bất ngờ với time.Time. Trên Go 1.23 trở về trước, dùng *time.Time với omitempty để có cùng hiệu quả.
MarshalIndent(v, prefix, indent) tạo đầu ra dễ đọc. Hãy dùng nó cho tệp cấu hình và khi gỡ lỗi; API thường gửi JSON gọn.
Chỉ các trường được export
encoding/json dùng reflection và chỉ thấy được các trường được export. Đây là bug JSON phổ biến nhất trong Go:
type point struct {
x, y int // lowercase: json.Marshal(point{1, 2}) gives {}
}
Không lỗi, không cảnh báo, chỉ ra {} khi mã hóa và các trường vẫn là zero khi giải mã. Hãy viết hoa tên trường và thêm tag cho các khóa chữ thường.
Cách giải mã khớp các trường
Khi unmarshal vào một struct:
- Các khóa được khớp với tên trong tag, hoặc với tên trường, không phân biệt hoa thường.
{"NAME": "x"}điền vào trường có tagjson:"name". - Các khóa không có trường tương ứng bị bỏ qua mà không báo gì.
- Các trường không có khóa tương ứng giữ nguyên giá trị hiện tại. Unmarshal không đặt lại chúng, nên giải mã vào một struct đã có dữ liệu sẽ trộn dữ liệu mới vào.
- Sai kiểu (một chuỗi ở chỗ struct có
int) trả về một*json.UnmarshalTypeError, nhưng các trường khác vẫn được điền.
Để từ chối các khóa không mong đợi, ví dụ trong một API nghiêm ngặt hoặc một tệp cấu hình có lỗi gõ, dùng một Decoder với DisallowUnknownFields:
Cấu trúc không biết trước: map[string]any
Khi không biết trước cấu trúc, hãy giải mã vào map[string]any hoặc any. Cách ánh xạ là cố định:
| JSON | Go |
|---|---|
| object | map[string]any |
| mảng | []any |
| chuỗi | string |
| số | float64 |
| true / false | bool |
| null | nil |
Đầu ra cho thấy hai cái bẫy. Mọi số đều là float64, nên m["stock"].(int) sẽ thất bại. Và số nguyên lớn hơn 2^53 mất độ chính xác khi là float64: id 9007199254740993 quay về thành ...992. UseNumber tránh được cả hai.
Nếu bạn biết một phần cấu trúc, hãy giải mã phần đó vào struct và dùng json.RawMessage cho phần còn lại. Nó giữ các byte thô của một trường để bạn giải mã sau, khi đã biết kiểu của nó.
Slice, map, con trỏ và nil
| Giá trị Go | JSON |
|---|---|
slice nil (var s []int) | null |
slice rỗng ([]int{}) | [] |
map nil | null |
con trỏ nil | null |
[]byte | một chuỗi base64 |
map[string]T | một object, các khóa được sắp xếp |
map[int]T | một object với các khóa số nguyên dưới dạng chuỗi |
Sự khác biệt giữa slice nil và slice rỗng quan trọng với các client API mong đợi một mảng. Hãy khởi tạo bằng []T{} hoặc make([]T, 0) khi trường phải là [].
Dùng trường con trỏ (*int, *bool) khi bạn cần phân biệt "không có" với "bằng zero" ở đầu vào. Sau khi unmarshal, một con trỏ nil nghĩa là khóa bị thiếu hoặc là null; một con trỏ tới 0 nghĩa là client đã gửi 0.
Luồng dữ liệu: Encoder và Decoder
json.Marshal và Unmarshal làm việc trên toàn bộ một slice byte. Với một io.Reader hoặc io.Writer (body HTTP, một tệp, stdin), dùng json.NewDecoder và json.NewEncoder. Decoder cũng có thể đọc lần lượt từng giá trị trong một chuỗi giá trị JSON:
Encoder.Encode ghi thêm một ký tự xuống dòng sau mỗi giá trị. Mặc định cả Marshal lẫn Encoder đều escape <, > và & thành \u003c, \u003e và \u0026, để JSON nhúng vào HTML một cách an toàn. SetEscapeHTML(false) tắt việc đó. Trong HTTP handler, json.NewDecoder(r.Body).Decode(&v) và json.NewEncoder(w).Encode(v) là cặp quen thuộc.
Tự định nghĩa cách mã hóa
Một kiểu có thể tự quyết định JSON của nó bằng cách triển khai json.Marshaler và json.Unmarshaler. Một trường hợp phổ biến là một enum dùng iota nên hiện ra dưới dạng tên chứ không phải số:
MarshalJSON có value receiver nên dùng được cho cả giá trị lẫn con trỏ; UnmarshalJSON cần pointer receiver vì nó sửa đổi giá trị. Những kiểu chỉ cần dạng chuỗi có thể triển khai encoding.TextMarshaler (MarshalText) thay thế, việc này cũng giúp chúng dùng được làm khóa của map.
Lỗi thường gặp
- Tên trường viết thường. Chúng bị bỏ qua mà không báo gì.
- Truyền giá trị vào
Unmarshal. Hàm này cần một con trỏ. - Bỏ qua lỗi. JSON sai định dạng và sai kiểu chỉ được báo qua lỗi đó.
- Cho rằng các số trong
map[string]anylàint. Chúng làfloat64. - Mong
omitemptybỏ đi struct rỗng hoặctime.Timezero. Hãy dùngomitzerotrên Go 1.24, hoặc dùng con trỏ. - Dựa vào thứ tự trường trong map. Các khóa của map được sắp xếp khi xuất ra; các trường struct giữ thứ tự khai báo.
Câu hỏi thường gặp
Làm sao chuyển một struct sang JSON trong Go?
Gọi json.Marshal(v), hàm trả về []byte và một lỗi. Chỉ các trường được export (tên bắt đầu bằng chữ hoa) mới được đưa vào. Dùng một struct tag như json:"name" trên trường để quyết định khóa JSON của nó. Để có đầu ra thụt lề, dùng json.MarshalIndent(v, "", " ").
Vì sao các trường của struct không có trong JSON đầu ra?
encoding/json chỉ thấy các trường được export. Một trường tên name (chữ thường) là vô hình với nó, cả khi marshal lẫn khi unmarshal. Hãy viết hoa tên trường và đặt khóa JSON bằng một tag như json:"name".
omitempty trong JSON của Go có tác dụng gì?
omitempty bỏ một trường khỏi đầu ra khi nó chứa giá trị rỗng: false, 0, "", một con trỏ hoặc interface nil, hay một slice hoặc map rỗng. Nó không coi struct hay time.Time là rỗng. Go 1.24 thêm omitzero, bỏ qua mọi giá trị là giá trị zero của kiểu đó (hoặc có phương thức IsZero() trả về true), kể cả struct và time.Time.
Làm sao đọc JSON không biết trước cấu trúc trong Go?
Unmarshal vào map[string]any (hoặc any). Object trở thành map[string]any, mảng thành []any, chuỗi thành string, boolean thành bool, và mọi số đều thành float64. Dùng type assertion để đọc giá trị, và Decoder.UseNumber nếu các số nguyên lớn phải giữ chính xác.