优化go代码,使用动态sql查询和递归

分享在工作中优化go代码的案例,通过使用递归和动态sql查询,避免每次新增类型时都要修改多处代码。

之前用go语言开发考试系统,最近闲下来决定改一下之前的狗屎代码。注意以下所有代码并非完全真实的源码。

本次代码场景如下:考试系统有多种题型,在后台接口中有导入和查询两个需求,如果新增了题型,就需要手动去多个代码文件中修改查询语句、循环赋值、定义变量。

动态sql

// 之前的版本
// 查询分类下有多少题目、多少单选题目、多选题目...
// categorySelect 查询
type categorySelect struct {
    CategoryID     uint `json:"category_id"`
    Num            uint `json:"num"`
    SingleChoice   uint `json:"single_choice"`
    MultipleChoice uint `json:"multiple_choice"`
    ShortAnswer    uint `json:"short_answer"`
    BlankAnswer    uint `json:"blank_answer"`
    TrueFalse      uint `json:"true_false"`
}

// 查询分类下题目数量
var categories []categorySelect
if err := categoryQuery.Model(&question{}).
    Select("category_id, count(*) as num, SUM(CASE WHEN type = 1 THEN 1 ELSE 0 END) as single_choice, SUM(CASE WHEN type = 2 THEN 1 ELSE 0 END) as multiple_choice, SUM(CASE WHEN type = 3 THEN 1 ELSE 0 END) as true_false, SUM(CASE WHEN type = 4 THEN 1 ELSE 0 END) as blank_answer, SUM(CASE WHEN type = 5 THEN 1 ELSE 0 END) as short_answer").
    Group("category_id").
    Find(&categories).Error; err != nil {
    return err
}

按之前的逻辑,每次增加新题型我都需要修改select语句,统计新题型。第一步优化就是使用动态sql拼接。好处很明显,每次修改sql select语句容易出错也很不优雅,还可能因为太多地方用到而忘记修改。

// 首先定义题型map, string对应枚举
var QuestionMap = map[string]uint{
    "single_choice":   SingleChoice,
    "multiple_choice": MultipleChoice,
    "true_false":      TrueFalse,
    "blank_answer":    BlankAnswer,
    "short_answer":    ShortAnswer,
}

// 然后遍历map,拼接sql
typeCountCases := make([]string, 0)
for k, v := range QuestionMap {
    typeCountCases = append(typeCountCases, fmt.Sprintf("SUM(CASE WHEN type = %d THEN 1 ELSE 0 END) as %s", v, k))
}

Select("category_id, count(*) as num, " + strings.Join(typeCountCases, ", ")).

优化及进度:20%,提升:20%。

递归统计子分类

这个代码要改的地方还不少,第二个就是改掉统计子分类的代码。因为分类有层级关系,而父分类的题目数量也需要加上所有子分类的和。因为最多只有三级分类,所以之前的代码离谱但能用。

// 依托代码,可以直接跳过
// 递归计算分类下题目数量
var numCategory func(list []QuestionCategory, numList []categorySelect) []categoryList
numCategory = func(list []QuestionCategory, numList []categorySelect) []categoryList {
    categories := make([]categoryList, 0)
    for _, v := range list {
        var category categoryList
        category.ID = v.ID
        category.Name = v.Name
        for _, num := range numList {
            if num.CategoryID == v.ID {
                category.TotalNum = num.Num
                category.QuestionNum = num.Num
                category.SingleChoiceNum = num.SingleChoice
                category.MultipleChoiceNum = num.MultipleChoice
            }
        }

        if len(v.Child) > 0 {
            category.Child = numCategory(v.Child, numList)
        }
        categories = append(categories, category)
    }
    return categories
}
final := numCategory(list, categoryArr)

for k, v := range final {
    if len(v.Child) > 0 {
        for k2, child := range v.Child {
            v.TotalNum += child.QuestionNum
            v.SingleChoiceNum += child.SingleChoiceNum
            v.MultipleChoiceNum += child.MultipleChoiceNum
            if len(child.Child) > 0 {
                for _, child2 := range child.Child {
                    v.TotalNum += child2.QuestionNum
                    v.Child[k2].TotalNum += child2.QuestionNum
                    v.SingleChoiceNum += child2.SingleChoiceNum
                    v.MultipleChoiceNum += child2.MultipleChoiceNum
                    v.Child[k2].SingleChoiceNum += child2.SingleChoiceNum
                    v.Child[k2].MultipleChoiceNum += child2.MultipleChoiceNum
                }
            }
        }
    }
    final[k] = v
}

这段代码说实话我看了都脸红,好像用上了递归,然后又在那用父分类+子分类+子子分类。好好好,纯脑浆代码,我觉得有可能是ai帮我写了一部分(ai:这代码我写不出来)

// 优化了一版
func numCategory(userInfo common.UserInfo, list []QuestionCategory, numList []categorySelect) []categoryList {
    var categories []categoryList
    for _, v := range list {
        var category categoryList
        category.ID = v.ID
        category.Name = v.Name
        category.Sort = v.Sort
        category.Level = v.Level
        category.ParentID = v.ParentID
        if len(v.Child) > 0 {
            category.Child = numCategory(userInfo, v.Child, numList)
        }
        category.ChangeAuth = true
        if userInfo.RoleID == define.Teacher && v.CreatorID != userInfo.UserID {
            category.ChangeAuth = false
        }

        for _, num := range numList {
            if num.CategoryID == category.ID {
                category.TotalNum = num.Num
                category.SingleChoiceNum = num.SingleChoice
                category.MultipleChoiceNum = num.MultipleChoice
                break
            }
        }

        for _, child := range category.Child {
            category.TotalNum += child.TotalNum
            category.SingleChoiceNum += child.SingleChoiceNum
            category.MultipleChoiceNum += child.MultipleChoiceNum
        }

        categories = append(categories, category)
    }
    return categories
}

说实话能写出之前那种代码,说明我完全不懂的递归怎么写,新版也是我让AI写的,这次必须得给递归拿下了。

进度:50%,提升:60%,恶心程度:100%。

反射赋值

优化完递归的代码已经好些了,但我继续问AI,能不能连定义变量和赋值都省去,AI说可以,跟我想的一样用反射实现。

// FillQuestionTypeNum 使用反射填充 QuestionTypeNum 结构体的字段
func FillQuestionTypeNum(target *QuestionTypeNum, source *QuestionTypeNum) {
    if target == nil || source == nil {
        return
    }
    targetValue := reflect.ValueOf(target).Elem()
    targetType := reflect.TypeOf(target).Elem()
    numValue := reflect.ValueOf(source).Elem()
    for i := 0; i < targetValue.NumField(); i++ {
        field := targetValue.Field(i)
        fieldName := targetType.Field(i).Name
        numField := numValue.FieldByName(fieldName)
        if field.Kind() == reflect.Uint && numField.IsValid() && numField.Kind() == reflect.Uint {
            field.SetUint(numField.Uint())
        }
    }
}

实现的还可以,就当我有点得意的时候,我突然意识到,这件事为什么要用到反射呢?直接抽象出一个赋值函数不就好了吗?

// 用来查询sql的结构体
type categorySelect struct {
   QuestionTypeNum
}
// 返回给前端的结构体
type categoryList struct {
    QuestionTypeNum
}

// 定义一个题目类型数量查询的结构体
type QuestionTypeNum struct {
    SingleChoiceNum uint
    MultipleChoiceNum uint
}

// 定义一个public的赋值函数
func FillQuestionTypeNum(target *categoryList, source *categorySelect) {
    if target == nil || source == nil {
        return
    }
    target.SingleChoiceNum = source.SingleChoiceNum
    ...
}

按照这样的方法,新增题型后,只需要新增QuestionTypeNum结构体,然后在FillQuestionTypeNum中赋值即可。也就比反射多了一步,我觉得这样就挺好了。

进度:80%,提升:100%,恶心程度:50%。

使用map替代switch

switch代码写起来非常好理解,(ai填充的也很好),但还是如果新增了题型就要去修个各个地方的switch判断题目类型的代码,改成map会更方便。

// 以前是以前
var questionType int
switch typeStr {
case "all":
    questionType = 0
case "single_choice":
    questionType = 1
case "multiple_choice":
    questionType = 2
case "true_or_false":
    questionType = 3
case "fill_blank":
    questionType = 4
case "short_answer":
    questionType = 5
default:
    continue
}

// 现在是现在
// 之前定义的题目类型map派上用场
var questionType int
val, ok := define.QuestionMap[typeStr]
if !ok {
    continue
}
questionType = val

进度:100%,提升:100%。

反思一下

此次优化代码算是圆满完成了,虽然肯定还是有不够合理的地方,但毕竟是后台代码,有点小瑕疵只要结果是正确的就还好。虽然提升确实很大,改完也舒服了,但这还是得益于之前写的太烂了,不知道是太赶了还是太划了,感觉写的既不简洁也不优雅,新增了题型要改动好多地方的代码。

此次的启发:

  • 写完的代码重新检查一下有惊喜
  • 有些明知道写的不够好的,也懒得去修改,这样真不行
  • 代码健壮性是第一位的,无论怎么样应该降低出错的概率

下一步是优化题目类型的设计模式,使用类似继承、组合的方式,尽可能减少在业务代码中大段的switch判断,同时也能保证业务代码的扩展性。

Licensed under CC BY-NC-SA 4.0
加载中...
感谢Jimmy 隐私政策