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.

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

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

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

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