☰ 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ữ
Tham khảo

Hướng dẫn chuyển đổi sang encoding/json/v2

Bản dịch từ go.dev/doc/jsonv2-migration

Được giới thiệu trong Go 1.27, package encoding/json/v2 là một bản sửa đổi lớn của package encoding/json ban đầu. Hướng dẫn này mô tả vì sao bạn có thể muốn chuyển từ package v1 sang v2, và cách thực hiện việc đó một cách an toàn.

Vì sao nên chuyển đổi?

Trước hết: bạn không bắt buộc phải chuyển! Package encoding/json sẽ không bao giờ biến mất. Nó nằm trong cam kết tương thích Go 1, nên các package dùng encoding/json sẽ tiếp tục hoạt động vô thời hạn.

Ngoài ra, encoding/json và encoding/json/v2 tương thích với nhau. Ví dụ, nếu một kiểu hiện thực hành vi marshal tuỳ biến bằng encoding/json/v2.MarshalerTo, thì những nơi gọi marshal kiểu này qua encoding/json.Marshal vẫn sẽ đi qua marshaler tuỳ biến đó. Tương tự, các struct tag json mới được giới thiệu trong Go 1.27 cùng với encoding/json/v2 cũng được encoding/json hỗ trợ.

Dù bạn không bắt buộc phải chuyển đổi, vẫn có vài lý do chính đáng để làm việc đó:

Thứ nhất, API mới dễ dùng hơn. Ví dụ, bạn có thể dễ dàng marshal vào một io.Writer bằng encoding/json/v2.MarshalWrite, thay vì phải cần đến một encoding/json.Encoder. encoding/json/v2.Marshalers cho phép ghi đè hành vi marshal của những kiểu cụ thể, ngay cả khi bạn không kiểm soát các kiểu đó. encoding/json/v2.MatchCaseInsenstiveNames cho phép kiểm soát việc phân biệt chữ hoa chữ thường khi khớp tên thành viên của JSON object với các field của struct Go.

Những cải tiến này rất hay, nhưng lý do tốt nhất để chuyển đổi là package v2 chọn các giá trị mặc định chặt chẽ hơn, tương thích tốt hơn với hệ sinh thái so với v1. Tài liệu encoding/json liệt kê đầy đủ mọi khác biệt, nhưng một số điểm nổi bật gồm:

  • Ở v1, các byte UTF-8 không hợp lệ bên trong một chuỗi bị âm thầm thay bằng ký tự thay thế Unicode. Ngược lại, ở v2 sự hiện diện của UTF-8 không hợp lệ dẫn đến lỗi.
  • Ở v1, một JSON object có tên trùng nhau được chấp nhận. Ngược lại, ở v2 một JSON object có tên trùng nhau dẫn đến lỗi.
  • Ở v1, một slice Go hoặc map Go bằng nil được marshal thành JSON null. Ngược lại, v2 marshal slice Go hoặc map Go bằng nil lần lượt thành JSON array rỗng hoặc JSON object rỗng.
  • Ở v1, lỗi không bao giờ được báo lúc chạy đối với các kiểu struct Go có dạng lỗi cấu trúc nào đó (ví dụ, một field tag sai định dạng). Ngược lại, v2 báo lỗi lúc chạy cho các kiểu Go không hợp lệ xét theo khía cạnh tuần tự hoá JSON.

Những thay đổi này được thiết kế để encoding/json/v2 tương thích tốt hơn với hệ sinh thái JSON rộng lớn, ít gây bất ngờ và ít dẫn đến lỗi hơn. Nhưng chúng không tương thích ngược; một số ứng dụng có thể phụ thuộc vào hành vi của v1. Vì vậy, việc chuyển sang v2 phải đi kèm kiểm thử cẩn thận để bảo đảm tính tương thích.

Thay đổi API so với thay đổi hành vi

Với các thao tác marshal và unmarshal đơn giản, API Go phần lớn tương thích ở mức ngôn ngữ, nên việc chuyển đổi API là chuyện nhỏ. Ví dụ, b, err := json.Marshal(v) sẽ vẫn biên dịch được chỉ bằng cách đổi import "encoding/json" thành import "encoding/json/v2".

Phần khó khăn khi chuyển từ v1 sang v2 không nằm ở khác biệt API, mà ở những thay đổi hành vi khi marshal và unmarshal. Hãy xét chương trình sau:

package main

import (
	"encoding/json"
	"fmt"
)

type Pet struct {
	Name      string
	Nicknames []string
}

func main() {
	pets := []Pet{
		{Name: "Oliver", Nicknames: []string{"Ollie", "Olliepop"}},
		{Name: "Remi"},
	}
	b, err := json.Marshal(pets)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}

Khi chạy, chương trình in ra:

[{"Name":"Oliver","Nicknames":["Ollie","Olliepop"]},{"Name":"Remi","Nicknames":null}]

Nếu ta chuyển chương trình này sang encoding/json/v2 một cách đơn giản bằng việc đổi import, chương trình vẫn biên dịch được. Khi chạy, nó in ra:

[{"Name":"Oliver","Nicknames":["Ollie","Olliepop"]},{"Name":"Remi","Nicknames":[]}]

Hãy để ý rằng field “Nicknames” của Remi đã đổi từ null thành []. Nếu đây là một chương trình mới, việc dùng mảng rỗng nhiều khả năng là một cải tiến tốt, nhưng trong một ứng dụng đã tồn tại, những bên tiêu thụ kết quả này ở phía sau có thể đang phụ thuộc vào sự hiện diện của null, nên thay đổi này có thể làm hỏng chúng.

Options

Tất cả những chỗ hành vi v2 khác biệt so với v1 đều được bao phủ bởi Options, cho phép chỉ định hành vi v1 khi dùng API v2. Để dùng API v2 nhưng vẫn chỉ định toàn bộ hành vi v1, hãy dùng DefaultOptionsV1:

package main

import (
	jsonv1 "encoding/json"
	"encoding/json/v2"
	"fmt"
)

type Pet struct {
	Name      string
	Nicknames []string
}

func main() {
	pets := []Pet{
		{Name: "Oliver", Nicknames: []string{"Ollie", "Olliepop"}},
		{Name: "Remi"},
	}
	b, err := json.Marshal(pets, jsonv1.DefaultOptionsV1())
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}

Khi chạy, chương trình lại in ra null:

[{"Name":"Oliver","Nicknames":["Ollie","Olliepop"]},{"Name":"Remi","Nicknames":null}]

Tài liệu DefaultOptionsV1 liệt kê đầy đủ tập option được dùng để tương thích với v1. Các option đặt sau ghi đè lên option đặt trước, nên bạn có thể dùng danh sách này để bật từng hành vi v2 một.

Chuyển đổi

Tuỳ theo loại ứng dụng và mức độ chấp nhận rủi ro, có vài cách khác nhau để tiếp cận việc chuyển sang v2:

Chuyển một lần

Nếu ứng dụng đơn giản hoặc chấp nhận rủi ro cao (việc chuyển đổi gây ra vấn đề cũng không hậu quả gì mấy), thì có lẽ không cần một quy trình chuyển đổi cầu kỳ. Chỉ cần cập nhật các nơi gọi để dùng encoding/json/v2, bảo đảm các test đều qua, rồi commit vào.

Nếu bạn gặp vấn đề tương thích, nguồn gốc khác biệt có thể đã rõ ngay từ thay đổi hoặc từ thông báo lỗi, khi đó bạn có thể đặt option tương thích phù hợp. Nếu nguồn gốc vấn đề không rõ ràng, bạn có thể muốn dùng một trong các cách dưới đây để hỗ trợ gỡ rối.

Đây cũng có thể là cách nhanh chóng để tìm và sửa những vấn đề hiển nhiên (chẳng hạn những vấn đề do unit test phát hiện) trước khi chuyển sang cách tiếp cận tinh tế hơn nhằm truy ra phần còn lại.

Từng option một

Nếu ứng dụng phức tạp hoặc mức chấp nhận rủi ro thấp, bạn có thể cần một cách tiếp cận chậm rãi và cẩn thận hơn.

Như đã nói ở trên, gọi Marshal hoặc Unmarshal với DefaultOptionsV1 khiến lời gọi hành xử y hệt encoding/json. Bước đầu tiên, hãy chuyển mọi lời gọi sang encoding/json/v2 với DefaultOptionsV1. Đây là một thay đổi đơn giản và an toàn; thực tế đây chính xác là cách encoding/json hiện thực Marshal và Unmarshal!

Các option truyền thêm sẽ ghi đè lên option trước đó, nên bạn có thể tắt từng option tương thích v1. Ví dụ, json.Marshal(v, jsonv1.DefaultOptionsV1(), json.FormatNilSliceAsNull(false)) sẽ hành xử như v1 ngoại trừ việc các slice nil được định dạng thành mảng rỗng.

Điều này mở ra một lối đi để chuyển đổi chậm rãi thay vì thay đổi mọi hành vi cùng lúc. Bạn có thể bật từng option một để chắc chắn về điều gì đang thay đổi, hoặc gom nhóm các option tương tự nhau. Ngoài ra, khi gỡ rối, các option này cho bạn cách chia đôi để khoanh vùng đúng thay đổi hành vi gây hỏng, tương tự cơ chế phát hiện option mà ta sẽ thấy ở phần jsonsplit bên dưới.

jsonsplit

github.com/go-json-experiment/jsonsplit là một package bọc JSON, hỗ trợ việc chuyển đổi bằng cách báo cáo những khác biệt giữa v1 và v2 trong môi trường production.

jsonsplit cung cấp các hàm bọc Marshal và Unmarshal vốn mặc định hành xử giống encoding/json, nhưng có thể được cấu hình lúc chạy để dùng v1, v2, hoặc cả hai.

Khi được cấu hình dùng cả hai (CallBothButReturnV1), jsonsplit.Marshal sẽ marshal đầu vào hai lần: một lần với v1 và một lần với v2. Nó sẽ báo cáo mọi khác biệt, nhưng vẫn trả về giá trị của v1 cho nơi gọi. Nhờ vậy một dịch vụ production có thể báo cáo các khác biệt mà không thay đổi hành vi của mình. Với tuỳ chọn AutoDetectOptions, jsonsplit thậm chí sẽ tự động xác định option cụ thể nào gây ra khác biệt.

Lưu ý rằng tính năng này có cái giá của nó. Marshal bằng cả v1 lẫn v2 để phát hiện khác biệt sẽ làm chi phí marshal tăng gần gấp đôi, và AutoDetectOptions còn thực hiện thêm nhiều lần marshal nữa để thu hẹp các option liên quan. Để giảm bớt các chi phí này, jsonsplit cho phép chỉ kiểm tra một tập con ngẫu nhiên các lời gọi thông qua SetMarshalCallRatio.

Ở đây, chúng tôi đã áp dụng jsonsplit vào ví dụ ban đầu:

package main

import (
	"fmt"

	"github.com/go-json-experiment/jsonsplit"
)

func init() {
	// Call both v1 and v2 so we can detect differences, but continue using
	// v1 output.
	jsonsplit.GlobalCodec.SetMarshalCallMode(jsonsplit.CallBothButReturnV1)

	// Specify that when a difference is detected, to auto-detect which
	// options are causing the difference.
	jsonsplit.GlobalCodec.AutoDetectOptions = true

	// Log every time we detect a difference between v1 and v2.
	jsonsplit.GlobalCodec.ReportDifference = func(d jsonsplit.Difference) {
		fmt.Printf("detected jsonv1-to-jsonv2 difference: %v\n", d)
	}
}

type Pet struct {
	Name      string
	Nicknames []string
}

func main() {
	pets := []Pet{
		{Name: "Oliver", Nicknames: []string{"Ollie", "Olliepop"}},
		{Name: "Remi"},
	}
	b, err := jsonsplit.Marshal(pets)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}

Khi chạy, chương trình báo cáo khác biệt và thậm chí chỉ ra rằng FormatNilSliceAsNull là nguyên nhân gây ra khác biệt đó:

detected jsonv1-to-jsonv2 difference: {"Caller":"main.main+5","Func":"Marshal","GoType":"[]main.Pet","JSONValueV1":[{"Name":"Oliver","Nicknames":["Ollie","Olliepop"]},{"Name":"Remi","Nicknames":null}],"JSONValueV2":[{"Name":"Oliver","Nicknames":["Ollie","Olliepop"]},{"Name":"Remi","Nicknames":[]}],"Options":["jsonv2.FormatNilSliceAsNull"]}
[{"Name":"Oliver","Nicknames":["Ollie","Olliepop"]},{"Name":"Remi","Nicknames":null}]

Chúng ta có thể chuyển đổi dịch vụ production một cách êm thấm bằng quy trình như sau:

  1. Chuyển các nơi gọi sang jsonsplit, đặt CallBothButReturnV1, AutoDetectOptions (không bắt buộc), và nối cách giám sát ưa thích của bạn vào ReportDifference (chẳng hạn ghi log hoặc xuất bản các chỉ số).

  2. Theo dõi môi trường production của bạn để phát hiện các khác biệt được báo cáo.

  3. Xử lý các khác biệt.

Ở những chỗ jsonsplit báo cáo khác biệt, hãy điều chỉnh option hoặc kiểu dữ liệu để bảo đảm kết quả giống hệt nhau.

Ví dụ, trong ví dụ ở trên, hãy truyền option json.FormatNilSliceAsNull(true). Trong các trường hợp khác, v2 có thể báo một vấn đề dễ sửa. Ví dụ, việc gắn struct tag JSON “string” cho một kiểu không hợp lệ (chẳng hạn một struct) bị bỏ qua ở v1, nhưng lại báo lỗi ở v2. Mặc dù ReportErrorsWithLegacySemantics sẽ chặn lỗi đó, hợp lý hơn là bỏ hẳn tag “string”. Dù sao nó cũng chẳng làm gì cả.

Lưu ý rằng một khác biệt trong kết quả không nhất thiết có nghĩa là hành vi ở phía sau bị hỏng, mà chỉ là có nguy cơ hỏng. Chúng ta điều chỉnh option ngay lúc này để có thể hoàn tất đại đa số việc chuyển đổi mà không phải dừng lại đánh giá từng thay đổi kết quả tinh tế, nhưng sau khi đã chuyển sang v2, bạn nên quay lại các chỗ đó để xác định xem có thể chuyển sang hành vi mới hay không.

  1. Chuyển sang v2.

Khi môi trường production của bạn ngừng báo cáo các khác biệt mới, bạn có thể chuyển sang hành vi v2 bằng cách đặt OnlyCallV2 hoặc CallBothButReturnV2 để tiếp tục kiểm tra khác biệt.

  1. Dọn dẹp.

Khi việc chuyển sang v2 đã được triển khai an toàn, hãy dọn dẹp phần chuyển đổi bằng cách đổi từ jsonsplit sang chính encoding/json/v2. Ở bước này, bạn có thể đánh giá các trường hợp ở mục (3) vốn đang giữ lại một phần hành vi v1 để xác định xem có an toàn để chuyển chúng sang hành vi v2 hay không.

Xem tài liệu jsonsplit để biết thêm chi tiết về việc chuyển đổi bằng jsonsplit.

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.