深入理解Golang之interface和reflect


前言

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 值不是有符號整型,則 panic。

func (v Value) Uint() uint64 // 獲取unit類型的值,如果 v 值不是無符號整型(包括 uintptr),則 panic。

func (v Value) Float() float64 // 獲取float類型的值,如果 v 值不是浮點型,則 panic。

func (v Value) Complex() complex128 // 獲取復數類型的值,如果 v 值不是復數型,則 panic。

func (v Value) Bool() bool // 獲取布爾類型的值,如果 v 值不是布爾型,則 panic。

func (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 值不是字節切片,則 panic。

func (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。
// i、j、k 不能超出 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) //設置map的key和value,前提必須是初始化以后,存在覆蓋、不存在添加

func (v Value) Set(x Value) // 將v的持有值修改為x的持有值。如果v.CanSet()返回假,會panic。x的持有值必須能直接賦給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 詳解】


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM