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,