Go 官方部落格

反射的規律

Rob Pike
2011 年 9 月 6 日

引言

在計算領域,反射是指程式檢查自身結構(特別是透過型別)的能力;它是一種超程式設計形式。反射也是一個很大的困惑來源。

在本文中,我們將透過解釋 Go 中的反射如何工作來澄清問題。每種語言的反射模型都不同(而且許多語言根本不支援反射),但本文是關於 Go 的,因此在本文的其餘部分,“反射”一詞應理解為“Go 中的反射”。

2022 年 1 月補充說明:這篇博文寫於 2011 年,早於 Go 中的引數化多型(也稱為泛型)。雖然語言的這一發展並未導致文章中的重要內容出錯,但在某些地方進行了修改,以避免混淆熟悉現代 Go 的讀者。

型別和介面

由於反射構建於型別系統之上,讓我們先回顧一下 Go 中的型別。

Go 是靜態型別的語言。每個變數都有一個靜態型別,即在編譯時已知且固定的精確型別:int, float32, *MyType, []byte 等等。如果我們宣告

type MyInt int

var i int
var j MyInt

那麼 i 的型別是 intj 的型別是 MyInt。變數 ij 具有不同的靜態型別,儘管它們具有相同的底層型別,但不能在不進行轉換的情況下相互賦值。

一種重要的型別類別是介面型別,它代表固定的方法集合。(在討論反射時,我們可以忽略介面定義在多型程式碼中作為約束的使用。)只要某個具體值實現了介面的方法,一個介面變數就可以儲存該值。一個眾所周知的例子是 io.Readerio.Writer,它們是 io 包中的 ReaderWriter 型別。

// 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)
}

任何實現了具有此簽名的 Read(或 Write)方法的型別,都被認為是實現了 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{}

或其等價別名,

any

它表示空的方法集合,任何值都滿足它,因為每個值都具有零個或多個方法。

有些人說 Go 的介面是動態型別的,但這具有誤導性。它們是靜態型別的:介面型別的變數始終具有相同的靜態型別,即使在執行時儲存在介面變數中的值可能改變型別,該值也始終會滿足介面。

我們需要對此進行精確描述,因為反射和介面密切相關。

介面的表示

Russ Cox 寫了一篇關於 Go 中介面值表示的詳細博文。在此無需重複全部細節,但一個簡化概要是必要的。

介面型別的變數儲存一對值:賦給該變數的具體值,以及該值的型別描述符。更精確地說,該值是實現該介面的底層具體資料項,而型別描述了該項的完整型別。例如,在執行以下程式碼後,

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

r 示意性地包含 (值, 型別) 對:(tty, *os.File)。請注意,*os.File 型別實現了除了 Read 方法之外的其他方法;儘管介面值只提供了對 Read 方法的訪問,但內部的值攜帶了關於該值的所有型別資訊。這就是為什麼我們可以做如下事情:

var w io.Writer
w = r.(io.Writer)

此賦值表示式是一個型別斷言;它斷言 r 內部的項也實現了 io.Writer 介面,因此我們可以將其賦值給 w。賦值後,w 將包含對 (tty, *os.File)。這與 r 中持有的對是相同的。介面的靜態型別決定了可以用介面變數呼叫哪些方法,儘管內部的具體值可能具有更多方法。

繼續,我們可以這樣做:

var empty interface{}
empty = w

並且我們的空介面值 empty 將再次包含相同的對:(tty, *os.File)。這很方便:空介面可以容納任何值,並且包含了我們可能需要知道的關於該值的所有資訊。

(這裡我們不需要型別斷言,因為在靜態上已知 w 滿足空介面。在我們從 Reader 轉移值到 Writer 的例子中,我們需要明確地使用型別斷言,因為 Writer 的方法不是 Reader 方法的子集。)

一個重要細節是,介面變數內部的對總是 (值, 具體型別) 的形式,不能是 (值, 介面型別) 的形式。介面不持有介面值。

現在我們可以開始反射了。

反射第一定律

1. 反射從介面值到反射物件。

在基本層面,反射只是一種機制,用於檢查儲存在介面變數內部的型別和值對。為了開始,我們需要了解 reflect 包中的兩個型別:TypeValue。這兩個型別提供了訪問介面變數內容的能力,而兩個簡單的函式,reflect.TypeOfreflect.ValueOf,可以從介面值中提取 reflect.Typereflect.Value 部分。(此外,從 reflect.Value 很容易獲取相應的 reflect.Type,但現在讓我們暫時將 ValueType 的概念分開。)

讓我們從 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。但它確實存在;正如 godoc 報告的那樣,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 函式,當然,會恢復值(從這裡開始,我們將省略模板程式碼,只關注可執行程式碼):

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

列印:

value: <float64 Value>

(我們顯式地呼叫 String 方法,因為預設情況下 fmt 包會深入 reflect.Value 內部以顯示具體值。String 方法則不會這樣做。)

reflect.Typereflect.Value 都有許多方法,使我們能夠檢查和操作它們。一個重要的例子是 Value 有一個 Type 方法,用於返回 reflect.ValueType。另一個例子是 TypeValue 都有一個 Kind 方法,返回一個常量,表示儲存的是哪種型別的項:UintFloat64Slice 等等。此外,Value 上還有像 IntFloat 這樣的方法,讓我們能夠獲取其中儲存的值(以 int64float64 的形式):

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

還有像 SetIntSetFloat 這樣的方法,但要使用它們,我們需要理解可設定性,這是反射第三定律的主題,將在下面討論。

反射庫有幾個值得單獨指出的特性。首先,為了保持 API 的簡單性,Value 的“獲取”和“設定”方法操作的是能夠容納該值的最大型別:例如,所有有符號整數都使用 int64。也就是說,ValueInt 方法返回一個 int64,而 SetInt 方法接受一個 int64;可能需要將其轉換為實際涉及的型別:

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.

第二個特性是反射物件的 Kind 描述的是底層型別,而不是靜態型別。如果一個反射物件包含使用者自定義整數型別的值,例如:

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

vKind 仍然是 reflect.Int,即使 x 的靜態型別是 MyInt,而不是 int。換句話說,Kind 無法區分 intMyInt,儘管 Type 可以。

反射第二定律

2. 反射從反射物件到介面值。

就像物理反射一樣,Go 中的反射也生成了自己的逆操作。

給定一個 reflect.Value,我們可以使用 Interface 方法恢復一個介面值;實際上,該方法將型別和值資訊重新打包成介面表示並返回結果:

// 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)

來列印反射物件 v 所代表的 float64 值。

然而,我們可以做得更好。fmt.Printlnfmt.Printf 等函式的引數都作為空介面值傳遞,然後 `fmt` 包在內部解包這些空介面,就像我們在前面的例子中所做的那樣。因此,要正確列印 reflect.Value 的內容,只需要將 Interface 方法的結果傳遞給格式化列印例程即可:

fmt.Println(v.Interface())

(自本文首次撰寫以來,fmt 包進行了一項更改,以便像這樣自動解包 reflect.Value,因此我們只需寫:

fmt.Println(v)

也能得到相同的結果,但為了清晰起見,我們在這裡保留 .Interface() 的呼叫。)

由於我們的值是 float64,如果需要,我們甚至可以使用浮點格式:

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

在這種情況下得到:

3.4e+00

再次強調,無需將 v.Interface() 的結果型別斷言為 float64;空介面值內部包含了具體值的型別資訊,Printf 會恢復它。

簡而言之,Interface 方法是 ValueOf 函式的逆操作,不同之處在於其結果始終是靜態型別 interface{}

重申:反射從介面值到反射物件,然後再返回。

反射第三定律

3. 要修改反射物件,該值必須是可設定的。

第三條定律是最微妙和令人困惑的,但如果我們從基本原則開始,就很容易理解。

下面是一段無法工作的程式碼,但值得研究。

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

如果執行這段程式碼,它會因為這條神秘的資訊而 panic:

panic: reflect.Value.SetFloat using unaddressable value

問題不在於值 7.1 不可定址;問題在於 v 不可設定。可設定性是反射 Value 的一個屬性,並非所有反射 Value 都具備此屬性。

ValueCanSet 方法報告 Value 的可設定性;在我們的例子中,

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)

我們是將 x 的一個副本傳遞給 reflect.ValueOf,因此作為 reflect.ValueOf 引數建立的介面值是 x 的副本,而不是 x 本身。因此,如果語句

v.SetFloat(7.1)

被允許成功,它也不會更新 x,儘管 v 看起來像是從 x 建立的。相反,它會更新儲存在反射值內部的 x 的副本,而 x 本身不會受到影響。這將是令人困惑且無用的,因此這是非法的,可設定性就是用來避免這個問題的屬性。

如果這看起來很奇怪,其實不然。這實際上是熟悉情況的不尋常表現形式。想想將 x 傳遞給函式:

f(x)

我們不會期望 f 能夠修改 x,因為我們傳遞的是 x 值的一個副本,而不是 x 本身。如果希望 f 直接修改 x,我們必須將 x 的地址(即 x 的指標)傳遞給函式:

f(&x)

這很簡單且很熟悉,反射的工作方式也一樣。如果我們想透過反射修改 x,就必須給反射庫一個指向我們要修改的值的指標。

讓我們這樣做。首先像往常一樣初始化 x,然後建立一個指向它的反射值,稱為 p

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 指向的內容,我們呼叫 ValueElem 方法,該方法透過指標進行間接訪問,並將結果儲存在一個名為 v 的反射 Value 中:

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

現在 v 是一個可設定的反射物件,正如輸出所示:

settability of v: true

並且由於它代表 x,我們終於可以使用 v.SetFloat 來修改 x 的值:

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

輸出結果如預期:

7.1
7.1

反射可能難以理解,但它所做的正是語言本身所做的事情,儘管是透過可能掩蓋實際情況的反射 TypeValue 來完成的。只需記住,反射 Value 需要某個東西的地址才能修改它們所代表的內容。

結構體

在之前的例子中,v 本身不是一個指標,它只是從一個指標派生出來的。這種情況常見於使用反射修改結構體欄位時。只要我們擁有結構體的地址,就可以修改其欄位。

下面是一個簡單的例子,用於分析結構體值 t。我們使用結構體的地址建立反射物件,因為稍後我們需要修改它。然後我們將 typeOfT 設定為其型別,並使用簡單的方法呼叫遍歷欄位(詳見reflect 包)。請注意,我們從結構體型別中提取欄位名稱,但欄位本身是普通的 reflect.Value 物件。

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 包含一個可設定的反射物件,我們可以修改結構體的欄位。

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 建立的,那麼對 SetIntSetString 的呼叫將失敗,因為 t 的欄位將不可設定。

結論

重申一下反射的定律:

  • 反射從介面值到反射物件。

  • 反射從反射物件到介面值。

  • 要修改反射物件,該值必須是可設定的。

一旦理解了這些定律,Go 中的反射就會變得更容易使用,儘管它仍然微妙。它是一個強大的工具,應謹慎使用,除非絕對必要,否則應避免使用。

反射還有很多我們未涵蓋的內容——在 channel 上傳送和接收、分配記憶體、使用切片和 map、呼叫方法和函式等等——但這篇文章已經夠長了。我們將在後面的文章中介紹其中的一些主題。

下一篇文章:Go image 包
上一篇文章:兩場 Go 講座:“Go 中的詞法掃描”和“Cuddle:一個 App Engine 演示”
部落格索引