节流函数(Throttling)是通过控制函数的执行频率来提高性能的一种技术,常用于处理频繁触发的事件,例如滚动事件、窗口大小调整、按键输入、鼠标移动等。 在没有节流的情况下,某些事件会在很短的时间内被触发多次,可能导致性能问题或不必要的资源消耗。例如,用户在滚动页面时,每次滚动都会触发一个事件,如果不加以控制,可能会导致浏览器频繁地执行相关回调函数,影响性能。 节流函数的工作原理 节流函数通过限制函数的执行频率来减少不必要的调用。它可以让函数在规定时间内只能执行一次,忽略其他调用,直到指定时间间隔结束才允许下一次执行。 例如 假设我们有一个需要响应滚动事件的回调函数,如果没有节流,用户每次滚动页面都会触发该函数,导致函数被执行非常频繁。通过节流,我们可以确保函数在一定时间内(比如每 200 毫秒)最多执行一次。 节流与防抖的区别 节流(Throttling):限制函数执行的频率。即在指定的时间间隔内,只允许函数执行一次。 防抖(Debouncing):限制函数的执行时机,直到事件停止触发一定时间后才执行。 节流函数的常见实现 通常通过 setTimeout 或 requestAnim.... 节流函数 go
修改yarn.lock文件里registry.nlark.com 改为 registry.npmmirror.com yarn 出现 【error Error: getaddrinfo ENOTFOUND registry.nlark.com at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:72:26) yarn
堆(Heap) 是一种特殊的完全二叉树,具有以下特点:每个节点的值都与其子节点的值存在某种特定的关系,通常有两种类型的堆:最大堆和最小堆。 最大堆(Max-Heap):每个节点的值大于或等于其子节点的值,堆顶元素是所有元素中的最大值。 最小堆(Min-Heap):每个节点的值小于或等于其子节点的值,堆顶元素是所有元素中的最小值。 1. 堆的基本特性 完全二叉树:堆是完全二叉树,这意味着树的每一层都被填满,除最底层外,最底层的节点按从左到右的顺序排列。 堆序性质: 最大堆:对于每个节点 i,都有 A[i] >= A[2i + 1] 且 A[i] >= A[2i + 2](子节点的值小于等于父节点的值)。 最小堆:对于每个节点 i,都有 A[i] <= A[2i + 1] 且 A[i] <= A[2i + 2](子节点的值大于等于父节点的值)。 2. 堆的常见操作 堆提供了以下几种常见的操作: 1. 插入(Insert) 将一个新元素插入堆中,插入后需要进行“上浮”操作(heapify-up),以确保堆序性质得以保持。 对于最大堆,插入元素时,需要与父节点比较,如.... 堆 数据结构
栈(Stack) 是一种特殊的线性数据结构,它遵循“后进先出”(LIFO, Last In First Out)原则,即最后插入的元素最先被删除。栈的操作仅限于一端(栈顶),所有的插入(入栈)和删除(出栈)操作都在栈顶进行。 1. 栈的基本操作 栈支持以下几种常见操作: 入栈(Push): 将一个元素添加到栈顶。 例如:Push(x) 将元素 x 插入到栈顶。 出栈(Pop): 从栈顶移除一个元素,并返回该元素。 例如:Pop() 移除并返回栈顶元素。 查看栈顶元素(Peek 或 Top): 返回栈顶的元素,但不删除它。 例如:Peek() 或 Top() 返回栈顶元素,但不出栈。 栈是否为空(isEmpty): 检查栈是否为空。 例如:isEmpty() 如果栈为空返回 true,否则返回 false。 栈的大小(Size): 返回栈中元素的个数。 例如:Size() 返回栈的元素个数。 2. 栈的应用 栈是计算机科学中非常重要的数据结构,它广泛应用于以下几种场景: 表达式求值: 中缀表达式转后缀(逆波兰表示法)和计算后缀表达式时使用栈。 函数调用: 栈用于存储函数调用.... 栈 数据结构
线性表(Linear List)是数据结构中最基础的一类,指的是由一系列相同类型的数据元素组成的集合,且每个元素有一个明确的前驱和后继元素。线性表的元素之间存在线性关系(顺序关系),因此可以通过位置或顺序来访问元素。 线性表是很多其他数据结构的基础,它是顺序存储和链式存储的集合。 1. 线性表的基本定义 线性表(Linear List)是一个线性结构,它包含以下特点: 元素之间有顺序关系:每个元素都有唯一的位置(除第一个和最后一个元素外,每个元素都有前驱和后继)。 线性关系:线性表中的元素可以通过顺序(索引)来访问。 2. 线性表的分类 线性表主要有两种常见的存储方式: 顺序存储结构(顺序表) 链式存储结构(链表) 顺序存储结构(顺序表) 顺序表使用一块连续的内存空间来存储线性表的元素,元素在内存中按顺序排列。 优点:存储密度高,访问速度快,支持通过索引直接访问元素。 缺点:插入和删除操作效率较低,特别是在中间插入或删除元素时需要移动大量元素。 链式存储结构(链表) 链表是由一系列节点构成的,每个节点包含数据部分和指向下一个节点的指针。节点在内存中并不需要连续存储。 优点:插入和删除操.... 线性表 数据结构
单向链表和双向链表是链式数据结构的两种主要形式,它们之间有几个关键的区别和各自的优缺点。下面我们将对这两者进行对比,以帮助你更好地理解它们的特点和应用场景。 1. 结构对比 单向链表: 每个节点只有一个指向下一个节点的指针(next)。 链表中的节点只能单向地遍历。 每个节点只知道如何访问下一个节点,不能反向访问。 双向链表: 每个节点有两个指针,一个指向下一个节点(next),另一个指向前一个节点(prev)。 可以在链表中进行双向遍历,从头节点到尾节点,或者从尾节点到头节点。 每个节点不仅知道如何访问下一个节点,还能访问到前一个节点,支持更灵活的操作。 2. 内存占用 单向链表:每个节点需要存储一个数据项和一个指向下一个节点的指针,因此每个节点只占用较少的内存。 双向链表:每个节点需要存储一个数据项、一个指向下一个节点的指针和一个指向前一个节点的指针,因此每个节点占用的内存是单向链表节点的两倍。 3. 插入与删除操作 单向链表: 插入操作:在头部插入节点的时间复杂度是 O(1),但在尾部插入时需要遍历到链表的尾部,时间复杂度为 O(n)。 删除操作:删除头节点是 O(1),但.... 单向链表和双向链表比较 数据结构
单向链表(Singly Linked List)是一种链式数据结构,其中的每个节点包含两个部分:数据和指向下一个节点的指针。单向链表中的每个节点只能向前指向下一个节点,无法向后访问,意味着只能从链表的头部开始遍历,直到最后一个节点。 单向链表的结构: 单向链表的节点通常包含以下两个部分: 数据域(data):存储节点的数据。 指向下一个节点的指针(next):指向链表中的下一个节点。 单向链表的操作: 插入操作: 在链表的头部插入节点。 在链表的尾部插入节点。 在链表的中间插入节点。 删除操作: 删除头节点。 删除尾节点(需要遍历整个链表)。 删除中间的某个节点。 查找操作: 查找指定数据的节点。 单向链表的优势: 节省空间:每个节点只需要一个指针来指向下一个节点,内存消耗比双向链表少。 结构简单:因为只需要一个指针来连接节点,代码实现相对简单。 单向链表的缺点: 只能单向遍历:只能从头节点开始,向后遍历链表,无法进行反向遍历。 删除操作较麻烦:如果想删除一个中间节点,需要访问该节点的前一个节点(通常从头节点开始遍历,找到该节点的前一个节点)。 单向链表的示意图: Head →.... 单向链表 数据结构
双向链表(Doubly Linked List)是一种链式数据结构,与单向链表不同,双向链表中的每个节点不仅包含指向下一个节点的指针(即 next),还包含指向前一个节点的指针(即 prev)。这种结构使得在链表中进行双向遍历更加方便。 双向链表的结构: 一个双向链表的节点通常包含以下三个部分: 数据域(data):存储节点的实际数据。 指向下一个节点的指针(next):指向链表中的下一个节点。 指向前一个节点的指针(prev):指向链表中的前一个节点。 双向链表的优势: 双向遍历:可以从头到尾遍历,也可以从尾到头遍历。 删除操作:删除节点时不需要从头遍历,能够直接通过 prev 指针找到前一个节点,相对单向链表效率更高。 双向链表的操作: 插入操作: 在链表的头部插入节点。 在链表的尾部插入节点。 在链表的中间插入节点。 删除操作: 删除头节点。 删除尾节点。 删除中间的某个节点。 查找操作: 查找指定数据的节点。 双向链表的示意图: Head <-> Node1 <-> Node2 <-> Node3 <-> Tail 在这.... 数据结构-双向链表 数据结构
// Bridge link multiply channels into one channel. // Play: https://go.dev/play/p/qmWSy1NVF-Y func (c *Channel[T]) Bridge(ctx context.Context, chanStream <-chan <-chan T) <-chan T { valStream := make(chan T) go func() { defer close(valStream) for { var stream <-chan T select { case maybeStream, ok := <-chanStream: if !ok { return } stream = maybeStream case <-ctx.Done(): return } for val := range c.OrDone(ctx, stream) { select { case valStream <- val: case <-ctx.Done(): }.... lancet concurrency Bridge lancet
// FanIn merge multiple channels into one channel. // Play: https://go.dev/play/p/2VYFMexEvTm func (c *Channel[T]) FanIn(ctx context.Context, channels ...<-chan T) <-chan T { out := make(chan T) go func() { var wg sync.WaitGroup wg.Add(len(channels)) for _, c := range channels { go func(c <-chan T) { defer wg.Done() for v := range c { select { case <-ctx.Done(): return case out <- v: } } }(c) } wg.Wait() close(out) }() return out } 你提供的 FanIn 函数用于将多个输入通道(channels)合并成一个输出通道(out)。这.... 有更新! lancet concurrency FanIn lancet
// Tee split one chanel into two channels, until cancel the context. // Play: https://go.dev/play/p/3TQPKnCirrP func (c *Channel[T]) Tee(ctx context.Context, in <-chan T) (<-chan T, <-chan T) { out1 := make(chan T) out2 := make(chan T) go func() { defer close(out1) defer close(out2) for val := range c.OrDone(ctx, in) { var out1, out2 = out1, out2 for i := 0; i < 2; i++ { select { case <-ctx.Done(): case out1 <- val: out1 = nil case out2 <- val: out2 = nil } } } }() return ou.... lancet concurrency Tee lancet
Go Channel 介绍 在 Go 语言中,Channel 是一种内置的数据结构,用于在多个 goroutine 之间进行通信。它是 Go 语言并发模型的核心之一,与 goroutine 配合使用,可以实现安全、有效的并发编程。 Channel 可以被看作是 goroutine 之间的管道,它提供了一种类型安全的通信方式,允许一个 goroutine 发送数据,另一个 goroutine 接收数据。Go 语言通过这种机制简化了并发编程的复杂性,避免了传统多线程编程中常见的锁(mutex)和共享变量的复杂性。 Channel 的基本特性 类型安全: 每个 Channel 都有一个指定的数据类型,只有相同类型的数据才能被传输。 同步机制: Channel 实现了同步机制,当一个 goroutine 向 Channel 发送数据时,发送操作会阻塞,直到有另一个 goroutine 从 Channel 中接收数据;反之亦然。也就是说,Channel 默认是同步的。 无锁通信: Go 的 Channel 提供了无锁的通信方式,避免了显式使用锁(如 mutex)来保证数据安全。 可以缓冲(.... 有更新! 认识协程间通讯的神器channel chan
import ( "context" "fmt" "github.com/duke-git/lancet/v2/concurrency" ) func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := concurrency.NewChannel[int]() genVals := func() <-chan <-chan int { out := make(chan (<-chan int)) go func() { defer close(out) for i := 1; i <= 5; i++ { stream := make(chan int, 1) stream <- i close(stream) out <- stream } }() return out } for v := range c.Bridge(ctx, genVals()) { fmt.Println(v) } // Output: // 1 // 2 .... lacet concurrency Bridge示例 lancet
带缓冲的通道相较于直接传递切片,在某些情况下可以更高效,主要体现在以下几个方面: 1. 避免内存拷贝 在 Go 中,切片是引用类型,传递切片本身只是传递一个指向底层数组的指针。如果切片的大小很大,或者是多个 goroutine 同时处理不同部分的数据,传递切片可能会导致额外的内存开销。例如,多个 goroutine 如果对同一个切片进行操作,可能会产生竞态条件或者需要同步,额外的内存拷贝和同步开销可能导致性能下降。 带缓冲通道的优势: 带缓冲的通道通过本身的缓冲区(一个固定大小的内存区域)存储数据,避免了在多个 goroutine 之间传递大量切片时需要复制数据的问题。数据只会在需要时被传递,这样可以降低内存开销。 2. 减少内存分配和垃圾回收负担 当使用切片时,如果每次都创建新的切片或者修改切片内容,会增加内存分配和垃圾回收的负担,尤其是在传递大型切片或者切片内容非常频繁的情况下。 带缓冲通道的优势: 带缓冲的通道通常会在创建时就分配一定的内存空间,通道的数据存储在这个缓冲区中,因此在数据传递时不会频繁分配内存。通道的缓冲区会自动管理内存的使用,减少了手动分配和回收内存的次数。 由于.... 相较于直接传递切片,带缓冲的通道在什么情况可以更高效 chan
在 Go 中,使用带缓冲的通道(buffered channel)代替切片传参是一种常见的优化策略,尤其是在并发编程中,可以避免直接传递切片可能带来的性能问题或内存使用问题。带缓冲的通道能够在不阻塞发送方的情况下存储一定数量的数据,使得多个协程可以并发地进行数据传输。 使用带缓冲的通道代替切片传参 1. 为什么使用带缓冲的通道代替切片传参? 并发性:使用带缓冲的通道可以提高并发性,尤其是在并发任务较多时,能够避免频繁的内存拷贝,且通道能够控制数据的传输。 内存效率:切片传递通常会涉及内存的拷贝或者引用传递,如果数据量较大,使用切片传参可能会增加内存使用。而使用带缓冲的通道,数据可以被按需传输,避免不必要的内存拷贝。 灵活性和同步:带缓冲的通道可以作为一个同步机制,提供更细粒度的控制,而不仅仅是一个数据传递工具。 2. 带缓冲的通道的工作原理 带缓冲的通道与普通通道的不同之处在于,带缓冲的通道允许在没有接收方的情况下先存储一定量的数据。发送数据不会立即阻塞,直到缓冲区已满。接收方则需要从通道中取出数据。 make(chan T, n) 创建一个缓冲区大小为 n 的通道。 发送数据到通道时.... 使用带缓冲的通道(buffered channel)代替切片传参 chan
// Or read one or more channels into one channel, will close when any readin channel is closed. // Play: https://go.dev/play/p/Wqz9rwioPww func (c *Channel[T]) Or(channels ...<-chan T) <-chan T { switch len(channels) { case 0: return nil case 1: return channels[0] } orDone := make(chan T) go func() { defer close(orDone) switch len(channels) { case 2: select { case <-channels[0]: case <-channels[1]: } default: select { case <-channels[0]: case <-channels[1]: case <-channels[2.... lancet concurrency Or(channels ...<-chan T) <-chan T lancet
在 Go 语言中,使用 range 遍历通道(channel)是一种常见的方式,用于接收通道中的数据。range 会自动处理通道的接收操作,直到通道关闭并且所有数据都被接收完为止。 range 遍历通道的基本用法 当你使用 range 遍历一个通道时,Go 会从通道中不断接收数据,直到通道关闭并且数据接收完毕。重要的一点是,只有在通道关闭后,range 才会停止接收数据。 示例 1:基本的通道遍历 下面的示例展示了如何使用 range 遍历一个通道,接收数据直到通道关闭: package main import "fmt" func main() { // 创建一个带缓冲区的通道 ch := make(chan int, 3) // 向通道发送数据 ch <- 1 ch <- 2 ch <- 3 // 关闭通道 close(ch) // 使用 range 遍历通道 for v := range ch { fmt.Println("接收到的值:", v) } } 输出: 接收到的值: 1 接收到的值: 2 接收到的值: 3 解释: 我们首先创建一个带缓冲区的通道 c.... 用range遍历channel chan
**DDoS(Distributed Denial of Service,分布式拒绝服务攻击)**是一种常见的网络攻击方式,其目的是通过大量的请求或数据流量,使目标系统(如网站、服务器、网络等)资源耗尽,无法正常响应合法用户的请求,从而导致服务中断、系统崩溃或者严重的延迟。 DDoS 攻击的工作原理 DDoS 攻击与传统的 DoS(Denial of Service)攻击的主要区别在于,DoS 攻击是由单一攻击源发起,而 DDoS 攻击则是通过多个分布式的计算机(称为“僵尸网络”或“botnet”)同时发起攻击,向目标系统发送大量的请求或数据流量,造成极大的压力。 攻击者控制大量计算机:攻击者通过恶意软件感染大量计算机,使它们成为“僵尸”或“机器人”(bot),这些被控制的计算机被称为“botnet”。 向目标发送大量请求:攻击者指挥这些受感染的计算机向目标系统发送大量的无用请求或数据流量。这些请求通常会消耗目标系统的带宽、计算能力、存储资源或其他关键资源。 资源耗尽:当攻击的流量或请求量超过目标系统的处理能力时,系统的资源(如CPU、内存、带宽等)会被耗尽,导致系统无法正常处理合法用.... DDoS 攻击 DDos
在 Go 语言中,gin.Context 和 context.Context 是两个不同的类型,它们分别属于不同的包(gin 和 context)。gin.Context 是 Gin 框架中的上下文类型,它包含了许多与 HTTP 请求相关的功能,如请求参数、请求头、请求方法、响应写入等。而 context.Context 是标准库中的上下文类型,用于跨 API 边界传递上下文信息,特别是用于处理超时、取消信号和传递请求范围内的值。 有时候,你可能需要将 gin.Context 中的一些键值对拷贝到 context.Context 中,尤其是在将 gin.Context 中的信息传递到其他层(如数据库、缓存操作或 goroutine 中)时,context.Context 提供的值传递能力很有用。 方法:从 gin.Context 拷贝键值对到 context.Context 在 gin.Context 中存储的键值对可以通过 gin.Context 的 Get 方法获取,而在 context.Context 中,你可以使用 context.WithValue 函数将键值对存储到新的上下.... 从gin.Context拷贝键值对到context.Context gin
在 Go 语言中,var _ time.Time 这样的写法其实并不常见,它的作用和含义需要从 Go 语言的类型系统和变量声明的角度来理解。 解释 var _ time.Time 这行代码声明了一个名为 _ 的变量,并且这个变量的类型是 time.Time。但是,_ 并不是一个有效的变量名,而是一个 空标识符(Blank Identifier)。 空标识符 _ 的作用 Go 中的 _ 被称为空标识符(Blank Identifier),它用于忽略某些值。任何赋值给 _ 的值都会被丢弃,不会赋给一个实际的变量。所以,_ 的作用就是告诉编译器或其他开发者:“我知道这里有个值,但我并不关心它。” 在你的代码中,var _ time.Time 声明了一个类型为 time.Time 的空标识符 _,但是这个变量不会被实际使用或存储。可以说,_ 在这里仅仅是一个类型声明的占位符。 使用场景 这种写法通常在以下几种情况中使用: 测试类型实现接口: 在 Go 中,你可以使用类型实现接口。为了测试某个类型是否实现了某个接口,你可以使用 _ 来尝试赋值,如果类型实现了接口,编译器不会报错;如果没有实现接.... var _ time.Time go