在 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 結構體,結構體包含三個字段,NameAgeEmail

encoding/json 會根據結構體字段上的 JSON Tag(標籤)進行序列化和反序列化。序列化時,JSON Tag 會作爲 JSON 字符串的 key,字段值作爲 JSON 字符串的 value。反序列化時,JSON 字符串的 key 所對應的值會被映射到具有同樣 JSON Tag 的結構體字段上。

Name 字段的 JSON Tagname,則對應的 JSON 字符串 keynameAge 字段的 JSON Tagage,則對應的 JSON 字符串 keyageEmail 字段沒有 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.ValueOfreflect.TypeOf,它們分別返回 reflect.Valuereflect.Type 類型。這兩個方法可以應用於任何類型對象(any)。

接下來對 reflect.Valuereflect.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)

實際上 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}

這裏打印了結構體每個字段的信息。

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

序列化

首先是實現序列化的代碼:

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

首先我們拿到對象 vreflect.Valuereflect.Type,待後續使用。

接着使用 strings.Builder 構造了一個用來保存 JSON 字符串信息的對象 jsonBuilder

構造 JSON 字符串分三步走:

  1. 先寫入 JSON 左花括號 { 內容到 jsonBuilder

  2. 根據結構體字段和值,構造 JSON 字符串的鍵值對 key/value 並寫入 jsonBuilder

  3. 最後寫入 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]stringmapkey 爲 JSON 字符串中的 keymapvalue 即爲 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 提供的 SetStringSetInt 方法設置字段的值。

現在,我們唯一沒有講解的邏輯就只剩下 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.ValueOfreflect.TypeOf,調用這兩個方法分別可以得到 reflect.Valuereflect.Type 類型。

有了這兩個類型及其方法,我們可以獲取任意一個 Go 對象的類型信息、值的詳細信息和操作值,可見反射之強大。

本文使用 reflect 反射包實現了一個簡易版本的 encoding/json

雖然是簡易版本,很多 case 和異常都沒有考慮,但這足夠我們學習 encoding/json 原理了,並且這也是一個很好的 reflect 實踐應用。

不過話雖如此,對於何時使用反射,我的觀點是:反射固然強大,有了它我們的代碼足夠靈活,但是過度使用反射會讓代碼變得複雜且混亂。所以非必要,儘量不要使用反射。

本文示例源碼我都放在了 GitHub 中,歡迎點擊查看。

希望此文能對你有所啓發。

延伸閱讀

聯繫我

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/w_f49hgqSGqgW4U57yHUww