自古以來,反射也是兵家必爭之地

2023-05-28 06:00:34

這幾天收到一個戰術性需求,將一大坨欄位序列化為特定格式的字串。

大概是下表這樣:

序號 欄位名 描述 是否必填
0 logVersion 紀錄檔版本
1 productName 產品
2 serviceName 服務
...
...
...
25 extend3 擴充套件欄位3
26 extend4 擴充套件欄位3
27 extend5 擴充套件欄位3
checklist-client com.CommonApiController uploadImage 2017-12-27 10:35:08 378 1.0 null null 192.168.35.12 EBJ4945 null null ylKLPAvAsoaWRnqGZhZ6xqZ6hkYxSrVKsQDOSOpwXgAAAA== 0 91 null null 0 202226d4-255f-891c-b627-9efc28ef366b 0 010 -1 null null null null null null

控制點1:必填欄位少,若可選欄位無值,該欄位序列化為「null」;

控制點2:序列化時只顯示欄位值(有序),欄位之間用空格區分。

這不就是自定義序列化 且設定序列化預設值?

真要我挨個欄位填充,我眼睛都要對花, 而且很容易漏掉欄位。

// 虛擬碼如下:
  b := bytes.Buffer{}
	b.WriteString("P1")
	b.WriteString(" ")
	b.WriteString("null")
	b.WriteString(" ")
	b.WriteString("null")
	b.WriteString(" ")
	b.WriteString("A")
  ...
  b.WriteString(" ")
  b.WriteString("null")
  log.Info(b.String())

根據"必填欄位極少,可選欄位預設設為null字串"的背景,我開始自定義序列化器:

  1. 使用struct來定義結構,便於對必填欄位賦值 (這個行為肉眼友好)
  2. 將struct的[欄位:欄位值]轉換為排好序的map鍵值對
  3. 對排好序的map鍵值對無腦序列化

將結構體轉換為 map, 這個行為涉及元型別的變動,聯想到反射。

自古以來,反射也是兵家必爭之地, 於是首次操刀golang的反射特性。

思路和虛擬碼很明確,實操時還是有2點障礙:

  1. golang付map做for迴圈,鍵值對的出現是隨機的。
  2. 函數傳參注意傳指標值,而不要傳結構體值。

關於第一個問題,利用網上的[提取key放在slice裡面,再根據key的排序取map值]的思路是想當然了。
我們的key是字串,sort.Strings()之後依舊不是自己的預期(預期是按照struct欄位出現的先後順序)。

所以對map做for迴圈時,能拿到與struct欄位出現順序一致的鍵值對就是關鍵。

取巧:

我們利用反射struct時的欄位順序,定義了一個按照struct欄位出現順序為鍵的map[int]string
這樣sort.Ints(keys) 排序之後,for map時依舊是我們預期的鍵值對順序。

func constructFixedMap(body interface{}) map[int]string {
   
   typ := reflect.TypeOf(body)  //TypeOf返回目標資料型別
   val := reflect.ValueOf(body)  //ValueOf返回目標資料的的值
   if typ.Kind() != reflect.Pointer {
   	fmt.Println("expect pointer")
   	return nil
   }

   typ = typ.Elem() // 返回指標所指向的原值
   val = val.Elem()
   mp := make(map[int]string, 20)
   for i := 0; i < typ.NumField(); i++ { 
   	if typ.Field(i).Type.Kind().String() == "string" {
   		if val.Field(i).String() == "" {    // 可選欄位,在反射時被修改
   			mp[i] = "null"
   		} else {
   			mp[i] = val.Field(i).String()     // 必填欄位,保持不變
   		}
   	} else {
   		if val.Field(i).CanInt() {
   			mp[i] = strconv.FormatInt(val.Field(i).Int(), 10)
   		} else {
   			mp[i] = "null"
   		}

   	}
   }
   return mp
}

記憶點回顧

  • golang反射在自定義序列化器中的運用。
  • 對map做for迴圈,鍵值對的出現是隨機的; 對keys排序,根據排序的keys再取map鍵值對要隨機應變。