☰
lancet 数据结构之 List:基于 Go 泛型切片的线性表实战指南
2026/10/13 2:23:43 网站建设 项目流程
  • 开发工具

【免费下载链接】lancet

A comprehensive, efficient, and reusable util function library of Go.

项目地址:https://gitcode.com/gh_mirrors/la/lancet
点击查看免费下载

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 的整体数据、单点取值以及容量信息,是后续一切操作的基础。

方法签名作用
Datafunc (l *List[T]) Data() []T返回底层数据切片
ValueOffunc (l *List[T]) ValueOf(index int) (*T, bool)返回 index 处的值指针;越界返回nil, false
Sizefunc (l *List[T]) Size() int返回元素个数
Capfunc (l *List[T]) Cap() int返回底层切片的容量
IsEmptyfunc (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) // true

ValueOf对下标做了显式边界检查(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()) // false

Cap返回的是底层切片的容量,而不是元素个数。例如用预分配容量的切片创建 List:

data := make([]int, 0, 100) li := list.NewList(data) fmt.Println(li.Cap()) // 100

Size、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 })) // -1

IndexOfFunc/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)) // false

Equal先比较两个列表长度,长度不等直接返回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()) // true

Clear将内部切片重置为长度 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)。

十一、使用建议与注意事项

  1. 边界安全:ValueOf、InsertAt、UpdateAt、DeleteAt、Swap都有越界保护,非法下标不会 panic,但也不会报错——调试时要注意"静默无操作"可能掩盖逻辑错误。
  2. 性能认知:
    • 头部/中间插入与删除是 O(n)(InsertAt、DeleteAt、DeleteIf);
    • Unique、Union等去重运算是 O(n²)(双重循环 +DeepEqual);
    • 大量去重场景可借道ListToMap或map换取 O(n) 复杂度;
    • Data()返回内部切片引用,只读请自行复制。
  3. 判等策略:Contain/IndexOf/LastIndexOf/Equal/Unique均基于reflect.DeepEqual,支持不可比较元素类型,但对大对象开销更高;追求极致性能且元素可比较时,可自行实现==版本。
  4. 方法语义分组(避免误用):
    • 原地修改且无返回值:Push、InsertAt、UpdateAt、DeleteAt、Swap、Reverse、Unique、Clear、DeleteIf(返回删除个数);
    • 返回新列表:Merge、Clone、SubList、Union、Intersection、Difference、SymmetricDifference;
    • 原地修改且返回是否变化:RetainAll、DeleteAll;
    • 返回指针+存在标志:ValueOf、PopFirst、PopLast。
  5. 迭代器注意:Iterator()依赖实验性的iterator包,生产代码建议用ForEach/for range代替。

综上,lancet 的List是一个接口简洁、边界防护完善、与 Go 泛型深度结合的线性表实现。无论是作为普通动态数组使用,还是借助其集合运算能力做数据清洗,都能在 list.go 及其测试中快速找到可直接复用的答案。

  • 开发工具

【免费下载链接】lancet

A comprehensive, efficient, and reusable util function library of Go.

项目地址:https://gitcode.com/gh_mirrors/la/lancet
点击查看免费下载

相关推荐

上一篇:如何让Degrees of Lewdity模组管理效率提升80%?DoL-Lyra工具全解析
下一篇:3步释放50GB空间:DriverStore Explorer驱动仓库深度清理指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询