《Go 語言程序設計》讀書筆記(十)反射

Go語言提供了一種機制在運行時更新變量和檢查它們的值、調用它們的方法和它們支持的操做,可是在編譯時並不知道這些變量的具體類型。這種機制被稱爲反射。反射也可讓咱們將類型自己做爲第一類的值類型處理。json

在本章,咱們將探討Go語言的反射特性,看看它能夠給語言增長哪些表達力,以及在兩個相當重要的API是如何用反射機制的:一個是fmt包提供的字符串格式功能,另外一個是相似encoding/jsonencoding/xml提供的針對特定協議的編解碼功能。反射是一個複雜的內省技術,不該該隨意使用,所以,儘管上面這些包內部都是用反射技術實現的,可是它們本身的API都沒有公開反射相關的接口。數組

爲什麼須要反射

有時候咱們須要編寫一個函數可以處理任何類型,一個你們熟悉的例子是fmt.Fprintf函數提供的字符串格式化處理邏輯,它能夠對任意類型的值格式化並打印,甚至支持用戶自定義的類型。讓咱們也來嘗試實現一個相似功能的函數。爲了簡單起見,咱們的函數只接收一個參數,而後返回和fmt.Sprint相似的格式化後的字符串。咱們實現的函數名也叫Sprint安全

咱們使用了switch類型分支首先來測試輸入參數是否實現了String方法,若是是的話就使用該方法。而後繼續增長類型測試分支,檢查是不是基於string、int、bool等基礎類型的動態類型,並在每種狀況下執行相應的格式化操做。函數

func Sprint(x interface{}) string {
    type stringer interface {
        String() string
    }
    switch x := x.(type) {
    case stringer:
        return x.String()
    case string:
        return x
    case int:
        return strconv.Itoa(x)
    // ...similar cases for int16, uint32, and so on...
    case bool:
        if x {
            return "true"
        }
        return "false"
    default:
        // array, chan, func, map, pointer, slice, struct
        return "???"
    }
}

可是咱們如何處理其它相似[]float64map[string][]string等類型呢?咱們固然能夠添加更多的測試分支,可是這些組合類型的數目基本是無窮的。還有如何處理url.Values等命名的類型呢?雖然類型分支能夠識別出底層的基礎類型是map[string][]string,可是它並不匹配url.Values類型,由於它們是兩種不一樣的類型,並且switch類型分支也不可能包含每一個相似url.Values的類型,這會致使對這些庫的循環依賴。工具

沒有一種方法來檢查未知類型的表示方式,咱們被卡住了。這就是咱們爲什麼須要反射的緣由測試

reflect.Type和reflect.Value

反射是由reflect 包提供支持. 它定義了兩個重要的類型, Type 和 Value. 一個 Type 表示一個Go類型. 它是一個接口, 有許多方法來區分類型和檢查它們的組件, 例如一個結構體的成員或一個函數的參數等. 惟一能反映 reflect.Type 實現的是接口的類型描述信息, 一樣的實體標識了動態類型的接口值.ui

函數 reflect.TypeOf 接受任意的 interface{} 類型, 並返回對應動態類型的reflect.Type:url

t := reflect.TypeOf(3)  // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t)          // "int"

其中 TypeOf(3) 調用將值 3 做爲 interface{} 類型參數傳入。將一個具體的值轉爲接口類型會有一個隱式的接口轉換操做, 它會建立一個包含兩個信息的接口值: 操做數的動態類型(這裏是int)和它的動態的值(這裏是3)。spa

由於 reflect.TypeOf 返回的是一個動態類型的接口值, 它老是返回具體的類型. 所以, 下面的代碼將打印 "*os.File" 而不是 "io.Writer". 稍後, 咱們將看到 reflect.Type 是具備識別接口類型的表達方式功能的.指針

var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // "*os.File"

要注意的是 reflect.Type 接口是知足 fmt.Stringer 接口的. 由於打印動態類型值對於調試和日誌是有幫助的, fmt.Printf 提供了一個簡短的 %T 標誌參數, 內部使用 reflect.TypeOf 的結果輸出:

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

reflect 包中另外一個重要的類型是 Value. 一個 reflect.Value 能夠持有一個任意類型的值. 函數 reflect.ValueOf 接受任意的 interface{} 類型, 並返回對應動態類型的reflect.Value. 和 reflect.TypeOf 相似, reflect.ValueOf 返回的結果也是對於具體的類型, 可是 reflect.Value 也能夠持有一個接口值.

v := reflect.ValueOf(3) // a reflect.Value
fmt.Println(v)          // "3"
fmt.Printf("%v\n", v)   // "3"
fmt.Println(v.String()) // NOTE: "<int Value>"

和 reflect.Type 相似, reflect.Value 也知足 fmt.Stringer 接口, 可是除非 Value 持有的是字符串, 不然 String 只是返回具體的類型. 使用 fmt 包的 %v 標誌參數, 將使用 reflect.Values 的結果格式化.

調用 Value 的 Type 方法將返回具體類型所對應的 reflect.Type:

t := v.Type()           // a reflect.Type
fmt.Println(t.String()) // "int"

一個 reflect.Value 和 interface{} 都能保存任意的值. 所不一樣的是, 一個空的接口隱藏了值對應的表示方式和全部的公開的方法, 所以只有咱們知道具體的動態類型才能使用類型斷言來訪問內部的值(就像上面那樣), 對於內部值並無特別可作的事情. 相比之下, 一個 reflect.Value 則有不少方法來檢查其內容, 不管它的具體類型是什麼. 讓咱們再次嘗試實現咱們的格式化函數 format.Any.

咱們使用 reflect.Value 的 Kind 方法來替代以前的類型 switch. 雖然仍是有無窮多的類型, 可是它們的kinds類型倒是有限的: Bool, String 和 全部數字類型的基礎類型; Array 和 Struct 對應的聚合類型; Chan, Func, Ptr, Slice, 和 Map 對應的引用相似; 接口類型; 還有表示空值的無效類型. (空的 reflect.Value 對應 Invalid 無效類型.)

package format

import (
    "reflect"
    "strconv"
)

// Any formats any value as a string.
func Any(value interface{}) string {
    return formatAtom(reflect.ValueOf(value))
}

// formatAtom formats a value without inspecting its internal structure.
func formatAtom(v reflect.Value) string {
    switch v.Kind() {
    case reflect.Invalid:
        return "invalid"
    case reflect.Int, reflect.Int8, reflect.Int16,
        reflect.Int32, reflect.Int64:
        return strconv.FormatInt(v.Int(), 10)
    case reflect.Uint, reflect.Uint8, reflect.Uint16,
        reflect.Uint32, reflect.Uint64, reflect.Uintptr:
        return strconv.FormatUint(v.Uint(), 10)
    // ...floating-point and complex cases omitted for brevity...
    case reflect.Bool:
        return strconv.FormatBool(v.Bool())
    case reflect.String:
        return strconv.Quote(v.String())
    case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
        return v.Type().String() + " 0x" +
            strconv.FormatUint(uint64(v.Pointer()), 16)
    default: // reflect.Array, reflect.Struct, reflect.Interface
        return v.Type().String() + " value"
    }
}

到目前爲止, 咱們的函數將每一個值視做一個不可分割沒有內部結構的, 所以它叫 formatAtom. 對於聚合類型(結構體和數組)只是打印類型的值, 對於引用類型(channels, functions, pointers, slices, 和 maps), 用十六進制打印類型的引用地址. 雖然還不夠理想, 可是依然是一個重大的進步, 而且 Kind 只關心底層表示, format.Any 也支持新命名的類型. 例如:

var x int64 = 1
var d time.Duration = 1 * time.Nanosecond
fmt.Println(format.Any(x))                  // "1"
fmt.Println(format.Any(d))                  // "1"
fmt.Println(format.Any([]int64{x}))         // "[]int64 0x8202b87b0"
fmt.Println(format.Any([]time.Duration{d})) // "[]time.Duration 0x8202b87e0"

反射訪問聚合類型

接下來,讓咱們看看如何改善聚合數據類型的顯示。咱們像構建一個用於調試用的Display函數,給定一個聚合類型x,打印這個值對應的完整的結構,同時記錄每一個發現的每一個元素的路徑。

在可能的狀況下,你應該避免在一個包中暴露和反射相關的接口。咱們將定義一個未導出的display函數用於遞歸處理工做,導出的是Display函數,它只是display函數簡單的包裝以接受interface{}類型的參數:

func Display(name string, x interface{}) {
    fmt.Printf("Display %s (%T):\n", name, x)
    display(name, reflect.ValueOf(x))
}

在display函數中,咱們使用了前面定義的打印基礎類型——基本類型、函數和chan等——元素值的formatAtom函數,可是咱們會使用reflect.Value的方法來遞歸顯示聚合類型的每個成員或元素。

func display(path string, v reflect.Value) {
    switch v.Kind() {
    case reflect.Invalid:
        fmt.Printf("%s = invalid\n", path)
    case reflect.Slice, reflect.Array:
        for i := 0; i < v.Len(); i++ {
            display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))
        }
    case reflect.Struct:
        for i := 0; i < v.NumField(); i++ {
            fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
            display(fieldPath, v.Field(i))
        }
    case reflect.Map:
        for _, key := range v.MapKeys() {
            display(fmt.Sprintf("%s[%s]", path,
                formatAtom(key)), v.MapIndex(key))
        }
    case reflect.Ptr:
        if v.IsNil() {
            fmt.Printf("%s = nil\n", path)
        } else {
            display(fmt.Sprintf("(*%s)", path), v.Elem())
        }
    case reflect.Interface:
        if v.IsNil() {
            fmt.Printf("%s = nil\n", path)
        } else {
            fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
            display(path+".value", v.Elem())
        }
    default: // basic types, channels, funcs
        fmt.Printf("%s = %s\n", path, formatAtom(v))
    }
}

讓咱們針對不一樣類型分別討論。

Slice和數組: 兩種的處理邏輯是同樣的。Len方法返回slice或數組值中的元素個數,Index(i)活動索引i對應的元素,返回的也是一個reflect.Value類型的值;若是索引i超出範圍的話將致使panic異常,這些行爲和數組或slice類型內建的len(a)a[i]等操做相似。display針對序列中的每一個元素遞歸調用自身處理,咱們經過在遞歸處理時向path附加「[i]」來表示訪問路徑。

雖然reflect.Value類型帶有不少方法,可是隻有少數的方法對任意值都是能夠安全調用的。例如,Index方法只能對Slice、數組或字符串類型的值調用,其它類型若是調用將致使panic異常。

結構體: NumField方法報告結構體中成員的數量,Field(i)reflect.Value類型返回第i個成員的值。成員列表包含了匿名成員在內的所有成員。經過在path添加「.f」來表示成員路徑,咱們必須得到結構體對應的reflect.Type類型信息,包含結構體類型和第i個成員的名字。要注意的是,結構體中未導出的成員對反射也是可見的。

Maps: MapKeys方法返回一個reflect.Value類型的slice,每個都對應map的能夠。和往常同樣,遍歷map時順序是隨機的。MapIndex(key)返回map中key對應的value。咱們向path添加「[key]」來表示訪問路徑。

指針: Elem方法返回指針指向的變量,仍是reflect.Value類型。即便指針是nil,這個操做也是安全的,在這種狀況下指針是Invalid無效類型,可是咱們能夠用IsNil方法來顯式地測試一個空指針,這樣咱們能夠打印更合適的信息。咱們在path前面添加「*」,並用括弧包含以免歧義。

接口: 再一次,咱們使用IsNil方法來測試接口是不是nil,若是不是,咱們能夠調用v.Elem()來獲取接口對應的動態值,而且打印對應的類型和值。

獲取結構體成員標籤

咱們使用構體成員標籤用於設置對應JSON對應的名字。其中json成員標籤讓咱們能夠選擇成員的名字和抑制零值成員的輸出。在本節,咱們將看到若是經過反射機制獲取成員標籤。

結構體類型的 reflect.Value的reflect.Type的Field方法將返回一個reflect.StructField,裏面含有每一個成員的名字、類型和可選的成員標籤等信息。其中成員標籤信息對應reflect.StructTag類型的字符串,而且它提供了Get方法用於解析和根據特定key提取子串,例以下面的http:"..."形式的子串。

下面的search函數是一個HTTP請求處理函數。它定義了一個匿名結構體類型的變量,用結構體的每一個成員表示HTTP請求的參數。其中結構體成員標籤指明瞭對於請求參數的名字,爲了減小URL的長度這些參數名一般都是神祕的縮略詞。Unpack將請求參數填充到合適的結構體成員中,這樣咱們能夠方便地經過合適的類型類來訪問這些參數。

import "gopl.io/ch12/params"

// search implements the /search URL endpoint.
func search(resp http.ResponseWriter, req *http.Request) {
    var data struct {
        Labels     []string `http:"l"`
        MaxResults int      `http:"max"`
        Exact      bool     `http:"x"`
    }
    data.MaxResults = 10 // set default
    if err := params.Unpack(req, &data); err != nil {
        http.Error(resp, err.Error(), http.StatusBadRequest) // 400
        return
    }

    // ...rest of handler...
    fmt.Fprintf(resp, "Search: %+v\n", data)
}
// Unpack populates the fields of the struct pointed to by ptr
// from the HTTP request parameters in req.
func Unpack(req *http.Request, ptr interface{}) error {
    if err := req.ParseForm(); err != nil {
        return err
    }

    // Build map of fields keyed by effective name.
    fields := make(map[string]reflect.Value)
    v := reflect.ValueOf(ptr).Elem() // the struct variable
    for i := 0; i < v.NumField(); i++ {
        fieldInfo := v.Type().Field(i) // a reflect.StructField
        tag := fieldInfo.Tag           // a reflect.StructTag
        name := tag.Get("http")
        if name == "" {
            name = strings.ToLower(fieldInfo.Name)
        }
        fields[name] = v.Field(i)
    }

    // Update struct field for each parameter in the request.
    for name, values := range req.Form {
        f := fields[name]
        if !f.IsValid() {
            continue // ignore unrecognized HTTP parameters
        }
        for _, value := range values {
            if f.Kind() == reflect.Slice {
                elem := reflect.New(f.Type().Elem()).Elem()
                if err := populate(elem, value); err != nil {
                    return fmt.Errorf("%s: %v", name, err)
                }
                f.Set(reflect.Append(f, elem))
            } else {
                if err := populate(f, value); err != nil {
                    return fmt.Errorf("%s: %v", name, err)
                }
            }
        }
    }
    return nil
}

注:咱們能夠經過調用reflect.ValueOf(&x).Elem(),來獲取任意變量x對應的可取地址的Value。

顯示類型的方法集

reflect.Type和reflect.Value都提供了一個Method方法。每次t.Method(i)調用將一個reflect.Method的實例,對應一個用於描述一個方法的名稱和類型的結構體。每次v.Method(i)方法調用都返回一個reflect.Value以表示對應的值,也就是說一個方法是綁定到它的接收者的。使用reflect.Value.Call方法,將能夠調用一個Func類型的Value,可是下面這個例子中只用到了它的類型。

咱們的最後一個例子是使用reflect.Type來打印任意值的類型和枚舉它的方法:

func Print(x interface{}) {
    v := reflect.ValueOf(x)
    t := v.Type()
    fmt.Printf("type %s\n", t)

    for i := 0; i < v.NumMethod(); i++ {
        methType := v.Method(i).Type()
        fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,
            strings.TrimPrefix(methType.String(), "func"))
    }
}

下面是屬於time.Duration*strings.Replacer兩個類型的方法:

methods.Print(time.Hour)
// Output:
// type time.Duration
// func (time.Duration) Hours() float64
// func (time.Duration) Minutes() float64
// func (time.Duration) Nanoseconds() int64
// func (time.Duration) Seconds() float64
// func (time.Duration) String() string

methods.Print(new(strings.Replacer))
// Output:
// type *strings.Replacer
// func (*strings.Replacer) Replace(string) string
// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)

反射的使用建議

經過反射能夠實現哪些功能。反射是一個強大並富有表達力的工具,可是它應該被當心地使用。

基於反射的代碼是比較脆弱的,對於每個會致使編譯器報告類型錯誤的問題,在反射中都有與之相對應的問題,不一樣的是編譯器會在構建時立刻報告錯誤,而反射則是在真正運行到的時候纔會拋出panic異常,多是寫完代碼好久以後的時候了,並且程序也可能運行了很長的時間。絕大多數使用反射的程序都須要很是當心地檢查每一個reflect.Value對於值的類型、是否可取地址,還有是否能夠被修改等。

避免這種因反射而致使的脆弱性的問題的最好方法是將全部的反射相關的使用控制在包的內部,若是可能的話避免在包的API中直接暴露reflect.Value類型,這樣能夠限制一些非法輸入。若是沒法作到這一點,在每一個有風險的操做前應作額外的類型檢查。以標準庫中的代碼爲例,當fmt.Printf收到一個非法的操做數是,它並不會拋出panic異常,而是打印相關的錯誤信息。程序雖然還有BUG,可是會更加容易診斷。

fmt.Printf("%d %s\n", "hello", 42) // "%!d(string=hello) %!s(int=42)"

反射一樣下降了程序的安全性,還影響了自動化重構和分析工具的準確性,由於它們沒法識別運行時才能確認的類型信息。

相關文章
相關標籤/搜索