您好,登錄后才能下訂單哦!
Go 語言提供了一種機制,在編譯時不知道類型的情況下,可更新變量、在運行時查看值、調用方法以及直接對它們的布局進行操作,這種機制稱為反射(reflection)。
本篇各章節的主要內容:
關于反射的文章,下面這篇也不錯的,條條理比較清晰,可以參考。
Go語言基礎之反射:https://www.liwenzhou.com/posts/Go/13_reflect/
有時候我們需要編寫一個函數,一個有能力統一處理各種值類型的函數。而這些類型可能無法共享同一個接口,也可能布局未知,還有可能這個類型在設計函數的時候還不存在。甚至這個類型會同時存在以上多個或全部的問題。
一個熟悉的例子是 fmt.Printf 中的格式化邏輯,它可以輸出任意類型的任意值,包括用戶自定義的類型。下面嘗試寫一個與 fmt.Sprint 類似的函數,只接收一個值然后返回字符串,函數名就稱為 Sprint。
先用一個類型分支來判斷這個參數是否定義了 String 方法,如果有就調用它。然后添加一些 switch 分支來判斷參數的動態類型是否是基本類型,再對每種類型采用不同的格式化操作:
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 "???"
}
}
到此,還沒有用到反射。
對于復合數據類型,也可以添加更多的分支。但是比如數組,不用的長度就是不一樣的類型,所以這樣的類型有無限多。另外還有自定義命名的類型。當我們無法透視一個未知類型的布局時,這段代碼就無法繼續,現在就需要反射了。
反射功能由 reflect 包提供,它定義了兩個重要的類型:
reflect.Type 是一個接口,每個 Type 表示 Go 語言的一個類型。
reflect.TypeOf 函數接受 interface{} 參數,以 reflect.Type 的形式返回動態類型:
t := reflect.TypeOf(3) // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"
因為 reflect.TypeOf 返回一個接口值對應的動態類型,所以它返回的總是具體類型而不是接口類型:
var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // "*os.File"
因為輸出一個接口值的動態類型在調試和日志中很常用,所以 fmt.Printf 提供了一個簡單的方式 %T,內部的實現就是 reflect.TypeOf:
fmt.Printf("%T\n", 3) // "int"
reflect.Value 是一個結構體類型,可以包含一個任意類型的值。
reflect.ValueOf 函數接受 interface{} 參數,將接口的動態值以 reflect.Value 的形式返回。與 reflect.TypeOf 類似,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.Value 也滿足 fmt.Stringer,但除非 Value 包含的是一個字符串,否則 String 方法的結果僅僅暴露類型。通常,需要 fmt 包的 %v 功能,它會對 reflect.Value 進行特殊處理。
Value 結構體的方法
調用 Value 的 Type 方法會把它的類型以 reflect.Type 方式返回:
t := v.Type() // a reflect.Type
fmt.Println(t.String()) // "int"
reflect.ValueOf 的逆操作是 reflect.Value.Interface 方法。它返回一個 interface{},即空接口值,與 reflect.Value 包含同一個具體值:
v := reflect.ValueOf(3) // a reflect.Value
x := v.Interface() // an interface{}
i := x.(int) // an int
fmt.Printf("%d\n", i) // "3"
reflect.Value 和 interface{} 都可以包含任意的值。二者的區別是空接口隱藏了值的布局信息、內置操作和相關方法,所以除非知道它的動態類型,并用一個類型斷言來滲透進去(就如上面的代碼那樣),否則對所包含的值能做的事情很少。作為對比,Value 有很多方法可以用來分析所包含的值,而不用知道它的類型。
使用反射的技術,第二次嘗試寫一個通用的格式化函數,這次名稱叫: fotmat.Any。
不用類型分支,這里用 reflec.Value 的 Kind 方法來區分不同的類型。盡管有無限種類型,但類型的分類(kind)只有少數幾種:
最后還有一個 Invalid 類型,表示它們還沒有任何的值。(reflect.Value 的零值就屬于 Invalid 類型。)
package format
import (
"reflect"
"strconv"
)
// Any 把任何值格式化為一個字符串
func Any(value interface{}) string {
return formatAtom(reflect.ValueOf(value))
}
// formatAtom 格式化一個值,且不分析它的內部結構
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)
// ... 浮點數和負數的分支省略了...
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)。對于聚合類型和接口,只輸出值的類型。對于引用類型,輸出類型和以十六進制表示的引用地址。這個結構仍然不夠理想,下一節會繼續改進。
因為 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.Durationaegqsqibtmh)) // "[]time.Duration 0x8202b87e0"
接下來改善組合類型的顯示。這次不再實現一個 fmt.Sprint,而是實現一個稱為 Display 的調試工具函數,這個函數對給定的一個復雜值x,輸出這個復雜值的完整結構,并對找到的每個元素標上這個元素的路徑。
應當盡量避免在包的 API 里暴露反射的相關內容,之后將定義一個未導出的函數 display 來做真正的遞歸處理,再暴露 Display,而 Display 則只是一個簡單的封裝:
func Display(name string, x interface{}) {
fmt.Printf("Display %s (%T):\n", name, x)
display(name, reflect.ValueOf(x))
}
在 display 中,使用之前定義的 formatAtom 函數來輸出基礎值,直接就把這個函數搬過來了。使用 reflect. Value 的一些方法來遞歸展示復雜類型的每個組成部分。當遞歸深入是,path 字符串會增長,表示是如何達到當前值的。
上兩節的示例都是在模擬實現 fmt.Sprint,結構都是通過 strconv 包轉成字符串然后返回的。這里就直接使用 fmt 包簡化了部分邏輯:
package display
import (
"fmt"
"reflect"
"strconv"
)
// formatAtom 格式化一個值,且不分析它的內部結構
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)
// ... 浮點數和負數的分支省略了...
case reflect.Bool:
if v.Bool() {
return "true"
}
return "false"
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"
}
}
func Display(name string, x interface{}) {
fmt.Printf("Display %s (%T):\n", name, x)
display(name, reflect.ValueOf(x))
}
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: // 基本類型、通道、函數
fmt.Printf("%s = %s\n", path, formatAtom(v))
}
}
接下來對 display 函數里的類型分支逐一進行分析。
slice與數組
兩者的邏輯一致。Len 方法返回元素的個數,Index(i) 會返回第 i 個元素,返回的元素的類型為 reflect.Value(如果i越界會崩潰)。這兩個方法與內置的 len(a) 和 a[i] 序列操作類型。在每個序列上遞歸調用了 display 函數,只是在路徑后追加了 "[i]"。
盡管 reflect.Value 有很多方法,但對于每個值,只有少量的方法可以安全調用。比如,Index 方法可以在 Slice、Arrar、String 類型的值上安全調用,但對于其他類型則會崩潰。
結構體
NumField 方法可以報告結構中的字段數,Field(i) 會返回第 i 個字段,返回的字段類型為 reflect.Value。字段列表包括了從匿名字段中做了類型提升的字段。 v.Field(i)
是第i個字段的值,v.Type().Field(i)
就是第i個字段的名稱,然后再 .name 就是名稱的字符串類型。
map
MapKeys 方法返回一個元素類型為 reflect.Value 的 slice,每個元素都是一個 map 的 key。與平常遍歷 map 的結果類似,順序是不固定的。MapIndex(key) 返回 key 對應的值。這里還是忽略了一些情形,map 的 key 也可能是超出 formatAtom 能處理的合法類型,比如數組、結構體、接口都可以是合法的key。這還需要再修改一點代碼,這里就沒有做。
指針
Elem 方法返回指針指向的變量,同樣也是以 reflect.Value 類型返回。這個方法在指針是 nil 時也能正確處理,但返回的結果屬于 Invalid 類型,所以用了 IsNil 來顯式檢測空指針,方便輸出一條合適的消息。為了避免歧義,在路徑前加了 * 外邊再套一層圓括號。
接口
再次使用 IsNil 來判斷接口是否為空。然后用 v.Elem() 獲取接口的動態值。再打印出對應的類型的值。
現在 Display 已經完成了,馬上就來實際使用一下。使用下面的這樣一個復雜的結構體來進行驗證:
package main
import "gopl/ch22/display"
type Movie struct {
Title, Subtitle string
Year int
Color bool
Actor map[string]string
Oscars []string
Sequel *string
}
func main() {
strangelove := Movie{
Title: "Dr. Strangelove",
Subtitle: "How I Learned to Stop Worrying and Love the Bomb",
Year: 1964,
Color: false,
Actor: map[string]string{
"Dr. Strangelove": "Peter Sellers",
"Grp. Capt. Lionel Mandrake": "Peter Sellers",
"Pres. Merkin Muffley": "Peter Sellers",
"Gen. Buck Turgidson": "George C. Scott",
"Brig. Gen. Jack D. Ripper": "Sterling Hayden",
`Maj. T.J. "King" Kong`: "Slim Pickens",
},
Oscars: []string{
"Best Actor (Nomin.)",
"Best Adapted Screenplay (Nomin.)",
"Best Director (Nomin.)",
"Best Picture (Nomin.)",
},
}
display.Display("strangelove", strangelove)
}
執行后輸出如下:
PS G:\Steed\Documents\Go\src\gopl\ch22\desplay_demo> go run main.go
Display strangelove (main.Movie):
strangelove.Title = "Dr. Strangelove"
strangelove.Subtitle = "How I Learned to Stop Worrying and Love the Bomb"
strangelove.Year = 1964
strangelove.Color = false
strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott"
strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden"
strangelove.Actor["Maj. T.J. \"King\" Kong"] = "Slim Pickens"
strangelove.Actor["Dr. Strangelove"] = "Peter Sellers"
strangelove.Actor["Grp. Capt. Lionel Mandrake"] = "Peter Sellers"
strangelove.Actor["Pres. Merkin Muffley"] = "Peter Sellers"
strangelove.Oscars[0] = "Best Actor (Nomin.)"
strangelove.Oscars[1] = "Best Adapted Screenplay (Nomin.)"
strangelove.Oscars[2] = "Best Director (Nomin.)"
strangelove.Oscars[3] = "Best Picture (Nomin.)"
strangelove.Sequel = nil
PS G:\Steed\Documents\Go\src\gopl\ch22\desplay_demo>
調用標準庫的內部結構
還可以使用 Display 來顯示標準庫類型的內部結構,比如: *os.File:
display.Display("os.Stderr", os.Stderr)
注意,即使是非導出的字段在反射下也是可見的。
還可以把 Display 作用在 reflect.Value 上,并且觀察它如何遍歷 *os.File 的類型描述符的內部結構:
display.Display("rV", reflect.ValueOf(os.Stderr))
調用指針
這里注意如下兩個例子的差異:
var i interface{} = 3
display.Display("i", i)
// 輸出:
// Display i (int):
// i = 3
display.Display("&i", &i)
// 輸出:
// Display &i (*interface {}):
// (*&i).type = int
// (*&i).value = 3
在第一個例子中,Display 調用 reflect.ValueOf(i),返回值的類型為 int。
在第二個例子中,Display 調用 reflect.ValueOf(&i),返回的類型為 Ptr,并且是一個指向i的指針。在 Display 的 Ptr 分支中,會調用 Elem 方法,返回一個代表變量 i 的 Value,其類型為 Interface。類似這種間接獲得的 Value 可以代表任何值,包括這里的接口。這是 display 函數遞歸調用自己,輸出接口的動態類型和動態值。
在當前的這個實現中,Display 在對象圖中存在循環引用時不會自行終止。比如出差一個首尾相連的鏈表:
// 一個指向自己的結構體
type Cycle struct{ Value int; Tail *Cycle }
var c Cycle
c = Cycle{42, &c}
display.Display("c", c)
執行后會輸出一個持續增長的展開式:
Display c (main.Cycle):
c.Value = 42
(*c.Tail).Value = 42
(*(*c.Tail).Tail).Value = 42
(*(*(*c.Tail).Tail).Tail).Value = 42
(*(*(*(*c.Tail).Tail).Tail).Tail).Value = 42
很多 Go 程序都會包含一些循環引用的數據。讓 Display 支持這類成環的數據結構需要些技巧,需要額外記錄迄今訪問的路徑,相應會帶來成本。
一個通用的解決方案需要 unsafe 語言特性,在之后的 unsafe 包的示例中,會有對循環引用的處理。
還有一個相對比較容易實現的思路,限制遞歸的層數。這個不是那么通用,也不是很完美。但是不需要借助 unsafe 就可以實現。
循環引用在 fmt.Sprint 中不構成一個大問題,因為它很少嘗試輸出整個結構體。比如,當遇到一個指針時,就只簡單地輸出指針的數字值,這樣就不是引用了。但如果遇到一個 slice 或 map 包含自身,它還是會卡住,只是不值得為了這種罕見的案例而去承擔處理循環引用的成本。
Display 現在可以作為一個顯示結構化數據的調試工具,只要再稍加修改,就可以用它來對任意 Go 對象進行編碼或編排,使之成為適用于進程間通信的消息。
Go 的標準庫已經支持了各種格式,包括:JSON、XML、ASN.1。另外還有一種廣泛使用的格式是 Lisp 語言中的 S表達式。與其他格式不同的是 S表達式還沒被 Go 標準庫支持,主要是因為它沒有一個公認的標準規范。
接下來就要定義一個包用于將任意的 Go 對象編碼為 S表達式,它需要支持以下的結構:
42 integer
"hello" string (帶有Go風格的引號)
foo symbol (未用引號括起來的名字)
(1 2 3) list (括號包起來的0個或多個元素)
布爾值一般用符號 t 表示真,用空列表 () 或者符號 nil 表示假,但為了簡化,這里的實現直接忽略了布爾值。通道和函數也被忽略了,因為它們的狀態對于反射來說是不透明的。這里的實現還忽略了實數、復數和接口。(部分實現可以后續進行添加完善。)
將 Go 語言的類型編碼為S表達式的方法如下:
編碼用單個遞歸調用函數 encode 來實現。它的結構上域上一節的 Display 在本質上是一致的:
package sexpr
import (
"bytes"
"fmt"
"reflect"
)
func encode(buf *bytes.Buffer, v reflect.Value) error {
switch v.Kind() {
case reflect.Invalid:
buf.WriteString("nil")
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Fprintf(buf, "%d", v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
fmt.Fprintf(buf, "%d", v.Uint())
case reflect.String:
fmt.Fprintf(buf, "%q", v.String())
case reflect.Ptr:
return encode(buf, v.Elem())
case reflect.Array, reflect.Slice: // (value ...)
buf.WriteByte('(')
for i := 0; i < v.Len(); i++ {
if i > 0 {
buf.WriteByte(' ')
}
if err := encode(buf, v.Index(i)); err != nil {
return err
}
}
buf.WriteByte(')')
case reflect.Struct: // ((name value) ...)
buf.WriteByte('(')
for i := 0; i < v.NumField(); i++ {
if i > 0 {
buf.WriteByte(' ')
}
fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name)
if err := encode(buf, v.Field(i)); err != nil {
return err
}
buf.WriteByte(')')
}
buf.WriteByte(')')
case reflect.Map: // ((key value) ...)
buf.WriteByte('(')
for i, key := range v.MapKeys() {
if i > 0 {
buf.WriteByte(' ')
}
buf.WriteByte('(')
if err := encode(buf, key); err != nil {
return err
}
buf.WriteByte(' ')
if err := encode(buf, v.MapIndex(key)); err != nil {
return err
}
buf.WriteByte(')')
}
buf.WriteByte(')')
default: // float, complex, bool, chan, func, interface
return fmt.Errorf("unsupported type: %s", v.Type())
}
return nil
}
// Marshal 把 Go 的值編碼為 S 表達式的形式
func Marshal(v interface{}) ([]byte, error) {
buf := new(bytes.Buffer)
if err := encode(buf, reflect.ValueOf(v)); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
Marshal 函數把上面的編碼器封裝成一個 API,它類似于其他 encoding/... 包里的 API。
繼續用上一節驗證 Display 的結構體來應用到這里:
package main
import (
"fmt"
"gopl/ch22/sexpr"
"os"
)
type Movie struct {
Title, Subtitle string
Year int
// Color bool
Actor map[string]string
Oscars []string
Sequel *string
}
func main() {
strangelove := Movie{
Title: "Dr. Strangelove",
Subtitle: "How I Learned to Stop Worrying and Love the Bomb",
Year: 1964,
// Color: false,
Actor: map[string]string{
"Dr. Strangelove": "Peter Sellers",
"Grp. Capt. Lionel Mandrake": "Peter Sellers",
"Pres. Merkin Muffley": "Peter Sellers",
"Gen. Buck Turgidson": "George C. Scott",
"Brig. Gen. Jack D. Ripper": "Sterling Hayden",
`Maj. T.J. "King" Kong`: "Slim Pickens",
},
Oscars: []string{
"Best Actor (Nomin.)",
"Best Adapted Screenplay (Nomin.)",
"Best Director (Nomin.)",
"Best Picture (Nomin.)",
},
}
b, err := sexpr.Marshal(strangelove)
if err != nil {
fmt.Fprintf(os.Stderr, "sexpr.Marshal err: %v", err)
}
fmt.Println(string(b))
}
由于現在不支持布爾值,所以會返回錯誤:
PS H:\Go\src\gopl\ch22\sexpr_demo> go run main.go
sexpr.Marshal err: unsupported type: bool[]
去掉結構體和數據中的Color字段后就正常了:
PS H:\Go\src\gopl\ch22\sexpr_demo> go run main.go
((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Love the Bomb") (Year 1964) (Actor (("Dr. Strangelove" "Peter Sellers") ("Grp. Capt. Lionel Mandrake" "Peter Sellers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "George C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \"King\" Kong" "Slim Pickens"))) (Oscars ("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (Nomin.)" "Best Picture (Nomin.)")) (Sequel nil))
PS H:\Go\src\gopl\ch22\sexpr_demo>
輸出的內容非常緊湊,不適合閱讀,不過作為格式化的編碼已經實現了。如果要輸出一個帶縮進和換行的美化的格式,要重新實現一個 encode 函數。
與 fmt.Print、json.Marshal、Display 這些一樣,sexpr.Marshal 在遇到循環引用的數據時也會無限循環。
接下來還可以繼續實現一個解碼器。不過在那之前,還要先了解一下如何用反射來更新程序中的變量。都在下一篇里。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。