深入淺出Go語言:反射機制及應用
2022-01-02由 視聽觀察室 發表于 農業
固定模型怎麼變通
本文包括以下部分:
什麼是反射?
如何檢查一個變數及其型別?
反射包
完整程式碼
應該用反射嗎?
現在我們一個一個地討論這些部分。
什麼是反射?
反射是程式在執行時能夠檢查變數、變數值和變數型別的能力。現在你可能不理解什麼意思,但是不要緊。在讀完本文,您將對反射有一個清晰的理解,所以請繼續跟隨我。
檢查一個變數及其型別需要什麼?
在學習反射時每個人碰到第一個問題就是程式執行時為何需要檢查變數型別,程式中的每一個變數都是由我們定義的,並且我們在編譯時就知道它的型別。這在大多數情況下是正確的,但並不總是如此。
讓我們寫一個簡單的程式,來解釋下。
package
main
import
(
“fmt”
)
func
main
() {
i
:
=
10
fmt
。
Printf
(
“%d %T”
,
i
,
i
)
}
在上面的程式碼中,變數i的型別在編譯的時候就知道了,並且在程式碼中列印。沒什麼特別的。
現在我們來看為何在執行時需要知道變數的型別。假設我們想要編寫一個簡單的函式,它接受一個結構體作為引數,並使用它建立一個SQL查詢語句。看以下程式碼:
package
main
import
(
)
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
(
)
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
))
}
createQuery函式使用結構體的ordId和customerId欄位生成sql查詢語句。程式輸出為:
現在將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
() {
}
我們的目標是實現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執行時反射。反射包能夠識別interface{}變數的底層型別和值。這也是我們需要的。createQuery函式接收interface{}型別引數,需要根據其具體型別和值來建立查詢語句。也是reflect包實現的功能。
在編寫通用createQuery函式之前,我們需要了解反射包中的一些型別和方法。讓我們一個一個來看看。
reflect。Type和reflect。Value
interface{}變數具體型別由reflect。Type表示,而reflect。Value表示其具體值。reflect。TypeOf()函式和reflect。ValueOf()函式分別返回reflect。Type和reflect。Value。這兩種型別是實現createQuery函式的基礎。讓我們編寫一個簡單的事例來理解這兩種型別。
package
main
import
(
“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
)
}
上面的程式碼,createQuery函式接收interface{}型別引數。reflect。TypeOf接收interface{}型別並返回reflect。Type,包含interface{}引數的具體型別。類似地,reflect。ValueOf函式也是接收interface{}型別返回reflect。Value,包含所傳遞的interface{}引數的具體值。
程式碼輸出為:
Type main。orderValue {456 56}
從輸出中,我們可以看到程式輸出了介面的具體型別和值。
reflect。Kind
在反射包中還有一種更重要的型別,稱為Kind。
反射包中的Kind和Type型別似乎很相似,但他們有一個區別,從下面的程式中可以清楚地看到。
package
main
import
(
)
type
order
struct
{
ordId
int
customerId
int
}
func
createQuery
(
q
interface
{}) {
t
:
=
reflect
。
TypeOf
(
q
)
k
:
=
t
。
Kind
()
fmt
。
Println
(
,
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
(
)
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
)
}
以上程式碼,我們先檢查引數q Kind是否為struct,因為NumField方法只對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
(
)
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
)
}
程式碼輸出:
type
:
int64
value
:
56
type
:
string
value
:
Naveen
完整程式碼
現在我們已經有了足夠的知識來完成createQuery函式,讓我們繼續完成它。
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
)
}
以上程式碼,我們先檢查傳入的引數是否為struct型別。然後使用reflect。Type的Name()方法獲取結構體名稱。並使用switch—case來判斷各欄位的具體型別,分別處理。
我們還添加了檢查,防止將不支援的型別傳遞給createQuery函式時程式崩潰。程式的其餘部分很好理解。我建議在適當的位置新增日誌,並檢查它們的輸出,以便更好地理解這個程式。
程式輸出為:
insert into order values(456,
56
)
insert into employee values(
“Naveen”
,
565
,
“Coimbatore”
,
90000
,
“India”
)
unsupported type
將欄位名新增到輸出查詢中,留給讀者作為練習。請嘗試更改程式列印查詢的格式,
應該使用反射嗎?
在展示了反射的實際應用之後,現在真正的問題來了。應該使用反射嗎?我想引用Rob Pike的話來回答這個問題。
Clear is better than clever。 Reflection is never clear。
反射在Go中是一個非常強大和先進的概念,應該謹慎使用。使用反射編寫清晰和可維護的程式碼是非常困難的。在任何可能的情況下都應該避免使用,只有在絕對必要的時候才應該使用。