Go reflect 反射实例解析

Go reflect 反射实例解析

——教坏小朋友系列

0 FBI WARNING

对于本文内容,看懂即可,完全不要求学会。


1 现象分析

网上关于golang反射的文章多如牛毛,可实际用反射的人,寥寥可数。

我分析大家不用反射的原因,大概有两点:

  • 不会
  • 笨重

1.1 不会

网上关于反射的文章很多,可多数都是在分析原理,并没有给出实例。而反射恰好处在一个很尴尬的点上:懂了原理不等于会用。

于是乎,大多数人高喊口号:“官方不推荐用这个库!”是的,官方不推荐用的库有两个:reflectunsafe(严格意义上来说,还有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.Scannerbufio.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。

为什么要实现所有可能参数的处理逻辑呢?参照官方库jsonreflect.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()

可以看到,变量gt已经没有关系,但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.LimtedReadercontext.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"))
})))

如果您看过ginecho的代码,就会明白,装饰器(在http中或叫middleware)的实现方式大同小异,比如gin.New().Use()。这些第三方库最大的好处在于,写法上更简捷易懂。

3.1.3 反射

不知道大家发现没有,大多数的第三方库,包括gin、echo,他们都是在解决路由和中间件的问题。但大家在处理逻辑时,拿到的还是最原始的http.Requesthttp.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,
    
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值