Go 反射無敵了,徹底懂了
今天應做的事沒有做,明天再早也是耽誤了。——裴斯泰洛齊
個人覺得,反射講得最透徹的還是官方博客。官方博客略顯晦澀,多讀幾遍就慢慢理解了。本文既是學習筆記,也是總結,如果想看官方文檔,那麼請移步到這裏:點擊這裏
1 反射的概念
-
反射提供一種讓程序檢查自身結構的能力
-
反射是困惑的源泉
第 1 條,再精確點的描述是 “反射是一種檢查 interface 變量的底層類型和值的機制”。第 2 條,很有喜感的自嘲,不過往後看就笑不出來了,因爲你很可能產生困惑.
想深入瞭解反射,必須深入理解類型和接口概念。下面開始複習一下這些基礎概念。
1.1 關於靜態類型
你肯定知道 Go 是靜態類型語言,比如”int”、”float32”、”[]byte” 等等。每個變量都有一個靜態類型,且在編譯時就確定了。那麼考慮一下如下一種類型聲明:
type Myint int
var i int
var j Myint
Q: i 和 j 類型相同嗎?A:i 和 j 類型是不同的。二者擁有不同的靜態類型,沒有類型轉換的話是不可以互相賦值的,儘管二者底層類型是一樣的。
1.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 接口,不過方法參數和返回值要跟接口聲明的一致。
接口類型的變量可以存儲任何實現該接口的值
1.3 特殊的 interface 類型
最特殊的 interface 類型爲空 interface 類型,即 interface {},前面說了,interface 用來表示一組方法集合,所有實現該方法集合的類型都被認爲是實現了該接口。那麼空 interface 類型的方法集合爲空,也就是說所有類型都可以認爲是實現了該接口。
一個類型實現空 interface 並不重要,重要的是一個空 interface 類型變量可以存放所有值,記住是所有值,這纔是最最重要的。這也是有些人認爲 Go 是動態類型的原因,這是個錯覺。
1.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.WriterA:使用類型斷言,如 w = r.(io.Writer). 意思是如果 r 所持有的元素如果同樣實現了 io.Writer 接口, 那麼就把值傳遞給 w。
2 反射三定律
前面之所以講類型,是爲了引出 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 類型對象
2.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(),還有其他很多方法可以操作,本文先不過多介紹,否則一不小心會會引起困惑。
2.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 類型的值。
2.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
反射對象是否可修改取決於其所存儲的值,回想一下函數傳參時是傳值還是傳址就不難理解上例中爲何失敗了。
上例中,傳入 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
3 總結
結合官方博客及本文,至少可以對反射理解個大概,還有很多方法沒有涉及。對反射的深入理解,個人覺得還需要繼續看的內容:
-
參考業界,尤其是開源框架中是如何使用反射的
-
研究反射實現原理,探究其性能優化的手段
4 關注公衆號
微信公衆號:堆棧 future
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/acu01W1Ch48OAjPRiZGKDw