go語言反射


反射是眾多編程語言中的一個非常實用的功能,它是一種能夠自描述、自控制的應用,Go語言也對反射提供了友好的支持。

Go語言中使用反射可以在編譯時不知道類型的情況下更新變量,在運行時查看值、調用方法以及直接對他們的布局進行操作。

由於反射是建立在類型系統(type system)上的,所以我們先來復習一下Go語言中的類型。

Go語言中的類型

Go語言是一門靜態類型的語言,每個變量都有一個靜態類型,類型在編譯的時候確定下來。

type MyInt int

var i int
var j MyInt

變量 i 的類型是 int,變量 j 的類型是 MyInt,雖然它們有着相同的基本類型,但靜態類型卻不一樣,在沒有類型轉換的情況下,它們之間無法互相賦值。

接口是一個重要的類型,它意味着一個確定的方法集合,一個接口變量可以存儲任何實現了接口的方法的具體值(除了接口本身),例如 io.Reader 和 io.Writer:

// Reader is the interface that wraps the basic Read method.
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer is the interface that wraps the basic Write method.
type Writer interface {
    Write(p []byte) (n int, err error)
}

如果一個類型聲明實現了 Reader(或 Writer)方法,那么它便實現了 io.Reader(或 io.Writer),這意味着一個 io.Reader 的變量可以持有任何一個實現了 Read 方法的的類型的值。

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

必須要弄清楚的一點是,不管變量 r 中的具體值是什么,r 的類型永遠是 io.Reader,由於Go語言是靜態類型的,r 的靜態類型就是 io.Reader。

在接口類型中有一個極為重要的例子——空接口:interface{}

它表示了一個空的方法集,一切值都可以滿足它,因為它們都有零值或方法。

有人說Go語言的接口是動態類型,這是錯誤的,它們都是靜態類型,雖然在運行時中,接口變量存儲的值也許會變,但接口變量的類型是不會變的。我們必須精確地了解這些,因為反射與接口是密切相關的。

關於接口我們就介紹到這里,下面我們看看Go語言的反射三定律。

反射第一定律:反射可以將“接口類型變量”轉換為“反射類型對象”

注:這里反射類型對象指 reflect.Type 和 reflect.Value。

從使用方法上來講,反射提供了一種機制,允許程序在運行時檢查接口變量內部存儲的 (value, type) 對。

在最開始,我們先了解下 reflect 包的兩種類型 Type 和 Value,這兩種類型使訪問接口內的數據成為可能,它們對應兩個簡單的方法,分別是 reflect.TypeOf 和 reflect.ValueOf,分別用來讀取接口變量的 reflect.Type 和 reflect.Value 部分。

當然,從 reflect.Value 也很容易獲取到 reflect.Type,目前我們先將它們分開。

首先,我們下看 reflect.TypeOf:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

運行結果如下:

type: float64

大家可能會疑惑,為什么沒看到接口?這段代碼看起來只是把一個 float64 類型的變量 x 傳遞給 reflect.TypeOf 並沒有傳遞接口。其實在 reflect.TypeOf 的函數簽名里包含一個空接口:

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

我們調用 reflect.TypeOf(x) 時,x 被存儲在一個空接口變量中被傳遞過去,然后 reflect.TypeOf 對空接口變量進行拆解,恢復其類型信息。

函數 reflect.ValueOf 也會對底層的值進行恢復:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    fmt.Println("value:", reflect.ValueOf(x))
}

運行結果如下:

value: 3.4

類型 reflect.Type 和 reflect.Value 都有很多方法,我們可以檢查和使用它們,這里我們舉幾個例子。

類型 reflect.Value 有一個方法 Type(),它會返回一個 reflect.Type 類型的對象。

Type 和 Value 都有一個名為 Kind 的方法,它會返回一個常量,表示底層數據的類型,常見值有:Uint、Float64、Slice 等。

Value 類型也有一些類似於 Int、Float 的方法,用來提取底層的數據:

  • Int 方法用來提取 int64
  • Float 方法用來提取 float64,示例代碼如下:
package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    v := reflect.ValueOf(x)
    fmt.Println("type:", v.Type())
    fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
    fmt.Println("value:", v.Float())
}

運行結果如下:

type: float64
kind is float64: true
value: 3.4

還有一些用來修改數據的方法,比如 SetInt、SetFloat。在介紹它們之前,我們要先理解“可修改性”(settability),這一特性會在下面進行詳細說明。

反射庫提供了很多值得列出來單獨討論的屬性,下面就來介紹一下。

首先是介紹下 Value 的 getter 和 setter 方法,為了保證 API 的精簡,這兩個方法操作的是某一組類型范圍最大的那個。比如,處理任何含符號整型數,都使用 int64,也就是說 Value 類型的 Int 方法返回值為 int64 類型,SetInt 方法接收的參數類型也是 int64 類型。實際使用時,可能需要轉化為實際的類型:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x uint8 = 'x'
    v := reflect.ValueOf(x)
    fmt.Println("type:", v.Type())                            // uint8.
    fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
    x = uint8(v.Uint())                                       // v.Uint returns a uint64.
}

運行結果如下:

type: uint8
kind is uint8: true

其次,反射對象的 Kind 方法描述的是基礎類型,而不是靜態類型。如果一個反射對象包含了用戶定義類型的值,如下所示:

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

上面的代碼中,雖然變量 v 的靜態類型是 MyInt,而不是 int,但 Kind 方法仍然會返回 reflect.Int。換句話說 Kind 方法不會像 Type 方法一樣區分 MyInt 和 int。

反射第二定律:反射可以將“反射類型對象”轉換為“接口類型變量”

和物理學中的反射類似,Go語言中的反射也能創造自己反面類型的對象。

根據一個 reflect.Value 類型的變量,我們可以使用 Interface 方法恢復其接口類型的值。事實上,這個方法會把 type 和 value 信息打包並填充到一個接口變量中,然后返回。

其函數聲明如下:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

然后,我們可以通過斷言,恢復底層的具體值:

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

上面這段代碼會打印出一個 float64 類型的值,也就是反射類型變量 v 所代表的值。

事實上,我們可以更好地利用這一特性,標准庫中的 fmt.Println 和 fmt.Printf 等函數都接收空接口變量作為參數,fmt 包內部會對接口變量進行拆包,因此 fmt 包的打印函數在打印 reflect.Value 類型變量的數據時,只需要把 Interface 方法的結果傳給格式化打印程序:

fmt.Println(v.Interface())

為什么不直接使用 fmt.Println(v)?因為 v 的類型是 reflect.Value,我們需要的是它的具體值,由於值的類型是 float64,我們也可以用浮點格式化打印它:

fmt.Printf("value is %7.1e\n", v.Interface())

運行結果如下:

3.4e+00

同樣,這次也不需要對 v.Interface() 的結果進行類型斷言,空接口值內部包含了具體值的類型信息,Printf 函數會恢復類型信息。

簡單來說 Interface 方法和 ValueOf 函數作用恰好相反,唯一一點是,返回值的靜態類型是 interface{}。

Go的反射機制可以將“接口類型的變量”轉換為“反射類型的對象”,然后再將“反射類型對象”轉換過去。

反射第三定律:如果要修改“反射類型對象”其值必須是“可寫的”

這條定律很微妙,也很容易讓人迷惑,但是如果從第一條定律開始看,應該比較容易理解。

下面這段代碼雖然不能正常工作,但是非常值得研究:

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic

如果運行這段代碼,它會拋出一個奇怪的異常:

panic: reflect: reflect.flag.mustBeAssignable using unaddressable value

這里問題不在於值7.1 不能被尋址,而是因為變量 v 是“不可寫的”,“可寫性”是反射類型變量的一個屬性,但不是所有的反射類型變量都擁有這個屬性。

我們可以通過 CanSet 方法檢查一個 reflect.Value 類型變量的“可寫性”,對於上面的例子,可以這樣寫:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    v := reflect.ValueOf(x)
    fmt.Println("settability of v:", v.CanSet())
}

運行結果如下:

settability of v: false

對於一個不具有“可寫性”的 Value 類型變量,調用 Set 方法會報出錯誤。

首先我們要弄清楚什么是“可寫性”,“可寫性”有些類似於尋址能力,但是更嚴格,它是反射類型變量的一種屬性,賦予該變量修改底層存儲數據的能力。“可寫性”最終是由一個反射對象是否存儲了原始值而決定的。

示例代碼如下:

var x float64 = 3.4
v := reflect.ValueOf(x)

這里我們傳遞給 reflect.ValueOf 函數的是變量 x 的一個拷貝,而非 x 本身,想象一下如果下面這行代碼能夠成功執行:

v.SetFloat(7.1)

如果這行代碼能夠成功執行,它不會更新 x,雖然看起來變量 v 是根據 x 創建的,相反它會更新 x 存在於反射對象 v 內部的一個拷貝,而變量 x 本身完全不受影響。這會造成迷惑,並且沒有任何意義,所以是不合法的。“可寫性”就是為了避免這個問題而設計的。

這看起來很詭異,事實上並非如此,而且類似的情況很常見。考慮下面這行代碼:

f(x)

代碼中,我們把變量 x 的一個拷貝傳遞給函數,因此不期望它會改變 x 的值。如果期望函數 f 能夠修改變量 x,我們必須傳遞 x 的地址(即指向 x 的指針)給函數 f,如下所示:

f(&x)

反射的工作機制與此相同,如果想通過反射修改變量 x,就要把想要修改的變量的指針傳遞給反射庫。

首先,像通常一樣初始化變量 x,然后創建一個指向它的反射對象,命名為 p:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    p := reflect.ValueOf(&x) // Note: take the address of x.
    fmt.Println("type of p:", p.Type())
    fmt.Println("settability of p:", p.CanSet())
}

運行結果如下:

type of p: *float64
settability of p: false

反射對象 p 是不可寫的,但是我們也不像修改 p,事實上我們要修改的是 *p。為了得到 p 指向的數據,可以調用 Value 類型的 Elem 方法。Elem 方法能夠對指針進行“解引用”,然后將結果存儲到反射 Value 類型對象 v 中:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    p := reflect.ValueOf(&x) // Note: take the address of x.
    v := p.Elem()
    fmt.Println("settability of v:", v.CanSet())

運行結果如下:

settability of v: true

由於變量 v 代表 x, 因此我們可以使用 v.SetFloat 修改 x 的值:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    p := reflect.ValueOf(&x) // Note: take the address of x.
    v := p.Elem()
    v.SetFloat(7.1)
    fmt.Println(v.Interface())
    fmt.Println(x)
}

運行結果如下:

7.1
7.1

反射不太容易理解,reflect.Type 和 reflect.Value 會混淆正在執行的程序,但是它做的事情正是編程語言做的事情。只需要記住:只要反射對象要修改它們表示的對象,就必須獲取它們表示的對象的地址。

結構體

我們一般使用反射修改結構體的字段,只要有結構體的指針,我們就可以修改它的字段。

下面是一個解析結構體變量 t 的例子,用結構體的地址創建反射變量,再修改它。然后我們對它的類型設置了 typeOfT,並用調用簡單的方法迭代字段。

需要注意的是,我們從結構體的類型中提取了字段的名字,但每個字段本身是正常的 reflect.Value 對象。

package main
import (
    "fmt"
    "reflect"
)
func main() {
    type T struct {
        A int
        B string
    }
    t := T{23, "skidoo"}
    s := reflect.ValueOf(&t).Elem()
    typeOfT := s.Type()
    for i := 0; i < s.NumField(); i++ {
        f := s.Field(i)
        fmt.Printf("%d: %s %s = %v\n", i,
            typeOfT.Field(i).Name, f.Type(), f.Interface())
    }
}

運行結果如下:

0: A int = 23
1: B string = skidoo

T 字段名之所以大寫,是因為結構體中只有可導出的字段是“可設置”的。

因為 s 包含了一個可設置的反射對象,我們可以修改結構體字段:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    type T struct {
        A int
        B string
    }
    t := T{23, "skidoo"}
    s := reflect.ValueOf(&t).Elem()
    s.Field(0).SetInt(77)
    s.Field(1).SetString("Sunset Strip")
    fmt.Println("t is now", t)
}

運行結果如下:

t is now {77 Sunset Strip}

如果我們修改了程序讓 s 由 t(而不是 &t)創建,程序就會在調用 SetInt 和 SetString 的地方失敗,因為 t 的字段是不可設置的。

總結

反射規則可以總結為如下幾條:

  • 反射可以將“接口類型變量”轉換為“反射類型對象”;
  • 反射可以將“反射類型對象”轉換為“接口類型變量”;
  • 如果要修改“反射類型對象”,其值必須是“可寫的”。

關於反射的golang官方博客文章:https://blog.golang.org/laws-of-reflection


免責聲明!

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



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