Go代码片段品鉴

作为一个服务端开发,熟悉我们服务的业务至关重要,但我们换工作的时候,也经常会换到别的业务场景下。比方说,我在成人教育的行业工作了2年,跳槽到了支付的行业。这么看的话,业务并不能成为服务端工程师的求职门槛。但我们可能会有被评价过缺少产品 sense,限制我们始终工作在一线。

抛开业务的特殊场景,我们所开发的功能其实大同小异,面临的问题也有很高的重复性。当遇到之前解决过的问题,我经常会回忆之前写过的代码,代码本身可能很烂,但总觉得它经过了线上的考验,是信的过的。但很严肃的结果是,自己完全回忆不起来。

这篇文章就是想细数一些之前写过的代码,或者,遇到的写的比较好的代码(博客中的、或者三方库中的),也可能是罗列一些代码的问题。总之,就是不成体系的代码片段。

Go协程封装

下面的代码展示的是启用协程的常规操作,为了处理协程内可能的 panic,在方法体第一行总需要 defer recover 这样的组合,当捕获异常时,还需要日志打印调用堆栈的信息。

这种写法其实非常不美观,对于有代码洁癖的人来说,如果在同一个函数体内赋值拷贝2次以上,他就会感觉很崩溃。有些没有代码洁癖的人,可能就是按需拷贝N次了,这对于有代码洁癖的人而言,会是更加崩溃的事情。毕竟,一个项目都是多个人共同在维护的,不是每个人都有代码洁癖。

go func() {
		defer func() {
			if r := recover(); r != nil {
				// 打印堆栈信息
			}
		}()

		// 异步逻辑处理
	}()

所以,很多人就开始想把 go defer recover 封装起来,下面就是其中的一种,异步调用 fun 函数,并明确传递需要的 args 参数,方法内部也做了简单的参数判断,最后,通过反射函数 Call 执行函数 fun。代码为了做到兼容,引入了反射,但也因为反射,会损失掉一部分性能。

这个封装除了性能,当发生 panic 时,打印堆栈日志的部分可能需要特别注意。一般来说,我们的日志都会输出打印日志的文件名和行号,但通过这样的封装,发生错误的文件和行号都变成了 GoFunc 所在的文件和行号。如果要变得更完善,还需要调整堆栈的层级。

func GoFunc(fun interface{}, args ...interface{}) (err error) {
	v := reflect.ValueOf(fun)
	go func(err error) {
		defer func() {
			if r := recover(); r != nil {
				// 打印堆栈日志
			}
		}()
		
		switch v.Kind() {
		case reflect.Func:
			pps := make([]reflect.Value, 0, len(args))
			for _, arg := range args {
				pps = append(pps, reflect.ValueOf(arg))
			}
			v.Call(pps)
		default:
			err = errors.New(fmt.Sprintf("func is not func, type=%v", v.Kind().String()))
		}
	}(err)
	return
}

IN 分批查询

SQL 查询时可能会遇到 IN 查询的情况,IN 中参数多少一般是由具体的业务请求来决定的,一般用户可能 IN 后面跟小于 5 个索引,特殊用户 IN 后面可能多一些。考虑一些极端的场景,如果 SQL 的 IN 中包含了成百上千个索引,我们要不要去查询底层数据?直接查询数据会引起什么问题吗?

引申个题外话,对于用户的任意请求,我们要做好防守,防守一些极端 case。 SQL 语句 LIMIT 就是一个很好的防守例子,我们给 LIMIT 设置一个预期内的最大值,如果用户的查询超过了这个最大值限制,就直接替换为这个预期内的最大值。

回归正题,IN 参数特别多会导致底层数据库的负载压力不均衡,命中极端 IN 查询的服务 CPU 可能会瞬间拉高,影响服务的其他查询。和大 KEY 的场景差不多,存储大 KEY 或者热 KEY 的服务,负载就会比其他服务高一些。

解决的思路也很简单,在调用端采用分批请求的方式,人为的将一次 IN 查询拆分成多次 IN 查询,关键点在于解决条件的拆分,以及结果集的合并。将一次请求拆分成并发的多次请求,最后将各路请求的结果进行合并,就是我们的工作。

下面展示了拆分的具体代码实现,注意,代码只是一个拆分模式,并不能直接运行。而且,代码还存在其他缺陷,没有支持上泛型、分批查询的数量限制也不能动态设置,但我觉问题不大,看关键点就够了。

func PatchQuery(ctx context.Context, keys []string,
	query func(ctx context.Context, keys []string, result *sync.Map)) *sync.Map {
	
	var result sync.Map
	var wg sync.WaitGroup

	patchSize := 64
	length := len(keys)

	for i := 0; i < length; {
		if i+patchSize >= length {
			patch := keys[i:]
			query(ctx, patch, &result)
			break
		}

		wg.Add(1)
		go func(index int) {
			defer wg.Done()

			patch := keys[index : index+patchSize]
			query(ctx, patch, &result)
		}(i)

		i += patchSize
	}

	wg.Wait()
	return &result
}

因为内部存在并发结果的合并,简单的引入了 sync Map 来解决并发。用 map 还有一个原因,就是 key 值可以通过 IN 的索引进行构建,通过 key 来查询到具体的结果。无论 IN 的条件是否是唯一索引,我们都可以正确的处理返回结果。

正确的使用切片是这里的重点,切片中的 [a:b] 是半闭半开的区间范围,一定不要混淆。另外,切片的最后一次查询是否要启用新的协程也值得考量,万事没绝对,我倒是觉得,不用是比较适当的。

你可能感兴趣的:(golang,java,开发语言)