Go语言与数据库开发:01-11
反射
Go語言提供了一種機制在運行時更新變量和檢查它們的值、調用它們的方法和它們支持的內
在操作,但是在編譯時并不知道這些變量的具體類型。這種機制被稱為反射。反射也可以讓
我們將類型本身作為第一類的值類型處理。
Go語言的反射特性,看看它可以給語言增加哪些表達力,以及在兩個至關重要的API是如何用
反射機制的:一個是fmt包提供的字符串格式功能,另一個是類似encoding/json和encoding/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 "???"
}
}
但是我們如何處理其它類似[]float64、map[string][]string等類型呢?我們當然可以添加更多的
測試分支,但是這些組合類型的數目基本是無窮的。還有如何處理url.Values等命名的類型
呢?雖然類型分支可以識別出底層的基礎類型是map[string][]string,但是它并不匹配
url.Values類型,因為它們是兩種不同的類型,而且switch類型分支也不可能包含每個類似
url.Values的類型,這會導致對這些庫的循環依賴。
沒有一種方法來檢查未知類型的表示方式,我們被卡住了。這就是我們為何需要反射的原
因。
reflect.Type和reflect.Value
反射是由 reflect 包提供支持. 它定義了兩個重要的類型, Type 和 Value. 一個 Type 表示一個
Go類型. 它是一個接口, 有許多方法來區分類型和檢查它們的組件, 例如一個結構體的成員或
一個函數的參數等. 唯一能反映 reflect.Type 實現的是接口的類型描述信息(§7.5), 同樣的實體
標識了動態類型的接口值.
函數 reflect.TypeOf 接受任意的 interface{} 類型, 并返回對應動態類型的reflect.Type:
t := reflect.TypeOf(3) // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"
其中 TypeOf(3) 調用將值 3 作為 interface{} 類型參數傳入.
因為 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("%Tn", 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("%vn", v) // "3"
fmt.Println(v.String()) // NOTE: ""
和 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.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("%dn", i) // "3"
一個 reflect.Value 和 interface{} 都能保存任意的值. 所不同的是, 一個空的接口隱藏了值對應
的表示方式和所有的公開的方法, 因此只有我們知道具體的動態類型才能使用類型斷言來訪問
內部的值(就像上面那樣), 對于內部值并沒有特別可做的事情. 相比之下, 一個 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"
}
}
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.Durationze8trgl8bvbq)) // "[]time.Duration 0x8202b87e0"
Display遞歸打印
接下來,讓我們看看如何改善聚合數據類型的顯示。我們并不想完全克隆一個fmt.Sprint函
數,我們只是像構建一個用于調式用的Display函數,給定一個聚合類型x,打印這個值對應的
完整的結構,同時記錄每個發現的每個元素的路徑。讓我們從一個例子開始。
e, _ := eval.Parse("sqrt(A / pi)")
Display("e", e)
Display函數的輸出如下:
Display e (eval.call):
e.fn = "sqrt"
e.args[0].type = eval.binary
e.args[0].value.op = 47
e.args[0].value.x.type = eval.Var
e.args[0].value.x.value = "A"
e.args[0].value.y.type = eval.Var
e.args[0].value.y.value = "pi"
在可能的情況下,你應該避免在一個包中暴露和反射相關的接口。我們將定義一個未導出的
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的方法來遞歸顯示聚合類型的每一個成
員或元素。在遞歸下降過程中,path字符串,從最開始傳入的起始值(這里是“e”),將逐步
增長以表示如何達到當前值(例如“e.args[0].value”)。
因為我們不再模擬fmt.Sprint函數,我們將直接使用fmt包來簡化我們的例子實現。
func display(path string, v reflect.Value) {
switch v.Kind() {
case reflect.Invalid:
fmt.Printf("%s = invalidn", 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 = niln", path)
} else {
display(fmt.Sprintf("(*%s)", path), v.Elem())
}
case reflect.Interface:
if v.IsNil() {
fmt.Printf("%s = niln", path)
} else {
fmt.Printf("%s.type = %sn", path, v.Elem().Type())
display(path+".value", v.Elem())
}
default: // basic types, channels, funcs
fmt.Printf("%s = %sn", 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()來
獲取接口對應的動態值,并且打印對應的類型和值。
現在我們的Display函數總算完工了,讓我們看看它的表現吧。
type Movie struct {
Title, Subtitle string
Year int
Color bool
Actor map[string]string
Oscars []string
Sequel *string
}
讓我們聲明一個該類型的變量,然后看看Display函數如何顯示它:
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("strangelove", strangelove)調用將顯示(strangelove電影對應的中文名是《奇愛博
士》):
Display strangelove (display.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
我們也可以使用Display函數來顯示標準庫中類型的內部結構,例如 *os.File 類型:
Display("os.Stderr", os.Stderr)
// Output:
// Display os.Stderr (*os.File):
// ((os.Stderr).file).fd = 2
// ((os.Stderr).file).name = "/dev/stderr"
// ((os.Stderr).file).nepipe = 0
要注意的是,結構體中未導出的成員對反射也是可見的。需要當心的是這個例子的輸出在不
同操作系統上可能是不同的,并且隨著標準庫的發展也可能導致結果不同。(這也是將這些
成員定義為私有成員的原因之一!)我們深圳可以用Display函數來顯示reflect.Value,來查
看 *os.File 類型的內部表示方式。 Display("rV", reflect.ValueOf(os.Stderr)) 調用的輸出如
下,當然不同環境得到的結果可能有差異:
Display rV (reflect.Value):
(*rV.typ).size = 8
(*rV.typ).hash = 871609668
(*rV.typ).align = 8
(*rV.typ).fieldAlign = 8
(*rV.typ).kind = 22
((rV.typ).string) = "*os.File"
(((*rV.typ).uncommonType).methods[0].name) = "Chdir"
((((rV.typ).uncommonType).methods[0].mtyp).string) = "func() error"
((((rV.typ).uncommonType).methods[0].typ).string) = "func(*os.File) error"
...
觀察下面兩個例子的區別:
var i interface{} = 3
Display("i", i)
// Output:
// Display i (int):
// i = 3
Display("&i", &i)
// Output:
// Display &i (*interface {}):
// (*&i).type = int
// (*&i).value = 3
在第一個例子中,Display函數將調用reflect.ValueOf(i),它返回一個Int類型的值。
reflect.ValueOf總是返回一個值的具體類型,因為它是從一個接口值提取的內容。
在第二個例子中,Display函數調用的是reflect.ValueOf(&i),它返回一個指向i的指針,對應Ptr
類型。在switch的Ptr分支中,通過調用Elem來返回這個值,返回一個Value來表示i,對應
Interface類型。一個間接獲得的Value,就像這一個,可能代表任意類型的值,包括接口類
型。內部的display函數遞歸調用自身,這次它將打印接口的動態類型和值。
目前的實現,Display如果顯示一個帶環的數據結構將會陷入死循環,例如首位項鏈的鏈表:
// a struct that points to itself
type Cycle struct{ Value int; Tail *Cycle }
var c Cycle
c = Cycle{42, &c}
Display("c", c)
Display會永遠不停地進行深度遞歸打印:
Display c (display.Cycle):
c.Value = 42
(*c.Tail).Value = 42
((c.Tail).Tail).Value = 42
(((*c.Tail).Tail).Tail).Value = 42
...ad infinitum...
許多Go語言程序都包含了一些循環的數據結果。Display支持這類帶環的數據結構是比較棘手
的,需要增加一個額外的記錄訪問的路徑;代價是昂貴的。
帶環的數據結構很少會對fmt.Sprint函數造成問題,因為它很少嘗試打印完整的數據結構。例
如,當它遇到一個指針的時候,它只是簡單第打印指針的數值。雖然,在打印包含自身的
slice或map時可能遇到困難,但是不保證處理這種是罕見情況卻可以避免額外的麻煩。
總結
以上是生活随笔為你收集整理的Go语言与数据库开发:01-11的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C/C++ 类型内存占用详解
- 下一篇: 软件工程敏捷开发01