Go reflect 反射實例解析
Go reflect 反射實例解析
——教壞小朋友系列
0 FBI WARNING
對於本文內容,看懂即可,完全不要求學會。
1 現象分析
網上關於golang反射的文章多如牛毛,可實際用反射的人,寥寥可數。
我分析大家不用反射的原因,大概有兩點:
- 不會
- 笨重
1.1 不會
網上關於反射的文章很多,可多數都是在分析原理,並沒有給出實例。而反射恰好處在一個很尷尬的點上:懂了原理不等於會用。
於是乎,大多數人高喊口號:“官方不推薦用這個庫!”是的,官方不推薦用的庫有兩個:reflect
和unsafe
(嚴格意義上來說,還有cgo
)。
附官方說法:
It's a powerful tool that should be used with care and avoided unless strictly necessary.
可是我又發現一個奇怪現象,看看下面這段代碼:
func bytes2str(p []byte) string {
return *(*string)(unsafe.Pointer(&p))
}
我不知道這段代碼是從哪裏流傳出去的(疑似官方庫strings.Builder
),然後這段代碼就被玩爛了,說好的不推薦使用unsafe
呢?
善意的提醒:
上面這段代碼和bufio.Scanner
或bufio.Reader
共用的時候,將出現彩蛋。看下面示例:
func main() {
s := "aaa\nbbb\ncc"
sc := bufio.NewScanner(strings.NewReader(s))
sc.Buffer(make([]byte, 4), 4)
sc.Scan()
t := bytes2str(sc.Bytes())
fmt.Println(t) // Output: aaa
sc.Scan()
fmt.Println(t) // Output: bbb
sc.Scan()
fmt.Println(t) // Output: ccb
}
和您想象中的結果一樣嗎?如果一樣,恭喜您。
書接前文,言歸正傳。最後,我想,大家嘴裏說着不用不用,其實不是不想用,而是“不會用”。當大家真正拿到實例,知道怎麼用的時候,“真香定律”便出現了。
反射也是一樣,大家都說反射不好理解、性能差、容易出錯,其實還不是因爲不會用?所以呢,今天我不講太多原理了,直接上乾貨,給出一些實例或模板出來,後面再遇到類似情況,就像上面bytes2str
一樣,拿來套用即可。
又是一段善意的提醒:
建議大家要學好閉包
和接口
,這樣在用反射的時候,將會更加得心應手(如果您能掌握unsafe
,那真真兒是極好的)。
1.2 笨重
大家在使用反射時,都在默認一個“無知”概念:我什麼都不知道,所以我要枚舉所有可能,我要實現所有可能。第一個“所有可能”要求我們檢查所有可能的輸入參數是否合法,第二個“所有可能”要求我們實現所有可能參數的處理邏輯。
爲什麼要仔細檢查所有可能輸入的參數?因爲reflect
庫稍加不慎就會panic。
爲什麼要實現所有可能參數的處理邏輯呢?參照官方庫json
、reflect.DeepEqual
的實現(其反射代碼之多、之複雜,讓人望而卻步),反射不就應該這樣用嗎?
但仔細想想,我們真的需要這兩個“所有可能”嗎?不一定。如果我們想做到“大而全”,那確實需要兩個“所有可能”,但如果我們不要“大而全”呢?我們拿反射處理特定情況行不行呢?
不得不說,正是這個無知概念,讓大家在使用反射時,像是戴着鐐銬在針尖上跳舞,如履薄冰,戰戰兢兢。今天,我們暫且拋卻這個概念,化巨闕爲瑞士軍刀。
從現在開始,我們準確的知道傳進來的參數是什麼。如果不知道,很簡單,限制死。
2 由高斯求和說起
請解決以下問題:寫程序輸出1+2+3+…+10的結果。
func main() {
var sum int
for i := 1; i <= 10; i++ {
sum += i
}
println(sum)
}
如果把這段代碼抽成函數呢?
func Gauss(n int) int {
var sum int
for i := 1; i <= n; i++ {
sum += i
}
return sum
}
請問,Gauss
函數爲什麼需要一個參數n
?很簡單,這樣該函數不止可以求從1加到10的結果,也可以求從1加到100,加到1000,10000的結果,而不用每次求和再重新寫段代碼。也就是說,該函數在某種情況下是通用的:求從1加到n的和。
通用性,使得同一段代碼可以在不同的地方被反覆調用,而不用每次都重新寫一遍相同的邏輯。
仔細觀察,你會發現一個祕密:99%的通用性代碼,是對“值”的通用。值是什麼?比如var a int = 123
,變量a的值是123。
還記得有種數據結構叫哈希嗎?用哈希的時候,要拋棄傳統通過下標找值
的思維,轉向通過值找下標
的思維,這樣哈希才能玩的賊6。
反射也是一樣,寫反射代碼時不要總想着對變量的值
操作,這時候需要同時對變量的值和類型
進行操作。比如像上面的Gauss
函數,可以對不同的n值進行求和,n的限制是大於等於0。對變量值的操作,大家都很熟悉。學習反射,最主要的,是學會對變量類型的操作。本文中所有反射例子,都會對類型進行限制,這也是用好反射的一個關鍵。
在接下的例子中,您可以試着從這個角度出發,去理解代碼的意圖。
3 實例
以下實例,或模板,都是針對官方庫的使用。大家可以重點關注一下對類型的操作。
3.1 http轉rpc模板
大家用go語言寫的最多的,應該就是web應用了,寫web應用的時候,大家又陷入一個怪圈中:有沒有一個好的框架?大家找來找去,發現還是gin和echo好用。
這裏,爲大家提供一種基於反射的http模板,竊以爲比gin和echo還要人性化一點。
技能點:
- 閉包
- 裝飾器
- 反射
3.1.1 閉包
go語言中閉包有兩種形式:
- 實例的方法
- 函數嵌套
3.1.1.1 實例的方法
type T string
func (t T) greating() {
println(t)
}
t := T("hello world!")
g := t.greating
g()
可以看到,變量g
和t
已經沒有關係,但g
還是可以訪問t
的內容,這是比較簡單的一類閉包實現,但不常用,因爲它總是可以被第二種形式替代。
3.1.1.2 函數嵌套
還是用一段廣爲流傳的代碼作示例好了:
func Incr() func() int {
var i int
return func() int {
i++
return i
}
}
incr := Incr()
println(incr()) // Output: 1
println(incr()) // Output: 2
println(incr()) // Output: 3
由此可見,閉包本身不難理解,是不是就像1+1=2
一樣簡單?好了,下面我們將用它推導微積分(手動狗頭)。
3.1.2 裝飾器
在go中,幾乎所有的接口,都可以使用裝飾器。比如常用的io.LimtedReader
、context.Context
等。http庫,要處理http請求,就要實現http.Handler
接口,實現該接口的方式有很多,http庫給出了非常方便一種:http.HandlerFunc
,接下來,我們用它來實現http裝飾器。
http庫對於HandlerFunc
的定義如下:
type HandlerFunc func(http.ResponseWriter, *http.Request)
這種形式的定義,註定我們將要用閉包的第二種形式:
func WithRecovery(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if e := recover(); e != nil {
log.Printf("uri = %v, panic error: %v, stack: %s", r.URL.Path, e, debug.Stack())
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
}
}()
handler.ServeHTTP(w, r)
})
}
使用方法:
http.NewServeMux().Handle("/ping", WithRecovery(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})))
如果您看過gin
或echo
的代碼,就會明白,裝飾器(在http中或叫middleware)的實現方式大同小異,比如gin.New().Use()
。這些第三方庫最大的好處在於,寫法上更簡捷易懂。
3.1.3 反射
不知道大家發現沒有,大多數的第三方庫,包括gin、echo,他們都是在解決路由和中間件的問題。但大家在處理邏輯時,拿到的還是最原始的http.Request
和http.ResponseWriter
,如果我們看過官方的rpc
庫,就會想,能不能把http請求轉成rpc格式呢?答案是:當然可以,用反射!我們先定義用法,再寫膠水代碼。假設用法如下:
type Context struct{} // 一些http信息
type PingReq struct{} // 參數
type PingResp struct{} // 業務返回值
// 最終業務邏輯函數格式:
func Ping(ctx *Context, req *PingReq, resp *PingResp) Error {
return nil
}
參照上述格式,我們來完成膠水邏輯:
- 獲取函數類型,從而獲取函數入參類型;
- 生成
*Context
、*PingReq
、*PingResp
; - 將函數包裝成
http.Handler
; - 調用函數,接收返回值;
- 輸出結果。
請先在心裏默唸:“我知道參數是什麼
”。OK,看主要代碼(後面有完整版):
// function should be like:
// func Ping(*Context, *PingReq, *PingResp) Error { return nil }
func WithFunc(function interface{}) http.Handler {
fn := reflect.ValueOf(function) // function代表Ping
fnTyp := fn.Type() // 獲取函數類型,從類型出發,創建變量
// Context是http信息,所有業務邏輯共用的數據類型,可以不用反射
// 這裏獲取Type時解指針,下面reflect.New的時候拿到的是指針值
arg1 := fnTyp.In(1).Elem() // type: PingReq
arg2 := fnTyp.In(2).Elem() // type: PingResp
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := &Context{} // 填入http請求信息
req := reflect.New(arg1) // 創建請求變量,type: *PingReq
json.NewDecoder(r.Body).Decode(req.Interface()) // 這步看項目需要自行處理
resp := reflect.New(arg2) // 創建返回值變量,type: *PingResp
// 函數調用,傳入預定的三個參數,接收Error返回值
out := fn.Call([]reflect.Value{reflect.ValueOf(ctx), req, resp})
if ret := out[0].Interface(); ret != nil {
err := ret.(Error)
Render(w, err.Code(), err.Error(), nil)
return
}
Render(w, 0, "", resp.Interface())
})
}
完整代碼:
type Context struct {
Trace string
API string
Func string
Header http.Header
Query url.Values
Uid string
}
type Error interface {
error
Code() int
}
type Middleware func(*Context) Error
// 不相關邏輯從簡
func Render(w http.ResponseWriter, code int, msg string, data interface{}) {
json.NewEncoder(w).Encode(map[string]interface{}{
"errcode": code,
"errmsg": msg,
"data": data,
})
}
// function should be like:
// func Ping(*Context, *PingReq, *PingResp) Error { return nil }
func WithFunc(function interface{}, mws ...Middleware) http.Handler {
fn := reflect.ValueOf(function)
fnName := runtime.FuncForPC(fn.Pointer()).Name()
fnTyp := fn.Type()
arg1 := fnTyp.In(1).Elem()
arg2 := fnTyp.In(2).Elem()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := &Context{
Trace: r.URL.Query().Get("trace_id"),
API: r.URL.Path,
Func: fnName,
Header: r.Header,
Query: r.URL.Query(),
}
for _, mw := range mws {
err := mw(ctx)
if err != nil {
mwName := runtime.FuncForPC(reflect.ValueOf(mw).Pointer()).Name()
log.Printf("WithFunc middleware %v ctx %+v: %v", mwName, ctx, err)
Render(w, err.Code(), err.Error(), nil)
return
}
}
req := reflect.New(arg1)
json.NewDecoder(r.Body).Decode(req.Interface())
resp := reflect.New(arg2)
out := fn.Call([]reflect.Value{reflect.ValueOf(ctx), req, resp})
if ret := out[0].Interface(); ret != nil {
err := ret.(Error)
Render(w, err.Code(), err.Error(), nil)
return
}
Render(w, 0, "", resp.Interface())
})
}
測試:
type PingReq struct{}
type PingResp struct{}
func Ping(ctx *Context, req *PingReq, resp *PingResp) Error {
return nil
}
http.NewServeMux().Handle("/ping", WithFunc(Ping))
這樣寫起邏輯來,是不是感覺清爽多了,而且不用關心底層是不是http了?
3.2 簡易ORM
在一般項目中,用到數據庫,比如mysql,是非常常見的事。大家用數據庫想到的第一件事,就是找個趁手的orm。網上比較流行的是gorm
和xorm
等。
那如果不用gorm和xorm,自己能不能寫個簡單實用的orm呢?
技能點:
- 反射
- 封裝
3.2.1 反射
官方庫database/sql
提供的接口很原始,比如讀數據,就像fmt.Scanf
一樣用,但項目代碼中,用scanf
的方式,會不會有點太原始了?比如像下面一樣:
type T struct {
Id int64 `db:"id"`
Name string `db:"name"`
Age int `db:"age"`
}
func main() {
db, _ := sql.Open("mysql", "xxx")
row, _ := db.QueryRow("select (id, name, age) from tbl where id=123")
var t T
row.Scan(&t.Id, &t.Name, &t.Age)
fmt.Println(t)
}
這樣寫一個兩個表還OK,多了之後肯定要崩潰,對每張表都要寫一套如何讀取數據的代碼,明明邏輯都是一樣的。這種枯燥的工作就很容易出錯。另外,加個字段要考慮數據庫字段和程序字段的一一嚴格對應。
我想,在多數人心目中,這樣原始的接口不太好用。所以很多人選擇了orm。那如果沒有orm,我們能不能把原始接口變的好用起來呢?答案是:當然可以。我們把這套相同的邏輯抽出來,用反射忽略類型hard code,即可適應所有數據的讀取工作。先看相同的邏輯:
var t T
fields := []string{"id", "name", "age"}
values := []interface{}{&t.Id, &t.Name, &t.Age}
sql := fmt.Sprintf("select (`%v`) from tbl where id=123", strings.Join(fields, "`, `"))
row, _ := db.QueryRow(sql)
row.Scan(values...)
由上面邏輯,我們可以看出,我們需要的是fields
和values
,我們只需要通過反射拿到這兩個數組即可。由此膠水邏輯爲:
- 獲取參數類型;
- 遍歷參數
struct
每個字段; - 通過每個字段得到數據庫字段名和該字段地址;
- 返回數據庫字段名列表和字段地址列表。
後續邏輯會通過字段名和字段地址讀取數據庫數據:
func SQLQueryFields(v interface{}) (fields []string, values []interface{}) {
val := reflect.Indirect(reflect.ValueOf(v))
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
if !val.Field(i).CanInterface() {
continue
}
// 數據庫字段名
fields = append(fields, typ.Field(i).Tag.Get("db"))
// 注意是指針,需要取址操作
values = append(values, val.Field(i).Addr().Interface())
}
return
}
可見,代碼量非常之少。誰說反射很笨重的?來看使用方法:
func main() {
var t T
fields, values := SQLQueryFields(&t)
fmt.Println(fields) // Output: [id name age]
// fmt.Sscanf("1 eachain 26", "%d %s %d", &t.Id, &t.Name, &t.Age)
fmt.Sscanf("1 eachain 26", "%d %s %d", values...)
fmt.Println(t) // Output: {1 eachain 26}
}
接下來就是如何讀多條記錄的問題了:
- 通過參數(列表)逐級獲取真實元素類型;
- 生成一個新元素,按上面邏輯讀取;
- 將新元素追加到列表中。
// slice type: *[]T or *[]*T
func QueryRows(rows *sql.Rows, slice interface{}) error {
ls := reflect.ValueOf(slice).Elem() // type: []T or []*T
elemTyp := ls.Type().Elem() // type: T or *T
isPtr := false
if elemTyp.Kind() == reflect.Ptr {
elemTyp = elemTyp.Elem() // type: T
isPtr = true
}
for rows.Next() {
elem := reflect.New(elemTyp) // type: *T
_, fields := SQLQueryFields(elem.Interface())
err := rows.Scan(fields...)
if err != nil {
return err
}
// 以下代碼翻譯成正常代碼即: ls = append(ls, elem)
if isPtr {
ls.Set(reflect.Append(ls, elem))
} else {
ls.Set(reflect.Append(ls, elem.Elem()))
}
}
return rows.Err()
}
是不是覺得寫個orm沒有想象中的辣麼難?
3.2.2 封裝
封裝不是本文重點,這裏不作具體介紹。但有了上面基礎,封裝一個類似gorm
這樣xxx.Select(fields).From(table).Where(cond).OrderBy(order).Limit(limit).Rows(&slice)
的鏈式調用,應該不難。
同樣的insert
、update
、delete
等操作是一樣的,這裏不再重複。
注意,本節到此爲止。
這只是個orm的雛形,點到即可,要想得到一個完善的orm,最終還是要寫成類似標準庫encoding/json
或三方庫gorm
那種形式的。但那已經不是本文要關心的點了。
3.3 兼容不同格式的API返回結果
在對接一些系統的時候,會發現接口返回格式各異,例如:
{"errcode": 1, "errmsg": "some error", "data": {"a": 789}}
{"err_code": 1, "err_msg": "some error", "data": {"a": 789}}
{"errcode": "1", "err_msg": "some error", "a": 789}
相信很多人遇到這種情況時,想到的第一件事不是怎麼解決,而是WTF。這種結果,能不能兼容呢?
提醒:
這種情況用以下代碼是不可行的:
var errcode json.Number
var errmsg string
m := map[string]interface{}{
"errcode": &errcode,
"err_code": &errcode,
"errmsg": &errmsg,
"err_msg": &errmsg,
}
json.Unmarshal(p, &m)
技能點:
- 接口
- 反射
3.3.1 接口
由於這裏主要在說json數據處理,所以在這隻提json.Unmarshaler
接口了。
在encoding/json
中,Unmarshaler
接口允許外部可以按自定義的格式解析json數據,這給了我們兼容不同json格式的可能。比如json.Number
。同樣的,我們也可以寫一個ErrCode
以兼容數字、字符串格式的json返回結果:
type ErrCode int
func (ec *ErrCode) UnmarshalJSON(p []byte) (err error) {
if p[0] == '"' {
p = p[1 : len(p)-1]
}
*(*int)(ec), err = strconv.Atoi(string(p))
return
}
如果發現p是字符串,我們去掉雙引號後按數字解析即可。
3.3.2 反射
我們首先明確一下要解決的問題:
- 兼容字段名errcode和err_code兩種格式,同理errmsg;
data
有時在json的data字段中,有時在外面平鋪展開。
我們在一開始就提到了,用map
是不能解決這個問題的,那如果用struct
呢?我們需要做什麼:
- 需要兩個字段,一個是errcode,一個是err_code,但由指針指向相同的地址。這樣無論出現哪個,最終都將解析到同一個值裏面;
- 將
data
放在json的data字段,同時將data
展開到外層。這樣無論哪種情況,都會被解析到,另一種被忽略; - 生成一個全新的struct,將指針指向傳入參數的內存地址,並返回。
來看示例代碼:
type CallError struct {
ErrCode int
ErrMsg string
}
func (ce CallError) Error() string {
return strconv.Itoa(ce.ErrCode) + ": " + ce.ErrMsg
}
func Join(err *CallError, data interface{}) interface{} {
var fields []reflect.StructField
var values []reflect.Value
// 第一個errcode,類型是*ErrCode,兼容數字和字符串,並指向err.ErrCode
fields = append(fields, reflect.StructField{
Name: "ErrCode1",
Tag: `json:"errcode"`,
Type: reflect.TypeOf(new(ErrCode)), // 類型是指針
})
values = append(values, reflect.ValueOf((*ErrCode)(&err.ErrCode)))
// 第二個err_code,類型同樣是*ErrCode,也指向err.ErrCode
fields = append(fields, reflect.StructField{
Name: "ErrCode2",
Tag: `json:"err_code"`,
Type: reflect.TypeOf(new(ErrCode)),
})
values = append(values, reflect.ValueOf((*ErrCode)(&err.ErrCode)))
// 第一個errmsg,指向err.ErrMsg
fields = append(fields, reflect.StructField{
Name: "ErrMsg1",
Tag: `json:"errmsg"`,
Type: reflect.TypeOf(new(string)),
})
values = append(values, reflect.ValueOf(&err.ErrMsg))
// 第二個err_msg,也指向err.ErrMsg
fields = append(fields, reflect.StructField{
Name: "ErrMsg2",
Tag: `json:"err_msg"`,
Type: reflect.TypeOf(new(string)),
})
values = append(values, reflect.ValueOf(&err.ErrMsg))
// data字段,並將參數data置於此
fields = append(fields, reflect.StructField{
Name: "Data",
Tag: `json:"data"`,
Type: reflect.TypeOf(new(interface{})).Elem(), // 類型是interface{}
})
values = append(values, reflect.ValueOf(data))
// 將參數data所有字段展開放到最外層
v := reflect.Indirect(reflect.ValueOf(data))
if v.Kind() == reflect.Struct { // 標註
t := v.Type()
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
f.Type = reflect.PtrTo(f.Type) // 類型是指針
fields = append(fields, f)
values = append(values, v.Field(i).Addr())
}
}
// 生成新struct,並將指針指向參數
dst := reflect.New(reflect.StructOf(fields))
v = dst.Elem()
for i := 0; i < len(values); i++ {
v.Field(i).Set(values[i])
}
// 返回可被json.Umarshal的對象
return dst.Interface()
}
上面標註的地方是通用寫法,還有一種不通用的簡便寫法:
t := v.Type()
fields = append(fields, reflect.StructField{
Anonymous: true,
Name: t.Name(),
Type: reflect.PtrTo(t),
})
values = append(values, v.Addr())
這種寫法要求data
不能帶有Method
,如果有,將panic。
有一點我沒在註釋裏面提及:所有字段均是以指針形式出現。爲什麼要這樣做?
設想:如果新struct各字段不用指針,當我們reflect.New(reflect.StructOf(fields))
的時候,go會爲該struct分配空間。當我們解析json的時候,最終結果都將解析到新生成的struct裏面去,也就是說,值會被寫入新分配的內存空間,而不是我們傳入參數的內存空間。竹籃打水一場空,這不是我們想要的結果。
可如果我們用指針,我們可以任意指定指針指向的空間。並且指針符合反射第三定律“可寫”的條件。最終我們將新生成struct的各個字段指向我們希望的內存空間:傳入的參數。這樣,當json解析的時候,會將結果寫入到我們想要的內存空間中。
3.4 公共字段操作
前排警告:
本節內容超綱,不要求看懂。
請求的返回結果中,有一些相同字段,需要在接口返回的時候,自動填充這些字段的值,比如:
type Resp struct {
Time int64 `json:"time"`
Server string `json:"server"`
Num int `json:"num"`
}
type Resp2 struct {
Time int64 `json:"time"`
Server string `json:"server"`
Name string `json:"name"`
}
type Resp3 struct {
Time int64 `json:"time"`
Server string `json:"server"`
WTF string `json:"wtf"`
}
要求:Time
自動填充當前時間,Server
自動填充hostname
。
真實情況是:返回結果,有可能是指針*Resp
,有可能不是指針Resp
。
如果是指針,用反射很好解決,因爲字段都是CanSet()
的,但如果出現不是指針的情況呢?爲什麼現實總是如此殘酷?也罷,會反射的我們總是可以見招拆招。
大家想到的第一個方案,很可能是深度拷貝(DeepCopy
),這段代碼在github上有人實現,通過深度拷貝,獲取一個可寫的reflect.Value
,從而寫入Time
和Server
。提前聲明,DeepCopy是正規解決方案。但本文一開始說了,不會用這種巨無霸的實現方式,我們要另闢蹊徑。
附DeepCopy方案示例代碼:
func autoSetTimeAndServer(resp interface{}) {
val := reflect.ValueOf(resp)
if val.Kind() != reflect.Ptr {
val = reflect.New(val.Type())
DeepCopy(val.Interface(), resp)
}
val = val.Elem()
val.FieldByName("Time").SetInt(time.Now().Unix())
hostname, _ := os.Hostname()
val.FieldByName("Server").SetString(hostname)
}
技能點:
- unsafe
- 反射
3.4.1 unsafe
本文一開始就提到了unsafe,即上文的bytes2str
,要用unsafe,需要對go數據底層存儲有一定的認知。比如爲什麼可以直接把[]byte
通過unsafe轉成string
,反過來由string
轉[]byte
行不行?
uintptr
和unsafe.Pointer
有什麼區別?
uintptr
是一個變量,unsafe.Pointer
是一個指針。這意味着如果GC在移動內存的時候,會更新unsafe.Pointer
,因爲它是指針。而不會修改uinptr
,因爲它只是個普通變量。因此,不要用uintptr
的變量保存內存地址,但可以用它保存偏移量。內存地址可能會因爲GC而變化,偏移量不會。
我們將要解決的這個問題,最終傳入的參數,肯定是以interface{}
的形式傳入,該interface{}
裏面可能是Resp
、*Resp
、Resp2
、*Resp2
等等各種情況,那我們先看一下reflect
庫中關於interface{}
的定義:
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
一個interface{}
由兩部分組成:typ
、val
(word
)。注意看,word
是個指針!這意味着什麼?CanSet
,是的,從某種意義上來說,它是可以被修改的。我們做個實驗:
type iface struct {
typ unsafe.Pointer // 我們知道類型信息,忽略該值
val unsafe.Pointer // 這裏我將字段名換成了val
}
func echo(x interface{}) {
println(x.(int)) // Output: 123
*(*int)((*iface)(unsafe.Pointer(&x)).val) = 456 // 標註
println(x.(int)) // Output: 456
}
func main() {
var i int = 123
echo(i)
println(i) // 想想輸出多少,爲什麼? // Output: 123
}
我們知道,如果將標註
的代碼換成reflect.ValueOf(x).SetInt(456)
,程序將panic,因爲變量x
不是CanSet
的。所以我們可以看到,reflect
不能做到事(指x.SetInt
),unsafe
做到了。
3.4.2 反射
有了上面的基礎,相信這一步非常簡單了,我們可以得到*Resp
的地址,要修改其中某個字段的值,還需要一樣東西:偏移量。我們將通過反射得到它:
func autoSetTimeAndServer(resp interface{}) {
typ := reflect.TypeOf(resp)
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
timeField, _ := typ.FieldByName("Time")
serverField, _ := typ.FieldByName("Server")
i := (*iface)(unsafe.Pointer(&resp))
*(*int64)(unsafe.Pointer(uintptr(i.val) + timeField.Offset)) = time.Now().Unix()
*(*string)(unsafe.Pointer(uintptr(i.val) + serverField.Offset)), _ = os.Hostname()
}
func send(resp interface{}) {
autoSetTimeAndServer(resp)
p, _ := json.Marshal(resp)
fmt.Println(string(p))
}
func main() {
send(&Resp{Num: 123})
// Output: {"time":1587802664,"server":"WTF","num":123}
send(Resp{Num: 456})
// Output: {"time":1587802664,"server":"WTF","num":456}
}
本例用到的反射知識不多,大多數是需要掌握unsafe
才能完成的操作,但將兩者結合起來,會有一定難度。所以這裏我將項目中實際遇到的情況精簡了很多,這樣大家可以更方便理解。
3.4.3 類型
前面小節中介紹的方法是非常容易理解的一種方法,不知道大家有沒有發現:該方法是在忽略類型的情況下實現需求的。我們在第二部分說了,反射最重要的是學會對類型的操作。下面我們將介紹對類型操作以實現需求。
在說具體操作前,我們需要先看reflect
庫的源碼:
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
func toType(t *rtype) Type {
if t == nil {
return nil
}
return t
}
簡化一下:
func TypeOf(i interface{}) Type {
return (*emptyInterface)(unsafe.Pointer(&i)).typ
}
由此可見,一個interface{}
中天然包含反射信息!下面我們自己組裝一個reflect.Type
試試(注意reflect.Type
本身是interface{}
):
func autoSetTimeAndServer(resp interface{}) {
typ := reflect.TypeOf("")
t := (*iface)(unsafe.Pointer(&typ)) // 獲取*reflect.rtype類型
r := (*iface)(unsafe.Pointer(&resp))
// 生成一個新的interface{}:
// typ是*reflect.rtype,確保該interface{}實現了reflect.Type;
// val是resp的類型,實際上我們是對該類型進行操作.
// 這個問題倒着想會比較輕鬆:
// 將一個*reflect.rtype類型的值轉成interface{}
// iface的typ和val各是什麼?
typ = *(*reflect.Type)(unsafe.Pointer(&iface{t.typ, r.typ}))
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
timeField, _ := typ.FieldByName("Time")
serverField, _ := typ.FieldByName("Server")
*(*int64)(unsafe.Pointer(uintptr(r.val) + timeField.Offset)) = time.Now().Unix()
*(*string)(unsafe.Pointer(uintptr(r.val) + serverField.Offset)), _ = os.Hostname()
}
如果您已經理解了上述操作,馬上您就會意識到,沒必要那麼麻煩,我們直接修改resp
的類型不就行了:
func autoSetTimeAndServer(resp interface{}) {
typ := reflect.TypeOf(resp)
if typ.Kind() != reflect.Ptr {
// 將類型變爲指針,以使resp CanSet
typ = reflect.PtrTo(typ)
(*iface)(unsafe.Pointer(&resp)).typ =
(*iface)(unsafe.Pointer(&typ)).val
}
// 現在resp肯定是指針,即val.CanSet()肯定爲true
val := reflect.ValueOf(resp).Elem()
val.FieldByName("Time").SetInt(time.Now().Unix())
hostname, _ := os.Hostname()
val.FieldByName("Server").SetString(hostname)
}
既然已經到這種程度了,那直接修改reflect.Value
可不可行呢?當然也可以,這裏不再贅述,只給出示例:
type rvalue struct {
typ unsafe.Pointer
ptr unsafe.Pointer
flag uintptr
}
func autoSetTimeAndServer(resp interface{}) {
val := reflect.ValueOf(resp)
if val.Kind() != reflect.Ptr {
typ := reflect.PtrTo(val.Type())
rv := (*rvalue)(unsafe.Pointer(&val))
rv.typ = (*iface)(unsafe.Pointer(&typ)).val
rv.flag = uintptr(reflect.Ptr)
}
val = val.Elem()
val.FieldByName("Time").SetInt(time.Now().Unix())
hostname, _ := os.Hostname()
val.FieldByName("Server").SetString(hostname)
}
4 反射練習
關於反射的理論文章很多,大家在看完理論後,會產生一種“我覺得我又行了”的錯覺。當實際用到反射的時候,卻發現,理論,離真刀真槍的實戰還有一定距離。所以,我在這裏給出一種練習方式:把普通代碼翻譯成反射代碼。
4.1 chan操作
本例旨在:
體現普通代碼和反射代碼別無二致。(反例例外)
我們經常會用chan當緩衝隊列用,舉個最簡單的例子:
ch := make(chan int)
go func() {
for i := 0; i < 3; i++ {
ch <- i
}
close(ch)
}()
for n := range ch {
println(n)
}
將上面代碼翻譯成反射代碼,該怎麼寫呢?很簡單,不需要解釋:
ch := reflect.MakeChan(reflect.ChanOf(reflect.BothDir,
reflect.TypeOf(int(0))), 0)
go func() {
for i := 0; i < 3; i++ {
ch.Send(reflect.ValueOf(i))
}
ch.Close()
}()
for {
n, ok := ch.Recv()
if !ok {
break
}
println(n.Int())
}
下面給出一個反例,用正常代碼不好實現,用反射卻異常輕鬆的一段代碼(非重點,不解釋):
func send(chs interface{}, value interface{}) int {
val := reflect.ValueOf(value)
ls := reflect.Indirect(reflect.ValueOf(chs))
n := ls.Len()
sc := make([]reflect.SelectCase, 0, n)
for i := 0; i < n; i++ {
sc = append(sc, reflect.SelectCase{
Chan: ls.Index(i),
Dir: reflect.SelectSend,
Send: val,
})
}
sc = append(sc, reflect.SelectCase{
Dir: reflect.SelectDefault,
})
i, _, _ := reflect.Select(sc)
if i == len(sc)-1 {
return -1 // default case
}
return i
}
func recv(chs interface{}, dst interface{}) int {
ls := reflect.Indirect(reflect.ValueOf(chs))
n := ls.Len()
sc := make([]reflect.SelectCase, 0, n)
for i := 0; i < n; i++ {
sc = append(sc, reflect.SelectCase{
Chan: ls.Index(i),
Dir: reflect.SelectRecv,
})
}
sc = append(sc, reflect.SelectCase{
Dir: reflect.SelectDefault,
})
i, v, ok := reflect.Select(sc)
if !ok {
return -1
}
reflect.ValueOf(dst).Elem().Set(v)
return i
}
func main() {
chs := make([]chan int, 10)
for i := 0; i < len(chs); i++ {
chs[i] = make(chan int, 1)
}
println("send to:", send(chs, 123))
// Output: send to 7
var v int
println("recv from:", recv(chs, &v))
// Output: recv from 7
println(v)
// Output: 123
}
如果用正常代碼,上面的邏輯怎麼實現?要知道數組chs
是變長的,有可能增長,也可能縮短,這種情況下,用正常代碼的select
寫法將會很難,比較簡單的一種實現方式是:
func init() {
rand.Seed(time.Now().UnixNano())
}
// 注意重新洗牌時要記錄原來的下標
func shuffle(chs []chan int) ([]chan int, []int) {
tmp := make([]chan int, len(chs))
idx := make([]int, len(chs))
copy(tmp, chs)
for i := 0; i < len(idx); i++ {
idx[i] = i
}
rand.Shuffle(len(tmp), func(i, j int) {
tmp[i], tmp[j] = tmp[j], tmp[i]
idx[i], idx[j] = idx[j], idx[i]
})
return tmp, idx
}
func send(chs []chan int, v int) int {
chs, idx := shuffle(chs) // 每次都要重新洗牌
for i, ch := range chs {
select {
case ch <- v:
return idx[i]
default:
}
}
return -1
}
func recv(chs []chan int, v *int) int {
chs, idx := shuffle(chs) // 每次都要重新洗牌
for i, ch := range chs {
select {
case *v = <-ch:
return idx[i]
default:
}
}
return -1
}
4.2 用反射寫鏈表
本例旨在:
反射代碼只是把普通代碼完全展開,完整的寫法。
一種經典的鏈表寫法:
type node struct {
Data interface{}
Next *node
}
type list *node
func makeList(ls *list, data ...interface{}) {
for _, d := range data {
*ls = &node{d, nil}
ls = (*list)(&(*ls).Next)
}
}
func main() {
var ls list
makeList(&ls, 1, true, "Hello world")
for p := ls; p != nil; p = p.Next {
fmt.Println(p.Data)
}
// Output:
// 1
// true
// Hello world
}
請將上面代碼makeList
函數用反射實現:
func makeList(ls interface{}, data ...interface{}) {
p := reflect.ValueOf(ls).Elem()
typ := p.Type().Elem()
for _, d := range data {
e := reflect.New(typ)
e.Elem().FieldByName("Data").Set(reflect.ValueOf(d))
p.Set(e)
p = e.Elem().FieldByName("Next")
}
}
4.3 鑽空子
看下面這段代碼:
var t struct{ a int }
println(reflect.ValueOf(&t).Elem().Field(0).CanAddr())
// Output: true
看到這的時候,是不是有人動了歪心思:如果CanAddr
,那是不是說可以通過它使未導出的字段可寫?很不幸,官方告訴你:不行。
var t struct{ a int }
v := reflect.ValueOf(&t).Elem().Field(0)
println(v.CanAddr()) // Output: true
println(v.Addr().Elem().CanSet()) // Output: false
println(v.Addr().CanInterface()) // Output: false
官方使未導出字段可讀已經是突破下限的仁慈了:
var t struct{ a int }
t.a = 123
println(reflect.ValueOf(&t).Elem().Field(0).Int())
// Output: 123
如果你就是想寫未導出的字段,能不能做到呢?參考前面unsafe
。
最後善意的提醒:
用unsafe
修改未導出字段,這種做法將破壞原有邏輯,十分危險!
type Buffer struct {
buf []byte
off int
bootstrap [64]byte
lastRead uint8
}
buf := bytes.NewBuffer(nil)
buf.WriteString("1234567890")
println(buf.String()) // Output: 1234567890
(*Buffer)(unsafe.Pointer(buf)).off = 5
println(buf.String()) // Output: 67890
如果是接口,記得要先用iface
過渡一下:
type iface struct {
typ unsafe.Pointer
val unsafe.Pointer
}
type digest struct {
h [5]uint32
x [64]byte
nx int
len uint64
}
hs := sha1.New()
hs.Write([]byte("1234567890"))
d := (*digest)((*iface)(unsafe.Pointer(&hs)).val)
println(d.len) // Output: 10
4.4 小結
結合上述例子來看,反射其實沒有什麼神祕之處,反射代碼和普通代碼在邏輯上是完全一致的,唯一不太一樣的點是:type
,普通代碼中的類型是直接聲明出來的,反射代碼中的類型需要自己推導。
5 結語
如果您已經看懂了本文所涉及的一些案例,恭喜您,反射對您已經不再神祕。反射已經由一把重劍巨闕化爲靈巧翻飛的瑞士軍刀。
關於反射的例子還有很多,但在這裏不再過多涉及,因爲大多例子和本文例子有異曲同工之妙。
在本文中,您看到的反射代碼,都相當的簡單,最重要的前提在於:我們對類型的限制。拋開大而全的理念,去做小而美,將更容易發揮反射的魅力。
不知道您有沒有注意到,在案例中,反射和非反射代碼,兩種寫法是混合交叉着在用的。這樣做的好處在於,非反射代碼能在一定程度上限制反射的使用,並保證反射代碼的正確性。
大家會看到,我只是給出了實現方案,而沒有給出優化方案,相信已經理解了本文內容的您,優化方案將很容易得到。