go-linq 从入门到实战

go-linq 是什么?

强大的 Go 语言集成查询的库。

go-linq 优点

  1. 完全是 Go 语言实现的库,无其他依赖库;
  2. 使用迭代器的模式实现懒加载,即不是用一个方法就执行一个方法,只有需要输出的时候才会执行;
  3. 并发安全;
  4. 通用的功能,使代码更加简洁;
  5. 数据集合支持多种类型:slice, array, map, channel, custom collections 。

go-linq 怎么用?

Go-linq 采用通用的控制逻辑,链式的方法调用来处理数据集合,类似于使用 ORM 来操作数据库,提炼通用的操作和控制,分离关注点,只需关注输入和输出。

比如:

From(slice) .Where(predicate) .Select(selector) .Union(data)

  • From:将数据集合生成 go-linq 的特殊类型 Query,不同的数据集提供了不同的迭代器
  • Where:go-linq 提供的通用控制方法,表示过滤满足条件的集合元素
  • Predicate:条件谓语,即 xxxxxx 的元素
  • Select:选择或者转换,selector 选择器或者转换器
  • Union:go-linq 提供的通用方法,用于组合两个数据集

我们只需要关注三个点:输入 -> 控制 -> 输出

每一步的操作只要明确这三点,就能很好的运用 go-linq 去简单代码和提升代码可读性。

输入

Go-linq 支持的多种数据集,生成 Query 结构,不同类型的数据集 Query 中的迭代器不同。

type Query struct {
    Iterate func() Iterator
}

支持以下数据集:

  1. Slice
  2. Array
  3. Map
  4. Channel
  5. Custom collections

数据集生成 Query 结构的方法:

// 支持任一类型的集合生成 query
func From(source interface{}) Query

// 只支持 string  类型, 生成 query
func FromString(source string) Query

// 只支持 channel 类型, 生成 query
func FromChannel(source <-chan interface{}) Query

// 支持自定义集合生成 query
func FromIterable(source Iterable) Query

// 在指定范围内, 生成集合
func Range(start, count int) Query

// 指定值和重复次数, 生成重复数据的集合
func Repeat(value interface{}, count int) Query

其中最常用的是 From(source interface{}) Query 方法,支持任一数据集类型生成 Query,甚至是自定义的集合,源码中使用反射根据类型生成 query:

From

  • 衍生方法: FromString, FromChannel, FromIterable, Range, Repeat
  • 含义: 通过数据集合生成 Query 结构,用于迭代控制

举例:

// case 1:
From("hello world")
FromString("hello world")

// case 2:
Range(1, 10)
Repeat("say 3 times", 3)

// case 3:
type user struct {
   name  string
   cards []string
}

userList := []*user{
   {name: "tom", cards: []string{"a", "b", "c"}},
   {name: "jack", cards: []string{"d", "e", "f"}},
   {name: "json", cards: []string{"g", "h", "i"}}
}

From(userList)

实战:

 /**
  * 构建人才相关搜索语句
  */
func buildTalentSearchClause(talentQuery *TalentQuery) (queries []elastic.Query) {

   linq.From(buildTalentSearchTermsQuery(talentQuery)).
      Concat(linq.From(buildTalentSearchBoolQuery(talentQuery))).
      Concat(linq.From(buildTalentSearchNestedQuery(talentQuery))).
      Concat(linq.From(buildTalentSearchCareerNestedQuery(talentQuery))).
      Concat(linq.From(buildTalentSearchEducationNestedQuery(talentQuery))).
      Concat(linq.From(buildTalentSearchRangeQuery(talentQuery))).ToSlice(&queries)

   return

}

控制

Go-linq 提供的通用数据集操作,可参考官方文档,日常开发中常用的大概有以下几类:

  • 去肥增瘦
  • 挑三拣四
  • 查漏补缺
  • 拉帮结伙

其中有三个关键字,对于理解控制方法非常重要:

  1. withT:T -> type,表示闭包的参数,指定为和数据集合元素一样的类型,避免使用时强转(常用);
  2. By:以什么为依据:比如:分组操作,可以指定以哪个参数来分组;
  3. ByT:也是以什么为依据。闭包中指定和数据集合一样的类型(常用)。

ForEach

  • 衍生方法: ForEachT, ForEachIndexed, ForEachIndexedT
  • 含义: 遍历数据集合的元素,同 for 循环

举例:

type user struct {
   name  string
   cards []string
}

userList := []*user{
   {name: "tom", cards: []string{"a", "b", "c"}},
   {name: "jack", cards: []string{"d", "e", "f"}},
   {name: "json", cards: []string{"g", "h", "i"}}
}

From(userList).ForEachT(func(user *user) {
   fmt.Printf("name:%s, cards:%v\n", user.name, userList)
})

output:
name:tom, cards:[0xc000076180 0xc0000761e0 0xc000076240]
name:jack, cards:[0xc000076180 0xc0000761e0 0xc000076240]
name:json, cards:[0xc000076180 0xc0000761e0 0xc000076240]

实战:

 /**
  * 各个评估结论的个数-映射关系
  */
func (self *evaluationSummaryService) getConclusionCount(evaluationList []*domain.Evaluation) (result conclusionCount) {

   result = make(conclusionCount)

   linq.From(evaluationList).ForEachT(
      func(evaluation *domain.Evaluation) {
         if *evaluation.CommitStatus == constants.CommitStatus_UnCommitted {
            result[constants.Conclusion_NoConclusion] += 1
         } else if evaluation.Conclusion != nil {
            result[*evaluation.Conclusion] += 1
         }
      },
   )
   
   return
}

Where

  • 衍生方法: WhereT, WhereIndexed, WhereIndexedT
  • 含义: 筛选出符合条件的集合元素

举例:

numList := []int64{1, 2, 3, 5, 8, 13}
query := From(numList).Where(func(i interface{}) bool {
   return i.(int64) > 8
})

fmt.Println(query.Results())

output:
[13]

实战:

 /**
  * 获取除了"已入职"以外的所有阶段
  */
func (self *evaluationSearchProdService) listAllStageIDWithoutHired(ctx context.Context) (result []string) {

   allStageData := facade.ApplicationFacade.ListAllStageWithCache(ctx)
   linq.From(allStageData).WhereT(func(item *aInf.StageInfo) bool {
      return *item.TypeA1 != aconstants.StageType_Hired
   }).SelectT(func(item *aInf.StageInfo) string {
      return *item.ID
   }).ToSlice(&result)

   return
}

Any

  • 衍生方法: AnyWith, AnyWithT
  • 含义: 任意一个,with / withT 可指定条件,即满足条件的任意一个

举例:

numList := []int64{1, 2, 3, 5, 8, 13}

query := From(numList).AnyWithT(func(i int64) bool {
   return i > 8
})

fmt.Println(query)

output:
true

实战:

 /**
  * 判断是否需要扩充阶段
  */
func (self *evaluationSearchProdService) needExtendStage(stageIDList []string) bool {
   return linq.From(stageIDList).AnyWithT(func(item string) bool {
      return item == constant.ApplicationStatusGroupInProgress
   })

}

Append / Prepend

  • 衍生方法:
  • 含义: 集合中插入元素,后插/前插,类似于列表的 append 方法

举例:

numList := []int64{1, 2, 3, 5, 8, 13}
query := From(numList).Append(100)
fmt.Println(query.Results())

output:
[1 2 3 5 8 13 100]

numList := []int64{1, 2, 3, 5, 8, 13}
query := From(numList).prepend(100)
fmt.Println(query.Results())

output:
[100 1 2 3 5 8 13]

实战: 暂无

Select

  • 衍生方法: SelectT, SelectMany, SelectManyBy, SelectManyByT, SelectIndexed, SelectIndexedT, SelectManyIndexedT, SelectManyIndexed, SelectManyByIndexed, SelectManyByIndexedT
  • 含义: 挑选出集合中的元素,可进行转化后组成新的集合,这是最常用的控制方法,效果同 flatmap 操作

举例:

// case 1:
numList := []int64{1, 2, 3, 5, 8, 13}
query := From(numList).SelectT(func(i int64) string {
   return fmt.Sprintf("num: %d\n", i)
})
fmt.Println(query.Results())

output:
[
    num: 1 
    num: 2 
    num: 3 
    num: 5 
    num: 8 
    num: 13
]

// case 2:
num1 := []int64{1, 2, 3}
num2 := []int64{4, 5, 6}
num3 := []int64{7, 8, 9}

numList := [][]int64{
   num1,
   num2,
   num3,
}

query := From(numList).SelectManyT(func(num []int64) Query {
   return From(num).SelectT(func(i int64) string {
      return fmt.Sprintf("num: %d", i)
   })
})

fmt.Println(query.Results())

output:
[
    num: 1 
    num: 2 
    num: 3 
    num: 4 
    num: 5 
    num: 6 
    num: 7 
    num: 8 
    num: 9
]

// case 3:
num1 := []int64{1, 2, 3}
num2 := []int64{4, 5, 6}
num3 := []int64{7, 8, 9}

numList := [][]int64{
   num1,
   num2,
   num3,
}

query := From(numList).SelectManyByT(
   func(num []int64) Query { return From(num) },
   func(item int64, num []int64) string { return fmt.Sprintf("num:%v, numList:%v\n", num, numList) },
)

fmt.Println(query.Results())

output:
[
 num:1, numList:[1 2 3]
 num:2, numList:[1 2 3]
 num:3, numList:[1 2 3]
 num:4, numList:[4 5 6]
 num:5, numList:[4 5 6]
 num:6, numList:[4 5 6]
 num:7, numList:[7 8 9]
 num:8, numList:[7 8 9]
 num:9, numList:[7 8 9]
]

实战:

 /**
  * 获取除了"已入职"以外的所有阶段
  */
func (self *evaluationSearchProdService) listAllStageIDWithoutHired(ctx context.Context) (result []string) {
   allStageData := facade.ApplicationFacade.ListAllStageWithCache(ctx)

   linq.From(allStageData).WhereT(func(item *aInf.StageInfo) bool {
      return *item.TypeA1 != aconstants.StageType_Hired
   }).SelectT(func(item *aInf.StageInfo) string {
      return *item.ID
   }).ToSlice(&result)

   return
}

Group

  • 衍生方法: GroupBy, GroupByT, GroupJoin, GroupJoinT
  • 含义: 根据字段来分组

注意:分组后产生的是一个新的数据结构 Group

type Group struct {
   Key   interface{}
   Group []interface{}
}

举例:

// case 1:
type Person struct {
   Name string
   Age  int64
}

personList := []*Person{
   {Name: "Jack", Age: 18},
   {Name: "Tom", Age: 23},
   {Name: "John", Age: 23},
   {Name: "Susan", Age: 38},
}

query := From(personList).GroupByT(
   func(person *Person) int64 { return person.Age },
   func(person *Person) string { return person.Name },
)

fmt.Println(query.Results())

output:
[{18 [Jack]} {23 [Tom John]} {38 [Susan]}]

实战:

 /**
  * 查询笔试阅卷数据, key: examID, value: examMarking list
  */
func queryExamMarkingMap(ctx context.Context, examList []*domain.Exam) (result map[string][]*domain.ExamMarking) {

   markingList := ExamMarkingService.QueryByExamIDList(ctx, util.ExtractStringFromSlice(examList, "ID"))
   result = make(map[string][]*domain.ExamMarking, len(markingList))

   // convert list to map, key: examID, value: examMarking list
   linq.From(markingList).
      WhereT(func(marking *domain.ExamMarking) bool {
         return marking != nil && util.IsNotStrBlankPtr(marking.ExamID)
      }).
      GroupByT(
         func(marking *domain.ExamMarking) string { return *marking.ExamID },
         func(marking *domain.ExamMarking) *domain.ExamMarking { return marking },
      ).
      ToMapByT(
         &result,
         func(g linq.Group) string { return g.Key.(string) },
         func(g linq.Group) (examMarkingList []*domain.ExamMarking) {
            linq.From(g.Group).SelectT(
               func(gVal interface{}) *domain.ExamMarking { return gVal.(*domain.ExamMarking) },
            ).ToSlice(&examMarkingList)
            return
         },
      )

   return
}

Join

  • 衍生方法: JoinT
  • 含义: 基于相同的键,将两个集合的元素进行关联,跟 sql 的 join 使用类似

举例:

// case1: 

fruits := []string{
   "apple",
   "banana",
   "apricot",
   "cherry",
   "clementine",
}

query := Range(1, 10).
   JoinT(From(fruits),
      func(num int) int { return num },
      func(fruit string) int { return len(fruit) },
      func(num int, fruit string) KeyValue {
         return KeyValue{Key: num, Value: fruit}
      })

fmt.Println(query.Results())

output:
[{5 apple} {6 banana} {6 cherry} {7 apricot} {10 clementine}]

// case 2:
type Person struct {
   Name string
   Age  int64
}

type Pet struct {
   Name  string
   Owner Person
}

magnus := Person{Name: "Hedlund, Magnus"}
terry := Person{Name: "Adams, Terry"}
charlotte := Person{Name: "Weiss, Charlotte"}

barley := Pet{Name: "Barley", Owner: Person{Name: "Adams, Terry"}}
boots := Pet{Name: "Boots", Owner: Person{Name: "Adams, Terry"}}
whiskers := Pet{Name: "Whiskers", Owner: Person{Name: "Weiss, Charlotte"}}
daisy := Pet{Name: "Daisy", Owner: Person{Name: "Hedlund, Magnus"}}

people := []Person{magnus, terry, charlotte}
pets := []Pet{barley, boots, whiskers, daisy}

query := From(people).
   JoinT(From(pets),
      func(person Person) Person { return person },
      func(pet Pet) Person { return pet.Owner },
      func(person Person, pet Pet) string {
         return fmt.Sprintf("%s - %s\n", person.Name, pet.Name)
      },
   )

fmt.Println(query.Results())

output:
[
 Hedlund, Magnus - Daisy
 Adams, Terry - Barley
 Adams, Terry - Boots
 Weiss, Charlotte - Whiskers
]

实战:

func (self *interviewerLarkCalendarLimiter) check(in AppointmentTimeSlice) bool {

   userIsBusyMap := make(map[string]bool, len(in))
   userBusyTimeMap := self.queryUserBusyTimeMap(in)

   From(in).
      JoinT(
         From(userBusyTimeMap),
         func(iter *AppointmentTime) interface{} { return iter.UserID },
         func(kv KeyValue) interface{} { return kv.Key },
         func(src *AppointmentTime, kv KeyValue) KeyValue {
            idle := src.RecommendTimeList.Reject(kv.Value.(utils.IntervalMixSlice))
            return KeyValue{Key: src.UserID, Value: src.RecommendTimeList.Equals(idle)}
         },
      ).
      ToMap(&userIsBusyMap)

   for _, isBusy := range userIsBusyMap {
      if isBusy {
         return false
      }
   }

   return true

}

Intersect

  • 衍生方法: IntersectBy, IntersectBy
  • 含义: 两个数据集合取交集,带 by 关键字的表示以字段为依据取交集

举例:

// case1: 值类型的基本数据类型
id1 := []int{44, 26, 92, 30, 71, 38}
id2 := []int{39, 59, 83, 47, 26, 4, 30}

query := From(id1).Intersect(From(id2))
fmt.Println(query.Results())

output:
[26 30]

// case1: 值类型的struct  类型
type Person struct {
   Name string
   Age  int64
}

p1 := Person{Name: "Jack", Age: 23}
p2 := Person{Name: "Jon", Age: 18}
p3 := Person{Name: "Tom", Age: 23}

aList := []Person{p1, p2}
bList := []Person{p2, p3}

var both []Person
From(aList).Intersect(From(bList)).ToSlice(&both)
fmt.Println(both)

output:
[{Jon 18}]

// case 3: 引用类型
type Person struct {
   Name string
   Age  int64
}

p1 := &Person{Name: "Jack", Age: 23}
p2 := &Person{Name: "Jon", Age: 18}
p3 := &Person{Name: "Tom", Age: 23}
p4 := &Person{Name: "Tom", Age: 23}

aList := []*Person{p1, p2, p4}
bList := []*Person{p2, p3}

var both []*Person
From(aList).Intersect(From(bList)).ToSlice(&both)

for _, p := range both {
   fmt.Printf("%p, %s, %d", p, p.Name, p.Age)
}

output:
0xc00000c0a0, Jon, 18

// case 4: 指定求交集的依据
type Person struct {
   Name string
   Age  int64
}

p1 := &Person{Name: "Jack", Age: 23}
p2 := &Person{Name: "Jon", Age: 18}
p3 := &Person{Name: "Tom", Age: 23}
p4 := &Person{Name: "Tom", Age: 23}

aList := []*Person{p1, p2, p4}
bList := []*Person{p2, p3}

var both []*Person
From(aList).
   IntersectByT(From(bList), func(i *Person) int64 {
      return i.Age
   }).
   ToSlice(&both)

for _, p := range both {
   fmt.Printf("%p, %s, %d\n", p, p.Name, p.Age)
}

output:
0xc00000c080, Jack, 23
0xc00000c0a0, Jon, 18

实战:

func selectAvailableInterviewerTime(ctx context.Context, appointmentTask *domain.InterviewAppointmentTask,
   filteredUserTimeList []*domain.InterviewAppointmentInterviewerTime) (result []*domain.InterviewAppointmentInterviewerTime) {

   From(appointmentTask.InterviewerTimeList).
      IntersectByT(
         From(filteredUserTimeList),
         func(interviewerTime *domain.InterviewAppointmentInterviewerTime) string { return *interviewerTime.UserID },
      ).
      DistinctByT(func(interviewerTime *domain.InterviewAppointmentInterviewerTime) int64 {
         return *interviewerTime.EndTime - *interviewerTime.BeginTime
      }).
      ToSlice(&result)

   log.Info(ctx, "[selectAvailableInterviewerTime] result: %v", data_tk.Show(result))

   return
}

Except

  • 衍生方法: ExceptBy, ExceptByT
  • 含义: 两个数据集合取差集,带 by 关键字的表示以字段为依据取差集

举例:

// case 1

type Person struct {
   Name string
   Age  int64
}

p1 := Person{Name: "Jack", Age: 23}
p2 := Person{Name: "Jon", Age: 18}
p3 := Person{Name: "Tom", Age: 23}

aList := []Person{p1, p2}
bList := []Person{p2, p3}

var both []Person
From(aList).Except(From(bList)).ToSlice(&both)
fmt.Println(both)

output:
[{Jack 23}]

实战: 暂无

Union

  • 衍生方法:
  • 含义: 两个集合的并集,相同的元素去重

举例:

// case1
id1 := []int{1, 2, 3, 4}
id2 := []int{3, 4, 5, 6}

query := From(id1).Union(From(id2))
fmt.Println(query.Results())

output:
[1 2 3 4 5 6]

实战: 暂无

Concat

  • 衍生方法:
  • 含义: 两个集合的并集,相同的元素不去重

举例:

// case1
id1 := []int{1, 2, 3, 4}
id2 := []int{3, 4, 5, 6}

query := From(id1).Concat(From(id2))
fmt.Println(query.Results())

output:
[1 2 3 4 3 4 5 6]

实战:

func (self *evaluationSearchProdService) supplyApplicationStageAndStatus(ctx context.Context,
   stageAndStatusIDList []string) (result []string) {

   if len(stageAndStatusIDList) == 0 {
      return
   }
   
   // 判断是否需要扩展阶段
   existExtend := self.needExtendStage(stageAndStatusIDList)
   if !existExtend {
      return stageAndStatusIDList
   }

   // 主要构成两部分: 1.前端传的阶段, 去掉"进行中"; 2.补充除了"已入职"的所有阶段
   // 即: "进行中"阶段 == 除了"已入职"阶段的阶段
   linq.From(
      self.filterStageIDWithoutInProgress(stageAndStatusIDList),
   ).Concat(
      linq.From(self.listAllStageIDWithoutHired(ctx)),
   ).SelectT(func(item string) string {
      return item
   }).ToSlice(&result)

   log.Info(ctx, "[supplyApplicationStageAndStatus] stage_id_list:%v", result)

   return

}

Distinct

  • 衍生方法: DistinctBy, DistinctByT
  • 含义: 数据集合去重,union() == concat().distinct()

举例:

// case1
id1 := []int{1, 2, 3, 4}
id2 := []int{3, 4, 5, 6}

query := From(id1).Concat(From(id2)).Distinct()
fmt.Println(query.Results())

output:
[1 2 3 4 5 6]

实战:

 /**

 * 补充投递来源

 */

func (self *evaluationSearchProdService) supplyApplicationSource(ctx context.Context, sourceIDList []string) (
   result []string) {

   if len(sourceIDList) == 0 {
      return
   }

   // 查询投递来源
   resumeSourceList := facade.TalentFacade.ListResumeSourceByIDList(ctx,tconstants.ResumeSourceUsagePtr(tconstants.ResumeSourceUsage_Application),
      util.BoolPtr(true),
      sourceIDList,
   ).ResumeSourceInfoList

   // 查找投递来源 ID, concat 前端传入的 applicationSource, 防止 talent 服务返回的 resumeSourceList 为空
   linq.From(resumeSourceList).
      SelectManyT(func(item *tInf.ResumeSourceInfo) linq.Query {
         return linq.From(builder.ExtractResumeSourceIDList(item))
      }).
      Concat(linq.From(sourceIDList)).
      Distinct().
      ToSlice(&result)

   return
}

输出

Last

  • 衍生方法: LastWith, LastWithT
  • 含义: 取数据集中的最后一个元素,带 with 关键字,表示以什么条件取最后一个元素

举例:

// case1
id1 := []int{1, 2, 3, 4}
query := From(id1).Last()
fmt.Println(query)

output:
4

实战: 暂无

First

  • 衍生方法: FirstWith, FirstWithT
  • 含义: 取数据集中的第一个元素,带 with 关键字,表示以什么条件取第一个元素

举例:

// case 1
id1 := []int{1, 2, 3, 4}

query := From(id1).FirstWithT(func(i int) bool {
   return i > 3
})
fmt.Println(query)

output:
4

实战:

 /**

 * 批量更新评估的 evaluation_status

 */

func (self *evaluationStatusService) batchUpdateEvaluationStatus(ctx context.Context, db *db.DB,
   evaluationList []*domain.Evaluation, evaluationStatus evalCts.ActivityStatus) {

   if len(evaluationList) == 0 {
      log.Info(ctx, "[evaluationStatusService][batchUpdateEvaluationStatus] evaluation list is empty")
      return
   }

   evaluationIDList := util.ExtractStringFromSlice(evaluationList, "ID")
   evaluationExtList := repo.EvaluationExtRepo.QueryByEvaluationIDList(ctx, db, evaluationIDList)
   if len(evaluationExtList) == 0 {
      log.Info(ctx, "[evaluationStatusService][batchUpdateEvaluationStatus] "+
         "evaluation ext list is empty by evaluation_id_list: [%v]", evaluationIDList)
      return
   }

   evaluationExtIDList := util.ExtractStringFromSlice(evaluationExtList, "ID")
   // 更新评估状态
   tempEvaluationExt := linq.From(evaluationExtList).First().(*domain.EvaluationExt)
   tempEvaluationExt.EvaluationStatus = evalCts.ActivityStatusPtr(evaluationStatus)
   tempEvaluationExt.BizModifyTime = util.Int64Ptr(util.GetMilliTimestamp())
   repo.EvaluationExtRepo.BatchUpdateByIDList(ctx, db, evaluationExtIDList, tempEvaluationExt)

   log.Info(ctx, "[evaluationStatusService][batchUpdateEvaluationStatus] "+
      "update evaluation_status [%v] for evaluation_id_list:[%v]", evaluationStatus, evaluationIDList)
}

Min

  • 衍生方法:
  • 含义: 取数据集中的最小的元素

举例:

// case 1
id1 := []int{1, 2, 3, 4}
query := From(id1).Min()
fmt.Println(query)

output:
1

实战:

 /**

 * 根据场次获取面试官日历范围

 */

func (self *calendarProdService) getCalendarRangeFromSession(ctx context.Context,
   jFSession *domain.JFSession) (startTime, endTime *int64) {

   if len(jFSession.SubSessionList) == 0 {
      return
   }

   var subSessionStartTimeMSList []int64
   // 取 SubSession startTime 列表
   linq.From(jFSession.SubSessionList).
      WhereT(func(item *domain.JFSubSession) bool {
         return item != nil && item.StartTime != nil
      }).
      SelectT(func(item *domain.JFSubSession) int64 {
         return *item.StartTime
      }).
      ToSlice(&subSessionStartTimeMSList)

   if linq.From(subSessionStartTimeMSList).Count() == 0 {
      return
   }

   start := linq.From(subSessionStartTimeMSList).Min().(int64)
   end := linq.From(subSessionStartTimeMSList).Max().(int64)

   // 判断 SubSession 最大的 startTime 是否超过 session 的 endTime
   if end > *jFSession.EndTime {
      end = *jFSession.EndTime
   }

   log.Info(ctx, "[getCalendarRangeFromSession] calendar start_time:%d, end_time:%d", start, end)

   return util.Int64Ptr(start), util.Int64Ptr(end)
}

Max

  • 衍生方法:
  • 含义: 取数据集中的最大的元素

举例:

// case 1
id1 := []int{1, 2, 3, 4, 4, 4}
query := From(id1).Max()
fmt.Println(query)

output:
4

实战:

 /**

 * 根据场次获取面试官日历范围

 */

func (self *calendarProdService) getCalendarRangeFromSession(ctx context.Context,
   jFSession *domain.JFSession) (startTime, endTime *int64) {
   
   if len(jFSession.SubSessionList) == 0 {
      return
   }

   var subSessionStartTimeMSList []int64
   // 取 SubSession startTime 列表
   linq.From(jFSession.SubSessionList).
      WhereT(func(item *domain.JFSubSession) bool {
         return item != nil && item.StartTime != nil
      }).
      SelectT(func(item *domain.JFSubSession) int64 {
         return *item.StartTime
      }).
      ToSlice(&subSessionStartTimeMSList)

   if linq.From(subSessionStartTimeMSList).Count() == 0 {
      return
   }



   start := linq.From(subSessionStartTimeMSList).Min().(int64)
   end := linq.From(subSessionStartTimeMSList).Max().(int64)

   // 判断 SubSession 最大的 startTime 是否超过 session 的 endTime
   if end > *jFSession.EndTime {
      end = *jFSession.EndTime
   }

   log.Info(ctx, "[getCalendarRangeFromSession] calendar start_time:%d, end_time:%d", start, end)

   return util.Int64Ptr(start), util.Int64Ptr(end)
}

Single

  • 相关方法: SingleWith, SingleWithT
  • 含义: 返回集合中的唯一元素,如果集合中的元素不唯一返回 nil

举例:

// case 1:
numList := []int64{1, 2, 3, 5, 8, 13}
query := From(numList).Single()
fmt.Println(query)

output:
nil

// case 2:
numList := []int64{1}
query := From(numList).Single()
fmt.Println(query)

output:
1

// case 3:
numList := []int64{1, 2, 3, 5, 8, 13}
query := From(numList).SingleWithT(func(i int64) bool {
   return i > 8
})
fmt.Println(query)

output:
13

实战: 暂无

ToSlice

  • 衍生方法:
  • 含义: 将数据集 query 的值输出到指定 slice 中,这是最常用的方法之一

举例:

// case1
type Person struct {
   Name string
   Age  int64
}

p1 := Person{Name: "Jack", Age: 23}
p2 := Person{Name: "Jon", Age: 18}
p3 := Person{Name: "Tom", Age: 23}

aList := []Person{p1, p2}
bList := []Person{p2, p3}

var both []Person
From(aList).Except(From(bList)).ToSlice(&both)
fmt.Println(both)

output:
[{Jack 23}]

实战:

func (self *evaluationSearchProdService) supplyApplicationStageAndStatus(ctx context.Context,
   stageAndStatusIDList []string) (result []string) {

   if len(stageAndStatusIDList) == 0 {
      return
   }

   // 判断是否需要扩展阶段
   existExtend := self.needExtendStage(stageAndStatusIDList)
   if !existExtend {
      return stageAndStatusIDList
   }
   
   // 主要构成两部分: 1.前端传的阶段, 去掉"进行中"; 2.补充除了"已入职"的所有阶段
   // 即: "进行中"阶段 == 除了"已入职"阶段的阶段
   linq.From(
      self.filterStageIDWithoutInProgress(stageAndStatusIDList),
   ).Concat(
      linq.From(self.listAllStageIDWithoutHired(ctx)),
   ).SelectT(func(item string) string {
      return item
   }).ToSlice(&result)

   log.Info(ctx, "[supplyApplicationStageAndStatus] stage_id_list:%v", result)

   return
}

ToMap

  • 衍生方法: ToMapBy, ToMapByT
  • 含义: 将数据集 query 的值输出到指定 map 中,这是最常用的方法之一

注意:要提前 alloc map

举例:

type Person struct {
   Name string
   Age  int64
}

p1 := Person{Name: "Jack", Age: 23}
p2 := Person{Name: "Jon", Age: 18}
p3 := Person{Name: "Tom", Age: 23}

aList := []Person{p1, p2}
bList := []Person{p2, p3}

pMap := make(map[string]int64)
From(aList).
   Except(From(bList)).
   ToMapByT(&pMap,
      func(p Person) string { return p.Name },
      func(p Person) int64 { return p.Age },
   )

fmt.Println(pMap)

output:
map[Jack:23]

实战:

func (self *interviewerLarkCalendarLimiter) queryUserBusyTimeMap(in AppointmentTimeSlice) (
   result map[string]utils.IntervalMixSlice) {

   startUTCDateTimestamp, endUTCDateTimestamp := in.getTotalDurationIgnoreTimezone(self.ctx)
   busyTimes := UserBusyPeriods(self.ctx, self.userList, startUTCDateTimestamp,
      endUTCDateTimestamp)

   result = make(map[string]utils.IntervalMixSlice, len(busyTimes))
   From(busyTimes).ToMapByT(
      &result,
      func(iter *AppointmentTime) interface{} { return iter.UserID },
      func(iter *AppointmentTime) utils.IntervalMixSlice { return iter.RecommendTimeList },
   )

   return
}

Contains

  • 衍生方法:
  • 含义: 数据集合中是否包含某个元素,不用 for 循环去遍历集合再判断

举例:

// case1
type Person struct {
   Name string
   Age  int64
}

p1 := Person{Name: "Jack", Age: 23}
p2 := Person{Name: "Jon", Age: 18}
p3 := Person{Name: "Tom", Age: 23}

aList := []Person{p1, p2}
bList := []Person{p2, p3}

isContains := From(aList).
   Except(From(bList)).
   Contains(p1)
   
fmt.Println(isContains)

output:
true

实战:

func (self *interviewAppointmentTaskProdService) checkAppointmentLinkExpiryAndStatus(appointment *domain.InterviewAppointmentTask) (result func() error) {

   return func() error {
      // 约面任务是否存在
       if appointment == nil {
           return bizerr.AppointmentTaskNotExit
       }
   
       // 约面链接是否失效
       if *appointment.TerminateTime < util.GetMilliTimestamp() {
           return bizerr.AppointmentLinkNotAvailable
       }
   
       // 约面任务是否可以确认/拒绝
       acceptStatusList := []intCts.AppointmentStatus{
           intCts.AppointmentStatus_Dating,
           intCts.AppointmentStatus_Selecting,
       }

       if !From(acceptStatusList).Contains(*appointment.AppointmentStatus) {
           return bizerr.AppointmentTaskIsCanceled

       }
   
       return nil
   }

}

综合实战

原来的逻辑:

 // 投递状态V2版本需要扩展, 为树形结构
if len(request.StageIDList) > 0 {
   existExtend := false
   expandIDList := make([]string, 0)
   for _, stageEnum := range request.StageIDList {
      if stageEnum == constant.ApplicationStatusGroupInProgress {
         existExtend = true
         break
      }
   }

   if existExtend {
      allStageData := facade.ApplicationFacade.ListAllStageWithCache(ctx)
      for _, v := range request.StageIDList {
         if v == constant.ApplicationStatusGroupInProgress {
            for _, stageData := range allStageData {
               if *stageData.TypeA1 != aconstants.StageType_Hired {
                  expandIDList = append(expandIDList, *stageData.ID)
               }
            }
         } else {
            expandIDList = append(expandIDList, v)
         }
      }
      request.StageIDList = expandIDList
   }

   log.Info(ctx, "Evaluation application_status expand %s", data_tk.Show(expandIDList))
}

使用 go-linq 重构后的逻辑:

// 补充投递状态
func (self *evaluationSearchProdService) supplyApplicationStageAndStatus(ctx context.Context,
   stageAndStatusIDList []string) (result []string) {

   if len(stageAndStatusIDList) == 0 {
      return
   }

   // 判断是否需要扩展阶段
   existExtend := self.needExtendStage(stageAndStatusIDList)
   if !existExtend {
      return stageAndStatusIDList
   }

   // 主要构成两部分: 1.前端传的阶段, 去掉"进行中"; 2.补充除了"已入职"的所有阶段
   // 即: "进行中"阶段 == 除了"已入职"阶段的阶段
   linq.From(
      self.filterStageIDWithoutInProgress(stageAndStatusIDList),
   ).Concat(
      linq.From(self.listAllStageIDWithoutHired(ctx)),
   ).SelectT(func(item string) string {
      return item
   }).ToSlice(&result)

   log.Info(ctx, "[supplyApplicationStageAndStatus] stage_id_list:%v", result)

   return
}

 /**
  * 判断是否需要扩充阶段
  */

func (self *evaluationSearchProdService) needExtendStage(stageIDList []string) bool {
   return linq.From(stageIDList).AnyWithT(func(item string) bool {
      return item == constant.ApplicationStatusGroupInProgress
   })
}

 /**
  * 过滤掉"进行中"阶段
  */
func (self *evaluationSearchProdService) filterStageIDWithoutInProgress(stageIDList []string) (result []string) {

   linq.From(stageIDList).WhereT(func(item string) bool {
      return item != constant.ApplicationStatusGroupInProgress
   }).ToSlice(&result)

   return
}



 /**
  * 获取除了"已入职"以外的所有阶段
  */
func (self *evaluationSearchProdService) listAllStageIDWithoutHired(ctx context.Context) (result []string) {

   allStageData := facade.ApplicationFacade.ListAllStageWithCache(ctx)
   
   linq.From(allStageData).WhereT(func(item *aInf.StageInfo) bool {
      return *item.TypeA1 != aconstants.StageType_Hired
   }).SelectT(func(item *aInf.StageInfo) string {
      return *item.ID
   }).ToSlice(&result)

   return
}

进阶

数据集合输入可以指定自定义的集合,只要实现对应的接口,实现自己的迭代器即可使用 go-linq 提供的控制方法和输出方法。

Iterate 和 CompareTo

示例:

数组的数组,进行求交集/差集/并集,目前自动计算推荐面试时间可用到,类似于下面的时间切片数组

  • timeList1: [ [10, 11], [12, 15], [16, 30] ]
  • timeList2: [ [15, 16], [16, 20] ]

对时间切片数组中的元素取交集/差集/并集

type MixTime [2]int64

func (f MixTime) CompareTo(c Comparable) int {
   a, b := f, c.(MixTime)
   if a[0] > b[0] {
      return 1
   } else if a[0] < b[0] {
      return -1
   }

   return 0
}



type MixTimeArray []MixTime

func (MixTimeArray) Iterate() Iterator {
   return func() (item interface{}, ok bool) {
      return
   }
}

func TestIterable() {
   q1 := MixTimeArray{
      {1, 2},
      {3, 4},
      {5, 6},
   }
   
   q2 := MixTimeArray{
      {1, 200},
      {3, 4},
      {7, 8},
   }

   res1 := From(q1).Intersect(From(q2)).Results()
   fmt.Printf("intersect: %v\n", res1)

   res2 := From(q1).Except(From(q2)).Results()
   fmt.Printf("Except: %v\n", res2)

   res3 := From(q1).Union(From(q2)).Results()
   fmt.Printf("Union: %v\n", res3)

   q3 := MixTimeArray{
      {1, 2},
      {3, 4},
      {5, 6},
   }

   res4 := From(q1).SequenceEqual(From(q2))
   res5 := From(q1).SequenceEqual(From(q3))
   fmt.Printf("Equal: q1 & q2 %v; q1 & q3 %v\n", res4, res5)
}

output:
intersect: [[3 4]]
Except: [[1 2] [5 6]]
Union: [[1 2] [3 4] [5 6] [1 200] [7 8]]
Equal: q1 & q2 false; q1 & q3 true

前车之鉴

使用 go-linq 的目的不是为了解决圈复杂度,而是为了让代码更简洁,更具有可读性,带来的好处之一就是圈复杂度降低了,从侧面也说明代码复杂度降低了。

  1. 分离关注点,明确输入和输出,再选择合适的控制方法;
  2. 使用带 withT 的关键字时,需要注意数据源的元素类型和闭包中指定的类型要一致,否则运行时 panic;
  3. 使用 groupBy 后的输出要注意 []interface{} 的类型转换,可以使用 selecT 进行单独转换,不能强制类型转换 []interface {} -> []type;
  4. 在使用 go-linq 时,尽量将复杂的闭包封装成小函数,好处:
    • 可读性更高
    • 小函数职责单一,可复用
    • 分支控制和逻辑处理独立
  5. 弄清楚每个 go-linq 方法的场景和特性,比较模糊的时候可以写 demo 测试一下:
    • 比如:takeWith,skipWith 理解和具体使用有混淆的地方;
    • 再比如:两个集合的交集/差集/并集之类的 set 操作,某些类型的数据集合不一定可以,需要类型支持 hashable 等等。
  6. 遇到比较复杂的转换时,可以从后往前思考,通过什么方法可以得到输出,就能想到什么样的输入,再层层往上推敲;
  7. go-linq 和设计模式一样,不是万能的,切记不要滥用,需要明白控制方法是为了解决什么问题;
  8. 除了官网的介绍以外,网上几乎没啥资料可查,可以相互讨论达到最佳实践。

加入我们

我们来自字节跳动飞书商业应用研发部(Lark Business Applications),目前我们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。我们关注的产品领域主要在企业经验管理软件上,包括飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 领域系统,也包括飞书审批、OA、法务、财务、采购、差旅与报销等系统。欢迎各位加入我们。

微信扫码发现职位&投递简历

官网投递:job.toutiao.com/s/FyL7DRg

猜你喜欢

转载自juejin.im/post/7122403456417021989