[譯] part 34: golang 反射 reflection

咔嘰咔嘰發表於2019-04-10

反射是 Go 的高階主題之一,我會盡量讓它變得簡單。

本教程包含以下部分。

  • 什麼是反射?
  • 檢查變數並找到其型別需要做什麼?
  • reflect
    • reflect.Typereflect.Value
    • reflect.Kind
    • NumField()Field() 方法
    • Int()String() 方法
  • 完整的程式
  • 應該使用反射嗎?

我們現在一個一個來討論這些部分。

什麼是反射

反射是為了程式在執行時檢查其變數和值並找到其型別。你可能不明白這意味著什麼,但沒關係。在本教程結束時,您將清楚地瞭解反射,請跟緊我。

檢查變數並找到其型別需要做什麼

在學習反射時,任何人都會想到的第一個問題是,為什麼我們需要檢查變數並在執行時找到它的型別,因為我們的程式中的每個變數都由我們定義的,我們在編譯時就知道它的型別。嗯,大部分時間都是如此,但並非總是如此。

我們用一個簡單的程式來解釋一下我的意思。

package main

import (  
    "fmt"
)

func main() {  
    i := 10
    fmt.Printf("%d %T", i, i)
}
複製程式碼

Run in playgound

在上面的程式中,i的型別在編譯時是已知的,我們在下一行列印它。這裡沒什麼神奇的。

現在讓我們理解在執行時知道變數型別的必要性。假設我們想編寫一個簡單的函式,它將結構作為引數,並使用它建立一個 SQL 插入查詢。

看看下的程式碼,

package main

import (  
    "fmt"
)

type order struct {  
    ordId      int
    customerId int
}

func main() {  
    o := order{
        ordId:      1234,
        customerId: 567,
    }
    fmt.Println(o)
}
複製程式碼

Run in playground

我們需要編寫一個函式,它將上面程式中的結構 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))
}
複製程式碼

Run in playground

第 12 行中的createQuery函式,使用oordIdcustomerId欄位建立插入查詢。該程式將輸出,

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{}作為引數。為簡單起見,我們只處理包含stringint型別欄位的結構,但這可以擴充套件為任何型別。

createQuery函式應該適用於任何結構。編寫此函式的唯一方法是檢查在執行時傳遞給它的結構引數的型別,找到它的欄位然後建立查詢。這是應用反射的地方。在本教程的後續步驟中,我們將學習如何使用reflect包實現此目的。

reflect

reflect 包在 Go 中實現了執行時的反射。reflect 包有助於識別底層具體型別和interface {}變數的值。這正是我們所需要的。createQuery函式採用interface {}引數,而建立查詢需要interface {}引數的具體型別和值。這正是反射包有用的地方。

在編寫我們的通用查詢生成器程式之前,我們首先需要知道reflect 包中的一些型別和方法。讓我們逐一看看它們。

reflect.Typereflect.Value

interface {}的具體型別由reflect.Type表示,底層值由reflect.Value表示。有兩個函式reflect.TypeOf()reflect.ValueOf(),它們分別返回reflect.Typereflect.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)

}
複製程式碼

Run in playground

在上面的程式中,第 13 行的createQuery函式以interface {}作為引數。第 14 行的函式reflect.TypeOfinterface {}作為引數,並返回包含傳遞的interface {}引數的具體型別的reflect.Type。類似,第 15 行中的·reflect.ValueOf·函式也將interface {}作為引數並返回reflect.Value,其中包含傳遞的interface {}引數的基礎值。

上述程式列印,

Type  main.order  
Value  {456 56}  
複製程式碼

從輸出中,我們可以看到程式列印了介面的具體型別和值。

reflect.Kind

反射包中有一個更重要的型別叫Kind

反射包中的KindType可能看起來相似,但它們之間存在差異,這將從下面的程式中清楚地看出。

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)
}
複製程式碼

Run in playground

上述程式輸出,

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)
}
複製程式碼

Run in playground

在上面的程式中的第 14 行,我們首先檢查qKind是否是結構,因為NumField方法僅適用於結構。該程式碼的其餘部分比較好懂,該程式輸出,

Number of fields 2  
Field:0 type:reflect.Value value:456  
Field:1 type:reflect.Value value:56  
複製程式碼

Int()String()方法

IntString方法有助於將reflect.Value分別提取為int64string型別。

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)

}
複製程式碼

Run in playground

在上面的程式中的第 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)

}
複製程式碼

Run in playground

在第 22 行,我們首先檢查傳遞的引數是否為結構。然後我們使用Name()方法從reflect.Type獲取結構的名稱。在下一行中,我們使用t並開始建立查詢。

第 28 行,case語句檢查當前欄位是否為reflect.Int,如果是的話,我們使用Int()方法將該欄位的值提取為int64if 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 中,反射是一個非常強大和先進的概念,應該謹慎使用。使用反射編寫清晰且可維護的程式碼非常困難。應儘可能避免使用,並且只有在絕對必要時才應使用。

如果喜歡我的教程,可以向我捐贈。您的捐款將幫助我建立更多精彩的教程。

相關文章