在 Go 中如何使用反射實現簡易版 encoding-json
在使用 Go 語言開發過程中,我們經常需要實現結構體到 JSON 字符串的序列化(Marshalling)或 JSON 字符串到結構體的反序列化(Unmarshalling)操作。Go 爲我們提供了 encoding/json 庫可以很方便的實現這一需求。
在本文中,我們將探索如何使用 Go 的反射機制自己來實現一個簡易版的 encoding/json 庫。這個過程不僅能幫助我們理解序列化和反序列化的基本原理,還能提供一種實用的反射使用方法,加深我們對反射的理解。
通過本文的學習,我們將實現一個能夠將結構體轉和 JSON 字符串互相轉換的包。
encoding/json
我們先來回顧下在 Go 中如何使用 encoding/json 庫實現結構體轉和 JSON 字符串互轉。
示例代碼如下:
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string
}
func main() {
{
user := User{
Name: "江湖十年",
Age: 20,
Email: "jianghushinian007@outlook.com",
}
jsonData, err := json.Marshal(user)
if err != nil {
fmt.Println("Error marshal to JSON:", err)
return
}
fmt.Printf("JSON data: %s\n", jsonData)
}
{
jsonData := `{"name": "江湖十年", "age": 20, "Email": "jianghushinian007@outlook.com"}`
var user User
err := json.Unmarshal([]byte(jsonData), &user)
if err != nil {
fmt.Println("Error unmarshal from JSON:", err)
return
}
fmt.Printf("User struct: %+v\n", user)
}
}
示例程序中定義了一個 User 結構體,結構體包含三個字段,Name、Age 和 Email。
encoding/json 會根據結構體字段上的 JSON Tag(標籤)進行序列化和反序列化。序列化時,JSON Tag 會作爲 JSON 字符串的 key,字段值作爲 JSON 字符串的 value。反序列化時,JSON 字符串的 key 所對應的值會被映射到具有同樣 JSON Tag 的結構體字段上。
Name 字段的 JSON Tag 是 name,則對應的 JSON 字符串 key 爲 name;Age 字段的 JSON Tag 是 age,則對應的 JSON 字符串 key 爲 age;Email 字段沒有 JSON Tag,則默認會使用 Email 作爲對應的 JSON 字符串 key。
執行示例代碼,得到如下輸出:
$ go run main.go
JSON data: {"name":"江湖十年","age":20,"Email":"jianghushinian007@outlook.com"}
User struct: {Name:江湖十年 Age:20 Email:jianghushinian007@outlook.com}
reflect 簡介
reflect 是 Go 語言爲我們提供的反射庫,用於在運行時檢查類型並操作對象。它是實現動態編程和元編程的基礎,使程序能夠在運行時獲取類型信息並進行相應的操作。
有如下示例代碼:
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string
}
func main() {
// 內置類型
{
age := 20
val := reflect.ValueOf(age)
typ := reflect.TypeOf(age)
fmt.Println(val, typ)
// 自定義結構體類型
{
user := User{
Name: "江湖十年",
Age: 20,
Email: "jianghushinian007@outlook.com",
}
val := reflect.ValueOf(user)
typ := reflect.TypeOf(user)
fmt.Println(val, typ)
}
}
執行示例代碼,得到如下輸出:
$ go run main.go
20 int
{江湖十年 20 jianghushinian007@outlook.com} main.User
reflect 最常用的兩個方法分別是 reflect.ValueOf 和 reflect.TypeOf,它們分別返回 reflect.Value 和 reflect.Type 類型。這兩個方法可以應用於任何類型對象(any)。
-
reflect.Value:表示一個 Go 值,它提供了一些方法,可以獲取值的詳細信息,也可以操作值,例如獲取值的類型、設置值等。 -
reflect.Type:表示一個 Go 類型,它提供了一些方法,可以獲取類型的詳細信息,例如類型的名稱(Name)、種類(Kind,基本類型、結構體、切片等)。
接下來對 reflect.Value 和 reflect.Type 類型的常用方法進行介紹,以如下實例化 User 結構體指針作爲被操作對象:
// 實例化 User 結構體指針
user := &User{
Name: "江湖十年",
Age: 20,
Email: "jianghushinian007@outlook.com",
}
reflect.Value 常用方法
reflect.Value 提供了 Kind 方法可以獲取對應的類型類別:
// 注意這裏傳遞的是指針類型
kind := reflect.ValueOf(user).Kind()
fmt.Println(kind)
kind = reflect.ValueOf(*user).Kind()
fmt.Println(kind)
kind = reflect.ValueOf(user).Elem().Kind()
fmt.Println(kind)
這段示例代碼將得到如下輸出:
ptr
struct
struct
這裏 Kind 方法返回的是 User 的底層類型 struct,以及 ptr 類型,ptr 代表指針類型。
值得注意的是,如果傳遞給 reflect.ValueOf 的是指針類型(user),需要使用 Elem 方法獲取指針指向的值;如果傳遞給 reflect.ValueOf 的是值類型(*user),則可以直接得到值。
使用指針類型的好處是可以使用 reflect.Value 提供的 Set<Type> 方法直接修改 user 字段的值,稍後講解。
reflect.Value 同樣提供了 Type 方法,可以得到 reflect.Type:
// 以下二者等價
tpy := reflect.ValueOf(user).Type()
fmt.Println(tpy)
tpy1 := reflect.TypeOf(user)
fmt.Println(tpy1)
fmt.Println(reflect.DeepEqual(tpy, tpy1))
這與 reflect.TypeOf 等價。
這段示例代碼將得到如下輸出:
*main.User
*main.User
true
我們有多種方式可以獲取結構體值字段:
nameField := reflect.ValueOf(user).Elem().FieldByName("Name")
ageField := reflect.ValueOf(user).Elem().FieldByIndex([]int{1})
emailField := reflect.ValueOf(user).Elem().Field(2)
-
FieldByName方法可以通過字段名獲取結構體字段。 -
FieldByIndex方法通過索引切片獲取結構體字段。 -
Field方法通過索引獲取結構體字段。
實際上 FieldByIndex 方法內部調用的也是 Field 方法。這裏的索引是結構體字段按照順序排序所在位置,即 Name 字段索引爲 0,Age 字段索引爲 1,Email 字段索引爲 2。
我們可以使用 NumField 獲取結構體字段總個數:
numField := reflect.ValueOf(*user).NumField()
fmt.Println(numField)
拿到結構體字段對象後,可以根據其具體類型獲取對應值:
fmt.Println(nameField.String())
fmt.Println(ageField.Int())
fmt.Println(emailField.String())
以上示例代碼將得到如下輸出:
3
江湖十年
20
jianghushinian007@outlook.com
因爲我們傳遞給 reflect.ValueOf 函數的是 User 結構體指針,所以可以使用 reflect.Value 提供的 Set<Type> 方法設置結構體字段的值:
nameField.SetString("jianghushinian") // 設置 Name 字段的值
ageField.SetInt(18) // 設置 Age 字段的值
emailField.SetString("jianghushinian007@163.com") // 設置 Email 字段的值
現在打印 user 對象:
fmt.Println(user)
得到輸出:
&{jianghushinian 18 jianghushinian007@163.com}
如果我們傳遞給 reflect.ValueOf 函數的不是 User 結構體指針,而是結構體對象:
nameField := reflect.ValueOf(*user).FieldByName("Name")
現在去設置字段值:
nameField.SetString("jianghushinian")
程序會直接 panic:
panic: reflect: reflect.Value.SetString using unaddressable value
此外,我們還可以總結一個規律,使用指針時,就需要通過 Elem 方法獲取指針指向的值,不使用指針就不需要調用 Elem 方法。
reflect.Type 常用方法
現在我們再來簡單介紹下 reflect.Type 的幾個常用方法。
reflect.Type 同樣提供瞭如下幾個方法,與 reflect.Value 對應:
nameField, _ := reflect.TypeOf(user).Elem().FieldByName("Name")
ageField := reflect.TypeOf(user).Elem().FieldByIndex([]int{1})
emailField := reflect.TypeOf(user).Elem().Field(2)
我們來輸出下這幾個對象的值:
fmt.Printf("%+v\n", nameField)
fmt.Printf("%+v\n", ageField)
fmt.Printf("%+v\n", emailField)
得到如下輸出:
{Name:Name PkgPath: Type:string Tag:json:"name" Offset:0 Index:[0] Anonymous:false}
{Name:Age PkgPath: Type:int Tag:json:"age" Offset:16 Index:[1] Anonymous:false}
{Name:Email PkgPath: Type:string Tag: Offset:24 Index:[2] Anonymous:false}
這裏打印了結構體每個字段的信息。
-
Name對應字段名。 -
PkgPath是包路徑。 -
Type是結構體字段類型。 -
Tag即爲字段標籤。 -
Offset是字段偏移量。如果你不清楚什麼是字段偏移量,可以參考我寫的另一篇文章《Go 語言中的結構體內存對齊你瞭解嗎?》。 -
Index是字段索引位置。 -
Anonymous表示是否爲匿名字段。比如如下結構體:
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string
string
}
這個結構體定義中,最後一個字段就是匿名字段。
現在我們想獲取結構體字段 JSON Tag,可以這樣做:
tag := nameField.Tag
fmt.Printf("%+v\n", tag)
fmt.Printf("%+v\n", tag.Get("json"))
將得到如下輸出:
json:"name"
name
reflect 基礎語法就講解到這裏,更多使用方法需要我們在以後的的實踐中去探索。
使用 reflect 實現 encoding/json
接下來就看看,如何使用 reflect 自己實現一個簡易版本的 encoding/json。
示例程序目錄結構如下:
$ tree
.
├── encoding
│ └── json
│ ├── decode.go
│ └── encode.go
├── go.mod
└── main.go
-
encoding/json/encode.go用於實現序列化功能。 -
encoding/json/decode.go用於實現反序列化功能。 -
main.go用來驗證這個簡易版的encoding/json功能。
序列化
首先是實現序列化的代碼:
package json
import (
"fmt"
"reflect"
"strconv"
"strings"
)
// Marshal 序列化
func Marshal(v any) (string, error) {
// 拿到對象 v 的 reflect.Value 和 reflect.Type
val := reflect.ValueOf(v)
if val.Kind() != reflect.Struct {
return "", fmt.Errorf("only structs are supported")
}
typ := val.Type()
// 用來保存 JSON 字符串
jsonBuilder := strings.Builder{}
// NOTE: 三步走拼接 JSON 字符串
// 1. JSON 左花括號
jsonBuilder.WriteString("{")
// 2. key/value
for i := 0; i < val.NumField(); i++ {
fieldVal := val.Field(i)
fieldType := typ.Field(i)
// 獲取 JSON 標籤
tag := fieldType.Tag.Get("json")
if tag == "" {
tag = fieldType.Name
}
jsonBuilder.WriteString(`"` + tag + `":`)
// 根據字段類型轉換,僅支持 string/int
switch fieldVal.Kind() {
case reflect.String:
jsonBuilder.WriteString(`"` + fieldVal.String() + `"`)
case reflect.Int:
jsonBuilder.WriteString(strconv.FormatInt(fieldVal.Int(), 10))
default:
return "", fmt.Errorf("unsupported field type: %s", fieldVal.Kind())
}
if i < val.NumField()-1 {
jsonBuilder.WriteString(",")
}
}
// 3. JSON 右花括號
jsonBuilder.WriteString("}")
return jsonBuilder.String(), nil
}
這段代碼中沒有新的 reflect 語法,我們都在前文中介紹了,這裏捋一下代碼邏輯。
所謂序列化操作,就是 Go 結構體轉 JSON 字符串的操作。
這裏函數名參考 encoding/json 同樣被定義爲 Marshal。
首先我們拿到對象 v 的 reflect.Value 和 reflect.Type,待後續使用。
接着使用 strings.Builder 構造了一個用來保存 JSON 字符串信息的對象 jsonBuilder。
構造 JSON 字符串分三步走:
-
先寫入 JSON 左花括號
{內容到jsonBuilder。 -
根據結構體字段和值,構造 JSON 字符串的鍵值對
key/value並寫入jsonBuilder。 -
最後寫入 JSON 右花括號
}內容到jsonBuilder。
函數最終返回 jsonBuilder.String() 即爲 JSON 字符串。
這裏面主要邏輯都在步驟 2 中。
首先會遍歷結構體每個字段,並使用如下方式獲取每個字段對應的 JSON Tag:
tag := fieldType.Tag.Get("json")
if tag == "" {
tag = fieldType.Name
}
當 JSON Tag 不存在,則默認使用結構體字段名作爲 JSON 字符串的 key,比如 User.Email 字段。
將 JSON key 和 : 寫入 jsonBuilder:
jsonBuilder.WriteString(`"` + tag + `":`)
然後根據結構體字段類型轉換成對應的 JSON 數據類型,寫入 jsonBuilder:
switch fieldVal.Kind() {
case reflect.String:
jsonBuilder.WriteString(`"` + fieldVal.String() + `"`)
case reflect.Int:
jsonBuilder.WriteString(strconv.FormatInt(fieldVal.Int(), 10))
default:
return "", fmt.Errorf("unsupported field type: %s", fieldVal.Kind())
}
每次循環末尾,判斷是否爲結構體最後一個字段,如果不是,則寫入分隔符 ,:
if i < val.NumField()-1 {
jsonBuilder.WriteString(",")
}
至此,序列化代碼邏輯大功告成。
我們可以使用如下示例代碼進行測試:
import simplejson "github.com/jianghushinian/blog-go-example/struct/encoding-json/encoding/json"
...
user := User{
Name: "江湖十年",
Age: 20,
Email: "jianghushinian007@outlook.com",
}
jsonData, err := simplejson.Marshal(user)
if err != nil {
fmt.Println("Error marshal to JSON:", err)
return
}
fmt.Printf("JSON data: %s\n", jsonData)
執行示例代碼,得到如下輸出:
$ go run main.go
JSON data: {"name":"江湖十年","age":20,"Email":"jianghushinian007@outlook.com"}
沒有任何問題,與原生的 encoding/json 中的 Marshal 方法表現一致。
反序列化
接下來是實現反序列化的代碼:
package json
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
)
// Unmarshal 反序列化
func Unmarshal(data []byte, v interface{}) error {
parsedData, err := parseJSON(string(data))
if err != nil {
return err
}
val := reflect.ValueOf(v).Elem()
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
fieldVal := val.Field(i)
fieldType := typ.Field(i)
// 獲取 JSON 標籤
tag := fieldType.Tag.Get("json")
if tag == "" {
tag = fieldType.Name
}
// 從解析的數據中獲取值
if value, ok := parsedData[tag]; ok {
switch fieldVal.Kind() {
case reflect.String:
fieldVal.SetString(value)
case reflect.Int:
intValue, err := strconv.Atoi(value)
if err != nil {
return err
}
fieldVal.SetInt(int64(intValue))
default:
return fmt.Errorf("unsupported field type: %s", fieldVal.Kind())
}
}
}
return nil
}
這段代碼中同樣沒有新的 reflect 語法。
所謂反序列化操作,就是 JSON 字符串轉 Go 結構體的操作。
這裏函數名參考 encoding/json 同樣被定義爲 Unmarshal,並且函數簽名也保持一致。
反序列化操作首先使用 parseJSON 函數解析傳遞進來的 JSON 數據,得到 parsedData。
parsedData 類型爲 map[string]string,map 的 key 爲 JSON 字符串中的 key,map 的 value 即爲 JSON 字符串中的 value。
接下來核心邏輯是遍歷結構體每個字段,並獲取字段對應的 JSON Tag:
tag := fieldType.Tag.Get("json")
if tag == "" {
tag = fieldType.Name
}
當 JSON Tag 不存在,則默認使用結構體字段名作爲 JSON 字符串的 key,比如 User.Email 字段。
然後根據 JSON Tag 從解析後的 parsedData 數據中獲取 key/value:
if value, ok := parsedData[tag]; ok {
switch fieldVal.Kind() {
case reflect.String:
fieldVal.SetString(value)
case reflect.Int:
intValue, err := strconv.Atoi(value)
if err != nil {
return err
}
fieldVal.SetInt(int64(intValue))
default:
return fmt.Errorf("unsupported field type: %s", fieldVal.Kind())
}
}
這裏根據結構體字段的類型,將 parsedData 中對應的字符串 value 轉換成對應類型。並使用 reflect.Value 提供的 SetString 和 SetInt 方法設置字段的值。
現在,我們唯一沒有講解的邏輯就只剩下 parseJSON 函數了。
parseJSON 函數定義如下:
// 簡易版 JSON 解析器,僅支持 string/int 且不考慮嵌套
func parseJSON(data string) (map[string]string, error) {
result := make(map[string]string)
data = strings.TrimSpace(data)
if len(data) < 2 || data[0] != '{' || data[len(data)-1] != '}' {
return nil, errors.New("invalid JSON")
}
data = data[1 : len(data)-1]
parts := strings.Split(data, ",")
for _, part := range parts {
kv := strings.SplitN(part, ":", 2)
if len(kv) != 2 {
return nil, errors.New("invalid JSON")
}
k := strings.Trim(strings.TrimSpace(kv[0]), `"`)
v := strings.Trim(strings.TrimSpace(kv[1]), `"`)
result[k] = v
}
return result, nil
}
parseJSON 實現了一個簡易版本的 JSON 字符串解析器,能夠將 JSON 字符串的 key/value 解析出來,並保存到 map[string]string 中。
我們可以使用如下示例代碼進行測試 Unmarshal 代碼邏輯是否正確:
import simplejson "github.com/jianghushinian/blog-go-example/struct/encoding-json/encoding/json"
...
jsonData := `{"name": "江湖十年", "age": 20, "Email": "jianghushinian007@outlook.com"}`
var user User
err := simplejson.Unmarshal([]byte(jsonData), &user)
if err != nil {
fmt.Println("Error unmarshal from JSON:", err)
return
}
fmt.Printf("User struct: %+v\n", user)
執行示例代碼,得到如下輸出:
$ go run main.go
User struct: {Name:江湖十年 Age:20 Email:jianghushinian007@outlook.com}
沒有任何問題,與原生的 encoding/json 中的 Unmarshal 方法表現一致。
總結
reflect 是 Go 語言爲我們提供的反射庫,用於在運行時檢查類型並操作對象。
reflect 最常用的兩個方法分別是 reflect.ValueOf 和 reflect.TypeOf,調用這兩個方法分別可以得到 reflect.Value 和 reflect.Type 類型。
有了這兩個類型及其方法,我們可以獲取任意一個 Go 對象的類型信息、值的詳細信息和操作值,可見反射之強大。
本文使用 reflect 反射包實現了一個簡易版本的 encoding/json。
雖然是簡易版本,很多 case 和異常都沒有考慮,但這足夠我們學習 encoding/json 原理了,並且這也是一個很好的 reflect 實踐應用。
不過話雖如此,對於何時使用反射,我的觀點是:反射固然強大,有了它我們的代碼足夠靈活,但是過度使用反射會讓代碼變得複雜且混亂。所以非必要,儘量不要使用反射。
本文示例源碼我都放在了 GitHub 中,歡迎點擊查看。
希望此文能對你有所啓發。
延伸閱讀
-
reflect@go1.22.5 Documentation:https://pkg.go.dev/reflect@go1.22.5
-
encoding/json@go1.22.5 Documentation:https://pkg.go.dev/encoding/json@go1.22.5
-
本文 GitHub 示例代碼:https://github.com/jianghushinian/blog-go-example/tree/main/struct/encoding-json
聯繫我
-
公衆號:Go 編程世界
-
微信:jianghushinian
-
郵箱:jianghushinian007@outlook.com
-
博客:https://jianghushinian.cn
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/w_f49hgqSGqgW4U57yHUww