反射是 Go 語言的高級主題之一。我會盡可能讓它變得簡單易懂。
本教程分為如下小節(jié)。
- 什么是反射?
- 為何需要檢查變量,確定變量的類型?
- reflect 包
- reflect.Type 和 reflect.Value
- reflect.Kind
- NumField() 和 Field() 方法
- Int() 和 String() 方法
- 完整的程序
- 我們應(yīng)該使用反射嗎?
讓我們來逐個討論這些章節(jié)。
什么是反射?
反射就是程序能夠在運行時檢查變量和值,求出它們的類型。你可能還不太懂,這沒關(guān)系。在本教程結(jié)束后,你就會清楚地理解反射,所以跟著我們的教程學(xué)習(xí)吧。
為何需要檢查變量,確定變量的類型?
在學(xué)習(xí)反射時,所有人首先面臨的疑惑就是:如果程序中每個變量都是我們自己定義的,那么在編譯時就可以知道變量類型了,為什么我們還需要在運行時檢查變量,求出它的類型呢?沒錯,在大多數(shù)時候都是這樣,但并非總是如此。
我來解釋一下吧。下面我們編寫一個簡單的程序。
package main
import (
"fmt"
)
func main() {
i := 10
fmt.Printf("%d %T", i, i)
}
在 playground 上運行
在上面的程序中,i 的類型在編譯時就知道了,然后我們在下一行打印出 i。這里沒什么特別之處。
現(xiàn)在了解一下,需要在運行時求得變量類型的情況。假如我們要編寫一個簡單的函數(shù),它接收結(jié)構(gòu)體作為參數(shù),并用它來創(chuàng)建一個 SQL 插入查詢。
考慮下面的程序:
package main
import (
"fmt"
)
type order struct {
ordId int
customerId int
}
func main() {
o := order{
ordId: 1234,
customerId: 567,
}
fmt.Println(o)
}
在 playground 上運行
在上面的程序中,我們需要編寫一個函數(shù),接收結(jié)構(gòu)體變量 o 作為參數(shù),返回下面的 SQL 插入查詢。
insert into order values(1234, 567)
這個函數(shù)寫起來很簡單。我們現(xiàn)在編寫這個函數(shù)。
package main
import (
"fmt"
)
type order struct {
ordId int
customerId int
}
func createQuery(o order) string {
i := fmt.Sprintf("insert into order values(%d, %d)", o.ordId, o.customerId)
return i
}
func main() {
o := order{
ordId: 1234,
customerId: 567,
}
fmt.Println(createQuery(o))
}
在 playground 上運行
在第 12 行,createQuery 函數(shù)用 o 的兩個字段(ordId 和 customerId),創(chuàng)建了插入查詢。該程序會輸出:
insert into order values(1234, 567)
現(xiàn)在我們來升級這個查詢生成器。如果我們想讓它變得通用,可以適用于任何結(jié)構(gòu)體類型,該怎么辦呢?我們用程序來理解一下。
package main
type order struct {
ordId int
customerId int
}
type employee struct {
name string
id int
address string
salary int
country string
}
func createQuery(q interface{}) string {
}
func main() {
}
我們的目標就是完成 createQuery 函數(shù)(上述程序中的第 16 行),它可以接收任何結(jié)構(gòu)體作為參數(shù),根據(jù)結(jié)構(gòu)體的字段創(chuàng)建插入查詢。
例如,如果我們傳入下面的結(jié)構(gòu)體:
o := order {
ordId: 1234,
customerId: 567
}
createQuery 函數(shù)應(yīng)該返回:
insert into order values (1234, 567)
類似地,如果我們傳入:
e := employee {
name: "Naveen",
id: 565,
address: "Science Park Road, Singapore",
salary: 90000,
country: "Singapore",
}
該函數(shù)會返回:
insert into employee values("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")
由于 createQuery 函數(shù)應(yīng)該適用于任何結(jié)構(gòu)體,因此它接收 interface{} 作為參數(shù)。為了簡單起見,我們只處理包含 string 和 int 類型字段的結(jié)構(gòu)體,但可以擴展為包含任何類型的字段。
createQuery 函數(shù)應(yīng)該適用于所有的結(jié)構(gòu)體。因此,要編寫這個函數(shù),就必須在運行時檢查傳遞過來的結(jié)構(gòu)體參數(shù)的類型,找到結(jié)構(gòu)體字段,接著創(chuàng)建查詢。這時就需要用到反射了。在本教程的下一步,我們將會學(xué)習(xí)如何使用 reflect 包來實現(xiàn)它。
reflect 包
在 Go 語言中,reflect 實現(xiàn)了運行時反射。reflect 包會幫助識別 interface{} 變量的底層具體類型和具體值。這正是我們所需要的。createQuery 函數(shù)接收 interface{} 參數(shù),根據(jù)它的具體類型和具體值,創(chuàng)建 SQL 查詢。這正是 reflect 包能夠幫助我們的地方。
在編寫我們通用的查詢生成器之前,我們首先需要了解 reflect 包中的幾種類型和方法。讓我們來逐個了解。
reflect.Type 和 reflect.Value
reflect.Type 表示 interface{} 的具體類型,而 reflect.Value 表示它的具體值。reflect.TypeOf() 和 reflect.ValueOf() 兩個函數(shù)可以分別返回 reflect.Type 和 reflect.Value。這兩種類型是我們創(chuàng)建查詢生成器的基礎(chǔ)。我們現(xiàn)在用一個簡單的例子來理解這兩種類型。
package main
import (
"fmt"
"reflect"
)
type order struct {
ordId int
customerId int
}
func createQuery(q interface{}) {
t := reflect.TypeOf(q)
v := reflect.ValueOf(q)
fmt.Println("Type ", t)
fmt.Println("Value ", v)
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
}
在 playground 上運行
在上面的程序中,第 13 行的 createQuery 函數(shù)接收 interface{} 作為參數(shù)。在第 14 行,reflect.TypeOf 接收了參數(shù) interface{},返回了reflect.Type,它包含了傳入的 interface{} 參數(shù)的具體類型。同樣地,在第 15 行,reflect.ValueOf 函數(shù)接收參數(shù) interface{},并返回了 reflect.Value,它包含了傳來的 interface{} 的具體值。
上述程序會打?。?/p>
Type main.order
Value {456 56}
從輸出我們可以看到,程序打印了接口的具體類型和具體值。
relfect.Kind
reflect 包中還有一個重要的類型:Kind。
在反射包中,Kind 和 Type 的類型可能看起來很相似,但在下面程序中,可以很清楚地看出它們的不同之處。
package main
import (
"fmt"
"reflect"
)
type order struct {
ordId int
customerId int
}
func createQuery(q interface{}) {
t := reflect.TypeOf(q)
k := t.Kind()
fmt.Println("Type ", t)
fmt.Println("Kind ", k)
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
}
在 playground 上運行
上述程序會輸出:
Type main.order
Kind struct
我想你應(yīng)該很清楚兩者的區(qū)別了。Type 表示 interface{} 的實際類型(在這里是 main.Order),而 Kind 表示該類型的特定類別(在這里是 struct)。
NumField() 和 Field() 方法
NumField() 方法返回結(jié)構(gòu)體中字段的數(shù)量,而 Field(i int) 方法返回字段 i 的 reflect.Value。
package main
import (
"fmt"
"reflect"
)
type order struct {
ordId int
customerId int
}
func createQuery(q interface{}) {
if reflect.ValueOf(q).Kind() == reflect.Struct {
v := reflect.ValueOf(q)
fmt.Println("Number of fields", v.NumField())
for i := 0; i v.NumField(); i++ {
fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
}
}
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
}
在 playground 上運行
在上面的程序中,因為 NumField 方法只能在結(jié)構(gòu)體上使用,我們在第 14 行首先檢查了 q 的類別是 struct。程序的其他代碼很容易看懂,不作解釋。該程序會輸出:
Number of fields 2
Field:0 type:reflect.Value value:456
Field:1 type:reflect.Value value:56
Int() 和 String() 方法
Int 和 String 可以幫助我們分別取出 reflect.Value 作為 int64 和 string。
package main
import (
"fmt"
"reflect"
)
func main() {
a := 56
x := reflect.ValueOf(a).Int()
fmt.Printf("type:%T value:%v\n", x, x)
b := "Naveen"
y := reflect.ValueOf(b).String()
fmt.Printf("type:%T value:%v\n", y, y)
}
在 playground 上運行
在上面程序中的第 10 行,我們?nèi)〕?reflect.Value,并轉(zhuǎn)換為 int64,而在第 13 行,我們?nèi)〕?reflect.Value 并將其轉(zhuǎn)換為 string。該程序會輸出:
type:int64 value:56
type:string value:Naveen
完整的程序
現(xiàn)在我們已經(jīng)具備足夠多的知識,來完成我們的查詢生成器了,我們來實現(xiàn)它把。
package main
import (
"fmt"
"reflect"
)
type order struct {
ordId int
customerId int
}
type employee struct {
name string
id int
address string
salary int
country string
}
func createQuery(q interface{}) {
if reflect.ValueOf(q).Kind() == reflect.Struct {
t := reflect.TypeOf(q).Name()
query := fmt.Sprintf("insert into %s values(", t)
v := reflect.ValueOf(q)
for i := 0; i v.NumField(); i++ {
switch v.Field(i).Kind() {
case reflect.Int:
if i == 0 {
query = fmt.Sprintf("%s%d", query, v.Field(i).Int())
} else {
query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())
}
case reflect.String:
if i == 0 {
query = fmt.Sprintf("%s\"%s\"", query, v.Field(i).String())
} else {
query = fmt.Sprintf("%s, \"%s\"", query, v.Field(i).String())
}
default:
fmt.Println("Unsupported type")
return
}
}
query = fmt.Sprintf("%s)", query)
fmt.Println(query)
return
}
fmt.Println("unsupported type")
}
func main() {
o := order{
ordId: 456,
customerId: 56,
}
createQuery(o)
e := employee{
name: "Naveen",
id: 565,
address: "Coimbatore",
salary: 90000,
country: "India",
}
createQuery(e)
i := 90
createQuery(i)
}
在 playground 上運行
在第 22 行,我們首先檢查了傳來的參數(shù)是否是一個結(jié)構(gòu)體。在第 23 行,我們使用了 Name() 方法,從該結(jié)構(gòu)體的 reflect.Type 獲取了結(jié)構(gòu)體的名字。接下來一行,我們用 t 來創(chuàng)建查詢。
在第 28 行,case 語句 檢查了當前字段是否為 reflect.Int,如果是的話,我們會取到該字段的值,并使用 Int() 方法轉(zhuǎn)換為 int64。if else 語句用于處理邊界情況。請?zhí)砑尤罩緛砝斫鉃槭裁葱枰?。在?34 行,我們用來相同的邏輯來取到 string。
我們還作了額外的檢查,以防止 createQuery 函數(shù)傳入不支持的類型時,程序發(fā)生崩潰。程序的其他代碼是自解釋性的。我建議你在合適的地方添加日志,檢查輸出,來更好地理解這個程序。
該程序會輸出:
insert into order values(456, 56)
insert into employee values("Naveen", 565, "Coimbatore", 90000, "India")
unsupported type
至于向輸出的查詢中添加字段名,我們把它留給讀者作為練習(xí)。請嘗試著修改程序,打印出以下格式的查詢。
insert into order(ordId, customerId) values(456, 56)
我們應(yīng)該使用反射嗎?
我們已經(jīng)展示了反射的實際應(yīng)用,現(xiàn)在考慮一個很現(xiàn)實的問題。我們應(yīng)該使用反射嗎?我想引用 Rob Pike 關(guān)于使用反射的格言,來回答這個問題。
清晰優(yōu)于聰明。而反射并不是一目了然的。
反射是 Go 語言中非常強大和高級的概念,我們應(yīng)該小心謹慎地使用它。使用反射編寫清晰和可維護的代碼是十分困難的。你應(yīng)該盡可能避免使用它,只在必須用到它時,才使用反射。
本教程到此結(jié)束。希望你們喜歡。祝你愉快。希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- 詳解Golang編程中的常量與變量
- Golang學(xué)習(xí)筆記(二):類型、變量、常量
- GO語言中的常量
- Go語言中常量定義方法實例分析
- GOLANG使用Context實現(xiàn)傳值、超時和取消的方法
- GOLANG使用Context管理關(guān)聯(lián)goroutine的方法
- Go并發(fā)調(diào)用的超時處理的方法
- Go routine調(diào)度詳解
- 淺談GoLang幾種讀文件方式的比較
- Golang常量iota的使用實例