Go系列之反射

Remember發表於2020-12-12

圖片

反射增強了語言的動態描述能力,你要問我什麼是動態,我只能說,所有可能產生意料之外情理之中的變化,都是動態。

概述

反射這個詞並不是特定語言持有的,相反很多語言擁有著自己的反射模型。老實說,我並不喜歡去用專業的術語去解釋一些概念性的東西,這樣往往觀看的人也雲裡霧裡,這些概念性的東西,每個人腦海中都有自己的“解釋語言“,隨他去吧。

我主要想談談為什麼需要反射,應用場景是什麼?其實在我看來,這兩個問題嚴格意義上是等價的,即 為什麼=應用場景,應用場景=為什麼。

go 作為靜態型別語言,如果沒有反射,很多能夠作用於 “任意型別” 的函式,實現起來會很麻煩。我舉一個最簡單的場景:


package main

// 會員
type member struct {
  Name  string
  Level int
}

// 遊客
type visitor struct {
  NickName string
}

func main() {
  m := member{
    Name:  "wqq",
    Level: 520,
  }
  v := visitor{
    NickName: "curry",
  }
  checkSomeTing(m)
  checkSomeTing(v)
}

func checkSomeTing(v interface{}) {
  if "如果是會員的話" {
    // ...
  } else {
    // ...
  }
}

上面 member 和 visitor 兩種結構體表示兩種身份,他們都需要經過公共的 checkSomeTing 操作,我們希望在這個函式中能根據不同的結構體,操作不同的邏輯。如果沒有反射,如何知道傳進來具體是什麼型別的值。(我只是單純的指出沒有反射情況下的問題,而不是吐槽上面這個設計)。

因此,我們需要能有一種方法,它可以在程式執行時獲取傳入引數真正的型別,如果是 struct 那麼這個 struct 有哪些屬性,哪些方法……。

Interface

說起 go 反射,就必然繞不開 interface 型別。在 go 中 interface 是一種特殊的型別,可以存放任何實現了其方法的值。如果是一個空 interface ,意味著可以傳遞任意型別值。interface 型別有(value,type) 對,而反射就是檢查 interface 的 (value,type) 對,所有的反射包中的方法,都是圍繞 reflect.Type 和 reflect.Value 進行的。reflect.Type 是一個介面型別,提供了一系列獲取 interface 資訊的介面。原始碼位置在 src/reflect/type.go。


type Type interface {
  // Methods applicable to all types.
  Align() int
  // FieldAlign returns the alignment in bytes of a value of
  // this type when used as a field in a struct.
  FieldAlign() int
  Method(int) Method
  String() string
  ........
}

而 reflect.Value 的型別被宣告成結構體。原始碼位置 src/reflect/value.go

type Value struct {
  // typ holds the type of the value represented by a Value.
  typ *rtype
  // Pointer-valued data or, if flagIndir is set, pointer to data.
  // Valid when either flagIndir is set or typ.pointers() is true.
  ptr unsafe.Pointer

  flag
}

可以看到,這個結構體的三個欄位都是私有的,沒有對外暴露任何欄位,但是它提供了一系列獲取資料和寫入等操作的方法。

圖片

反射三大定律

go 官方提供了三大定律來說明反射,我們也從這三個定律中學習如何 使用反射。

  • 定律一:反射可以將 interface 變數轉換為反射物件。

package main

import (
  "fmt"
  "reflect"
)

func main() {
  var a float64 = 32
  var b int64 = 32
  doSomeTing(a)
  doSomeTing(b)
}

func doSomeTing(res interface{}) {
  t := reflect.TypeOf(res)
  v := reflect.ValueOf(res)
  fmt.Println("型別是:", t)
  fmt.Println("值是:", v)
}

程式列印結果:

圖片

我們定義了兩個變數,他們的型別分別是 float64 和 int64 ,傳入 doSomeTing 函式,此函式引數型別為空的 interface ,因此可以接收任意型別引數,最終我們通過 reflect.TypeOf 獲取了變數的真實型別,通過 reflect.ValueOf 獲取變數真實的值。

我們可以再試試通過結構體使用其他操作方法。

package main

import (
  "fmt"
  "reflect"
)

type user struct {
  Name string
  Age  int
}

func main() {
  var u = user{
    Age:  18,
    Name: "wuqq",
  }
  v := reflect.ValueOf(u)
  t := reflect.TypeOf(u)

  for i := 0; i < t.NumField(); i++ {
    filed := t.Field(i)
    value := v.Field(i).Interface()
    fmt.Printf("field:%v type:%v value:%v\n", filed.Name, filed.Type, value)
  }
}

執行結果:

圖片

上面就不解釋了,主要解釋迴圈裡面的,我們通過 reflect.type 的 NumField 獲取結構體中個數,通過 reflect.type 的 Field 方法下標獲取屬性名,通過 interface() 獲取對應屬性值。

  • 定律二:反射可以將反射物件還原成 interface 物件。
package main

import (
  "fmt"
  "reflect"
)

type user struct {
  Name string
  Age  int
}

func main() {
  var u = user{
    Age:  18,
    Name: "wuqq",
  }
  v := reflect.ValueOf(u)

  var user2 user = v.Interface().(user)
  fmt.Printf("使用者:%+v\n",user2)
}

u 變數轉換成反射物件 v,v 又通過 interface() 介面轉換成 interface 物件,再通過顯性型別轉換成 user 結構體物件,賦值給型別為 user 的變數 user2 。

  • 定律三:反射物件是否可修改,取決於 value 值是否可設定

我們在通過反射將任意型別的變數(不管什麼型別最終傳遞到 reflect.TypeOf 或者 reflect.ValueOf 都會隱式轉換成 interface)轉換成反射物件,那麼理論上我們就可以基於反射物件設定其所持有的值。


type user struct {
  Name string
  Age  int
}

func main() {
  var u = user{
    Age:  18,
    Name: "wuqq",
  }
  v := reflect.ValueOf(u)
  v.FieldByName("Name").SetString("curry")
    fmt.Printf("v的值:%+v",v)
}

上面程式碼我們想的是通過反射物件修改結構體屬性 Name 值為 curry 。當執行這段程式碼時,會報執行恐慌(panic)。

圖片

錯誤的原因正是值是不可修改的。

反射物件是否可修改取決於其所儲存的值,上面 reflect.ValueOf 傳入的其實是 u 的值,而非它的本身。(想想函式傳值的時候是傳值還是傳址),既然是傳值,那麼通過修改 v 的值當然不可能修改到 u 的值,我們要設定的應該是指標所指向的內容,即 *u 。


type user struct {
  Name string
  Age  int
}

func main() {
  var u = user{
    Age:  18,
    Name: "wuqq",
  }
  v := reflect.ValueOf(&u)
  v.Elem().FieldByName("Name").SetString("curry")
  fmt.Printf("v的值:%+v",v)
}

首先我們通過 &u 傳入 u 變數實際儲存的地址。然後通過反射物件中的 Elem() 獲得指標所指向的 value 。

執行結果值已然被修改。

圖片

結尾

關於反射,我還想說說它不好的地方:

  • 作為靜態語言,編碼過程中,編譯器可以提前發現一些型別錯誤,但是反射程式碼是不行的(如果可以請告知)。可能會因為 bug 導致執行恐慌。

  • 反射對效能影響比較大,對於一些注重執行效率的關鍵點,儘量避免使用反射。

還有其他有趣的操作,推薦先多看幾遍官方的一篇部落格:

blog.golang.org/laws-of-reflection

參考資料:

draveness.me/golang/docs/part2-fou...

www.bookstack.cn/read/GoExpertProg...

圖片

本作品採用《CC 協議》,轉載必須註明作者和本文連結
吳親庫裡

相關文章