- 开发工具
【免费下载链接】lancet
A comprehensive, efficient, and reusable util function library of Go.
lancet 是一个全面、高效、可复用的 Go 工具函数库,其datastructure子包提供了一系列常用数据结构。本篇指南聚焦其中的List:一种以 Go 切片(slice)实现的线性表。你将掌握如何创建 List、完成元素的增删改查、比较复制、集合运算与遍历转换,并深入理解其底层实现原理(基于reflect.DeepEqual的判等策略、就地删除算法、迭代器设计等),从而在日常开发中正确、高效地使用这一结构。
一、List 是什么:线性表 + 切片实现
从源码结构看,List[T]是一个泛型线性表,内部仅仅包装了一个切片字段,没有引入链表节点、指针等额外结构:
// datastructure/list/list.go type List[T any] struct { data []T }定义位于 list.go,文件顶部的包注释明确写道:"list is a linear table, implemented with slice"(List 是线性表,用切片实现)。这意味着:
- 内存连续:元素存储在连续内存中,支持 O(1) 随机访问,这与链表形成鲜明对比;
- 自动扩容:借助 Go 切片的
append机制动态增长; - 泛型支持:项目
go.mod声明go 1.18(见 go.mod),List[T any]对元素类型无任何约束,可以存放任意类型,包括结构体、指针、接口。
注意:
list.go所在目录虽然是datastructure/list/,但文件声明的包名是datastructure。因此既可以直接导入子包路径,也可以导入父包路径(见下节),二者指向同一组符号。
二、快速上手:导入与创建
2.1 导入方式
英文文档给出的导入方式为子包路径:
import ( list "github.com/duke-git/lancet/v2/datastructure/list" )中文版文档则使用父包路径(效果相同):
import ( "github.com/duke-git/lancet/v2/datastructure" )两种方式均可使用,之后通过list.NewList(...)或datastructure.NewList(...)调用。模块路径与版本见 go.mod。
2.2 创建与查看
package main import ( "fmt" list "github.com/duke-git/lancet/v2/datastructure/list" ) func main() { li := list.NewList([]int{1, 2, 3}) fmt.Println(li) }NewList的签名与实现非常直接——接收一个切片并包装成 List 指针返回(list.go):
func NewListT any *List[T] { return &List[T]{data: data} }三、基础访问:Data / ValueOf / Size / Cap / IsEmpty
这一组方法负责读取 List 的整体数据、单点取值以及容量信息,是后续一切操作的基础。
| 方法 | 签名 | 作用 |
|---|---|---|
Data | func (l *List[T]) Data() []T | 返回底层数据切片 |
ValueOf | func (l *List[T]) ValueOf(index int) (*T, bool) | 返回 index 处的值指针;越界返回nil, false |
Size | func (l *List[T]) Size() int | 返回元素个数 |
Cap | func (l *List[T]) Cap() int | 返回底层切片的容量 |
IsEmpty | func (l *List[T]) IsEmpty() bool | 判断列表是否为空 |
3.1 Data:取回原始切片
li := list.NewList([]int{1, 2, 3}) data := li.Data() fmt.Println(data) // [1 2 3]Data直接返回内部切片引用(list.go),并不做拷贝。修改返回的切片会直接影响 List 内部数据,需要只读语义时请自行复制。
3.2 ValueOf:带越界保护的单点取值
li := list.NewList([]int{1, 2, 3}) v, ok := li.ValueOf(0) fmt.Println(*v) // 1 fmt.Println(ok) // trueValueOf对下标做了显式边界检查(list.go):
if index < 0 || index >= len(l.data) { return nil, false } return &l.data[index], true因此它比直接切片访问更安全——越界时不会 panic,而是通过ok标志告知调用方。测试用例也验证了这一行为(list_test.go):ValueOf(3)对长度为 3 的列表返回false。
3.3 Size / Cap / IsEmpty
li := list.NewList([]int{1, 2, 3, 4}) fmt.Println(li.Size()) // 4 fmt.Println(li.IsEmpty()) // falseCap返回的是底层切片的容量,而不是元素个数。例如用预分配容量的切片创建 List:
data := make([]int, 0, 100) li := list.NewList(data) fmt.Println(li.Cap()) // 100Size、Cap、IsEmpty分别对应len(l.data)、cap(l.data)、len(l.data) == 0(list.go、list.go),测试见 list_test.go。
四、元素查找:Contain / IndexOf / LastIndexOf 及其 Func 变体
查找类方法共 6 个,分为两组:按值精确查找(依赖reflect.DeepEqual)与按谓词函数查找(依赖调用方传入的f func(T) bool)。
4.1 按值查找
li := list.NewList([]int{1, 2, 3, 1}) fmt.Println(li.Contain(1)) // true fmt.Println(li.Contain(0)) // false fmt.Println(li.IndexOf(1)) // 0 fmt.Println(li.IndexOf(0)) // -1 fmt.Println(li.LastIndexOf(1)) // 3 fmt.Println(li.LastIndexOf(0)) // -1三个方法的返回值约定非常清晰:Contain返回布尔值;IndexOf/LastIndexOf未找到时返回-1;LastIndexOf返回最后一次出现的下标。
实现要点:它们并未使用==直接比较,而是统一使用reflect.DeepEqual(list.go)。这意味着即使元素类型不支持==(如包含切片字段的结构体、map等不可比较类型),查找依然可以工作;同时也意味着对可比较类型而言,性能略低于直接==,但对普通应用场景完全够用。Contain本质上就是"找到即返回 true"的遍历(list.go)。
4.2 按谓词函数查找
li := list.NewList([]int{1, 2, 3, 1}) fmt.Println(li.IndexOfFunc(func(a int) bool { return a == 1 })) // 0 fmt.Println(li.IndexOfFunc(func(a int) bool { return a == 0 })) // -1 fmt.Println(li.LastIndexOfFunc(func(a int) bool { return a == 1 })) // 3 fmt.Println(li.LastIndexOfFunc(func(a int) bool { return a == 0 })) // -1IndexOfFunc/LastIndexOfFunc把"匹配规则"完全交给调用方(list.go),从源码看它们只是把reflect.DeepEqual(v, value)替换为f(v),其余遍历逻辑与按值版本一致。这让查找能力从"判等"扩展到"任意条件"——例如查找第一个大于 10 的元素、查找某个结构体满足特定字段条件的下标等。
实战提示:当元素类型可比较、列表规模大、只做精确匹配时,可以考虑用==手写循环以获得更好性能;当元素不可比较或需要自定义匹配逻辑时,*Func变体是更合适的选择。
五、增删改:Push / InsertAt / UpdateAt / DeleteAt / PopFirst / PopLast / DeleteIf
这是日常使用最频繁的一组操作。所有修改类方法都直接作用于调用者自身(除返回值明确的以外),并带有严格的越界保护:下标非法时"什么都不做",绝不 panic。
5.1 Push:末尾追加
li := list.NewList([]int{1, 2, 3}) li.Push(4) fmt.Println(li.Data()) // [1 2 3 4]Push就是一次append(list.go),语义等价于InsertAtLast(源码中InsertAtLast内部即调用l.InsertAt(len(l.data), value),见 list.go)。
5.2 InsertAt:任意位置插入
li := list.NewList([]int{1, 2, 3}) li.InsertAt(-1, 0) // 越界,无操作 fmt.Println(li.Data()) // [1 2 3] li.InsertAt(4, 0) // 越界,无操作 fmt.Println(li.Data()) // [1 2 3] li.InsertAt(3, 4) // 插入到末尾 fmt.Println(li.Data()) // [1 2 3 4] li.InsertAt(1, 9) // 插入到中间 fmt.Println(li.Data()) // [1 9 2 3 4]InsertAt的合法性区间是0 <= index <= len(data)(即可以插入到末尾,但不允许超过末尾),实现为经典的切片拼接(list.go):
if index < 0 || index > size { return } l.data = append(data[:index], append([]T{value}, data[index:]...)...)从实现可以看出,在头部或中间插入是 O(n) 操作(需要整体后移元素);若频繁在头部插入,应当考虑使用本仓库提供的链表结构(datastructure/link下的 doublylink.go)或队列(datastructure/queue)。
5.3 UpdateAt:更新指定下标
li := list.NewList([]int{1, 2, 3}) li.UpdateAt(-1, 0) // 越界,无操作 fmt.Println(li.Data()) // [1 2 3] li.UpdateAt(2, 4) fmt.Println(li.Data()) // [1 2 4] li.UpdateAt(3, 5) // 越界,无操作 fmt.Println(li.Data()) // [1 2 4]UpdateAt要求0 <= index < size(注意与InsertAt的区间差异,它不能更新到size处),越界直接返回(list.go)。
5.4 DeleteAt:删除指定下标
li := list.NewList([]int{1, 2, 3, 4}) li.DeleteAt(-1) // 越界,无操作 fmt.Println(li.Data()) // [1 2 3 4] li.DeleteAt(4) // 越界,无操作 fmt.Println(li.Data()) // [1 2 3 4] li.DeleteAt(0) fmt.Println(li.Data()) // [2 3 4] li.DeleteAt(2) fmt.Println(li.Data()) // [2 3]删除末尾元素时只做切片截断,其余位置通过append(data[:index], data[index+1:]...)前移元素(list.go):
if index < 0 || index > size-1 { return } if index == size-1 { data = data[:index] } else { data = append(data[:index], data[index+1:]...) }5.5 PopFirst / PopLast:弹出首尾元素
li := list.NewList([]int{1, 2, 3}) v, ok := li.PopFirst() fmt.Println(*v) // 1 fmt.Println(ok) // true fmt.Println(li.Data()) // [2 3] v2, ok2 := li.PopLast() fmt.Println(*v2) // 3 fmt.Println(ok2) // true fmt.Println(li.Data()) // [2]PopFirst/PopLast相当于"取出并删除首/尾元素",返回值为元素指针 + 是否存在。对空列表调用会返回nil, false,不会 panic(list.go),测试覆盖了空列表场景(list_test.go)。当列表为空时,解引用返回的指针前务必检查ok。
5.6 DeleteIf:按条件批量删除
l := list.NewList([]int{1, 1, 1, 1, 2, 3, 1, 1, 4, 1, 1, 1, 1, 1, 1}) fmt.Println(l.DeleteIf(func(a int) bool { return a == 1 })) // 12 fmt.Println(l.Data()) // [2 3 4]DeleteIf删除所有满足f(data[i])的元素,并返回被删除的元素个数。从源码看它采用"就地压缩"策略:不满足条件的元素依次向前覆盖,满足条件的元素被跳过(list.go):
for index := 0; index < len(data); index++ { if !f(data[index]) { continue } if index == size-1 { data = data[:index] } else { data = append(data[:index], data[index+1:]...) index-- } c++ }因此该操作同样为 O(n) 级(每次删除涉及元素移动)。测试还验证了"没有命中时返回 0 且列表不变"(list_test.go)。
六、比较与复制:Equal / Clone / Merge / SubList
6.1 Equal:逐元素深度比较
li1 := list.NewList([]int{1, 2, 3, 4}) li2 := list.NewList([]int{1, 2, 3, 4}) li3 := list.NewList([]int{1, 2, 3}) fmt.Println(li1.Equal(li2)) // true fmt.Println(li1.Equal(li3)) // falseEqual先比较两个列表长度,长度不等直接返回false;长度相等时再逐元素reflect.DeepEqual(list.go)。与"按值查找"一样,它支持包含不可比较字段的结构体元素的深度比较。
6.2 Clone:浅拷贝副本
li := list.NewList([]int{1, 2, 3}) cloneList := li.Clone() fmt.Println(cloneList.Data()) // [1 2 3]Clone通过copy复制底层切片数据(list.go):
cl := NewList(make([]T, len(l.data))) copy(cl.data, l.data)注意这是浅拷贝:复制的是切片本身和其中的元素值;如果元素本身是引用类型(指针、map、slice),副本与原件仍共享底层数据。测试通过list1.Equal(list2)验证克隆结果与原列表相等(list_test.go)。
6.3 Merge:合并两个列表(不改动原列表)
li1 := list.NewList([]int{1, 2, 3, 4}) li2 := list.NewList([]int{4, 5, 6}) li3 := li1.Merge(li2) fmt.Println(li3.Data()) // [1 2 3 4 4 5 6]Merge返回一个新列表,内容为l与other首尾相接,保留重复元素,且不修改li1、li2本身(list.go)。如果希望合并时去重,应使用下一节的Union。
6.4 SubList:切片子列表
l := list.NewList([]int{1, 2, 3, 4, 5, 6}) fmt.Println(l.SubList(2, 5).Data()) // [3 4 5]SubList(fromIndex, toIndex)取[fromIndex, toIndex)左闭右开区间。与直接切片不同,它复制出一段独立切片再包装(list.go):
data := l.data[fromIndex:toIndex] subList := make([]T, len(data)) copy(subList, data) return NewList(subList)这意味着对子列表的修改不会影响原列表,避免了两者共享底层数组带来的意外副作用。仓库为此专门编写了性能基准BenchmarkSubSlice(list_test.go)。
七、顺序调整:Swap / Reverse / Clear
7.1 Swap:交换两个下标的值
li := list.NewList([]int{1, 2, 3, 4}) li.Swap(0, 3) fmt.Println(li.Data()) // [4 2 3 1]Swap(i, j)要求两个下标都在[0, size)内,越界时无操作(list.go)。实现就是一次经典的三元交换:
l.data[i], l.data[j] = l.data[j], l.data[i]7.2 Reverse:反转顺序
li := list.NewList([]int{1, 2, 3, 4}) li.Reverse() fmt.Println(li.Data()) // [4 3 2 1]Reverse使用双指针从两端向中间交换(list.go),时间复杂度 O(n/2),是原地操作。
7.3 Clear:清空数据
li := list.NewList([]int{1, 2, 3}) li.Clear() fmt.Println(li.Data()) // [] fmt.Println(li.IsEmpty()) // trueClear将内部切片重置为长度 0 的空切片(list.go),原数据被丢弃;IsEmpty随之返回true。
八、集合运算:Unique / Union / Intersection / Difference / SymmetricDifference / RetainAll / DeleteAll
这部分是 List 的"集合能力",适合做去重、交并差等数据处理。
8.1 Unique:原地去重
li := list.NewList([]int{1, 2, 2, 3, 4}) li.Unique() fmt.Println(li.Data()) // [1 2 3 4]Unique保留首次出现的元素,删除后续重复项,保留原始相对顺序。从源码看它的实现是双重循环 +reflect.DeepEqual(list.go),时间复杂度 O(n²);对大规模数据做去重时,可考虑先用ListToMap或外部map辅助以换取 O(n) 性能。
8.2 Union:并集(去重)
li1 := list.NewList([]int{1, 2, 3, 4}) li2 := list.NewList([]int{4, 5, 6}) li3 := li1.Union(li2) fmt.Println(li3.Data()) // [1 2 3 4 5 6]Union先把两个列表拼起来,再调用Unique去重,返回新列表,不改动原列表(list.go)。
8.3 Intersection:交集
li1 := list.NewList([]int{1, 2, 3, 4}) li2 := list.NewList([]int{4, 5, 6}) li3 := li1.Intersection(li2) fmt.Println(li3.Data()) // [4]Intersection遍历l,凡是other也包含的元素加入结果(list.go)。注意:它以l为主序,结果中保留的是l中的元素及其相对顺序。
8.4 Difference:差集(属于 l 但不属于 other)
list1 := list.NewList([]int{1, 2, 3}) list2 := list.NewList([]int{1, 2, 4}) list3 := list1.Difference(list2) fmt.Println(list3.Data()) // [3]Difference的语义是"返回在原列表中、但不在给定列表中的元素",即l - other。实现上是先求交集,再过滤掉交集元素(list.go)。
8.5 SymmetricDifference:对称差集
list1 := list.NewList([]int{1, 2, 3}) list2 := list.NewList([]int{1, 2, 4}) list3 := list1.SymmetricDifference(list2) fmt.Println(list3.Data()) // [3 4]SymmetricDifference是交集的对偶运算:返回"只属于其中一个列表"的元素(即(l ∪ other) - (l ∩ other))。实现分两段遍历:先取l中不在交集里的元素,再取other中不在交集里的元素(list.go)。
8.6 RetainAll / DeleteAll:保留 / 批量删除
list := list.NewList([]int{1, 2, 3, 4}) retain := list.NewList([]int{1, 2}) list.RetainAll(retain) fmt.Println(list.Data()) // [1 2] del := list.NewList([]int{2, 3}) list.DeleteAll(del) fmt.Println(list.Data()) // [1]这两个方法语义上是一对互补操作:
RetainAll(other):只保留other中也包含的元素(交集语义,但原地修改调用者);DeleteAll(other):删除other中包含的所有元素(差集语义,原地修改)。
两者的返回值都是bool,表示列表是否被实际修改。从源码看,它们共用同一个私有方法batchRemove(list, complement bool)(list.go):
func (l *List[T]) RetainAll(list *List[T]) bool { return l.batchRemove(list, true) } func (l *List[T]) DeleteAll(list *List[T]) bool { return l.batchRemove(list, false) }batchRemove采用双指针就地压缩:保留的元素依次前移,最后截断切片。测试覆盖了RetainAll三种保留集(部分命中、完全命中、含不存在的元素)与DeleteAll三种删除集的场景(list_test.go)。
易混淆点:
RetainAll/DeleteAll是原地修改并返回"是否改变"的布尔值;而Union/Intersection/Difference/SymmetricDifference是返回新列表、不改动调用者。选择时注意区分。
九、遍历与转换:ForEach / Iterator / ListToMap
9.1 ForEach:对每个元素执行动作
list := list.NewList([]int{1, 2, 3, 4}) result := make([]int, 0) list.ForEach(func(i int) { result = append(result, i) }) fmt.Println(result) // [1 2 3 4]ForEach接收一个consumer func(T),按顺序对每个元素调用(list.go)。它没有返回值,适合做副作用型操作(收集、打印、累加等)。
9.2 Iterator:顺序迭代器
list := list.NewList([]int{1, 2, 3, 4}) iterator := list.Iterator() result := make([]int, 0) for iterator.HasNext() { item, _ := iterator.Next() result = append(result, item) } fmt.Println(result) // [1 2 3 4]Iterator返回iterator.Iterator[T]接口实例,内部实现是iterator.FromSlice(l.data)(list.go),即一个SliceIterator(iterator.go)。该迭代器支持HasNext()与Next() (T, bool)两个核心方法。
需要特别说明的是:本仓库的iterator包目前处于实验阶段。源码文件头部的注释明确警告:该包功能简单且有限、正在开发中、不建议在生产环境使用(iterator.go)。因此,日常遍历优先使用ForEach、Data()或直接的for range;Iterator更多用于了解迭代器模式在 Go 中的可行形态。
9.3 ListToMap:列表转映射
list := list.NewList([]int{1, 2, 3, 4}) result := ListToMap(list, func(n int) (int, bool) { return n, n > 1 }) fmt.Println(result) // map[1:false 2:true 3:true 4:true]ListToMap是包级泛型函数,签名:
func ListToMapT any, K comparable, V any (K, V)) map[K]V它通过iteratee把每个元素映射为(key, value)键值对并写入 map(list.go)。注意两个细节:
K必须是comparable(Go 中 map 键的基本要求);- 若
iteratee对多个元素产生相同 key,后者覆盖前者,这与 Go map 的赋值语义一致。
结合ListToMap还可以实现 O(n) 的去重或按字段建立索引等常见需求。
十、源码质量与同类结构
10.1 测试覆盖
list_test.go 几乎为每个公开方法都编写了对应的单元测试,并大量使用internal.NewAssert断言库,覆盖了关键边界:空列表的PopFirst/PopLast(返回nil, false)、越界下标的InsertAt/UpdateAt/DeleteAt(无操作)、DeleteIf无命中时返回 0 等。这些测试同时是理解方法语义最可靠的"活文档"。
10.2 同类结构:CopyOnWriteList
在 copyonwritelist.go 中,仓库还提供了线程安全的CopyOnWriteList[T](写时复制列表),其Data、Size、Contain、IndexOf等 API 与List风格相近,但内部以sync.RWMutex保护并通过复制-替换实现并发安全。如果你的场景是"读多写少"的并发访问,可以参考该实现(详见文档 copyonwritelist.md)。
十一、使用建议与注意事项
- 边界安全:
ValueOf、InsertAt、UpdateAt、DeleteAt、Swap都有越界保护,非法下标不会 panic,但也不会报错——调试时要注意"静默无操作"可能掩盖逻辑错误。 - 性能认知:
- 头部/中间插入与删除是 O(n)(
InsertAt、DeleteAt、DeleteIf); Unique、Union等去重运算是 O(n²)(双重循环 +DeepEqual);- 大量去重场景可借道
ListToMap或map换取 O(n) 复杂度; Data()返回内部切片引用,只读请自行复制。
- 头部/中间插入与删除是 O(n)(
- 判等策略:
Contain/IndexOf/LastIndexOf/Equal/Unique均基于reflect.DeepEqual,支持不可比较元素类型,但对大对象开销更高;追求极致性能且元素可比较时,可自行实现==版本。 - 方法语义分组(避免误用):
- 原地修改且无返回值:
Push、InsertAt、UpdateAt、DeleteAt、Swap、Reverse、Unique、Clear、DeleteIf(返回删除个数); - 返回新列表:
Merge、Clone、SubList、Union、Intersection、Difference、SymmetricDifference; - 原地修改且返回是否变化:
RetainAll、DeleteAll; - 返回指针+存在标志:
ValueOf、PopFirst、PopLast。
- 原地修改且无返回值:
- 迭代器注意:
Iterator()依赖实验性的iterator包,生产代码建议用ForEach/for range代替。
综上,lancet 的List是一个接口简洁、边界防护完善、与 Go 泛型深度结合的线性表实现。无论是作为普通动态数组使用,还是借助其集合运算能力做数据清洗,都能在 list.go 及其测试中快速找到可直接复用的答案。
- 开发工具
【免费下载链接】lancet
A comprehensive, efficient, and reusable util function library of Go.
相关推荐
fast_align 性能优化:OpenMP 多核并行与 libtcmalloc 两步走,把百万句对词对齐压进半小时
fast_align 性能优化:OpenMP 多核并行与 libtcmalloc 两步走,把百万句对词对齐压进半小时 fast_align 是一个无监督词对齐工
开发工具Lancet 数据结构实战:Go 泛型双栈 ArrayStack 与 LinkedStack 全解析
Lancet 数据结构实战:Go 泛型双栈 ArrayStack 与 LinkedStack 全解析 栈(Stack)是计算机科学中最基础、应用最广泛的抽象数据
开发工具ok-ww 鸣潮后台挂机自动化教程:从安装到一键刷声骸的完整指南
ok ww 鸣潮后台挂机自动化教程:从安装到一键刷声骸的完整指南 ok ww 是一款免费开源的鸣潮后台挂机自动化工具:它通过周期性截取游戏画面做图像识别,再用
GUI 自动化计算机视觉RPA人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考