1. 前言
個人覺得,反射講得最透徹的還是官方博客。官方博客略顯晦澀,多讀幾遍就慢慢理解了。 本文既是學習筆記,也是總結。 官方博客地址:https://blog.golang.org/laws-of-reflection
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.Reader
interface類型,無論其存儲什麼值。
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)中的typereflect.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. 總結
結合官方博客及本文,至少可以對反射理解個大概,還有很多方法沒有涉及。 對反射的深入理解,個人覺得還需要繼續看的內容:
- 參考業界,尤其是開源框架中是如何使用反射的
- 研究反射實現原理,探究其性能優化的手段