Go基础学习06-Golang标准库container/list(双向链表)深入讲解;延迟初始化技术;Element;List;Ring
基础介绍
单向链表中的每个节点包含数据和指向下一个节点的指针。其特点是每个节点只知道下一个节点的位置,使得数据只能单向遍历。
示意图如下:

双向链表中的每个节点都包含指向前一个节点和后一个节点的指针。这使得在双向链表中可以从前向后或从后向前遍历。
示意图如下:

结合上面的图就很容易明白单、双链表的定义。其中双向链表可以从前向后,也可以从后向前遍历,操作起来也更加方便。
接下来我们看看官方给的例子:
import ("container/list""fmt"
)func Example() {// Create a new list and put some numbers in it.l := list.New()e4 := l.PushBack(4)e1 := l.PushFront(1)l.InsertBefore(3, e4)l.InsertAfter(2, e1)// Iterate through list and print its contents.for e := l.Front(); e != nil; e = e.Next() {fmt.Println(e.Value)}// Output:// 1// 2// 3// 4
}
首先调用list.New()创建一个双向链表,然后添加元素Element,最后从头遍历链表,打印每个元素的值。
从上可以看出,container/list提供了两个结构 List、Element。
- List
- Element
在Java中实现的双向链表也是这样做的,只是元素一般命名为Node而已。
container/list源码分析
Element
// Element is an element of a linked list.
type Element struct {// Next and previous pointers in the doubly-linked list of elements.// To simplify the implementation, internally a list l is implemented// as a ring, such that &l.root is both the next element of the last// list element (l.Back()) and the previous element of the first list// element (l.Front()).next, prev *Element// The list to which this element belongs.list *List// The value stored with this element.Value any
}
Element 一共定义了四个字段,分别是指向前一个节点的 prev,指向下一个节点的 next,存储值的 Value,以及 此元素属于哪个list。
平常自己在定义双向链表 Node 的结构的时候,一般是不会有 list 这个元素的,为什么官方给的有这个元素呢?
- Element 的 list 字段是小写的,那意味着外部使用者是无法获取和定义此字段的,也就是说外部使用者无法通过 Element 来操作 链表。在通篇读过源码后,发现 Element.list 是用于判断插入、移动、删除等操作的元素是否属于此链表,所以我认为增加 list 字段的原因主要是安全性。
比如防止在多维链表操作的时候,错误的加入了不属于此链表的节点,有了 list 字段后,就可以做判断,防止这类情况产生。- 对于Element的扩展字段list可以将其理解为与链表的跟节点root绑定的一个值,通过Element.list可以获取这个链表,从而获取root值,用于一些判断,主要在源码中使用,有链表基础的建议查阅源码学习
Element 只有两个方法,即 Next()、Prev(),源代码如下:
// Next returns the next list element or nil.
func (e *Element) Next() *Element {if p := e.next; e.list != nil && p != &e.list.root {return p}return nil
}// Prev returns the previous list element or nil.
func (e *Element) Prev() *Element {if p := e.prev; e.list != nil && p != &e.list.root {return p}return nil
}
看到这里,官方给的实现方式,并不是简单的 e.prev、e.next,而是多了p != &e.list.root的判断,为什么会有这个判断呢?
因为container/list起始是一个环形链表,那么就需要有一个特殊的节点切断这种环形关系,root就是用来做这个标识的节点。
这样做有什么好处呢?
root 字段是链表的根节点,它并不直接存储数据,而是一个空节点(Element 类型)。这个空节点被用作链表的哨兵节点(Sentinel Node)或者叫做标志节点(Dummy Node)。
这个哨兵节点的作用是为了简化链表的操作。通过将哨兵节点作为链表的根节点,在实际的链表操作中,就无需考虑头节点为空的情况,即空链表和非空链表的操作逻辑变得更加统一和简化。
- 简化逻辑: 哨兵节点的引入避免了对空链表的特殊处理。无论链表是否为空,头节点(哨兵节点之后的第一个节点)始终存在,这样在操作链表时就无需针对空链表做额外的判断。
- 边界条件更清晰: 有了哨兵节点,链表的头部和尾部都有了固定的节点作为标志,使得链表操作时边界条件更加清晰。
- 提高代码的一致性: 通过哨兵节点,链表的操作逻辑更加统一,减少了特殊情况下的代码分支,提高了代码的一致性和可读性。
List
List结构
// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {root Element // sentinel list element, only &root, root.prev, and root.next are usedlen int // current list length excluding (this) sentinel element
}// Init initializes or clears list l.
func (l *List) Init() *List {l.root.next = &l.rootl.root.prev = &l.rootl.len = 0return l
}// New returns an initialized list.
func New() *List { return new(List).Init() }// Len returns the number of elements of list l.
// The complexity is O(1).
func (l *List) Len() int { return l.len }
因为container/list 是一个环形链表,所以只用提供一个节点就可以了。
注意:刚初始化时,即调用New生成的链表对象,此时的 root.next、root.prev 都是指向root 自己的 。当使用 PushBack或者PushFront方法后,root.next 表示 Head Node,root.prev 表示 Tail Node。注意 List.len 的长度是不包含 root 节点的。方法后,root.next 表示 Head Node,root.prev 表示 Tail Node。注意 List.len 的长度是不包含 root 节点的。

获取头尾节点
// Front returns the first element of list l or nil if the list is empty.
func (l *List) Front() *Element {if l.len == 0 {return nil}return l.root.next
}// Back returns the last element of list l or nil if the list is empty.
func (l *List) Back() *Element {if l.len == 0 {return nil}return l.root.prev
}
root.next 表示 Head Node,root.prev 表示 Tail Node。
Go中链表的延迟初始化机制
示例代码:
// 声明一个链表但未初始化,随后使用链表的延迟初始化技术在插入元素时进行初始化(lazyInit)l2 := list.List{}fmt.Println(l2)l2.PushBack(3)fmt.Println(l2)
运行结果:
{{<nil> <nil> <nil> <nil>} 0}
{{0xc0000b62a0 0xc0000b62a0 <nil> <nil>} 1}
使用l2 := list.List{}声明的链表l2,可以正常使用,但仅仅凭借l2这个链表是无法正常使用的,需要结果Go中链表的延迟初始化 机制才可以使用。
查看Go中container/list的源码中的PushBack()方法:
// PushBack inserts a new element e with value v at the back of list l and returns e.
func (l *List) PushBack(v any) *Element {l.lazyInit()return l.insertValue(v, l.root.prev)
}// lazyInit lazily initializes a zero List value.
func (l *List) lazyInit() {if l.root.next == nil {l.Init()}
}// Init initializes or clears list l.
func (l *List) Init() *List {l.root.next = &l.rootl.root.prev = &l.rootl.len = 0return l
}
通过上述源码可以看到lazeInit()这个函数,这个函数就是链表的延迟初始化技术,只有在使用到的时候才对链表进行初始化。
- 延迟初始化:把初始化操作延后,仅在实际需要的时候才进行。延迟初始化的优点在于“延后”,它可以分散初始化操作带来的计算量和存储空间消耗。
例如,如果我们需要集中声明非常多的大容量切片的话,那么那时的 CPU 和内存空间的使用量肯定都会一个激增,并且,只有设法让其中的切片及其底层数组被回收,内存使用量才会有所降低。
如果数组是可以被延迟初始化的,那么计算量和存储空间的压力就可以被分散到实际使用它们的时候。这些数组被实际使用的时间越分散,延迟初始化带来的优势就会越明显。:可以对照Java中的数组的扩容,以及Go中切片的扩容都是延迟化思想的体现。
- 延迟初始化是否有缺点以及Go是如何减小延迟初始化对链表操作的影响
延迟初始化的缺点恰恰也在于“延后”,如果我在调用链表的每个方法的时候,它们都需要先去判断链表是否已经被初始化,那这也会是一个计算量上的浪费。在这些方法被非常频繁地调用的情况下,这种浪费的影响就开始显现了,程序的性能将会降低。
>在Go语言链表设计中只对插入操作如:PushFront方法、PushBack方法、PushBackList方法以及PushFrontList方法在执行时总是会先判断链表的状态,并在必要时进行初始化,一旦初始化后,后续在执行这些方法的时候就无需进行初始化,可以直接使用。
- 此外在Go语言设计中对于删除元素、移动元素,以及一些用于插入元素的方法中,只要判断一下传入的元素中指向所属链表的指针,是否与当前链表的指针相等就可以了。如果不相等,就一定说明传入的元素不是这个链表中的,后续的操作就不用做了。反之,就一定
说明这个链表已经被初始化了。此时使用到Element元素的list属性,这个操作是内部源码调用的。
// Remove removes e from l if e is an element of list l.
// It returns the element value e.Value.
// The element must not be nil.
func (l *List) Remove(e *Element) any {if e.list == l {// if e.list == l, l must have been initialized when e was inserted// in l or l == nil (e is a zero Element) and l.remove will crashl.remove(e)}return e.Value
}
- 对于Front方法和Back方法,只需对链表的长度进行判断即可,一旦发现链表的长度为0直接返回nil就好了。
container包中的list和ring包中的List和Ring的区别
container/ring包中的Ring类型实现的是一个循环链表,也就是我们俗称的环。其实List在内部就是一个循环链表。它的根元素永远不会持有任何实际的元素值,而该元素的存在,就是为了连接这个循环链表的首尾两端。
所以也可以说,List的零值是一个只包含了根元素,但不包含任何实际元素值的空链表。那么,既然Ring和List在本质上都是循环链表,那它们到底有什么不同呢?最主要的不同有下面几种。
- Ring类型的数据结构仅由它自身即可代表,而List类型则需要由它以及Element类型联合表示。这是表示方式上的不同,也是结构复杂度上的不同。
- 一个Ring类型的值严格来讲,只代表了其所属的循环链表中的一个元素,而一个List类型的值则代表了一个完整的链表。这是表示维度上的不同。
- 在创建并初始化一个Ring值的时候,我们可以指定它包含的元素的数量,但是对于一个List值来说,却不能这样做(也没有必要这样做)。循环链表一旦被创建,其长度是不可变的。这是两个代码包中的New函数在功能上的不同,也是两个类型在初始化值方面的第一个不同。
- 仅通过var r ring.Ring语句声明的r将会是一个长度为1的循环链表,而List类型的零值则是一个长度为0的链表。别忘了List中的根元素不会持有实际元素值,因此计算长度时不会包含它。这是两个类型在初始化值方面的第二个不同。
- Ring值的Len方法的算法复杂度是 O(N) 的,而List值的Len方法的算法复杂度则是 O(1)的。这是两者在性能方面最显而易见的差别。其他的不同基本上都是方法方面的了。比如,循环链表也有用于插入、移动或删除元素的方法,不过用起来都显得更抽象一些,等等。
上述内容参考极客时间Go语言核心36讲
Go语言提供的链表常用API
// Remove removes e from l if e is an element of list l.
// It returns the element value e.Value.
// The element must not be nil.
func (l *List) Remove(e *Element) any {if e.list == l {// if e.list == l, l must have been initialized when e was inserted// in l or l == nil (e is a zero Element) and l.remove will crashl.remove(e)}return e.Value
}// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *List) PushFront(v any) *Element {l.lazyInit()return l.insertValue(v, &l.root)
}// PushBack inserts a new element e with value v at the back of list l and returns e.
func (l *List) PushBack(v any) *Element {l.lazyInit()return l.insertValue(v, l.root.prev)
}// InsertBefore inserts a new element e with value v immediately before mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List) InsertBefore(v any, mark *Element) *Element {if mark.list != l {return nil}// see comment in List.Remove about initialization of lreturn l.insertValue(v, mark.prev)
}// InsertAfter inserts a new element e with value v immediately after mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List) InsertAfter(v any, mark *Element) *Element {if mark.list != l {return nil}// see comment in List.Remove about initialization of lreturn l.insertValue(v, mark)
}// MoveToFront moves element e to the front of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List) MoveToFront(e *Element) {if e.list != l || l.root.next == e {return}// see comment in List.Remove about initialization of ll.move(e, &l.root)
}// MoveToBack moves element e to the back of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List) MoveToBack(e *Element) {if e.list != l || l.root.prev == e {return}// see comment in List.Remove about initialization of ll.move(e, l.root.prev)
}// MoveBefore moves element e to its new position before mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List) MoveBefore(e, mark *Element) {if e.list != l || e == mark || mark.list != l {return}l.move(e, mark.prev)
}// MoveAfter moves element e to its new position after mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List) MoveAfter(e, mark *Element) {if e.list != l || e == mark || mark.list != l {return}l.move(e, mark)
}// PushBackList inserts a copy of another list at the back of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List) PushBackList(other *List) {l.lazyInit()for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {l.insertValue(e.Value, l.root.prev)}
}// PushFrontList inserts a copy of another list at the front of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List) PushFrontList(other *List) {l.lazyInit()for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {l.insertValue(e.Value, &l.root)}
}
贴一个其他博主使用Go中的container/list实现的一个二维链表
有了上面的基础后,我们再来实战下。
需求:实现一个二维链表,要求第一维以价格从低到高排序,第二维以时间从小到大排序。
package mainimport ("container/list""fmt""sort""strings""time"
)type Order struct {Price float64CreatedTime time.Time
}// TwoDList 二维链表,要求第一维以价格从低到高排序,第二维以时间从小到大排序。
type TwoDList struct {// 索引相同,即表示价格相同,同一索引的链表节点,越靠后时间越大// 索引越大,价格越高Rows []*list.List
}func NewTwoDList() *TwoDList {return &TwoDList{Rows: make([]*list.List, 0),}
}func (tdl *TwoDList) AddNode(price float64, createdTime time.Time) {order := &Order{Price: price, CreatedTime: createdTime}// 1、index := sort.Search(len(tdl.Rows), func(i int) bool {return tdl.Rows[i].Front().Value.(*Order).Price >= order.Price})if index == len(tdl.Rows) {// 此价格不存在 tdl 中, 新增newList := list.New()newList.PushFront(order)tdl.Rows = append(tdl.Rows, newList)return}// 判断 index 处的价格是否和 order.Price 相等,// 相等, 则往链表添加// 不相等, 则需要先将 index 之后的往后移一位if order.Price != tdl.Rows[index].Front().Value.(*Order).Price {newList := list.New()newList.PushFront(order)// 插入元素 newListtdl.Rows = append(tdl.Rows[:index], append([]*list.List{newList}, tdl.Rows[index:]...)...)return}// 时间从小到大排curRow := tdl.Rows[index]insertPosition := curRow.Front()for insertPosition != nil && order.CreatedTime.After(insertPosition.Value.(*Order).CreatedTime) {insertPosition = insertPosition.Next()}if insertPosition == nil {curRow.PushBack(order)} else {curRow.InsertBefore(order, insertPosition)}
}func (tdl *TwoDList) Print() {for i, row := range tdl.Rows {fmt.Printf("index: %d\n", i)for node := row.Front(); node != nil; node = node.Next() {order := node.Value.(*Order)fmt.Printf("order price: %f, time: %v \n", order.Price, order.CreatedTime)}fmt.Println(strings.Repeat("-", 20))}
}func main() {// 创建一个新的二维链表myTwoDList := NewTwoDList()// 向二维链表添加节点myTwoDList.AddNode(100, time.Now())myTwoDList.AddNode(75, time.Now().Add(time.Hour))myTwoDList.AddNode(75, time.Now().Add(time.Hour))myTwoDList.AddNode(150, time.Now().Add(2*time.Hour))myTwoDList.AddNode(75, time.Now().Add(3*time.Hour))myTwoDList.AddNode(200, time.Now().Add(4*time.Hour))// 打印二维链表myTwoDList.Print()
}
参考链接
博客园-点击
相关文章:
Go基础学习06-Golang标准库container/list(双向链表)深入讲解;延迟初始化技术;Element;List;Ring
基础介绍 单向链表中的每个节点包含数据和指向下一个节点的指针。其特点是每个节点只知道下一个节点的位置,使得数据只能单向遍历。 示意图如下: 双向链表中的每个节点都包含指向前一个节点和后一个节点的指针。这使得在双向链表中可以从前向后或从后…...
多层时间轮原理以及使用
文章目录 背景常用定时器实现 任务队列时间轮介绍基本结构指针移动定时任务插入循环任务插入代码示例 多层时间轮使用流程 代码 背景 在软件开发中,定时器是一个极为常用的组件,它发挥着至关重要的作用。通过定时器,开发者能够精确地控制程序…...
鸿蒙HarmonyOS开发生态
1、官网 华为开发者联盟-HarmonyOS开发者官网,共建鸿蒙生态 2、开发工具IDE下载及使用 https://developer.huawei.com/consumer/cn/ 3、使用帮助文档 4、发布到华为应用商店 文档中心...
vue中使用jsencrypt加密
vue中封装并使用jsencrypt加密 一般在项目搭建过程中用户注册、登录、修改密码、重置密码等功能都需要用到密码加密的功能,所以把jsencrypt进行封装使用,使代码更加简洁,流程如下: 1、安装jsencrypt npm install jsencrypt2、在…...
SpirngBoot核心思想之一AOP
简介: AOP(Aspect-Oriented Programming,面向切面编程) 是一种用于解决软件系统中横切关注点的编程范式。在企业级开发中,很多非业务功能(如日志、事务、权限、安全等)需要在多个模块中执行&am…...
足球预测推荐软件:百万数据阐述百年足球历史-大数据模型量化球员成就值
我开始创建这个模型是从梅西22世界杯夺冠第二天开始准备的,当时互联网上充斥了太多了个人情感的输出,有的人借题对C罗冷嘲热讽,有的人质疑梅西的阿根廷被安排夺冠不配超越马拉多纳做GOAT。作为一个从2002年开始看球的球迷,说实话有…...
AD中如何批量修改丝印的大小,节省layout时间
先选中一个元器件的丝印,然后右键,选择“查找相似项” 直接点击OK,这时会发现所有的丝印都会被选中 然后点击右上角的按键,修改其属性。...
Ps:堆栈
将多张类似的图像图层堆叠在一起形成图像堆栈 Stack,通过应用不同的堆栈模式,可以从这些图像中提取有用的信息或达到特定的视觉效果。 Photoshop 中的堆栈技术被广泛应用于摄影后期处理、科学研究、环境监测与分析等领域。 例如,它常用于减少…...
獨立IP和共用IP有什麼區別?
什麼是獨立IP? 獨立IP指的是一個IP地址專屬於一個用戶或設備。無論是網站、伺服器還是其他線上服務,獨立IP都意味著該IP地址不會與其他用戶或設備共用。獨立IP通常用於需要高安全性和穩定性的場景,比如企業網站、電子商務平臺和需要SSL證書的…...
枢纽云EKP数字门户模板上线!轻松复刻胖东来官网,实现数字化逆袭
数字化转型的浪潮中,胖东来凭借着其独特的企业文化和对员工福利的重视,走进了大众视野。近期,胖东来推出了“不开心假”等员工关怀,又一次引发了大众的广泛关注。这种关怀不仅仅提升了员工的幸福感,也间接的改善了顾客…...
从自动化到智能化:AI如何推动业务流程自动化
引言:从自动化到智能化的必然趋势 在当今数字化时代,企业为了提升效率、降低成本,纷纷采用自动化技术来简化重复性任务。然而,传统自动化仅限于标准化操作,无法应对复杂的决策和多变的市场环境。随着人工智能ÿ…...
Selenium与数据库结合:数据爬取与存储的技术实践
目录 一、Selenium与数据库结合的基础概念 1.1 Selenium简介 1.2 数据库简介 1.3 Selenium与数据库结合的优势 二、Selenium爬取数据的基本步骤 2.1 环境准备 2.2 编写爬虫代码 2.3 数据提取 2.4 异常处理 三、数据存储到数据库 3.1 数据库连接 3.2 数据存储 3.3 …...
在 Docker 中进入 Redis 容器后,可以通过以下方法查看 Redis 版本:
文章目录 1、info server2、redis-cli -v 1、info server [rootlocalhost ~]# docker exec -it spzx-redis redis-cli 127.0.0.1:6379> auth 123456 OK 127.0.0.1:6379> info server # Server redis_version:6.2.6 redis_git_sha1:00000000 redis_git_dirty:0 redis_bui…...
Windows 10 系统安装 FFmpeg 查看、转换、编辑音频文件
1、FFmpeg官网:FFmpeg 点击下载 可以选择下载full版本 下载之后解压到指定目录,在系统环境变量 Path 里面新增环境变量 打开CMD终端运行 ffmpeg -version 查看是否安装成功。 2、基本命令 查看音频基本信息 ffprobe 1.mp3 ##输出 [mp3 000002ab334405…...
反调试防护-API
IsDebuggerPresent() CheckRemoteDebuggerPresent() 其内部实际调用NtQueryInformationProcess() bool _stdcall ThreadCall() {while (true){BOOL pbDebuggerPresent FALSE;CheckRemoteDebuggerPresent(GetCurrentProcess(), &pbDebuggerPresent);if (pbDebuggerPres…...
【视频讲解】非参数重采样bootstrap逻辑回归Logistic应用及模型差异Python实现
全文链接:https://tecdat.cn/?p37759 分析师:Anting Li 本文将深入探讨逻辑回归在心脏病预测中的应用与优化。通过对加州大学欧文分校提供的心脏病数据集进行分析,我们将揭示逻辑回归模型的原理、实现过程以及其在实际应用中的优势和不足…...
Linux系统中命令wc
wc(word count)命令是Linux和Unix系统中用于计算字数的一个非常实用的工具。它可以统计文件的字节数、字数、行数等信息。默认情况下,wc命令会输出这三个统计值,但你也可以通过选项来指定只输出其中的某些值。 基本用法 wc [选项…...
redis集群部署
创建ConfigMap redis-cm.yaml apiVersion: v1 kind: ConfigMap metadata:name: redis-cluster data:update-node.sh: |#!/bin/shREDIS_NODES"/data/nodes.conf"sed -i -e "/myself/ s/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/${POD_IP}/&quo…...
VUE条件树查询
看如下图所示的功能,是不是可高级了?什么,你没看懂?拜托双击放大看! 是的,我最近消失了一段时间就是在研究这个玩意的实现,通过不懈努力与钻研并参考其他人员实现并加以改造,很好&am…...
vue框架学习 -- 日历控件 FullCalendar 使用总结
最近在项目中要实现日期排班的功能,正好要用到日历视图的控件,经过对比发现,vue 中 使用 FullCalendar 可以实现相关需求,下面对使用过程做一个总结。 一. 引入 FullCalendar 控件 package.json 中添加相关依赖 "dependen…...
UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...
MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...
dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明
AI 领域的快速发展正在催生一个新时代,智能代理(agents)不再是孤立的个体,而是能够像一个数字团队一样协作。然而,当前 AI 生态系统的碎片化阻碍了这一愿景的实现,导致了“AI 巴别塔问题”——不同代理之间…...
使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
【Linux】自动化构建-Make/Makefile
前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具:make/makfile 1.背景 在一个工程中源文件不计其数,其按类型、功能、模块分别放在若干个目录中,mak…...
Kafka主题运维全指南:从基础配置到故障处理
#作者:张桐瑞 文章目录 主题日常管理1. 修改主题分区。2. 修改主题级别参数。3. 变更副本数。4. 修改主题限速。5.主题分区迁移。6. 常见主题错误处理常见错误1:主题删除失败。常见错误2:__consumer_offsets占用太多的磁盘。 主题日常管理 …...
