深入理解Golang之interface和reflect

Turling_hu發表於2019-10-13

前言

interface(即介面),是Go語言中一個重要的概念和知識點,而功能強大的reflect正是基於interface。本文即是對Go語言中的interfacereflect相關知識較為全面的梳理,也算是我階段學習的總結,以期溫故而知新。文章較長,請讀者做好心理準備。

interface(介面)

定義

在Go語言中,如果自定義型別(比如struct)實現了某個interface中的所有方法,那麼就可以說這個型別實現了這個介面。介面可如下定義:

type 介面名稱 interface {
    method1(引數列表) 返回值列表
    method1(引數列表) 返回值列表
    ...
}
複製程式碼

interface是一組方法的集合,但並不需要實現這些方法,並且interface沒有變數interface中的方法集合可以表示一個物件的特徵和能力,當自定義型別需要使用這些方法時,可以根據需要把這些方法實現出來。舉個例子:

package main

import (
	"fmt"
)

type Animal interface {
    Eat()
    Run()
}

type Dog struct {
    Name string
}

type Cat struct {
    Name string
}

func (dog *Dog) Eat() {
    fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
    fmt.Printf("%s is running.", dog.Name)
}

func (cat *Cat) Eat() {
    fmt.Printf("%s is eating.", cat.Name)
}

func (cat *Cat) Run() {
    fmt.Printf("%s is running.", cat.Name)
}

func main() {
	var animal1 Animal
	animal1 = &Dog{"doggy"}
	animal1.Eat()
	animal1.Run()

	var animal2 Animal
	animal2 = &Cat{"catty"}
	animal2.Eat()
	animal2.Run()
}
複製程式碼

上面即定義了一個Animal介面,以及Dog型別和Cat型別。Dog型別和Cat型別都實現了Animal介面中的方法,所以Dog和Cat都是Animal型別。 同時介面本身不能建立例項,但從上例可以看出,介面型別的變數可以指向一個實現了該介面的自定義型別的例項。interface型別預設是一個指標(引用型別),如果沒有對interface初始化就使用,那麼會輸出nil

空介面

空介面interface{}沒有任何方法,所以所有型別都實現了空介面, 即我們可以把任何一個變數賦值給空介面。修改一下上面的main函式:

func main() {
    var animal interface{}
	dog := &Dog{"doggy"}
	animal = dog
	fmt.Println(animal)
}
複製程式碼

執行結果:

&{doggy}
複製程式碼

介面繼承

一個介面可以繼承多個其他介面,如果要實現這個介面,那麼必須將所繼承的所有介面中的方法都實現。

package main

import (
	"fmt"
)

type Eater interface {
	Eat()
}

type Runner interface {
	Run()
}

type Animal interface {
	Eater
	Runner
}

// 這裡定義一個Dog的struct,並實現eat方法和run方法,這樣就實現了動物的介面
type Dog struct {
	Name string
}

func (dog *Dog) Eat() {
	fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
	fmt.Printf("%s is running.", dog.Name)
}

func main() {
	var animal1 Animal
	animal1 = &Dog{"doggy"}
	animal1.Eat()
	animal1.Run()
}

複製程式碼

型別斷言

當我們不確定某個介面變數裡儲存的是什麼型別的變數時,我們可以利用型別斷言來判斷變數型別。

var animal1 Animal
animal1 = &Dog{"doggy"}
dog := animal1.(*Dog)
複製程式碼

在進行型別斷言時,如果型別不匹配,就會報panic, 因此需要加上檢測機制,如果成功就 ok,否則也不要報 panic

var animal1 Animal
animal1 = &Dog{"doggy"}

if dog, ok := animal1.(*Dog); ok {
	fmt.Println("convert success")
    dog.Run()
} else {
	fmt.Println("convert fail")
}
複製程式碼

另外我們也可以使用switch-type語法進行型別斷言:

package main

import (
	"fmt"
)

type Eater interface {
	Eat()
}

type Runner interface {
	Run()
}

type Animal interface {
	Eater
	Runner
}

type Dog struct {
	Name string
}

type Cat struct {
	Name string
}

func (dog *Dog) Eat() {
	fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
	fmt.Printf("%s is running.", dog.Name)
}

func (cat *Cat) Eat() {
	fmt.Printf("%s is eating.", cat.Name)
}

func (cat *Cat) Run() {
	fmt.Printf("%s is running.", cat.Name)
}

func TypeJudge(animals ...interface{}) {
	for index, animal := range animals {
		switch animal.(type) {
		case *Dog:
			fmt.Printf("第%d個引數是Dog型別\n", index)
		case *Cat:
			fmt.Printf("第%d個引數是Cat型別\n", index)
		default:
			fmt.Println("不確定型別")
		}
	}
}

func main() {
	var animal1 Animal
	animal1 = &Dog{"doggy"}

	var animal2 Animal
	animal2 = &Cat{"catty"}

	TypeJudge(animal1, animal2)
}
複製程式碼

作用

interface對於Go語言的意義在於其實現了泛型,比如在一個函式中需要能接收不同型別的引數或者返回不同型別的值,而不是一開始就指定引數或者返回值的型別,這樣就可以讓函式支援所有型別:

func FuncName(arg1 interface{}, rest ...interface{}) interface{} {
    // ...
}
複製程式碼

面嚮物件語言比如C++、Java都有多型的特性,可以說interface是Go語言中實現多型的一種形式。同一個interface,可以讓不同的類(自定義型別)實現,從而可以呼叫同一個函式名的函式但實現完全不同的功能。

有時我們能夠利用interface實現非常巧妙的功能:通常我們定義一個切片(slice)都會指定一個具體的型別,但是我們有時需要切片中的元素可以任何型別的變數,這個時候interface就派上用場了。下面是在go程式碼中update資料庫表中資料時,利用interface實現的騷操作,讀者可以體會一下interface帶來的便利:

func generateSQLForUpdatingArticle(article model.ArticleStruct) (string, []interface{}) {
	var columns = make([]string, 0)
	var arguments = make([]interface{}, 0)

	if len(article.CommentCount) > 0 {
		columns = append(columns, "comment_count = ?")
		arguments = append(arguments, article.CommentCount)
	}

	if len(article.Source) > 0 {
		columns = append(columns, "source = ?")
		arguments = append(arguments, article.Source)
	}

	if len(article.Summary) > 0 {
		columns = append(columns, "summary = ?")
		arguments = append(arguments, article.Summary)
	}

	if len(article.Content) > 0 {
		columns = append(columns, "content = ?")
		arguments = append(arguments, article.Content)
	}

	sql := fmt.Sprintf("UPDATE article_structs SET %s WHERE sid = %s", strings.Join(columns, ","), article.Sid)
	return sql, arguments
}

func UpdateArticle(article model.ArticleStruct) error {
	sql, arguments := generateSQLForUpdatingArticle(article)
	if err := db.Exec(sql, arguments...).Error; err != nil {
		log.Println("Updating article failed with error:", err)
		return err
	}
	return nil
}
複製程式碼

然而,空介面interface{} 雖然能儲存任意的值,但也帶來了一個問題:一個空的介面會隱藏值對應的表示方式和所有的公開的方法,因此只有我們知道具體的動態型別才能使用型別斷言來訪問內部的值, 對於內部值並沒有特別可做的事情;如果我們事先不知道空介面指向的值的具體型別,我們可能就束手無策了。

這個時候我們想要知道一個介面型別的變數具體是什麼(什麼型別),有什麼能力(有哪些方法),就需要一面“鏡子”能夠反射(reflect)出這個變數的具體內容。在Go語言中也正好有這樣的工具——reflect

reflect(反射)

概念

在電腦科學領域,反射是指一類應用,它們能夠自描述和自控制。也就是說,這類應用通過採用某種機制來實現對自己行為的描述(self-representation)和監測(examination),並能根據自身行為的狀態和結果,調整或修改應用所描述行為的狀態和相關的語義。

支援反射的語言可以在程式編譯期將變數的反射資訊,如欄位名稱、型別資訊、結構體資訊等整合到可執行檔案中,並給程式提供介面訪問反射資訊,這樣就可以在程式執行期獲取型別的反射資訊,並且有能力修改它們。

在講反射之前,我們需要了解一下Golang關於型別設計的一些原則:

變數包含兩部分:type(型別)和value(值)。

type 分為 static typeconcrete type。其中static type是我們在編碼階段用到的資料型別,如int、string、bool等等;而concrete type則是runtime系統看見的型別。

介面型別的變數在型別斷言時能否成功,取決於concrete type 而不是 static type

在Go語言中指定型別的變數的型別都是靜態的,即static type,其在建立變數的時候就已經確定;而反射主要是配合interface型別變數來使用的,這些變數的型別都是concrete type

在Go的實現中,每個interface型別的變數都有一個對應的pair, pair中記錄了實際變數的valuetype

(value, type)
複製程式碼

interface型別變數包含了兩個指標,分別指向實際變數的值(value)和型別(對應concrete type)。interface及其pair的存在,是Golang實現反射的前提,而反射也正是用來檢測介面型別變數內部儲存的值和型別的一種機制。說到這裡,自然也就要引出reflect包中的兩個資料類TypeValue

reflect.Type和reflect.Value

reflect.Type

reflect包中Type介面定義如下:

type Type interface {
    // Kind返回該介面的具體分類
    Kind() Kind
    // Name返回該型別在自身包內的型別名,如果是未命名型別會返回""
    Name() string
    // PkgPath返回型別的包路徑,即明確指定包的import路徑,如"encoding/base64"
    // 如果型別為內建型別(string, error)或未命名型別(*T, struct{}, []int),會返回""
    PkgPath() string
    // 返回型別的字串表示。該字串可能會使用短包名(如用base64代替"encoding/base64")
    // 也不保證每個型別的字串表示不同。如果要比較兩個型別是否相等,請直接用Type型別比較。
    String() string
    // 返回要儲存一個該型別的值需要多少位元組;類似unsafe.Sizeof
    Size() uintptr
    // 返回當從記憶體中申請一個該型別值時,會對齊的位元組數
    Align() int
    // 返回當該型別作為結構體的欄位時,會對齊的位元組數
    FieldAlign() int
    // 如果該型別實現了u代表的介面,會返回真
    Implements(u Type) bool
    // 如果該型別的值可以直接賦值給u代表的型別,返回真
    AssignableTo(u Type) bool
    // 如該型別的值可以轉換為u代表的型別,返回真
    ConvertibleTo(u Type) bool
    // 返回該型別的字位數。如果該型別的Kind不是Int、Uint、Float或Complex,會panic
    Bits() int
    // 返回array型別的長度,如非陣列型別將panic
    Len() int
    // 返回該型別的元素型別,如果該型別的Kind不是Array、Chan、Map、Ptr或Slice,會panic
    Elem() Type
    // 返回map型別的鍵的型別。如非對映型別將panic
    Key() Type
    // 返回一個channel型別的方向,如非通道型別將會panic
    ChanDir() ChanDir

    // 返回struct型別的欄位數(匿名欄位算作一個欄位),如非結構體型別將panic
    NumField() int
    // 返回struct型別的第i個欄位的型別,如非結構體或者i不在[0, NumField())內將會panic
    Field(i int) StructField
    // 返回索引序列指定的巢狀欄位的型別,
    // 等價於用索引中每個值鏈式呼叫本方法,如非結構體將會panic
    FieldByIndex(index []int) StructField
    // 返回該型別名為name的欄位(會查詢匿名欄位及其子欄位),
    // 布林值說明是否找到,如非結構體將panic
    FieldByName(name string) (StructField, bool)
    // 返回該型別第一個欄位名滿足函式match的欄位,布林值說明是否找到,如非結構體將會panic
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    // 如果函式型別的最後一個輸入引數是"..."形式的引數,IsVariadic返回真
    // 如果這樣,t.In(t.NumIn() - 1)返回引數的隱式的實際型別(宣告型別的切片)
    // 如非函式型別將panic
    IsVariadic() bool
    // 返回func型別的引數個數,如果不是函式,將會panic
    NumIn() int
    // 返回func型別的第i個引數的型別,如非函式或者i不在[0, NumIn())內將會panic
    In(i int) Type
    // 返回func型別的返回值個數,如果不是函式,將會panic
    NumOut() int
    // 返回func型別的第i個返回值的型別,如非函式或者i不在[0, NumOut())內將會panic
    Out(i int) Type
    // 返回該型別的方法集中方法的數目
    // 匿名欄位的方法會被計算;主體型別的方法會遮蔽匿名欄位的同名方法;
    // 匿名欄位導致的歧義方法會濾除
    NumMethod() int
    // 返回該型別方法集中的第i個方法,i不在[0, NumMethod())範圍內時,將導致panic
    // 對非介面型別T或*T,返回值的Type欄位和Func欄位描述方法的未繫結函式狀態
    // 對介面型別,返回值的Type欄位描述方法的簽名,Func欄位為nil
    Method(int) Method
    // 根據方法名返回該型別方法集中的方法,使用一個布林值說明是否發現該方法
    // 對非介面型別T或*T,返回值的Type欄位和Func欄位描述方法的未繫結函式狀態
    // 對介面型別,返回值的Type欄位描述方法的簽名,Func欄位為nil
    MethodByName(string) (Method, bool)
    // 內含隱藏或非匯出方法
}
複製程式碼

我們可以通過reflect.TypeOf接受任意interface{}型別,並返回對應的動態型別reflect.Type

num := reflect.TypeOf(1)
fmt.Println(num.String())
fmt.Println(num)
複製程式碼

看一下TypeOf()的實現程式碼:

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}
複製程式碼

可以發現TypeOf函式的引數型別是一個interface{},並且在函式內部將這裡的具體值1進行一個隱式轉換,轉換為一個空介面型別的變數,這個變數包含兩部分資訊:1這個變數的動態型別(為int)和動態值(為1);最後TypeOf的返回值是reflect.Type型別(我們稱為反射型別物件),這樣就能夠呼叫上面Type介面的方法獲取所需的變數資訊。

  • 當反射物件的型別是原始資料型別時:
func main() {
    var s string
    rString := reflect.TypeOf(s)
    fmt.Println(rString)         //string
    fmt.Println(rString.Name())  //string,返回表示型別名稱的字串
    fmt.Println(rString.Kind())  //string,返回 reflect.Kind 型別的常量
}
複製程式碼
  • 當反射物件的型別是指標型別時:
type Dog struct {
	Name string
	Age  int
}

func main() {
    dogPtr := &Dog{"doggy"}
    rDogPtr := reflect.TypeOf(dogPtr)
    
    fmt.Println(rDogPtr.Name())  // 為空
    fmt.Println(rDogPtr.Kind())  // ptr
    
    // Elem()可以獲取指標指向的實際變數
    rDog := rDogPtr.Elem()
    fmt.Println(rDogPtr.Name())  // Dog
    fmt.Println(rDogPtr.Kind())  // struct
}
複製程式碼

可以發現從指標獲取反射物件時,不能直接使用Name()Kind(),這樣只能得到該指標的資訊。這時可以使用Elem()獲取指標指向的實際變數。

  • 當反射物件的型別是結構體型別時:

如果反射物件的型別是結構體,可以通過 NumField()Field() 方法獲得結構體成員的詳細資訊。

type Dog struct {
	Name string
	Age  int
}

func main() {
	dog := Dog{"doggy", 2}
	rDog := reflect.TypeOf(dog)

	fmt.Printf("%v ", rDog.Name()) // Dog
	fmt.Println(rDog.Kind())       // struct

	for index := 0; index < rDog.NumField(); index++ {
		fmt.Printf("%v ", rDog.Field(index).Name)
		fmt.Println(rDog.Field(index).Type)
	}
}
複製程式碼

執行輸出:

Dog struct
Name string
Age int
複製程式碼
reflect.Value

reflect包中Value型別定義如下:

type Value struct {
    // typ holds the type of the value represented by a Value.
	typ *rtype

	// Pointer-valued data or, if flagIndir is set, pointer to data.
	// Valid when either flagIndir is set or typ.pointers() is true.
	ptr unsafe.Pointer
	
	// flag holds metadata about the value.
	flag
}
複製程式碼

可以看到Value型別包含一個型別指標、一個值指標以及標誌資訊。同時Value型別還有很多方法,其中用於獲取值方法:

func (v Value) Int() int64 // 獲取int型別值,如果 v 值不是有符號整型,則 panicfunc (v Value) Uint() uint64 // 獲取unit型別的值,如果 v 值不是無符號整型(包括 uintptr),則 panicfunc (v Value) Float() float64 // 獲取float型別的值,如果 v 值不是浮點型,則 panicfunc (v Value) Complex() complex128 // 獲取複數型別的值,如果 v 值不是複數型,則 panicfunc (v Value) Bool() bool // 獲取布林型別的值,如果 v 值不是布林型,則 panicfunc (v Value) Len() int // 獲取 v 值的長度,v 值必須是字串、陣列、切片、對映、通道。

func (v Value) Cap() int  // 獲取 v 值的容量,v 值必須是數值、切片、通道。

func (v Value) Index(i int) reflect.Value // 獲取 v 值的第 i 個元素,v 值必須是字串、陣列、切片,i 不能超出範圍。

func (v Value) Bytes() []byte // 獲取位元組型別的值,如果 v 值不是位元組切片,則 panicfunc (v Value) Slice(i, j int) reflect.Value // 獲取 v 值的切片,切片長度 = j - i,切片容量 = v.Cap() - i。
// v 必須是字串、數值、切片,如果是陣列則必須可定址。i 不能超出範圍。

func (v Value) Slice3(i, j, k int) reflect.Value  // 獲取 v 值的切片,切片長度 = j - i,切片容量 = k - i。
// ijk 不能超出 v 的容量。i <= j <= k。
// v 必須是字串、數值、切片,如果是陣列則必須可定址。i 不能超出範圍。

func (v Value) MapIndex(key Value) reflect.Value // 根據 key 鍵獲取 v 值的內容,v 值必須是對映。
// 如果指定的元素不存在,或 v 值是未初始化的對映,則返回零值(reflect.ValueOf(nil)func (v Value) MapKeys() []reflect.Value // 獲取 v 值的所有鍵的無序列表,v 值必須是對映。
// 如果 v 值是未初始化的對映,則返回空列表。

func (v Value) OverflowInt(x int64) bool // 判斷 x 是否超出 v 值的取值範圍,v 值必須是有符號整型。

func (v Value) OverflowUint(x uint64) bool  // 判斷 x 是否超出 v 值的取值範圍,v 值必須是無符號整型。

func (v Value) OverflowFloat(x float64) bool  // 判斷 x 是否超出 v 值的取值範圍,v 值必須是浮點型。

func (v Value) OverflowComplex(x complex128) bool // 判斷 x 是否超出 v 值的取值範圍,v 值必須是複數型。
複製程式碼

用於設定值方法:

func (v Value) SetUint(x uint64)  // 設定無符號整型的值

func (v Value) SetFloat(x float64) // 設定浮點型別的值

func (v Value) SetComplex(x complex128) //設定複數型別的值

func (v Value) SetBool(x bool) //設定布林型別的值

func (v Value) SetString(x string) //設定字串型別的值

func (v Value) SetLen(n int)  // 設定切片的長度,n 不能超出範圍,不能為負數。

func (v Value) SetCap(n int) //設定切片的容量

func (v Value) SetBytes(x []byte) //設定位元組型別的值

func (v Value) SetMapIndex(key, val reflect.Value) //設定mapkeyvalue,前提必須是初始化以後,存在覆蓋、不存在新增

func (v Value) Set(x Value) // 將v的持有值修改為x的持有值。如果v.CanSet()返回假,會panicx的持有值必須能直接賦給v持有值的型別。
複製程式碼

其他方法:

結構體相關:
func (v Value) NumField() int // 獲取結構體欄位(成員)數量

func (v Value) Field(i int) reflect.Value  //根據索引獲取結構體欄位

func (v Value) FieldByIndex(index []int) reflect.Value // 根據索引鏈獲取結構體巢狀欄位

func (v Value) FieldByName(string) reflect.Value // 根據名稱獲取結構體的欄位,不存在返回reflect.ValueOf(nil)

func (v Value) FieldByNameFunc(match func(string) bool) Value // 根據匹配函式 match 獲取欄位,如果沒有匹配的欄位,則返回零值(reflect.ValueOf(nil))


通道相關:
func (v Value) Send(x reflect.Value)// 傳送資料(會阻塞),v 值必須是可寫通道。

func (v Value) Recv() (x reflect.Value, ok bool) // 接收資料(會阻塞),v 值必須是可讀通道。

func (v Value) TrySend(x reflect.Value) bool // 嘗試傳送資料(不會阻塞),v 值必須是可寫通道。

func (v Value) TryRecv() (x reflect.Value, ok bool) // 嘗試接收資料(不會阻塞),v 值必須是可讀通道。

func (v Value) Close() // 關閉通道


函式相關
func (v Value) Call(in []Value) (r []Value) // 通過引數列表 in 呼叫 v 值所代表的函式(或方法)。函式的返回值存入 r 中返回。
// 要傳入多少引數就在 in 中存入多少元素。
// Call 即可以呼叫定參函式(引數數量固定),也可以呼叫變參函式(引數數量可變)。

func (v Value) CallSlice(in []Value) []Value // 呼叫變參函式
複製程式碼

同樣地,我們可以通過reflect.ValueOf接受任意interface{}型別,並返回對應的動態型別reflect.Value

v := reflect.ValueOf(2)
fmt.Println(v)  // 2
fmt.Println(v.String()) // <int Value>
複製程式碼

看一下reflect.ValueOf的實現程式碼:

func ValueOf(i interface{}) Value {
	if i == nil {
		return Value{}
	}

	// TODO: Maybe allow contents of a Value to live on the stack.
	// For now we make the contents always escape to the heap. It
	// makes life easier in a few places (see chanrecv/mapassign
	// comment below).
	escapes(i)

	return unpackEface(i)
}

// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))
	// NOTE: don't read e.word until we know whether it is really a pointer or not.
	t := e.typ
	if t == nil {
		return Value{}
	}
	f := flag(t.Kind())
	if ifaceIndir(t) {
		f |= flagIndir
	}
	return Value{t, e.word, f}
}
複製程式碼

escapes() 涉及棧和堆的物件分配以及逃逸分析,有興趣的可以看 William Kennedy 寫的系列文章: Go 語言機制之逃逸分析

reflect.TypeOf類似,ValueOf函式的引數型別是一個interface{},在函式內部將入參進行一個隱式轉換,轉換為一個空介面型別的變數,最終返回一個Value物件,並且reflect.ValueOf返回值也是反射型別物件

可以注意到Value物件中也包含了實際值的型別資訊,通過ValueType() 方法將返回具體型別所對應的reflect.Type:

v := reflect.ValueOf(2)
t := v.Type()
fmt.Println(t) // int
fmt.Println(t.String()) // int
複製程式碼

通過relfect.Value獲取實際變數的資訊

現在我們知道了通過reflect.ValueOf可以將介面型別變數轉換成反射型別變數,當然我們也可以通過reflect.Value.Interface方法逆操作回去,然後通過斷言的方式得到實際值:

v := reflect.ValueOf(2)
i := v.Interface()
if num, ok := i.(int); ok { // 型別斷言
	fmt.Println(num)
}
複製程式碼

但通常在實際場景中,我們其實並不知道原始值的型別,這裡就需要利用reflect.Typereflect.Value的方法探索原始值的資訊。下面通過一個例子說明:

package main

import (
	"fmt"
	"reflect"
)

type Dog struct {
	Name string
	Age  int
}

func (dog *Dog) Eat() {
	fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
	fmt.Printf("%s is running.", dog.Name)
}

func (dog Dog) Sleep() {
	fmt.Printf("%s is sleeping.", dog.Name)
}

func (dog Dog) Jump() {
	fmt.Printf("%s is jumping.", dog.Name)
}

func main() {
	doggy := Dog{"doggy", 2}
	checkFieldAndMethod(doggy)

	fmt.Println("")
	tommy := &Dog{"tommy", 2}
	checkFieldAndMethod(tommy)
}

func checkFieldAndMethod(input interface{}) {
	inputType := reflect.TypeOf(input)
	fmt.Println("Type of input is :", inputType.Name())
	inputValue := reflect.ValueOf(input)
	fmt.Println("Value of input is :", inputValue)

	// 如果input原始型別時指標,通過Elem()方法或者Indirect()獲取指標指向的值
	if inputValue.Kind() == reflect.Ptr {
		inputValue = inputValue.Elem()
		// inputValue = reflect.Indirect(inputValue)
		fmt.Println("Value input points to is :", inputValue)
	}

	//使用NumField()得到結構體中欄位的數量,遍歷得到欄位的值Field(i)和型別Field(i).Type()
	for i := 0; i < inputValue.NumField(); i++ {
		field := inputValue.Type().Field(i)
		value := inputValue.Field(i).Interface()
		fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
	}

	// 獲取方法
	for i := 0; i < inputType.NumMethod(); i++ {
		m := inputType.Method(i)
		fmt.Printf("%s: %v\n", m.Name, m.Type)
	}
}
複製程式碼

執行之後輸出:

Type of input is : Dog
Value of input is : {doggy 2}
Name: string = doggy
Age: int = 2
Jump: func(main.Dog)
Sleep: func(main.Dog)

Type of input is : 
Value of input is : &{tommy 2}
Value input points to is : {tommy 2}
Name: string = tommy
Age: int = 2
Eat: func(*main.Dog)
Jump: func(*main.Dog)
Run: func(*main.Dog)
Sleep: func(*main.Dog)
複製程式碼

利用反射獲取原始值得型別和方法的步驟如下:

  • 判斷原始值是值變數還是指標變數,如果是指標變數,則通過Elem()方法或者Indirect()獲取指標指向的值;
  • 使用NumField()得到結構體中欄位的數量,遍歷得到欄位的值Field(i)和型別Field(i).Type()
  • 使用NumMethod()得到結構體的方法,遍歷得到方法的名稱和型別。

另外,在使用reflect.Value過程有時會對Elem()方法和Indirect()有些迷惑,搞不清這兩個方法的區別,這裡總結一下:

// 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

// Indirect returns the value that v points to.
// If v is a nil pointer, Indirect returns a zero Value.
// If v is not a pointer, Indirect returns v.
func Indirect(v Value) Value
複製程式碼
  • Elem返回v持有的介面保管的值的Value封裝,或者v持有的指標指向的值的Value封裝。如果v的Kind不是InterfacePtrpanic;如果v持有的值為nil,會返回Value零值。
  • Indirect返回v持有的指標指向的值的Value封裝。如果v持有的值為nil,會返回Value零值。如果v持有的變數不是指標,那麼將返回原值v。

也就是說,當v持有的變數是指標時,Elem()方法和Indirect()是等價的。

細心的讀者可能發現對於值變數和指標變數,通過反射獲取到的變數方法有些差異,這個問題就留給讀者自己思考吧。

通過relfect.Value修改實際變數的資訊

當通過relfect.Value修改實際變數的資訊是常用到以下反射值物件的方法:

func (v Value) Elem() Value  
//Elem()返回v持有的介面保管的值的Value封裝,或者v持有的指標指向的值的Value封裝,類似於*操作,此時的Value表示的是Value的元素且可以定址。

func (v Value) Addr() Value 
//Addr()返回一個持有指向v變數地址的指標的Value封裝,類似於&操作。

func (v Value) CanAddr() bool
//CanAddr()返回是否可以獲取v持有值的指標。可以獲取指標的值被稱為可定址的。

func (v Value) CanSet() bool
//CanSet()返回v持有的值是否可以被修改
複製程式碼

然而,值得注意的是並不是所有reflect.Value型別的反射值都可以修改,考慮下面這個例子:

package main 

import(
    "fmt"
    "reflect"
)

func main() {
    a := 1
	rA := reflect.ValueOf(a)
	fmt.Println(rA.CanSet()) //false

	rAptr := reflect.ValueOf(&a)
	rA2 := rAptr.Elem()
	fmt.Println(rA2.CanSet()) //true
	rA2.SetInt(2)
	fmt.Println(rA2.Int()) //2
}
複製程式碼

修改反射型別變數的值有兩個條件:

  • 反射型別變數的值是addressable的,即可取地址的;
  • 反射型別變數的值來自匯出欄位。

有一些修改反射型別變數是可定址的,有一些則不是:

package main

import (
    "reflect"
    "fmt"
)

func main() {
    x := 2
    a := reflect.ValueOf(2)
    b := reflect.ValueOf(x)
    c := reflect.ValueOf(&x)
    d := c.Elem()
    fmt.Println(a.CanAddr()) // false
    fmt.Println(b.CanAddr()) // false
    fmt.Println(c.CanAddr()) // false
    fmt.Println(d.CanAddr()) // true

}
複製程式碼

對於非指標變數x,通過reflect.ValueOf(x)返回的 reflect.Value是不可取地址的。但是對於d,它是c的解引用方式生成的,指向另一個變數,因此是可 取地址的。我們可以通過呼叫reflect.ValueOf(&x).Elem(),獲取到x對應的可取地址的反射值。

對於結構體型別變數,如果成員欄位沒有匯出,那麼雖然可以被訪問,但不能通過反射修改:

package main

import (
	"fmt"
	"reflect"
)

type Dog struct {
	Name string
	Age  int
	sex  string
}

func main() {
    rDog := reflect.ValueOf(&Dog{}).Elem()
	vAge := rDog.FieldByName("Age")
	vAge.SetInt(1)

	vSex := rDog.FieldByName("sex")
	vSex.SetString("male")
}
複製程式碼

執行出現報錯:SetString使用的值來自於一個未匯出的欄位。

panic: reflect: reflect.Value.SetString using value obtained using unexported field
複製程式碼

為了能修改這個值,需要將該欄位匯出。將Dog型別中的 sex成員首字母大寫即可。

修改可取地址的reflect.Value持有的變數值,除了可以通過反射的Set系列方法,還可以通過從反射型別變數獲取實際值的指標來修改:

package main

import (
    "reflect"
    "fmt"
)

func main() {
    x := 1
    v := reflect.ValueOf(&x).Elem()
    px := v.Addr().Interface().(*int)
    *px = 2
    fmt.Print(x) //2
}
複製程式碼

首先呼叫Addr()方法,返回 一個持有指向變數的指標的Value;然後在Value上呼叫Interface()方法,返回一個 interface{},裡面包含指向變數的指標;最後通過型別斷言得到普通指標來修改變數的值。

通過反射呼叫函式

如果反射值物件(reflect.Value)持有值的型別為函式時,可以通過 reflect.Value 呼叫該函式。

func (v Value) Call(in []Value) []Value
複製程式碼

Call方法使用輸入的引數in呼叫v持有的函式。引數in是反射值物件的切片,即[]reflect.Value;呼叫完成時,函式的返回值通過 []reflect.Value 返回。

package main 

import(
    "fmt"
    "reflect"
)
func add(a, b int) int {

    return a + b
}

func main() {

    // 將函式add包裝為反射值物件
    funcValue := reflect.ValueOf(add)

    // 建構函式add的引數, 傳入兩個整型值
    paramList := []reflect.Value{reflect.ValueOf(5), reflect.ValueOf(10)}

    // 反射呼叫函式Call()
    retList := funcValue.Call(paramList)

    // 獲取第一個返回值, 取整數值
    fmt.Println(retList[0].Int()) //返回 15
}
複製程式碼

如果需要通過反射呼叫結構體的方法,可以利用MethodByName方法來完成:

func (v Value) MethodByName(name string) Value
//返回v的名為name的方法的已繫結(到v的持有值的)狀態的函式形式的Value封裝。
複製程式碼

舉例:

package main 

import(
    "fmt"
    "reflect"
)

type Dog struct {
	Name string
	Age  int
}

func (dog *Dog) SetName(name string){
    dog.Name = name
}

func main() {
    dog := Dog{}
	rDog := reflect.ValueOf(&dog)
	paramList1 := []reflect.Value{reflect.ValueOf("doggy")}
	rDog.MethodByName("SetName").Call(paramList1)
	fmt.Println(dog.Name) //doggy
}
複製程式碼

值得注意的是,反射呼叫函式的過程需要構造大量的 reflect.Value 和中間變數,對函式引數值進行逐一檢查,還需要將呼叫引數複製到呼叫函式的引數記憶體中。呼叫完畢後,還需要將返回值轉換為 reflect.Value,使用者還需要從中取出呼叫值。因此反射呼叫函式的效能問題尤為突出,不建議大量使用反射函式呼叫。

總結

本文介紹了Go語言中interface的定義、用法以及副作用,並由此引入reflect,通過大量示例詳細介紹了reflect的概念,通過reflect獲取值、修改值的用法,以及呼叫函式的用法。內容上可以說相當詳實具體了,在此過程中也讓筆者自己對這部分的知識有了更深刻的認識,也希望有幸能帶給讀者一點幫助吧。

參考資料

【Golang標準庫文件】

【Golang的反射reflect深入理解和示例】

【Go addressable 詳解】

相關文章