Golang 反射簡單應用--參數校驗


以下內容為個人學習總結,如果有不准確的地方,歡迎指出!

說實話我之前用Python基本沒怎么用過反射,估計在Golang里面也一樣,在大多數應用和服務中並不常見。

提到反射,就必須要提一下Golang反射的三大定律

  • 1 可以將interface{}類型轉換為reflect類型。
  • 2 通過反射對象可以獲取 interface{} 變量。
  • 3 值是否可以被更改,能被尋址。(概念不好理解,后面demo解釋)

原文

  • 1 Reflection goes from interface value to reflection object.
  • 2 Reflection goes from reflection object to interface value.
  • 3 To modify a reflection object, the value must be settable.
    三大定律原出處:
    https://blog.golang.org/laws-of-reflection

反射的一般使用場景

  • 不確定預定類型的參數,需要根據參數的類型來執行不同的操作。

當然也可以使用Assertion來判斷類型,但是這種方式非常麻煩。

// 通過斷言判斷類型
func AssertionType(v interface{}){
	switch v.(type){
	case int, int16, int32, int64:
		fmt.Printf("整數類型 %d \n", v)
	case userInfo:
		fmt.Printf("是 userInfo 類型 %v  ", v)
		fmt.Printf("年齡 %d \n", v.(userInfo).Age)
		// 斷言調用方法
		v.(userInfo).SayAge()
	default:
		fmt.Println("沒有匹配到")
	}
}

"但是我們如何處理其它類似[]float64、map[string][]string等類型呢?我們當然可以添加更多的測試分支,但是這些組合類型的數目基本是無窮的。還有如何處理類似url.Values這樣的具名類型呢?即使類型分支可以識別出底層的基礎類型是map[string][]string,但是它並不匹配url.Values類型,因為它們是兩種不同的類型,而且switch類型分支也不可能包含每個類似url.Values的類型,這會導致對這些庫的依賴。"--出自Go語言程序設計

沒有辦法來檢查未知類型的表示方式,我們被卡住了。這就是我們為何需要反射的原因。

Golang中反射常用的方法

  • reflect.TypeOf 獲取類型 以及相關信息(對應第一條定律)
  • reflect.ValueOf 獲取數據的運行時的表示(對應第二條定律)

其他基礎方法(部分)

  • 結構體反射常用 假設user為實例化結構體
    • reflect.TypeOf(user).Field(int) // 獲取 指定結構體下角標屬性的類型
    • reflect.TypeOf(user).Field(int).Tag.Get("foo") // 獲取指定下角標屬性的結構體標簽 內部再調用 Lookup方法
    • reflect.ValueOf(user).NumField() // 獲取結構體屬性數量
    • reflect.ValueOf(user).Kind() // 獲取對應的類型
    • reflect.ValueOf(user).MethodByName("SayName").Call([]reflect.Value{reflect.ValueOf("測試")}) // 傳參數調用方法
  • map slice獲取獲取 (n為map或者slice)
    • reflect.ValueOf(n).Len() // 獲取長度

一些關於反射的演示

// 創建一個結構體
type userInfo struct {
	User     string `json:"user"foo:"test_tag"`
	Avatar   string `json:"avatar"bar:""`
	Age      int    `json:"age"`
}
// 定義兩個方法
func (u userInfo) SayName(name string){
	fmt.Printf("用戶名是 %s\n", name)
}
// 方法
func (u userInfo) SayAge(){
	fmt.Printf("年齡是 %d\n", u.Age)
}

func TestReflect(t *testing.T) {

	// 實例化結構體
	user := userInfo{User: "Nike", Age:18}

	// 斷言判斷類型 功能不如反射強大
	AssertionType(user)
	AssertionType(1111)

	typ := reflect.TypeOf(user) // 獲取reflect的類型
	t.Log(typ)

	val := reflect.ValueOf(user) // 獲取reflect的值
	t.Log(val)

	kd := val.Kind() // 獲取到st對應的類別
	t.Log(kd)

	num := val.NumField() // 獲取值字段的數量
	t.Log(num)

	// 通過反射調用方法
	m1 := val.MethodByName("SayName")
	//m1 := val.Method(0)
	m1.Call([]reflect.Value{reflect.ValueOf("測試")}) // 傳參數
	// 私有方法不可反射調用  Java反射可以暴力調用私有方法
	m2 := val.MethodByName("SayAge")
	m2.Call([]reflect.Value{}) // 不傳參數

	tagVal := typ.Field(2) // 獲取index為2的類型信息
	val = val.Field(2)     // 獲取index為2實例化后的值

	t.Log(tagVal)
	t.Log(val)
	t.Log(tagVal.Type) // 輸出結構體字段的類型
	t.Log(tagVal.Name) // 輸出結構體字段名稱

	// 獲取結構體的tag 沒有則為空 Get 實際就是調用的Lookup
	t.Log(tagVal.Tag.Get("foo"))
	// 返回兩個值 第一個為tag值 第二個為bool值 true表示設置了此tag 無論是否為空字符串
	t.Log(tagVal.Tag.Lookup("foo"))
	t.Log(typ.Field(1).Tag.Lookup("bar"))     // 設置了tag為bar 但是為空字符串 依舊為true
	t.Log(typ.Field(1).Tag.Lookup("any_tag")) // 沒有設置此tag 就為false

	// 必須使用地址 才可以修改原來的值 否則會panic (反射第三定律,值可以被修改)
	modifyVal(&user)

	t.Log(user)
}

// 通過反射修改值
func modifyVal(user interface{}){

	// 獲取變量的指針
	pVal := reflect.ValueOf(user) // 獲取reflect的值

	// 獲取指針指向的變量
	v := pVal.Elem()
	// 找到並更新變量的值
	v.FieldByName("User").SetString("Jack")

}

反射的簡單應用

最簡單的例子,通過反射對比slice或者map是否相等

規定Golang中slice,map,func不能用 == 比較
https://stackoverflow.com/questions/37900696/why-cant-go-slice-be-used-as-keys-in-go-maps-pretty-much-the-same-way-arrays-ca
https://golang.org/ref/spec#Comparison_operators

sliceA := []int{1, 2, 3}
sliceB := []int{1, 2, 3}
//fmt.Println(sliceA == sliceB) // panic 
// 可以用反射對比
fmt.Println(reflect.DeepEqual(sliceA, sliceB)) 

利用反射設計一個校驗參數的方法

主要參考 gin-vue-admin 參數驗證 我自己添加了一個正則校驗的方法。
比如定義一個verify來驗證結構體里面的方法是否合法

// 存放驗證規則的地方 一個字段可以有多個校驗方法
type Rules map[string][]string

var (
	UserInfoVerify = Rules{"Page": {Ge("1")}, "PageSize": {Le("50")}, "Name": {Regex(`^\d{3}$`), NotEmpty()}}
)

type UserInfo struct {
	Page     int    `json:"page"`
	PageSize int    `json:"page_size"`
	Name     string `json:"name"`
}

func TestVerify(t *testing.T) {

	// 參數信息  可以和請求的參數信息綁定
	u := UserInfo{Page: 1, PageSize: 30, Name: "234"} // 合法
	//u := UserInfo{Page:1, PageSize:30, Name: "1234"} // Name非法

	// 驗證參數是否合法 verify 自定義的校驗蠶農書
	if err := verify(u, UserInfoVerify); err != nil {
		t.Log(fmt.Sprintf("驗證失敗 %s \n", err))
	} else {
		t.Log("success")
	}
}

// 核心函數
func verify(st interface{}, roleMap Rules) (err error) {

	// 限定 比較返回值為 以下幾個
	compareMap := map[string]bool{
		"lt": true,
		"le": true,
		"eq": true,
		"ne": true,
		"ge": true,
		"gt": true,
	}

	typ := reflect.TypeOf(st)
	val := reflect.ValueOf(st)

	// 判斷待驗證參數 是否是結構體 不是直接返回錯誤
	if val.Kind() != reflect.Struct {
		return errors.New("expect struct")
	}

	// 遍歷結構體的所有字段
	for i := 0; i < val.NumField(); i++ {

		// 獲取反射后的具體字段
		tagVal := typ.Field(i)
		val := val.Field(i)

		// 判斷此字段是否有校驗規則 >0 則說明有
		if len(roleMap[tagVal.Name]) > 0 {

			// 循環此字段的校驗規則(一個字段可以存在多個校驗規則)  規則為 各個判斷類型函數 返回值
			for _, v := range roleMap[tagVal.Name] {
				switch {
				// 非空判斷
				case v == "notEmpty":
					if isBlank(val) {
						return errors.New(tagVal.Name + "值不能為空")
					}
				// 正則校驗
				case strings.Split(v, "=")[0] == "regex":
					if !isRegexMatch(val.String(), v) {
						return errors.New(tagVal.Name + "正則校驗不合法" + v)
					}

				// 比較符判斷 分割返回值里面的 = 符號  compareMap 確保輸入的函數正確
				case compareMap[strings.Split(v, "=")[0]]:
					// 比較值    val 為反射字段后的值  v為 lt=1等校驗值
					if !compareVerify(val, v) {
						return errors.New(tagVal.Name + "長度或值不在合法范圍," + v)
					}
				default:
					fmt.Println("檢查 Rules 校驗函數輸入是否正確: " + v)
				}
			}
		}
	}
	return nil
}

以上代碼不完整,完整代碼見GitHub地址

反射應用參數校驗代碼地址

原文地址見個人博客

參數校驗GitHub地址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM