用Go写业务系统需要制造哪些轮子?

如果之前主要是用Java做业务系统 ,那么想用go重写的话还是比较痛苦的,最主要的原因就是你会发现要啥没啥,需要自己重写(造轮子)。下面列举了一些需要施工的基础设施。

错误处理

在Java中,只要你没有刻意的使用4参数的Exception构造方法去定义自己的异常类,那么默认情况下都是会记录调用栈的,这样基本上就能马上定位到事故第一现场,排查效率很高。Go则不然,如果使用默认的error机制,那么在报错的时候你得到的只是一个简单的字符串,没有任何现场信息。我在调试的时候最大的痛苦也是如此,报错了,但一时很难快速定位到出错的代码,如果是比较陈旧的项目,那就更不知道这个错误是在哪返回的了。不仅如此,因为go里如果遇到panic且没有被"捕获",那么就会直接导致进程退出,整个服务直接崩溃,这也是不可接受的。
为了解决错误现场的问题,我们可以自己定义一个结构体,它在实现error接口的同时,再添加一个PrevError的字段用于记录上层错误,类似于Java Exception的cause()方法:

type Error struct {
    Message string
    PrevError error
}

然后定义一个Wrap()方法,在遇到错误时,先将先前的错误传进去,然后再填写一条符合本层逻辑的描述信息:

// prevError: 原始错误
// src: 可以填写源文件名
// desp: 新error对象的错误描述
func Wrap(prevError error, src string, desp string) error {
    var msg string
    if "" != src {
        msg = "[" + src + "] " + desp
    } else {
        msg = desp
    }

    err := &Error{
        Message: msg,
        PrevError: prevError,
    }

    return err
}
if nil != err {
    return er.Wrap(err, sourceFile, "failed to convert id")
}

注意第二个参数src, 这里可以直接通过硬编码的形式将当前源文件名传进去,这样日志中就会出现

[xxxx.go] failed to convert id

方便错误排查。相比较标准库的runtime.Call()方法我更倾向于自己手动把文件名传进来,由于行号会经常变动就不传了,而文件名很少改动,因此这是开销最低的记录现场的方法。
有了自定义的错误以后,在最上层(一般是你的HTTP框架的Handler函数)获取到error后还需要把这个错误链条打印出来,如:

func Message(e error) string {
    thisErr := e

    strBuilder := bytes.Buffer{}
    nestTier := 0
    for {
        for ix := 0; ix < nestTier; ix++ {
            strBuilder.WriteString("\t")
        }
        strBuilder.WriteString(thisErr.Error())
        strBuilder.WriteString("\n")

        myErrType, ok := thisErr.(*Error)
        if !ok || nil == myErrType.PrevError {
            break
        }

        thisErr = myErrType.PrevError
        nestTier++
    }

    return strBuilder.String()
}

直接使用Message()函数打印错误链:

// 调用用户逻辑
        resp, err := handlerFunc(ctx)
        if nil != err {

            log.Println(er.Message(err))
            return
        }

效果如下:

2019/07/26 17:28:48 failed to query task
    [query_task.go] failed to parse record
        [db.go] failed to parse record
            [query_task.go] failed to convert id
                strconv.Atoi: parsing "": invalid syntax

嗯,是不是有点意思了?对于业务错误这样是可以的,因为类似于参数格式不对、参数不存在这样的问题是会经常发生的,使用这种方式能以最小的开销将问题记录下来。但对于panic来说,我们需要在最上层使用recover()debug.Stack()函数拿到更加详细的错误信息:

        // 处理panic防止进程退出
        defer func() {
            if err := recover(); err != nil {
                log.Println(err)
                log.Println(string(debug.Stack()))
                                // ... ...
            }
        }()

因为go里遇到panic如果没有recover,整个进程都会直接退出 ,这显然是不可接受的,因此上面的方式是必须的,我们不想因为一个空指针就让整个服务直接挂掉。(听起来有点像C++?)

HTTP请求路由

因为我用的HTTP框架fasthttp是不带Router的,因此需要我们选择一个第三方的Router实现,比如fasthttprouter。这样一来我们启动在启动的时候就要有一个注册路由的过程,比如

router.GET("/a/b/c", xxxFunc)
router.POST("/efg/b", yyyFunc)

确实远远没有SpringMVC里直接写Controller来的方便。

请求参数绑定

想直接定义一个结构体,然后请求来了参数就自动填写到对应字段上?不好意思,没有。fasthttp中获取参数的姿势是这样的:

func GetQueryArg(ctx *fasthttp.RequestCtx, key string) string {
    buf := ctx.QueryArgs().Peek(key)
    if nil == buf {
        return ""
    }

    return string(buf)
}

对,拿到以后还是个字节数据,还需要你手动转成string,不仅如此,你还得进行非空判断,如果想获取int类型,还需要调用转换函数strconv.Atoi(),然后再判断一下转换是否成功,十分繁琐。如果想实现像SpringMVC那样的参数绑定,你需要自己写一套通过反射创建对象并根据字段名设置参数值的逻辑。不过笔者认为这一步并不是必须的,写几个工具方法也能解决问题,比如上面。

数据库查询

好吧,最痛苦的还是查数据库。标准库中定义的数据库查询接口非常难用,难用到发指,远不如JDBC规范好使。里面最反人类的就是这个rows.Scan()方法,因为它接收interface{}类型的参数,所以你还得把你的具体类型"转换"成interface{}才参传进去:

    values := make([]sql.RawBytes, len(columns))
    scanArgs := make([]interface{}, len(columns))
    for i := range columns {
        // 反人类的操作!!!
        scanArgs[i] = &values[i]
    }

    for rows.Next() {
        err = rows.Scan(scanArgs...)

此外,你肯定不想每次查数据都要把这一套Prepare... Query... Scan... Next写一遍吧,所以需要做一下封装,比如可以将结果集转成一个map, 然后调用用户自定义的传进来的函数来处理,如:

// 执行查询语句;
// processor: 行处理函数, 每读取到一行都会调用一次processor
func ExecuteQuery(querySql string, processor func(resultMap map[string]string) error, args ...interface{}) error {}
    for rows.Next() {
        err = rows.Scan(scanArgs...)
        if nil != err {
            return err
        }

        // 行数据转成map
        resultMap := make(map[string]string)
        for ix, val := range values {
            key := columns[ix]
            resultMap[key] = string(val)
        }

        // 调用用户逻辑
        err = processor(resultMap)
        if nil != err {
            return er.Wrap(err, srcFile, "failed to parse record")
        }
    }

即便这样,用户的处理函数processor()也是非常丑陋的:

    err := db.ExecuteQuery(sql, func(result map[string]string) error {
        task := vo.PvTask{}

        taskIdStr, _ := result["id"]
        taskId, err := strconv.Atoi(taskIdStr)
        if nil != err {
            return er.Wrap(err, sourceFile, "failed to convert id")
        }
        task.TaskId = taskId

        taskName, _ := result["task_name"]
        task.TaskName = taskName

        status, _ := result["status"]
        task.Status = status

        createByStr, _ := result["create_by"]
        createBy, err := strconv.Atoi(createByStr)
        if nil != err {
            return er.Wrap(err, sourceFile, "failed to load create_by")
        }
        task.CreatedBy = createBy

        update, _ := result["update_time"]
        task.UpdateTime = update

        tasks = append(tasks, &task)

        return nil
    }, args...)

一个字段一个字段的读,还得进行错误判断,要死人的。
上面这个问题解决方案只有一个,那就是使用第三方的ORM框架。然而,现在三方ORM眼花缭乱,没有一个公认的权威,这样就为项目埋下很多隐患,比如日后你用的框架可能不维护了,可能要换框架,可能有奇怪的bug等等。笔者建议还是自己写一套吧,遇到问题修改起来也方便。

数据库事务

想在方法上标注@Transactional来开启事务?不好意思,想多了。你要手动使用db.Start(), db.Commit(), db.Rollback()

日志框架问题

日志框架到底用哪个一直是非常让我头疼的问题。标准库的log包缺乏自动切割文件的基本功能,github上star最多的logrus居然不能输出人看着舒服的日志格式,还美其名曰鼓励结构化。你结构化方便程序解析也好,关键是你也得提供一个正常的日志输出格式吧?之前用过log4go,可惜已经不维护了。
这个问题至今无解,实在不行,自己写吧。

组件初始化顺序问题

我们已经被Spring给惯坏了,只管把@Component写好,然后Spring会自己帮你初始化,尤其是顺序也帮你安排好了。然而,go不行。因为没有spring这样的IoC框架,所以你必须自己手动触发每个模块的初始化工作,比如先初始化日志,加载配置文件,再初始化数据库连接、Redis连接,然后是请求路由的注册,等等等等,大概长这样:

    // 初始化日志库
    initLogger()

    // 加载配置文件
    log.Println("load config")
    config := config.LoadConfig("gopv.yaml")
    log.Println(config)

    // 加载SQL配置
    template.InitSqlMap("sql-template/pv-task.xml")

    // 初始化Router
    log.Println("init router")
    router := initRouter(config)

    // 初始化DB
    log.Println("init db")
    initDb(config)

而且顺序要把握好,比如日志框架要放在所有模块之前初始化,否则日志框架可能会有问题。

分包问题

在Java里,你A文件import B里定义的类,然后 B文件又import A文件定义的类,这是OK的。但go不行,编译时会直接报循环引用错误。所以在包的定义上真的就不能随心所欲了,每次创建新的package,你都要考虑好,不能出现循环引用,这有时候还是很隔应人的。当然你可以说,如果出现A import B, B import A,那就是代码有问题,从哲学上来看貌似没问题。但现实是在Java中这种情况很普遍。

依赖问题

这个在go1.11以后可以说已经不算是大问题了,使用官方的module即可。但是在此之前,go的依赖管理就是一场灾难。

或许有一天能出现一个权威的框架来一站式的解决上面这些问题,只有那时候,Go才能变成实现业务系统的好语言。在此之前,还是老老实实的做基础应用吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,444评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,867评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,157评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,312评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,673评论 3 289
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,802评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,743评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,470评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,696评论 2 250
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,187评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,538评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,188评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,127评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,889评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,741评论 2 274

推荐阅读更多精彩内容

  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 4,870评论 0 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,039评论 1 32
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,353评论 0 15
  • stream TARS 框架的编解码工具 结构体的使用示例我们演示结构体在三个典型场景的使用方法:第一种场景:当结...
    宫若石阅读 1,546评论 0 1
  • 1. 分布式系统核心问题 参考书籍:《区块链原理、设计与应用》 一致性问题例子:两个不同的电影院买同一种电影票,如...
    molscar阅读 874评论 0 0