1. 程式人生 > >Golang 介面與反射知識要點

Golang 介面與反射知識要點

目錄

  • Golang 介面與反射知識要點
    • 1. 介面型別變數
    • 2. 型別斷言
    • 3. 鴨子型別
    • 4. 反射機制
    • 5. reflect 包
      • TypeOf()、ValueOf()
      • Type()、Kind()
      • Interface()
    • 6. 反射物件的可設定性
      • SetXXX(), CanSet()
      • Elem()
    • 7. Struct 的反射
      • NumField(), Type.Field(i int)
      • Value.Field(i int)
    • 參考文件

Golang 介面與反射知識要點

這篇文章以 Go 官方經典部落格 The Laws of Reflection 為基礎,詳細介紹文中涉及的知識點,並有所擴充套件。

1. 介面型別變數

首先,我們談談介面型別的記憶體佈局(memory layout),其他基礎型別、Struct、Slice、Map、指標型別的記憶體佈局會在以後單獨分析。介面變數的值包含兩部分內容:賦值給介面型別變數的實際值(concrete value),實際值的型別資訊(type descriptor)。兩部分在一起構成介面的值(interface value)。

介面變數的這兩部分內容由兩個字來儲存(假設是 32 位系統,那麼一個字就是 32 位),第一個字指向 itable (interface table)。itable 表示 interface 和實際型別的轉換資訊。itable 開頭是一個儲存了變數實際型別的描述資訊,接著是一個由函式指標組成的列表。注意 itable 中的函式和介面型別相對應,而不是和動態型別。例如下面例子,itable 只關聯了 Stringer 中定義的 String 方法,而 Binary 中定義的 Get 方法則不在其中。對於每個 interface 和實際型別,只要在程式碼中存在引用關係, go 就會在執行時為這一對具體的 <Interface, Type> 生成 itable 資訊。

第二個字稱為 data,指向實際的資料。例子中,賦值語句 var s Stringer = b 實際上對b做了拷貝,而不是對b進行引用。存放在介面變數中的資料大小可能任意,但介面只提供了一個字來專門儲存真實資料,所以賦值語句在堆上分配了一塊記憶體,並將該字設定為對這塊記憶體的引用。

type Stringer interface {
    String() string
}

type Binary uint64

func (i Binary) String() string {
    return strconv.Uitob64(i.Get(), 2)
}

func (i Binary) Get() uint64 {
    return uint64(i)
}

b := Binary(200)
var s Stringer = b

Go 是靜態型別語言(statically typed)。一個介面型別的不同變數總是有同樣靜態型別,儘管在執行時,介面變數的儲存的實際值會改變。下面例子中,無論 r 被賦予的什麼實際值,r 的型別總是 io.Reader。

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

2. 型別斷言

型別斷言是一個使用在介面變數上的操作。

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

在這個例子中,r 被賦予了 tty 的一個拷貝,所以實際值是 tty。而實際型別是 *os.File。需要注意到,*os.File 型別自身還實現了除介面方法 Read 以外的方法。儘管介面變數只能訪問 Read 方法,但介面的 data 字部分裡攜帶了實際值的全部資訊。因此我們可以有如下操作:

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

該賦值語句後邊是一個型別斷言。它斷言的是 r 變數攜帶的元素,同時是 io.Writer 介面的實現,所以我們才能把 r 賦值給 w。賦值後的 w 可以訪問 Write 方法,但無法訪問 Read 方法了。

3. 鴨子型別

鴨子型別(duck typing)是動態型別和某些靜態語言用到的一種物件推斷風格。一個物件有效的語義,不是由繼承自特定的類或實現特定的介面,而是由"當前方法和屬性的集合"決定。這個概念也可以表述為:

當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。

鴨子型別像多型一樣工作,但是沒有繼承。在鴨子型別中,關注點在於物件的行為,能作什麼;而不是關注物件所屬的型別。在常規型別中,我們能否在一個特定場景中使用某個物件取決於這個物件的型別,而在鴨子型別中,則取決於這個物件是否具有某種屬性或者方法 —— 即只要具備特定的屬性或方法,能通過鴨子型別測試,就可以使用。鴨子型別的缺點是沒有任何靜態檢查,如型別檢查、屬性檢查、方法簽名檢查等。

Go 語言雖然是靜態語言,但在介面型別中使用了鴨子型別。不同於其他鴨子型別語言的是,它實現了在編譯時進行靜態檢查,比如變數是否實現介面方法、呼叫介面方法時引數個數是否相符,同時也不失鴨子型別帶來的靈活和自由。

4. 反射機制

  • 什麼是反射機制?

在電腦科學中,反射是指計算機程式在執行時(Run time)可以訪問、檢測和修改它本身狀態或行為的一種能力。用比喻來說,反射就是程式在執行的時候能夠“觀察”並且修改自己的行為。

簡單來說,反射只是一種機制,在程式執行時獲得物件型別資訊和記憶體結構。通常高階語言藉助反射機制來解決,編譯時無法知道變數具體型別,而只有等到執行時才能檢查值和型別的問題。不同語言的反射模型不盡相同,有些語言還不支援反射。對於低階語言,比如組合語言,由於自身可以直接和記憶體打交道,所以無需反射機制。

  • 使用反射的場景?

Go 語言中使用反射的場景:有時候需要根據某些條件決定呼叫哪個函式,比如根據使用者的輸入來決定,但事先無法不知道接受到的引數是什麼型別,全部以 interface{} 型別接受。這時就需要對函式的引數進行反射,在執行期間動態地執行函式。感興趣的讀者可以參考 fmt.Sprint(a ...interface{}) 方法的原始碼。

5. reflect 包

TypeOf()、ValueOf()

reflect 包封裝了很多簡單的方法(reflect.TypeOf 和 reflect.ValueOf)來動態獲得型別資訊和實際值(reflect.Type,reflect.Value)。

var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))  // 列印 type: float64

var r io.Reader = strings.NewReader("Hello")
fmt.Println("type:", reflect.TypeOf(r))  // 列印 type: *strings.Reader

reflect.TypeOf 方法的函式簽名是 func TypeOf(i interface{}) Type 。它接受任意型別的變數。當我們呼叫 reflect.TypeOf(x) 時,x 首先儲存在一個空介面型別中,作為傳參。reflect.TypeOf 解析空介面,恢復 x 的型別資訊。而呼叫 reflect.ValueOf 則可以恢復 x 實際值。

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String()) // 列印 value: <float64 Value>

Type()、Kind()

reflect.Type 和 reflect.Value 都提供了很多方法支援來操作他們。1. reflect.Value 的 Type() 方法返回實際型別資訊;2. reflect.Type 和 reflect.Value 都有 Kind() 方法,來獲得實際值的底層型別,結果對應的是 reflect 包中定義的常量;3. reflect.Value 的那些以型別名為方法名的方法,比如 Int()、Float(),能獲得實際值。

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

列印結果:

shell script type: float64 kind is float64: true value: 3.4

有一點需要注意的是,Kind() 方法返回的是反射物件的底層型別,而不是靜態型別。比如,如果反射物件接受一個使用者定義的整數型變數:

func main() {
    type MyInt int
    var x MyInt = 7
    v := reflect.ValueOf(x)
    fmt.Println("type:", v.Type())
    fmt.Println("kind is int:", v.Kind() == reflect.Int)
    fmt.Println("value:", v.Int())
}

列印結果:
shell script type: main.MyInt kind is int: true value: 7

v 呼叫 Kind() 仍是 reflect.Int,即使 x 的靜態型別是 MyInt 而不是 int。總而言之,Kind() 方法無法區分來自 MyInt 的整數型和 int 型,但 Type() 方法可以。

Interface()

Interface() 方法能從 reflect.Value 變數中恢復介面值,是 ValueOf() 的逆向。注意的是,Interface() 方法返回總是靜態型別 interface{}。

6. 反射物件的可設定性

SetXXX(), CanSet()

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)  // will panic: reflect.Value.SetFloat using unaddressable value

執行上面的例子,我們可以發現 v 不可修改(settable)。可設定性(Settability)是 reflect.Value 的一個特性,但不是所有的 Value 都有。

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())  // settability of v: false

Elem()

由於是 x 的一個拷貝傳入 reflect.ValueOf,所以 reflect.ValueOf 建立的介面值也是 x 的一個拷貝,不是原 x 本身。因此修改反射物件,無法修改 x,反射物件不具有可設定性。

顯然,要使反射物件具有可設定性。傳入 reflect.ValueOf 的引數應該是 x 的地址,即 &x。

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())  // type of p: *float64
fmt.Println("settability of p:", p.CanSet())  // settability of p: false

反射物件 p 仍是不可設定的,因為我們不是要設定 p,而是 p 所指向的內容。使用 Elem 方法獲取。

// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
func (v Value) Elem() Value
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())  // settability of v: true

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

7. Struct 的反射

NumField(), Type.Field(i int)

我們用 struct 的地址來建立反射物件,這樣後續我們可以修改這個 struct:

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

Type.Field(i int) 方法返回欄位資訊,一個 StructField 型別的物件,包含欄位名等。

列印結果:

0: A int = 23
1: B string = skidoo

Value.Field(i int)

T 的欄位必須是首字母大寫的才可以設定,因為只有暴露的 struct 欄位,才具有可設定性。

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t) // t is now {77 Sunset Strip}

Value.Field(i int) 返回 struct s 的欄位實際值,所以可以用來設定操作。注意 Type.Field(i int) 和 Value.Field(i int) 的用途區別:前者總是負責和實際型別資訊獲取相關的操作,後者是與實際值相關的操作。

參考文件

The Laws of Reflection

Go Data Structures: Interfaces

Go 語言的資料結構:Interfaces

淺析 Golang Interface 實現原理

深度解密Go語言之