- 原文地址:Part 34: Reflection
- 原文作者:Naveen R
- 譯者:咔嘰咔嘰 轉載請註明出處。
反射是 Go 的高階主題之一,我會盡量讓它變得簡單。
本教程包含以下部分。
- 什麼是反射?
- 檢查變數並找到其型別需要做什麼?
reflect
包reflect.Type
和reflect.Value
reflect.Kind
NumField()
和Field()
方法Int()
和String()
方法
- 完整的程式
- 應該使用反射嗎?
我們現在一個一個來討論這些部分。
什麼是反射
反射是為了程式在執行時檢查其變數和值並找到其型別。你可能不明白這意味著什麼,但沒關係。在本教程結束時,您將清楚地瞭解反射,請跟緊我。
檢查變數並找到其型別需要做什麼
在學習反射時,任何人都會想到的第一個問題是,為什麼我們需要檢查變數並在執行時找到它的型別,因為我們的程式中的每個變數都由我們定義的,我們在編譯時就知道它的型別。嗯,大部分時間都是如此,但並非總是如此。
我們用一個簡單的程式來解釋一下我的意思。
package main
import (
"fmt"
)
func main() {
i := 10
fmt.Printf("%d %T", i, i)
}
複製程式碼
在上面的程式中,i
的型別在編譯時是已知的,我們在下一行列印它。這裡沒什麼神奇的。
現在讓我們理解在執行時知道變數型別的必要性。假設我們想編寫一個簡單的函式,它將結構作為引數,並使用它建立一個 SQL 插入查詢。
看看下的程式碼,
package main
import (
"fmt"
)
type order struct {
ordId int
customerId int
}
func main() {
o := order{
ordId: 1234,
customerId: 567,
}
fmt.Println(o)
}
複製程式碼
我們需要編寫一個函式,它將上面程式中的結構 o
作為引數並返回以下 SQL 插入語句,
insert into order values(1234, 567)
複製程式碼
這個功能很容易實現。讓我們現在就搞。
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))
}
複製程式碼
第 12 行中的createQuery
函式,使用o
的ordId
和customerId
欄位建立插入查詢。該程式將輸出,
insert into order values(1234, 567)
複製程式碼
現在讓我們的createQuery
進入下一個級別。如果我們想要概括我們的createQuery
並使其適用於任何結構,該怎麼辦?讓我解釋一下我使用程式的意思。
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() {
}
複製程式碼
我們的目標是完成以上程式第 16 行的createQuery
函式,以便它將任何結構作為引數,並基於結構欄位建立插入查詢。
例如,如果我們傳遞下面的結構,
o := order {
ordId: 1234,
customerId: 567
}
複製程式碼
我們的createQuery
函式應該返回,
insert into order values (1234, 567)
複製程式碼
同樣的我們傳下面的結構,
e := employee {
name: "Naveen",
id: 565,
address: "Science Park Road, Singapore",
salary: 90000,
country: "Singapore",
}
複製程式碼
應該返回,
insert into employee values("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")
複製程式碼
由於createQuery
函式應該與任何結構一起使用,因此它將interface{}
作為引數。為簡單起見,我們只處理包含string
和int
型別欄位的結構,但這可以擴充套件為任何型別。
createQuery
函式應該適用於任何結構。編寫此函式的唯一方法是檢查在執行時傳遞給它的結構引數的型別,找到它的欄位然後建立查詢。這是應用反射的地方。在本教程的後續步驟中,我們將學習如何使用reflect
包實現此目的。
reflect
包
reflect
包在 Go 中實現了執行時的反射。reflect
包有助於識別底層具體型別和interface {}
變數的值。這正是我們所需要的。createQuery
函式採用interface {}
引數,而建立查詢需要interface {}
引數的具體型別和值。這正是反射包有用的地方。
在編寫我們的通用查詢生成器程式之前,我們首先需要知道reflect
包中的一些型別和方法。讓我們逐一看看它們。
reflect.Type
和 reflect.Value
interface {}
的具體型別由reflect.Type
表示,底層值由reflect.Value
表示。有兩個函式reflect.TypeOf()
和reflect.ValueOf()
,它們分別返回reflect.Type
和reflect.Value
。這兩種型別是建立查詢生成器的基礎。讓我們寫一個簡單的例子來理解這兩種型別。
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)
}
複製程式碼
在上面的程式中,第 13 行的createQuery
函式以interface {}
作為引數。第 14 行的函式reflect.TypeOf
將interface {}
作為引數,並返回包含傳遞的interface {}
引數的具體型別的reflect.Type
。類似,第 15 行中的·reflect.ValueOf·函式也將interface {}
作為引數並返回reflect.Value
,其中包含傳遞的interface {}
引數的基礎值。
上述程式列印,
Type main.order
Value {456 56}
複製程式碼
從輸出中,我們可以看到程式列印了介面的具體型別和值。
reflect.Kind
反射包中有一個更重要的型別叫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)
}
複製程式碼
上述程式輸出,
Type main.order
Kind struct
複製程式碼
我想你現在會清楚兩者之間的差異。 Type
表示interface {}
的實際型別,在這個例子中,它是main.Order
。而Kind
表示型別的特定種類。在這個例子中,它是一個struct
。
NumField()
和Field()
方法
NumField()
方法返回結構中的欄位數,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)
}
複製程式碼
在上面的程式中的第 14 行,我們首先檢查q
的Kind
是否是結構,因為NumField
方法僅適用於結構。該程式碼的其餘部分比較好懂,該程式輸出,
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)
}
複製程式碼
在上面的程式中的第 10 行,我們將reflect.Value
提取為int64
,在第 13 行,我們把它作為string
提取出來。這個程式列印,
type:int64 value:56
type:string value:Naveen
複製程式碼
完整的程式
現在我們有足夠的知識來完成我們的查詢生成器,那就讓我們繼續吧。
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)
}
複製程式碼
在第 22 行,我們首先檢查傳遞的引數是否為結構。然後我們使用Name()
方法從reflect.Type
獲取結構的名稱。在下一行中,我們使用t
並開始建立查詢。
第 28 行,case
語句檢查當前欄位是否為reflect.Int
,如果是的話,我們使用Int()
方法將該欄位的值提取為int64
。 if else
語句用於處理邊緣情況,請新增日誌以瞭解為何需要它。類似的邏輯用於提取string
。
我們還新增了一些檢查,以防止在將不支援的型別傳遞給createQuery
函式時導致程式崩潰。該程式的其他程式碼也比較好懂,建議在適當的位置新增日誌並檢查其輸出以更好地理解該程式。
程式輸出,
insert into order values(456, 56)
insert into employee values("Naveen", 565, "Coimbatore", 90000, "India")
unsupported type
複製程式碼
留下一個練習給讀者,如果我們要將欄位名稱新增到輸出查詢中,該怎麼修改?請嘗試更改程式以列印如下格式的查詢,
insert into order(ordId, customerId) values(456, 56)
複製程式碼
應該使用反射嗎
我們展示了一個反射的用途。現在有一個實際的問題,你應該使用反射嗎?我想引用 Rob Pike 關於使用反射的回答來解釋這個問題。
Clear is better than clever. Reflection is never clear. -- 清晰比聰明更好,但是反射不清晰
在 Go 中,反射是一個非常強大和先進的概念,應該謹慎使用。使用反射編寫清晰且可維護的程式碼非常困難。應儘可能避免使用,並且只有在絕對必要時才應使用。
如果喜歡我的教程,可以向我捐贈。您的捐款將幫助我建立更多精彩的教程。