Golang 接口與反射知識要點

  • 2019 年 10 月 4 日
  • 筆記

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

Golang Interface Memory Layout

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. 反射機制

  • 什麼是反射機制?

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

簡單來說,反射只是一種機制,在程序運行時獲得對象類型信息和內存結構。通常高級語言藉助反射機制來解決,編譯時無法知道變量具體類型,而只有等到運行時才能檢查值和類型的問題。不同語言的反射模型不盡相同,有些語言還不支持反射。對於低級語言,比如彙編語言,由於自身可以直接和內存打交道,所以無需反射機制。

  • 使用反射的場景?

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() 方法返回對應的 reflect.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 的整型,但 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()

我們用 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 = %vn", i,          typeOfT.Field(i).Name, f.Type(), f.Interface())  }

打印結果:

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

Value.Field()

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}

參考文件

The Laws of Reflection

Go Data Structures: Interfaces

Go 語言的數據結構:Interfaces

淺析 Golang Interface 實現原理

深度解密Go語言之反射