拯救你的Go代码

2,892 阅读6分钟

稍微有点标题党,不过很短,可以看

背景

最近一直在参加开发公司新运营系统的引擎部分,写了很多很多代码,也逐渐产生了一些自己对此类业务的解决方案的简单想法。运营系统干的事,其实就是支持运营人员配置活动。所谓活动其实可以简单定义为:一系列条件都满足就执行某个(些)动作。对应到程序员的黑话就是:一坨if else之后执行某(几)个函数

if else

说到写if else,大多数程序员可能都会会心一笑,毕竟不论写啥系统,代码里大部分语句还是if else。业务系统就是对PM的需求堆if else,基础服务就是对OS资源和网络问题堆if else。越来越多的需求造成了代码的膨胀,如何来管理这坨if else,就衍生出了设计模式:通过各种手段来把if else划分到更小的粒度。当然,还没有任何一套方法论可以解决if else复杂性的问题,因为if else反映的其实是真实业务的需求,它其实代表了业务的复杂性。就像鲁迅或者马云或者巴菲特说的:“不论用什么方法,if else不会凭空消失,只会从一个地方转移到另一个地方”。但是,在面对特定的场景,特定的业务,代码的阅读性、可维护性以及扩展性是可以得到提升的。

规则引擎

规则引擎不是一个新东西,而且已经存在了很长时间了,市面上也有各种规则引擎。比如大家常用的iptables,就是一种规则引擎,crontab也是一种规则引擎。我们平时写的业务逻辑,其实也是规则引擎,只是用一种不那么明显的方式来体现。比如:“如果重置的密码没有包含大小写和标点符号,就不让提交”,“如果用户是抽奖送的VIP虽然不播广告但给他弹窗”……这些业务逻辑,无论怎么优化,要处理的代码分支是一定存在的。规则引擎通过自定义的一套语法(DSL),提供了编程语言所不具备的语法糖,来最大限度减少开发量。同时大部分规则都需要支持用户随意配置,因此DSL大多是解释执行。用户在后台配置一条规则比如,游戏中用户在线1000分钟后扣20的点卡:

        When
        {
           ?customer: Customer(totalTime >=1000);
        }
        Then
        {
           execute {?customer.setAmount(getAmount()-20.00);
        } 

市面上比较常用的几款规则引擎都属于非常重量级的,用户必须去学习它的DSL才能使用,当然基本上都是JAVA的包。这有一篇文章是几款规则引擎的比较。没有深度用过,我也没法对此进行评价,但由于其概念实在太多且使用场景受限,我觉得它始终是个临时方案,毕竟“大道至简”。而且DSL会越写越复杂,真正用到语法糖的地方还是少数,到最后变成另一种蹩脚的而且领域受限的编程语言了。虽然如此,但由于其规则可动态增加,老规则便于复用,对于提供通用解决方案的一些软件公司来说,规则引擎还是很有吸引力的。毕竟代码不需要动,加一些规则就能再次销售。

但是对于进行业务开发的RD来说,其实需求很简单,如果一个东西:

  • 正确
  • 简单
  • 能减少繁琐的工作

那么这便是一个有吸引力的解决方案,如果它能再快点儿,那就是一个绝妙的主意了。对于减少if else,我觉得Linq是一个非常好的方案。

Linq

Linq是C#中的一种常见技术,用过的都觉得爽。它就是一种语法糖,编译器会编译成真正的代码而不是在运行期解释执行,因此效率也很高。Linq代码大概就是这样:

        // Specify the data source.
        int[] scores = new int[] { 97, 92, 81, 60 };

        // Define the query expression.
        IEnumerable<int> scoreQuery =
            from score in scores
            where score > 80
            select score;

可以看到,在代码中一些逻辑如果能这样表达,那代码将会变得非常简单。linq基本上就是把sql语句搬到了C#中,唯一的区别就是它把select放在了最后而sql是在前面。这其实也是因为IDE智能提示的需求,而不是查询结构本身要这么设计。把select放在最后,IDE就知道你前面用过哪些变量,就可以分析出你可以select什么从而进行提示。但是Linq是C#中的技术,和编译器是强相关的,想移植到别的语言,还是很不容易的。当然,社区也是有一些尝试的,比如go的移植版linq-go。然而,你懂的,由于go本身不支持泛型,编译器也不支持linq语法,因此使用起来当然还是比较蹩脚,而且肯定是interface{}和类型推断满天飞了……

sql

其实我们常用的sql中的where部分是完备的,Linq也是把sql搬到了C#中。换句话说,sql的where部分其实可以组合任意的布尔逻辑。所有RD都会使sql,而且是经常使sql,既然想从编译器的方向搞事情太麻烦(尤其是go是一门以简单著称的语言,官方对于加关键字是深恶痛绝的,更别说语法糖了),那么把mysql查询功能干掉,解释执行where比较的那部分功能做成一个函数提供出来呢,比如查询一个人是不是顶级程序员可以这么查:

sql := `sex='male' 
        and (
            dislike in ('girl', 'woman', 'female') 
            or 
            hobby in ('dress up', 'makeup')
        )`

但是由于没有表数据,那么真正用于比较的数据就需要业务方自己来提供了,以上功能可以写为:

func isTopProgrammer(userInfo User) bool {
    sql := `sex='male' 
        and (
            dislike in ('girl', 'woman', 'female') 
            or 
            hobby in ('dress up', 'makeup')
        )`
    ok,_ := yql.Match(sql, map[string]interface{}{
        "sex": userInfo.Sex,
        "dislike": userInfo.Dislike,
        "hobby": userInfo.Hobbies,
    })
    return ok
}

yql其实就是我最近搞的一个lib求star,求pr,求指导),帮你执行sql的比较。

如果这么搞,其实业务逻辑中经常变动的部分可以抽象到配置文件里,比如:

// 先定义好不同case的处理函数
type handleFunc func(map[string]interface{}) error
var handlers = map[int]handleFunc {
    1: sendEmail,
    2: sendCoupon,
    3: sendSms,
    4: sendAlert2Boss,
    5: runAway,
}
// 从当前请求中解析数据
data := resolveDataFromPostParams(request.Body)
// 从配置文件加载规则
rules := loadRuleFromConf()

//枚举每条规则进行匹配,如果匹配成功则执行对应handler
for _,rule := range rules {
    success,err := yql.Match(rule.SQL, data)
    if nil != err || !success {
        continue
    }
    handler := handlers[rule.ID]
    handler(data)
    break
}

当然这种方式也有利有弊,比如说,把规则分离到配置文件中,在debug时得几个文件之间跳来跳去地看代码,比较蛋疼(少写点bug就行了,哈哈)。优点之一比如说可以利用推送平台实时更新配置文件,从而达到代码热更新的目的。

最后

其实yql还有很多应用场景(我感觉),当然也有一些不足,比如功能还比较单一,解释执行,快不快其实也没和谁对比过……不过希望大家能够尝试尝试,如果能够提升工作效率,还是不错的。求关注,求star