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

2024-04-21 05:08

本文主要是介绍用Go写业务系统需要制造哪些轮子?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

错误处理

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

type Error struct {Message stringPrevError error
}

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

// prevError: 原始错误
// src: 可以填写源文件名
// desp: 新error对象的错误描述
func Wrap(prevError error, src string, desp string) error {var msg stringif "" != 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 := estrBuilder := bytes.Buffer{}nestTier := 0for {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.PrevErrornestTier++}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 idstrconv.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}// 行数据转成mapresultMap := 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 = taskIdtaskName, _ := result["task_name"]task.TaskName = taskNamestatus, _ := result["status"]task.Status = statuscreateByStr, _ := result["create_by"]createBy, err := strconv.Atoi(createByStr)if nil != err {return er.Wrap(err, sourceFile, "failed to load create_by")}task.CreatedBy = createByupdate, _ := result["update_time"]task.UpdateTime = updatetasks = 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")// 初始化Routerlog.Println("init router")router := initRouter(config)// 初始化DBlog.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才能变成实现业务系统的好语言。在此之前,还是老老实实的做基础应用吧。

这篇关于用Go写业务系统需要制造哪些轮子?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/922213

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

基于人工智能的图像分类系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 图像分类是计算机视觉中的一个重要任务,目标是自动识别图像中的对象类别。通过卷积神经网络(CNN)等深度学习技术,我们可以构建高效的图像分类系统,广泛应用于自动驾驶、医疗影像诊断、监控分析等领域。本文将介绍如何构建一个基于人工智能的图像分类系统,包括环境

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【区块链 + 人才服务】可信教育区块链治理系统 | FISCO BCOS应用案例

伴随着区块链技术的不断完善,其在教育信息化中的应用也在持续发展。利用区块链数据共识、不可篡改的特性, 将与教育相关的数据要素在区块链上进行存证确权,在确保数据可信的前提下,促进教育的公平、透明、开放,为教育教学质量提升赋能,实现教育数据的安全共享、高等教育体系的智慧治理。 可信教育区块链治理系统的顶层治理架构由教育部、高校、企业、学生等多方角色共同参与建设、维护,支撑教育资源共享、教学质量评估、

业务中14个需要进行A/B测试的时刻[信息图]

在本指南中,我们将全面了解有关 A/B测试 的所有内容。 我们将介绍不同类型的A/B测试,如何有效地规划和启动测试,如何评估测试是否成功,您应该关注哪些指标,多年来我们发现的常见错误等等。 什么是A/B测试? A/B测试(有时称为“分割测试”)是一种实验类型,其中您创建两种或多种内容变体——如登录页面、电子邮件或广告——并将它们显示给不同的受众群体,以查看哪一种效果最好。 本质上,A/B测

软考系统规划与管理师考试证书含金量高吗?

2024年软考系统规划与管理师考试报名时间节点: 报名时间:2024年上半年软考将于3月中旬陆续开始报名 考试时间:上半年5月25日到28日,下半年11月9日到12日 分数线:所有科目成绩均须达到45分以上(包括45分)方可通过考试 成绩查询:可在“中国计算机技术职业资格网”上查询软考成绩 出成绩时间:预计在11月左右 证书领取时间:一般在考试成绩公布后3~4个月,各地领取时间有所不同

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识