![reflection](https://raw.githubusercontent.com/studygolang/gctt-images/master/golang-series/reflection-golang-3.png)歡迎來到 [Golang 系列教程](https://studygolang.com/subject/2)的第 34 篇。反射是 Go 語言的進階主題之一。我會儘可能讓它變得簡單易懂。本教程分為如下小節。- 什麼是反射?- 為何需要檢查變數,確定變數的類型?- reflect 包 - reflect.Type 和 reflect.Value - reflect.Kind - NumField() 和 Field() 方法 - Int() 和 String() 方法- 完整的程式- 我們應該使用反射嗎?讓我們來逐個討論這些章節。## 什麼是反射?反射就是程式能夠在運行時檢查變數和值,求出它們的類型。你可能還不太懂,這沒關係。在本教程結束後,你就會清楚地理解反射,所以跟著我們的教程學習吧。## 為何需要檢查變數,確定變數的類型?在學習反射時,所有人首先面臨的疑惑就是:如果程式中每個變數都是我們自己定義的,那麼在編譯時間就可以知道變數類型了,為什麼我們還需要在運行時檢查變數,求出它的類型呢?沒錯,在大多數時候都是這樣,但並非總是如此。我來解釋一下吧。下面我們編寫一個簡單的程式。```gopackage mainimport ( "fmt")func main() { i := 10 fmt.Printf("%d %T", i, i)}```[在 playground 上運行](https://play.golang.org/p/1oZzPCCG2Qw)在上面的程式中,`i` 的類型在編譯時間就知道了,然後我們在下一行列印出 `i`。這裡沒什麼特別之處。現在瞭解一下,需要在運行時求得變數類型的情況。假如我們要編寫一個簡單的函數,它接收結構體作為參數,並用它來建立一個 SQL 插入查詢。考慮下面的程式:```gopackage mainimport ( "fmt")type order struct { ordId int customerId int}func main() { o := order{ ordId: 1234, customerId: 567, } fmt.Println(o)}```[在 playground 上運行](https://play.golang.org/p/1oZzPCCG2Qw)在上面的程式中,我們需要編寫一個函數,接收結構體變數 `o` 作為參數,返回下面的 SQL 插入查詢。```insert into order values(1234, 567)```這個函數寫起來很簡單。我們現在編寫這個函數。```gopackage mainimport ( "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 上運行](https://play.golang.org/p/jhz4VHKIlQ5)在第 12 行,`createQuery` 函數用 `o` 的兩個欄位(`ordId` 和 `customerId`),建立了插入查詢。該程式會輸出:```bashinsert into order values(1234, 567)```現在我們來升級這個查詢產生器。如果我們想讓它變得通用,可以適用於任何結構體類型,該怎麼辦呢?我們用程式來理解一下。```gopackage maintype 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` 函數(上述程式中的第 16 行),它可以接收任何結構體作為參數,根據結構體的欄位建立插入查詢。例如,如果我們傳入下面的結構體:```goo := order { ordId: 1234, customerId: 567}````createQuery` 函數應該返回:```insert into order values (1234, 567)```類似地,如果我們傳入:```go 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 包在 Go 語言中,[`reflect`](https://golang.org/pkg/reflect/) 實現了運行時反射。`reflect` 包會協助識別 [`interface{}`](https://studygolang.com/articles/12266) 變數的底層具體類型和具體值。這正是我們所需要的。`createQuery` 函數接收 `interface{}` 參數,根據它的具體類型和具體值,建立 SQL 查詢。這正是 `reflect` 包能夠協助我們的地方。在編寫我們通用的查詢產生器之前,我們首先需要瞭解 `reflect` 包中的幾種類型和方法。讓我們來逐個瞭解。### reflect.Type 和 reflect.Value`reflect.Type` 表示 `interface{}` 的具體類型,而 `reflect.Value` 表示它的具體值。`reflect.TypeOf()` 和 `reflect.ValueOf()` 兩個函數可以分別返回 `reflect.Type` 和 `reflect.Value`。這兩種類型是我們建立查詢產生器的基礎。我們現在用一個簡單的例子來理解這兩種類型。```gopackage mainimport ( "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 上運行](https://play.golang.org/p/81BS-bEfbCg)在上面的程式中,第 13 行的 `createQuery` 函數接收 `interface{}` 作為參數。在第 14 行,[`reflect.TypeOf`](https://golang.org/pkg/reflect/#TypeOf) 接收了參數 `interface{}`,返回了[`reflect.Type`](https://golang.org/pkg/reflect/#Type),它包含了傳入的 `interface{}` 參數的具體類型。同樣地,在第 15 行,[`reflect.ValueOf`](https://golang.org/pkg/reflect/#ValueOf) 函數接收參數 `interface{}`,並返回了 [`reflect.Value`](https://golang.org/pkg/reflect/#Value),它包含了傳來的 `interface{}` 的具體值。上述程式會列印:```Type main.orderValue {456 56}```從輸出我們可以看到,程式列印了介面的具體類型和具體值。### relfect.Kind`reflect` 包中還有一個重要的類型:[`Kind`](https://golang.org/pkg/reflect/#Kind)。在反射包中,`Kind` 和 `Type` 的類型可能看起來很相似,但在下面程式中,可以很清楚地看出它們的不同之處。```gopackage mainimport ( "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 上運行](https://play.golang.org/p/Xw3JIzCm54T)上述程式會輸出:```Type main.orderKind struct```我想你應該很清楚兩者的區別了。`Type` 表示 `interface{}` 的實際類型(在這裡是 **`main.Order`**),而 `Kind` 表示該類型的特定類別(在這裡是 **`struct`**)。### NumField() 和 Field() 方法[`NumField()`](https://golang.org/pkg/reflect/#Value.NumField) 方法返回結構體中欄位的數量,而 [`Field(i int)`](https://golang.org/pkg/reflect/#Value.Field) 方法返回欄位 `i` 的 `reflect.Value`。```gopackage mainimport ( "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 上運行](https://play.golang.org/p/FBHfJfuTaEe)在上面的程式中,因為 `NumField` 方法只能在結構體上使用,我們在第 14 行首先檢查了 `q` 的類別是 `struct`。程式的其他代碼很容易看懂,不作解釋。該程式會輸出:```Number of fields 2Field:0 type:reflect.Value value:456Field:1 type:reflect.Value value:56```### Int() 和 String() 方法[`Int`](https://golang.org/pkg/reflect/#Value.Int) 和 [`String`](https://golang.org/pkg/reflect/#Value.String) 可以協助我們分別取出 `reflect.Value` 作為 `int64` 和 `string`。```gopackage mainimport ( "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 上運行](https://play.golang.org/p/UIllrLVoGwI)在上面程式中的第 10 行,我們取出 `reflect.Value`,並轉換為 `int64`,而在第 13 行,我們取出 `reflect.Value` 並將其轉換為 `string`。該程式會輸出:```type:int64 value:56type:string value:Naveen```## 完整的程式現在我們已經具備足夠多的知識,來完成我們的查詢產生器了,我們來實現它把。```gopackage mainimport ( "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 上運行](https://play.golang.org/p/82Bi4RU5c7W)在第 22 行,我們首先檢查了傳來的參數是否是一個結構體。在第 23 行,我們使用了 `Name()` 方法,從該結構體的 `reflect.Type` 擷取了結構體的名字。接下來一行,我們用 `t` 來建立查詢。在第 28 行,[case 語句](https://studygolang.com/articles/11957) 檢查了當前欄位是否為 `reflect.Int`,如果是的話,我們會取到該欄位的值,並使用 `Int()` 方法轉換為 `int64`。[if else 語句](https://studygolang.com/articles/11902)用於處理邊界情況。請添加日誌來理解為什麼需要它。在第 34 行,我們用來相同的邏輯來取到 `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`](https://en.wikipedia.org/wiki/Rob_Pike) 關於使用反射的格言,來回答這個問題。> 清晰優於聰明。而反射並不是一目瞭然的。反射是 Go 語言中非常強大和進階的概念,我們應該小心謹慎地使用它。使用反射編寫清晰和可維護的代碼是十分困難的。你應該儘可能避免使用它,只在必須用到它時,才使用反射。本教程到此結束。希望你們喜歡。祝你愉快。**上一教程 - [函數是一等公民](https://studygolang.com/articles/12789)****上一教程 - [讀取檔案](https://studygolang.com/articles/14669)**
via: https://golangbot.com/reflection/
作者:Nick Coghlan 譯者:Noluye 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
2114 次點擊