go-linq 是什么?
强大的 Go 语言集成查询的库。
- Github:github.com/ahmetb/go-l…
- Doc:pkg.go.dev/github.com/…
go-linq 优点
- 完全是 Go 语言实现的库,无其他依赖库;
- 使用迭代器的模式实现懒加载,即不是用一个方法就执行一个方法,只有需要输出的时候才会执行;
- 并发安全;
- 通用的功能,使代码更加简洁;
- 数据集合支持多种类型: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
}
支持以下数据集:
- Slice
- Array
- Map
- Channel
- 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 提供的通用数据集操作,可参考官方文档,日常开发中常用的大概有以下几类:
- 去肥增瘦
- 挑三拣四
- 查漏补缺
- 拉帮结伙
其中有三个关键字,对于理解控制方法非常重要:
- withT:T -> type,表示闭包的参数,指定为和数据集合元素一样的类型,避免使用时强转(常用);
- By:以什么为依据:比如:分组操作,可以指定以哪个参数来分组;
- 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 的目的不是为了解决圈复杂度,而是为了让代码更简洁,更具有可读性,带来的好处之一就是圈复杂度降低了,从侧面也说明代码复杂度降低了。
- 分离关注点,明确输入和输出,再选择合适的控制方法;
- 使用带 withT 的关键字时,需要注意数据源的元素类型和闭包中指定的类型要一致,否则运行时 panic;
- 使用 groupBy 后的输出要注意 []interface{} 的类型转换,可以使用 selecT 进行单独转换,不能强制类型转换 []interface {} -> []type;
- 在使用 go-linq 时,尽量将复杂的闭包封装成小函数,好处:
- 可读性更高
- 小函数职责单一,可复用
- 分支控制和逻辑处理独立
- 弄清楚每个 go-linq 方法的场景和特性,比较模糊的时候可以写 demo 测试一下:
- 比如:takeWith,skipWith 理解和具体使用有混淆的地方;
- 再比如:两个集合的交集/差集/并集之类的 set 操作,某些类型的数据集合不一定可以,需要类型支持 hashable 等等。
- 遇到比较复杂的转换时,可以从后往前思考,通过什么方法可以得到输出,就能想到什么样的输入,再层层往上推敲;
- go-linq 和设计模式一样,不是万能的,切记不要滥用,需要明白控制方法是为了解决什么问题;
- 除了官网的介绍以外,网上几乎没啥资料可查,可以相互讨论达到最佳实践。
加入我们
我们来自字节跳动飞书商业应用研发部(Lark Business Applications),目前我们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。我们关注的产品领域主要在企业经验管理软件上,包括飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 领域系统,也包括飞书审批、OA、法务、财务、采购、差旅与报销等系统。欢迎各位加入我们。
微信扫码发现职位&投递简历