深度解密Go語言之反射

来源:https://www.cnblogs.com/qcrao-2018/archive/2019/05/06/10822655.html
-Advertisement-
Play Games

Go 作為一門靜態語言,相比 Python 等動態語言,在編寫過程中靈活性會受到一定的限制。但是通過介面加反射實現了類似於動態語言的能力:可以在程式運行時動態地捕獲甚至改變類型的信息和值。 ...


目錄

反射和 Interface 息息相關,而 Interface 是我們上一篇文章的內容。在開始正文前,和大家說點題外話。

上一篇關於 Interface 的文章發出後,獲得了很多的關註和閱讀。比如,登上了 GoCN 的每日新聞第一條:

gocn

可能是編輯者覺得這篇文章稱不上“深度解密”,把標題給小小地改動了下,哈哈~~

在博客園登上了 48 小時閱讀排行榜:

博客園

在開發者頭條 APP (類似於今日頭條,不過內容都是技術相關的,還挺有意思的)上收穫了 150 收藏,並被推薦到首頁最顯眼的 banner 位置,閱讀量達到了 1w 多,只是不知道這個數字是否是真實的,有點難以相信。

開發者頭條

很多同學在後臺向我反映文章太長了,不利於閱讀,建議拆分一下。我非常理解,讀屏時代,大家需要快速地讀完全文,拿到收益。而碼農桃花源的文章都非常長,讀者很難在短時間內讀完,並且獲得相應的收益。

首先非常感謝大家的建議!其實我的想法是這樣的:大家都在說,現在是一個信息嚴重過載的時代,信息多得看不完,不免產生很多焦慮。但是,我想說,優質的信息真有那麼多嗎?在我看來,文章的水平都是參差不齊,很多毫無內容和價值,大家把時間浪費在這些信息上面是很不值得的。因為你讀了這些文章,就沒有了讀其他好的文章的精力。

所以,碼農桃花源想做一個優質信息源,提供優質的內容。每一篇文章都是有深度,有內容,有收穫。一篇文章,我一般得花費 2 周左右,算是半月更,和那些日更的沒法比。當然,只是在數量上沒法比。而這個時代,最不缺的就是數量。

另外,文章長也算是我的一個特色。我完全可以拆分成上、中、下等等,但我希望一次性交付給我的讀者所有有價值的內容。這樣,你可以集中一個小時或是更長時間,精讀完一篇文章。

閑話結束,今天要講的內容是反射,進入正題。

什麼是反射

直接看維基百科上的定義:

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

那我就要問個問題了:不用反射就不能在運行時訪問、檢測和修改它本身的狀態和行為嗎?

問題的回答,其實要首先理解什麼叫訪問、檢測和修改它本身狀態或行為,它的本質是什麼?

實際上,它的本質是程式在運行期探知對象的類型信息和記憶體結構,不用反射能行嗎?可以的!使用彙編語言,直接和內層打交道,什麼信息不能獲取?但是,當編程遷移到高級語言上來之後,就不行了!就只能通過反射來達到此項技能。

不同語言的反射模型不盡相同,有些語言還不支持反射。《Go 語言聖經》中是這樣定義反射的:

Go 語言提供了一種機制在運行時更新變數和檢查它們的值、調用它們的方法,但是在編譯時並不知道這些變數的具體類型,這稱為反射機制。

為什麼要用反射

需要反射的 2 個常見場景:

  1. 有時你需要編寫一個函數,但是並不知道傳給你的參數類型是什麼,可能是沒約定好;也可能是傳入的類型很多,這些類型並不能統一表示。這時反射就會用的上了。
  2. 有時候需要根據某些條件決定調用哪個函數,比如根據用戶的輸入來決定。這時就需要對函數和函數的參數進行反射,在運行期間動態地執行函數。

在講反射的原理以及如何用之前,還是說幾點不使用反射的理由:

  1. 與反射相關的代碼,經常是難以閱讀的。在軟體工程中,代碼可讀性也是一個非常重要的指標。
  2. Go 語言作為一門靜態語言,編碼過程中,編譯器能提前發現一些類型錯誤,但是對於反射代碼是無能為力的。所以包含反射相關的代碼,很可能會運行很久,才會出錯,這時候經常是直接 panic,可能會造成嚴重的後果。
  3. 反射對性能影響還是比較大的,比正常代碼運行速度慢一到兩個數量級。所以,對於一個項目中處於運行效率關鍵位置的代碼,儘量避免使用反射特性。

反射是如何實現的

上一篇文章講到了 interface,它是 Go 語言實現抽象的一個非常強大的工具。當向介面變數賦予一個實體類型的時候,介面會存儲實體的類型信息,反射就是通過介面的類型信息實現的,反射建立在類型的基礎上。

Go 語言在 reflect 包里定義了各種類型,實現了反射的各種函數,通過它們可以在運行時檢測類型的信息、改變類型的值。

types 和 interface

Go 語言中,每個變數都有一個靜態類型,在編譯階段就確定了的,比如 int, float64, []int 等等。註意,這個類型是聲明時候的類型,不是底層數據類型。

Go 官方博客里就舉了一個例子:

type MyInt int

var i int
var j MyInt

儘管 i,j 的底層類型都是 int,但我們知道,他們是不同的靜態類型,除非進行類型轉換,否則,i 和 j 不能同時出現在等號兩側。j 的靜態類型就是 MyInt

反射主要與 interface{} 類型相關。前面一篇關於 interface 相關的文章已經探討過 interface 的底層結構,這裡再來複習一下。

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32
    bad    bool
    inhash bool
    unused [2]byte
    fun    [1]uintptr
}

其中 itab 由具體類型 _type 以及 interfacetype 組成。_type 表示具體類型,而 interfacetype 則表示具體類型實現的介面類型。

iface 結構體全景

實際上,iface 描述的是非空介面,它包含方法;與之相對的是 eface,描述的是空介面,不包含任何方法,Go 語言里有的類型都 “實現了” 空介面。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

相比 ifaceeface 就比較簡單了。只維護了一個 _type 欄位,表示空介面所承載的具體的實體類型。data 描述了具體的值。

eface 結構體全景

還是用 Go 官方關於反射的博客里的例子,當然,我會用圖形來詳細解釋,結合兩者來看會更清楚。順便提一下,搞技術的不要害怕英文資料,要想成為技術專家,讀英文原始資料是技術提高的一條必經之路。

先明確一點:介面變數可以存儲任何實現了介面定義的所有方法的變數。

Go 語言中最常見的就是 ReaderWriter 介面:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

接下來,就是介面之間的各種轉換和賦值了:

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

首先聲明 r 的類型是 io.Reader,註意,這是 r 的靜態類型,此時它的動態類型為 nil,並且它的動態值也是 nil

之後,r = tty 這一語句,將 r 的動態類型變成 *os.File,動態值則變成非空,表示打開的文件對象。這時,r 可以用<value, type>對來表示為: <tty, *os.File>

r=tty

註意看上圖,此時雖然 fun 所指向的函數只有一個 Read 函數,其實 *os.File 還包含 Write 函數,也就是說 *os.File 其實還實現了 io.Writer 介面。因此下麵的斷言語句可以執行:

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

之所以用斷言,而不能直接賦值,是因為 r 的靜態類型是 io.Reader,並沒有實現 io.Writer 介面。斷言能否成功,看 r 的動態類型是否符合要求。

這樣,w 也可以表示成 <tty, *os.File>,僅管它和 r 一樣,但是 w 可調用的函數取決於它的靜態類型 io.Writer,也就是說它只能有這樣的調用形式: w.Write()w 的記憶體形式如下圖:

w = r.(io.Writer)

r 相比,僅僅是 fun 對應的函數變了:Read -> Write

最後,再來一個賦值:

var empty interface{}
empty = w

由於 empty 是一個空介面,因此所有的類型都實現了它,w 可以直接賦給它,不需要執行斷言操作。

empty=w

從上面的三張圖可以看到,interface 包含三部分信息:_type 是類型信息,*data 指向實際類型的實際值,itab 包含實際類型的信息,包括大小、包路徑,還包含綁定在類型上的各種方法(圖上沒有畫出方法),補充一下關於 os.File 結構體的圖:

struct_type

這一節的最後,複習一下上一篇關於 interface 的文章,提到的一個技巧,這裡再展示一下:

先參考源碼,分別定義一個“偽裝”的 iface 和 eface 結構體。

type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type itab struct {
    inter uintptr
    _type uintptr
    link uintptr
    hash  uint32
    _     [4]byte
    fun   [1]uintptr
}

type eface struct {
    _type uintptr
    data unsafe.Pointer
}

接著,將介面變數占據的記憶體內容強制解釋成上面定義的類型,再列印出來:

package main

import (
    "os"
    "fmt"
    "io"
    "unsafe"
)

func main() {
    var r io.Reader
    fmt.Printf("initial r: %T, %v\n", r, r)

    tty, _ := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0)
    fmt.Printf("tty: %T, %v\n", tty, tty)

    // 給 r 賦值
    r = tty
    fmt.Printf("r: %T, %v\n", r, r)

    rIface := (*iface)(unsafe.Pointer(&r))
    fmt.Printf("r: iface.tab._type = %#x, iface.data = %#x\n", rIface.tab._type, rIface.data)

    // 給 w 賦值
    var w io.Writer
    w = r.(io.Writer)
    fmt.Printf("w: %T, %v\n", w, w)

    wIface := (*iface)(unsafe.Pointer(&w))
    fmt.Printf("w: iface.tab._type = %#x, iface.data = %#x\n", wIface.tab._type, wIface.data)

    // 給 empty 賦值
    var empty interface{}
    empty = w
    fmt.Printf("empty: %T, %v\n", empty, empty)

    emptyEface := (*eface)(unsafe.Pointer(&empty))
    fmt.Printf("empty: eface._type = %#x, eface.data = %#x\n", emptyEface._type, emptyEface.data)
}

運行結果:

initial r: <nil>, <nil>
tty: *os.File, &{0xc4200820f0}
r: *os.File, &{0xc4200820f0}
r: iface.tab._type = 0x10bfcc0, iface.data = 0xc420080020
w: *os.File, &{0xc4200820f0}
w: iface.tab._type = 0x10bfcc0, iface.data = 0xc420080020
empty: *os.File, &{0xc4200820f0}
empty: eface._type = 0x10bfcc0, eface.data = 0xc420080020

r,w,empty 的動態類型和動態值都一樣。不再詳細解釋了,結合前面的圖可以看得非常清晰。

反射的基本函數

reflect 包里定義了一個介面和一個結構體,即 reflect.Typereflect.Value,它們提供很多函數來獲取存儲在介面里的類型信息。

reflect.Type 主要提供關於類型相關的信息,所以它和 _type 關聯比較緊密;reflect.Value 則結合 _typedata 兩者,因此程式員可以獲取甚至改變類型的值。

reflect 包中提供了兩個基礎的關於反射的函數來獲取上述的介面和結構體:

func TypeOf(i interface{}) Type 
func ValueOf(i interface{}) Value

TypeOf 函數用來提取一個介面中值的類型信息。由於它的輸入參數是一個空的 interface{},調用此函數時,實參會先被轉化為 interface{}類型。這樣,實參的類型信息、方法集、值信息都存儲到 interface{} 變數里了。

看下源碼:

func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

這裡的 emptyInterface 和上面提到的 eface 是一回事(欄位名略有差異,欄位是相同的),且在不同的源碼包:前者在 reflect 包,後者在 runtime 包。 eface.typ 就是動態類型。

type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

至於 toType 函數,只是做了一個類型轉換:

func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

註意,返回值 Type 實際上是一個介面,定義了很多方法,用來獲取類型相關的各種信息,而 *rtype 實現了 Type 介面。

type Type interface {
    // 所有的類型都可以調用下麵這些函數

    // 此類型的變數對齊後所占用的位元組數
    Align() int
    
    // 如果是 struct 的欄位,對齊後占用的位元組數
    FieldAlign() int

    // 返回類型方法集里的第 `i` (傳入的參數)個方法
    Method(int) Method

    // 通過名稱獲取方法
    MethodByName(string) (Method, bool)

    // 獲取類型方法集里導出的方法個數
    NumMethod() int

    // 類型名稱
    Name() string

    // 返回類型所在的路徑,如:encoding/base64
    PkgPath() string

    // 返回類型的大小,和 unsafe.Sizeof 功能類似
    Size() uintptr

    // 返回類型的字元串表示形式
    String() string

    // 返回類型的類型值
    Kind() Kind

    // 類型是否實現了介面 u
    Implements(u Type) bool

    // 是否可以賦值給 u
    AssignableTo(u Type) bool

    // 是否可以類型轉換成 u
    ConvertibleTo(u Type) bool

    // 類型是否可以比較
    Comparable() bool

    // 下麵這些函數只有特定類型可以調用
    // 如:Key, Elem 兩個方法就只能是 Map 類型才能調用
    
    // 類型所占據的位數
    Bits() int

    // 返回通道的方向,只能是 chan 類型調用
    ChanDir() ChanDir

    // 返回類型是否是可變參數,只能是 func 類型調用
    // 比如 t 是類型 func(x int, y ... float64)
    // 那麼 t.IsVariadic() == true
    IsVariadic() bool

    // 返回內部子元素類型,只能由類型 Array, Chan, Map, Ptr, or Slice 調用
    Elem() Type

    // 返回結構體類型的第 i 個欄位,只能是結構體類型調用
    // 如果 i 超過了總欄位數,就會 panic
    Field(i int) StructField

    // 返回嵌套的結構體的欄位
    FieldByIndex(index []int) StructField

    // 通過欄位名稱獲取欄位
    FieldByName(name string) (StructField, bool)

    // FieldByNameFunc returns the struct field with a name
    // 返回名稱符合 func 函數的欄位
    FieldByNameFunc(match func(string) bool) (StructField, bool)

    // 獲取函數類型的第 i 個參數的類型
    In(i int) Type

    // 返回 map 的 key 類型,只能由類型 map 調用
    Key() Type

    // 返回 Array 的長度,只能由類型 Array 調用
    Len() int

    // 返回類型欄位的數量,只能由類型 Struct 調用
    NumField() int

    // 返回函數類型的輸入參數個數
    NumIn() int

    // 返回函數類型的返回值個數
    NumOut() int

    // 返回函數類型的第 i 個值的類型
    Out(i int) Type

    // 返回類型結構體的相同部分
    common() *rtype
    
    // 返回類型結構體的不同部分
    uncommon() *uncommonType
}

可見 Type 定義了非常多的方法,通過它們可以獲取類型的一切信息,大家一定要完整的過一遍上面所有的方法。

註意到 Type 方法集的倒數第二個方法 common
返回的 rtype類型,它和上一篇文章講到的 _type 是一回事,而且源代碼里也註釋了:兩邊要保持同步:

 // rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {
    size       uintptr
    ptrdata    uintptr
    hash       uint32
    tflag      tflag
    align      uint8
    fieldAlign uint8
    kind       uint8
    alg        *typeAlg
    gcdata     *byte
    str        nameOff
    ptrToThis  typeOff
}

所有的類型都會包含 rtype 這個欄位,表示各種類型的公共信息;另外,不同類型包含自己的一些獨特的部分。

比如下麵的 arrayTypechanType 都包含 rytpe,而前者還包含 slice,len 等和數組相關的信息;後者則包含 dir 表示通道方向的信息。

// arrayType represents a fixed array type.
type arrayType struct {
    rtype `reflect:"array"`
    elem  *rtype // array element type
    slice *rtype // slice type
    len   uintptr
}

// chanType represents a channel type.
type chanType struct {
    rtype `reflect:"chan"`
    elem  *rtype  // channel element type
    dir   uintptr // channel direction (ChanDir)
}

註意到,Type 介面實現了 String() 函數,滿足 fmt.Stringer 介面,因此使用 fmt.Println 列印的時候,輸出的是 String() 的結果。另外,fmt.Printf() 函數,如果使用 %T 來作為格式參數,輸出的是 reflect.TypeOf 的結果,也就是動態類型。例如:

fmt.Printf("%T", 3) // int

講完了 TypeOf 函數,再來看一下 ValueOf 函數。返回值 reflect.Value 表示 interface{} 里存儲的實際變數,它能提供實際變數的各種信息。相關的方法常常是需要結合類型信息和值信息。例如,如果要提取一個結構體的欄位信息,那就需要用到 _type (具體到這裡是指 structType) 類型持有的關於結構體的欄位信息、偏移信息,以及 *data 所指向的內容 —— 結構體的實際值。

源碼如下:

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

// 分解 eface
func unpackEface(i interface{}) Value {
    e := (*emptyInterface)(unsafe.Pointer(&i))

    t := e.typ
    if t == nil {
        return Value{}
    }
    
    f := flag(t.Kind())
    if ifaceIndir(t) {
        f |= flagIndir
    }
    return Value{t, e.word, f}
}

從源碼看,比較簡單:將先將 i 轉換成 *emptyInterface 類型, 再將它的 typ 欄位和 word 欄位以及一個標誌位欄位組裝成一個 Value 結構體,而這就是 ValueOf 函數的返回值,它包含類型結構體指針、真實數據的地址、標誌位。

Value 結構體定義了很多方法,通過這些方法可以直接操作 Value 欄位 ptr 所指向的實際數據:

// 設置切片的 len 欄位,如果類型不是切片,就會panic
 func (v Value) SetLen(n int)
 
 // 設置切片的 cap 欄位
 func (v Value) SetCap(n int)
 
 // 設置字典的 kv
 func (v Value) SetMapIndex(key, val Value)

 // 返回切片、字元串、數組的索引 i 處的值
 func (v Value) Index(i int) Value
 
 // 根據名稱獲取結構體的內部欄位值
 func (v Value) FieldByName(name string) Value
 
 // ……

Value 欄位還有很多其他的方法。例如:

// 用來獲取 int 類型的值
func (v Value) Int() int64

// 用來獲取結構體欄位(成員)數量
func (v Value) NumField() int

// 嘗試向通道發送數據(不會阻塞)
func (v Value) TrySend(x reflect.Value) bool

// 通過參數列表 in 調用 v 值所代表的函數(或方法
func (v Value) Call(in []Value) (r []Value) 

// 調用變參長度可變的函數
func (v Value) CallSlice(in []Value) []Value 

不一一列舉了,反正是非常多。可以去 src/reflect/value.go 去看看源碼,搜索 func (v Value) 就能看到。

另外,通過 Type() 方法和 Interface() 方法可以打通 interfaceTypeValue 三者。Type() 方法也可以返回變數的類型信息,與 reflect.TypeOf() 函數等價。Interface() 方法可以將 Value 還原成原來的 interface。

這裡引用老錢《快學Go語言第十五課——反射》的一張圖:

三者關係
總結一下:TypeOf() 函數返回一個介面,這個介面定義了一系列方法,利用這些方法可以獲取關於類型的所有信息; ValueOf() 函數返回一個結構體變數,包含類型信息以及實際值。

用一張圖來串一下:

value rtype

上圖中,rtye 實現了 Type 介面,是所有類型的公共部分。emptyface 結構體和 eface 其實是一個東西,而 rtype 其實和 _type 是一個東西,只是一些欄位稍微有點差別,比如 emptyface 的 word 欄位和 eface 的 data 欄位名稱不同,但是數據型是一樣的。

反射的三大定律

根據 Go 官方關於反射的博客,反射有三大定律:

  1. Reflection goes from interface value to reflection object.
  1. Reflection goes from reflection object to interface value.
  1. To modify a reflection object, the value must be settable.

第一條是最基本的:反射是一種檢測存儲在 interface 中的類型和值機制。這可以通過 TypeOf 函數和 ValueOf 函數得到。

第二條實際上和第一條是相反的機制,它將 ValueOf 的返回值通過 Interface() 函數反向轉變成 interface 變數。

前兩條就是說 介面型變數反射類型對象 可以相互轉化,反射類型對象實際上就是指的前面說的 reflect.Typereflect.Value

第三條不太好懂:如果需要操作一個反射變數,那麼它必須是可設置的。反射變數可設置的本質是它存儲了原變數本身,這樣對反射變數的操作,就會反映到原變數本身;反之,如果反射變數不能代表原變數,那麼操作了反射變數,不會對原變數產生任何影響,這會給使用者帶來疑惑。所以第二種情況在語言層面是不被允許的。

舉一個經典例子:

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

執行上面的代碼會產生 panic,原因是反射變數 v 不能代表 x 本身,為什麼?因為調用 reflect.ValueOf(x) 這一行代碼的時候,傳入的參數在函數內部只是一個拷貝,是值傳遞,所以 v 代表的只是 x 的一個拷貝,因此對 v 進行操作是被禁止的。

可設置是反射變數 Value 的一個性質,但不是所有的 Value 都是可被設置的。

就像在一般的函數里那樣,當我們想改變傳入的變數時,使用指針就可以解決了。

var x float64 = 3.4
p := reflect.ValueOf(&x)
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

輸出是這樣的:

type of p: *float64
settability of p: false

p 還不是代表 xp.Elem() 才真正代表 x,這樣就可以真正操作 x 了:

v := p.Elem()
v.SetFloat(7.1)
fmt.Println(v.Interface()) // 7.1
fmt.Println(x) // 7.1

關於第三條,記住一句話:如果想要操作原變數,反射變數 Value 必須要 hold 住原變數的地址才行。

反射相關函數的使用

代碼樣例

網路上各種博客文章里使用反射的樣例代碼非常多,讀過這篇文章後,基本沒有看不懂的,哈哈!不過,我這裡還是舉一個例子,並講解一番:

package main

import (
    "reflect"
    "fmt"
)

type Child struct {
    Name     string
    Grade    int
    Handsome bool
}

type Adult struct {
    ID         string `qson:"Name"`
    Occupation string
    Handsome   bool
}

// 如果輸入參數 i 是 Slice,元素是結構體,有一個欄位名為 `Handsome`,
// 並且有一個欄位的 tag 或者欄位名是 `Name` ,
// 如果該 `Name` 欄位的值是 `qcrao`,
// 就把結構體中名為 `Handsome` 的欄位值設置為 true。
func handsome(i interface{}) {
    // 獲取 i 的反射變數 Value
    v := reflect.ValueOf(i)

    // 確定 v 是一個 Slice
    if v.Kind() != reflect.Slice {
        return
    }

    // 確定 v 是的元素為結構體
    if e := v.Type().Elem(); e.Kind() != reflect.Struct {
        return
    }

    // 確定結構體的欄位名含有 "ID" 或者 json tag 標簽為 `name`
    // 確定結構體的欄位名 "Handsome"
    st := v.Type().Elem()

    // 尋找欄位名為 Name 或者 tag 的值為 Name 的欄位
    foundName := false
    for i := 0; i < st.NumField(); i++ {
        f := st.Field(i)
        tag := f.Tag.Get("qson")

        if (tag == "Name" || f.Name == "Name") && f.Type.Kind() == reflect.String {
            foundName = true
            break
        }
    }

    if !foundName {
        return
    }

    if niceField, foundHandsome := st.FieldByName("Handsome"); foundHandsome == false || niceField.Type.Kind() != reflect.Bool {
        return
    }

    // 設置名字為 "qcrao" 的對象的 "Handsome" 欄位為 true
    for i := 0; i < v.Len(); i++ {
        e := v.Index(i)
        handsome := e.FieldByName("Handsome")

        // 尋找欄位名為 Name 或者 tag 的值為 Name 的欄位
        var name reflect.Value
        for j := 0; j < st.NumField(); j++ {
            f := st.Field(j)
            tag := f.Tag.Get("qson")

            if tag == "Name" || f.Name == "Name" {
                name = v.Index(i).Field(j)
            }
        }

        if name.String() == "qcrao" {
            handsome.SetBool(true)
        }
    }
}

func main() {
    children := []Child{
        {Name: "Ava", Grade: 3, Handsome: true},
        {Name: "qcrao", Grade: 6, Handsome: false},
    }

    adults := []Adult{
        {ID: "Steve", Occupation: "Clerk", Handsome: true},
        {ID: "qcrao", Occupation: "Go Programmer", Handsome: false},
    }

    fmt.Printf("adults before handsome: %v\n", adults)
    handsome(adults)
    fmt.Printf("adults after handsome: %v\n", adults)

    fmt.Println("-------------")

    fmt.Printf("children before handsome: %v\n", children)
    handsome(children)
    fmt.Printf("children after handsome: %v\n", children)
}

代碼運行結果:

adults before handsome: [{Steve Clerk true} {qcrao Go Programmer false}]
adults after handsome: [{Steve Clerk true} {qcrao Go Programmer true}]
-------------
children before handsome: [{Ava 3 true} {qcrao 6 false}]
children after handsome: [{Ava 3 true} {qcrao 6 true}]

代碼主要做的事情是:找出傳入的參數為 Slice,並且 Slice 的元素為結構體,如果其中有一個欄位名是 Name 或者是 標簽名稱為 Name,並且還有一個欄位名是 Handsome 的情形。如果找到,並且欄位名稱為 Name 的實際值是 qcrao 的話,就把另一個欄位 Handsome 的值置為 true。

程式並不關心傳入的結構體到底是什麼,只要它的欄位名包含 NameHandsome,都是 handsome 函數要工作的對象。

註意一點,Adult 結構體的標簽 qson:"Name",中間是沒有空格的,否則 Tag.Get("qson") 識別不出來。

未導出成員

利用反射機制,對於結構體中未導出成員,可以讀取,但不能修改其值。

註意,正常情況下,代碼是不能讀取結構體未導出成員的,但通過反射可以越過這層限制。另外,通過反射,結構體中可以被修改的成員只有是導出成員,也就是欄位名的首字母是大寫的。

一個可取地址的 reflect.Value 變數會記錄一個結構體成員是否是未導出成員,如果是的話則拒絕修改操作。
CanAddr 不能說明一個變數是否可以被修改。
CanSet 則可以檢查對應的 reflect.Value 是否可取地址並可被修改。

package main

import (
    "reflect"
    "fmt"
)

type Child struct {
    Name     string
    handsome bool
}

func main() {
    qcrao := Child{Name: "qcrao", handsome: true}

    v := reflect.ValueOf(&qcrao)

    f := v.Elem().FieldByName("Name")
    fmt.Println(f.String())

    f.SetString("stefno")
    fmt.Println(f.String())

    f = v.Elem().FieldByName("handsome")
    
    // 這一句會導致 panic,因為 handsome 欄位未導出
    //f.SetBool(true)
    fmt.Println(f.Bool())
}

執行結果:

qcrao
stefno
true

上面的例子中,handsome 欄位未導出,可以讀取,但不能調用相關 set 方法,否則會 panic。反射用起來一定要小心,調用類型不匹配的方法,會導致各種 panic。

反射的實際應用

反射的實際應用非常廣:IDE 中的代碼自動補全功能、對象序列化(json 函數庫)、fmt 相關函數的實現、ORM(全稱是:Object Relational Mapping,對象關係映射)……

這裡舉 2 個例子:json 序列化和 DeepEqual 函數。

json 序列化

開發過 web 服務的同學,一定用過 json 數據格式。json 是一種獨立於語言的數據格式。最早用於瀏覽器和伺服器之間的實時無狀態的數據交換,並由此發展起來。

Go 語言中,主要提供 2 個函數用於序列化和反序列化:

func Marshal(v interface{}) ([]byte, error)
func Unmarshal(data []byte, v interface{}) error

兩個函數的參數都包含 interface,具體實現的時候,都會用到反射相關的特性。

對於序列化和反序列化函數,均需要知道參數的所有欄位,包括欄位類型和值,再調用相關的 get 函數或者 set 函數進行實際的操作。

DeepEqual 的作用及原理

在測試函數中,經常會需要這樣的函數:判斷兩個變數的實際內容完全一致。

例如:如何判斷兩個 slice 所有的元素完全相同;如何判斷兩個 map 的 key 和 value 完全相同等等。

上述問題,可以通過 DeepEqual 函數實現。

func DeepEqual(x, y interface{}) bool

DeepEqual 函數的參數是兩個 interface,實際上也就是可以輸入任意類型,輸出 true 或者 flase 表示輸入的兩個變數是否是“深度”相等。

先明白一點,如果是不同的類型,即使是底層類型相同,相應的值也相同,那麼兩者也不是“深度”相等。

type MyInt int
type YourInt int

func main() {
    m := MyInt(1)
    y := YourInt(1)

    fmt.Println(reflect.DeepEqual(m, y)) // false
}

上面的代碼中,m, y 底層都是 int,而且值都是 1,但是兩者靜態類型不同,前者是 MyInt,後者是 YourInt,因此兩者不是“深度”相等。

在源碼里,有對 DeepEqual 函數的非常清楚地註釋,列舉了不同類型,DeepEqual 的比較情形,這裡做一個總結:

類型 深度相等情形
Array 相同索引處的元素“深度”相等
Struct 相應欄位,包含導出和不導出,“深度”相等
Func 只有兩者都是 nil 時
Interface 兩者存儲的具體值“深度”相等
Map 1、都為 nil;2、非空、長度相等,指向同一個 map 實體對象,或者相應的 key 指向的 value “深度”相等
Pointer 1、使用 == 比較的結果相等;2、指向的實體“深度”相等
Slice 1、都為 nil;2、非空、長度相等,首元素指向同一個底層數組的相同元素,即 &x[0] == &y[0] 或者 相同索引處的元素“深度”相等
numbers, bools, strings, and channels 使用 == 比較的結果為真

一般情況下,DeepEqual 的實現只需要遞歸地調用 == 就可以比較兩個變數是否是真的“深度”相等。

但是,有一些異常情況:比如 func 類型是不可比較的類型,只有在兩個 func 類型都是 nil 的情況下,才是“深度”相等;float 類型,由於精度的原因,也是不能使用 == 比較的;包含 func 類型或者 float 類型的 struct, interface, array 等。

對於指針而言,當兩個值相等的指針就是“深度”相等,因為兩者指向的內容是相等的,即使兩者指向的是 func 類型或者 float 類型,這種情況下不關心指針所指向的內容。

同樣,對於指向相同 slice, map 的兩個變數也是“深度”相等的,不關心 slice, map 具體的內容。

對於“有環”的類型,比如迴圈鏈表,比較兩者是否“深度”相等的過程中,需要對已比較的內容作一個標記,一旦發現兩個指針之前比較過,立即停止比較,並判定二者是深度相等的。這樣做的原因是,及時停止比較,避免陷入無限迴圈。

來看源碼:

func DeepEqual(x, y interface{}) bool {
    if x == nil || y == nil {
        return x == y
    }
    v1 := ValueOf(x)
    v2 := ValueOf(y)
    if v1.Type() != v2.Type() {
        return false
    }
    return deepValueEqual(v1, v2, make(map[visit]bool), 0)
}

首先查看兩者是否有一個是 nil 的情況,這種情況下,只有兩者都是 nil,函數才會返回 true。

接著,使用反射,獲取x,y 的反射對象,並且立即比較兩者的類型,根據前面的內容,這裡實際上是動態類型,如果類型不同,直接返回 false。

最後,最核心的內容在子函數 deepValueEqual 中。

代碼比較長,思路卻比較簡單清晰:核心是一個 switch 語句,識別輸入參數的不同類型,分別遞歸調用 deepValueEqual 函數,一直遞歸到最基本的數據類型,比較 int,string 等可以直接得出 true 或者 false,再一層層地返回,最終得到“深度”相等的比較結果。

實際上,各種類型的比較套路比較相似,這裡就直接節選一個稍微複雜一點的 map 類型的比較:

// deepValueEqual 函數
// ……

case Map:
    if v1.IsNil() != v2.IsNil() {
        return false
    }
    if v1.Len() != v2.Len() {
        return false
    }
    if v1.Pointer() == v2.Pointer() {
        return true
    }
    for _, k := range v1.MapKeys() {
        val1 := v1.MapIndex(k)
        val2 := v2.MapIndex(k)
        if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) {
            return false
        }
    }
    return true
    
// ……   

和前文總結的表格裡,比較 map 是否相等的思路比較一致,也不需要多說什麼。說明一點,visited 是一個 map,記錄遞歸過程中,比較過的“對”:

type visit struct {
    a1  unsafe.Pointer
    a2  unsafe.Pointer
    typ Type
}

map[visit]bool

比較過程中,一旦發現比較的“對”,已經在 map 里出現過的話,直接判定“深度”比較結果的是 true

總結

Go 作為一門靜態語言,相比 Python 等動態語言,在編寫過程中靈活性會受到一定的限制。但是通過介面加反射實現了類似於動態語言的能力:可以在程式運行時動態地捕獲甚至改變類型的信息和值。

Go 語言的反射實現的基礎是類型,或者說是 interface,當我們使用反射特性時,實際上用到的就是存儲在 interface 變數中的和類型相關的信息,也就是常說的 <type, value> 對。

只有 interface 才有反射的說法。

反射在 reflect 包中實現,涉及到兩個相關函數:

func TypeOf ( i interface{} ) Type
func ValueOf ( i interface{} ) Value

Type 是一個介面,定義了很多相關方法,用於獲取類型信息。Value 則持有類型的具體值。Type、Value、Interface 三者間通過函數 TypeOf,ValueOf,Interface 進行相互轉換。

最後溫習一下反射三大定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

翻譯一下:

  1. 反射將介面變數轉換成反射對象 Type 和 Value;
  2. 反射可以通過反射對象 Value 還原成原先的介面變數;
  3. 反射可以用來修改一個變數的值,前提是這個值可以被修改。

QR

參考資料

【維基百科中文】https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%B0%84_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)

【碼洞老錢 反射】https://juejin.im/post/5c2040d76fb9a049c643d9bd

【Go官方博客 reflection】https://blog.golang.org/laws-of-reflection

【GCTT譯文,不錯】https://mp.weixin.qq.com/s/dkgJ_fA0smvpv69t5Nv-7A

【json庫 源碼分析】https://zhuanlan.zhihu.com/p/37165706

【reflect 代碼例子和圖比較好】https://blog.gopheracademy.com/advent-2018/interfaces-and-reflect/

【反射使用講得不錯】https://juejin.im/post/5a75a4fb5188257a82110544

【介面和反射的關係 ,english】https://blog.gopheracademy.com/advent-2018/interfaces-and-reflect/

【總結成知識點】http://www.cnblogs.com/susufufu/p/7653579.html

【Type Value】https://colobu.com/2016/07/09/dive-into-go-13/

【講得比較清晰簡單】https://www.lijiaocn.com/%E7%BC%96%E7%A8%8B/2017/11/06/golang-reflection.html

【DeepEqual】https://github.com/Chasiny/Blog/blob/master/blog/go/package/go-reflect-deepequal.md

【反射使用場景】https://yq.aliyun.com/articles/599584


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 什麼是原子操作? 原子操作和資料庫的ACID有啥關係? AtomicInteger是怎麼實現原子操作的? AtomicInteger是有什麼缺點? ...
  • Python基礎之單例模式,內容包括 單例設計模式介紹,__new__方法,Python中的單例。其中,單例設計模式 包括 設計模式,單例模式;__new__方法 包括 __new__作用,重寫__new__方法註意,重寫new方法示例;Python中的單例 包括 單例,單例設計模式思路分析,用ne... ...
  • Python面向對象之類屬性類方法靜態方法,內容包括 類的結構,類屬性和實例屬性,類方法和靜態方法,方法綜合案例等。其中,類的結構 包括 實例,類是一個特殊的對象;類屬性和實例屬性 包括 類屬性的定義及使用,屬性的查找機制-向上查找;類方法和靜態方法 包括 類方法,靜態方法;方法綜合案例 僅包含 游... ...
  • 通過 python爬蟲入門:什麼是爬蟲,怎麼玩爬蟲? 我們知道了什麼是爬蟲 也知道了爬蟲的具體流程 那麼在我們要對某個網站進行爬取的時候 要對其數據進行分析 就要知道應該怎麼請求 就要知道獲取的數據是什麼樣的 所以我們要學會怎麼抓咪咪! 哦,不對。 我們要學會怎麼數據抓包 雖然小饅頭也是包的一種 o ...
  • 因為目前java非常火,應用非常的廣泛,是目前最火的行業之一,競爭很大,工資很高,未來發展也極好。首先告訴你的是,零基礎學習開始學習java肯定難,java的專業程度本身就不簡單,學習這事本來就是一件非常煎熬的事情,人都不願意學習,可是沒辦法,為了生存掌握一個技能,你必須學。 提醒大家幾點 一:盲目 ...
  • 目錄 1、python介紹 現在企業大量使用的有python2跟python3 安裝python解釋器,目前linux 、mac系統自帶有了,windowss系統中 打開官網 https://www.python.org/downloads/windows/ 分別有版本2和版本3 選擇自己電腦的操作 ...
  • import re def chen_chu(str): res = re.search("[^()]+",str) res_l = re.split("[*/]",res.group()) res_f_l = re.findall("[*/]",res.group()) n = None for ... ...
  • 01 內容大綱 1. is == id 用法 2. 代碼塊 3. 同一代碼塊下的緩存機制 4. 不同代碼塊下的緩存機制(小數據池) 5. 總結 6. 集合(瞭解) 7. 深淺copy 02 具體內容 1.id is == id是記憶體地址。 你只要創建一個數據(對象)那麼都會在記憶體中開闢一個空間,將這 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...