Go 反射機制 頂 原 薦

1. 前言

個人覺得,反射講得最透徹的還是官方博客。官方博客略顯晦澀,多讀幾遍就慢慢理解了。 本文既是學習筆記,也是總結。 官方博客地址:https://blog.golang.org/laws-of-reflection

2. 反射概念

官方對此有個非常簡明的介紹,兩句話耐人尋味:

  1. 反射提供一種讓程序檢查自身結構的能力
  2. 反射是困惑的源泉

第1條,再精確點的描述是“反射是一種檢查interface變量的底層類型和值的機制”。
第2條,很有喜感的自嘲,不過往後看就笑不出來了,因爲你很可能產生困惑.

想深入瞭解反射,必須深入理解類型和接口概念。下面開始複習一下這些基礎概念。

2.1 關於靜態類型

你肯定知道Go是靜態類型語言,比如"int"、"float32"、"[]byte"等等。每個變量都有一個靜態類型,且在編譯時就確定了。
那麼考慮一下如下一種類型聲明:

type Myint int

var i int
var j Myint

Q: i 和j 類型相同嗎?
A:i 和j類型是不同的。 二者擁有不同的靜態類型,沒有類型轉換的話是不可以互相賦值的,儘管二者底層類型是一樣的。

2.2 特殊的靜態類型interface

interface類型是一種特殊的類型,它代表方法集合。 它可以存放任何實現了其方法的值。

經常被拿來舉例的是io包裏的這兩個接口類型:

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

任何類型,比如某struct,只要實現了其中的Read()方法就被認爲是實現了Reader接口,只要實現了Write()方法,就被認爲是實現了Writer接口,不過方法參數和返回值要跟接口聲明的一致。

接口類型的變量可以存儲任何實現該接口的值。

2.3 特殊的interface類型

最特殊的interface類型爲空interface類型,即interface {},前面說了,interface用來表示一組方法集合,所有實現該方法集合的類型都被認爲是實現了該接口。那麼空interface類型的方法集合爲空,也就是說所有類型都可以認爲是實現了該接口。

一個類型實現空interface並不重要,重要的是一個空interface類型變量可以存放所有值,記住是所有值,這纔是最最重要的。 這也是有些人認爲Go是動態類型的原因,這是個錯覺。

2.4 interface類型是如何表示的

前面講了,interface類型的變量可以存放任何實現了該接口的值。還是以上面的io.Reader爲例進行說明,io.Reader是一個接口類型,os.OpenFile()方法返回一個File結構體類型變量,該結構體類型實現了io.Reader的方法,那麼io.Reader類型變量就可以用來接收該返回值。如下所示:

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

那麼問題來了。
Q: r的類型是什麼?
A: r的類型始終是io.Readerinterface類型,無論其存儲什麼值。

Q:那File類型體現在哪裏?
A:r保存了一個(value, type)對來表示其所存儲值的信息。 value即爲r所持有元素的值,type即爲所持有元素的底層類型

Q:如何將r轉換成另一個類型結構體變量?比如轉換成io.Writer
A:使用類型斷言,如w = r.(io.Writer). 意思是如果r所持有的元素如果同樣實現了io.Writer接口,那麼就把值傳遞給w。

3. 反射三定律

前面之所以講類型,是爲了引出interface,之所以講interface是想說interface類型有個(value,type)對,而反射就是檢查interface的這個(value, type)對的。具體一點說就是Go提供一組方法提取interface的value,提供另一組方法提取interface的type.

官方提供了三條定律來說明反射,比較清晰,下面也按照這三定律來總結。

反射包裏有兩個接口類型要先了解一下.

  • reflect.Type 提供一組接口處理interface的類型,即(value, type)中的type
  • reflect.Value提供一組接口處理interface的值,即(value, type)中的value

下面會提到反射對象,所謂反射對象即反射包裏提供的兩種類型的對象。

  • reflect.Type 類型對象
  • reflect.Value類型對象

3.1 反射第一定律:反射可以將interface類型變量轉換成反射對象

下面示例,看看是如何通過反射獲取一個變量的值和類型的:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x float64 = 3.4
	t := reflect.TypeOf(x)  //t is reflext.Type
	fmt.Println("type:", t)

	v := reflect.ValueOf(x) //v is reflext.Value
	fmt.Println("value:", v)
}

程序輸出如下:

type: float64
value: 3.4

注意:反射是針對interface類型變量的,其中TypeOf()ValueOf()接受的參數都是interface{}類型的,也即x值是被轉成了interface傳入的。

除了reflect.TypeOf()reflect.ValueOf(),還有其他很多方法可以操作,本文先不過多介紹,否則一不小心會會引起困惑。

3.2 反射第二定律:反射可以將反射對象還原成interface對象

之所以叫'反射',反射對象與interface對象是可以互相轉化的。看以下例子:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x float64 = 3.4

	v := reflect.ValueOf(x) //v is reflext.Value

	var y float64 = v.Interface().(float64)
	fmt.Println("value:", y)
}

對象x轉換成反射對象v,v又通過Interface()接口轉換成interface對象,interface對象通過.(float64)類型斷言獲取float64類型的值。

3.3 反射第三定律:反射對象可修改,value值必須是可設置的

通過反射可以將interface類型變量轉換成反射對象,可以使用該反射對象設置其持有的值。在介紹何謂反射對象可修改前,先看一下失敗的例子:

package main

import (
	"reflect"
)

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

如下代碼,通過反射對象v設置新值,會出現panic。報錯如下:

panic: reflect: reflect.Value.SetFloat using unaddressable value

錯誤原因即是v是不可修改的。

反射對象是否可修改取決於其所存儲的值,回想一下函數傳參時是傳值還是傳址就不難理解上例中爲何失敗了。

上例中,傳入reflect.ValueOf()函數的其實是x的值,而非x本身。即通過v修改其值是無法影響x的,也即是無效的修改,所以golang會報錯。

想到此處,即可明白,如果構建v時使用x的地址就可實現修改了,但此時v代表的是指針地址,我們要設置的是指針所指向的內容,也即我們想要修改的是*v。 那怎麼通過v修改x的值呢?

reflect.Value提供了Elem()方法,可以獲得指針向指向的value。看如下代碼:

package main

import (
"reflect"
	"fmt"
)

func main() {
	var x float64 = 3.4
	v := reflect.ValueOf(&x)
	v.Elem().SetFloat(7.1)
	fmt.Println("x :", v.Elem().Interface())
}

輸出爲:

x : 7.1

4. 總結

結合官方博客及本文,至少可以對反射理解個大概,還有很多方法沒有涉及。 對反射的深入理解,個人覺得還需要繼續看的內容:

  • 參考業界,尤其是開源框架中是如何使用反射的
  • 研究反射實現原理,探究其性能優化的手段
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章