gocookbook icon indicating copy to clipboard operation
gocookbook copied to clipboard

go cook book

Results 90 gocookbook issues
Sort by recently updated
recently updated
newest added

### 算法思想 与令牌桶是“反向”的算法,当有请求到来时先放到木桶中,worker以固定的速度从木桶中取出请求进行相应。如果木桶已经满了,直接返回请求频率超限的错误码或者页面。 ![](https://cdn.learnku.com/uploads/images/202012/18/6964/CLZ3Nmzaat.jpeg!large) ### 适用场景 流量最均匀的限流方式,一般用于流量“整形”,例如保护数据库的限流。先把对数据库的访问加入到木桶中,worker再以db能够承受的qps从木桶中取出请求,去访问数据库。不太适合电商抢购和微博出现热点事件等场景的限流。 ### go语言实现 ``` // 漏桶 // 一个固定大小的桶,请求按照固定的速率流出 // 如果桶是空的,不需要流出请求 // 请求数大于桶的容量,则抛弃多余请求 type LeakyBucket struct { rate float64 // 每秒固定流出速率 capacity float64 // 桶的容量...

在并发编程里,`sync.WaitGroup`并发原语的使用频率非常高,它经常用于协同等待的场景:`goroutine` A 在检查点(checkpoint)等待一组执行任务的 worker `goroutine` 全部完成,如果在执行任务的这些 `goroutine` 还没全部完成, `goroutine` A 就会阻塞在检查点,直到所有woker `goroutine` 都完成后才能继续执行。 如果在woker `goroutine`的执行过程中遇到错误并想要处理该怎么办?`WaitGroup`并没有提供传播错误的功能,遇到这种场景我们改怎么办?`Go`语言在扩展库的提供了`ErrorGroup`并发原语正好适合在这种场景下使用,它在`WaitGroup`的基础上还提供了,**错误传播以及上下文取消的功能**。 `Go`扩展库通过`errorgroup.Group`提供`ErrorGroup`原语的功能,它有三个方法可调用: ```go func WithContext(ctx context.Context) (*Group, context.Context) func (g *Group) Go(f func() error) func (g...

信号量是并发编程中常见的一种同步机制,在需要控制访问资源的进程数量时就会用到信号量,维基百科对信号量的解释如下。 > 信号量的概念是计算机科学家 **Dijkstra** (Dijkstra算法的发明者)提出来的,广泛应用在不同的操作系统中。系统中,会给每一个进程一个信号量,代表每个进程当前的状态,未得到控制权的进程,会在特定的地方被迫停下来,等待可以继续进行的信号到来。 > 如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。在linux系统中,二进制信号量(binary semaphore)又称[互斥锁](https://zh.m.wikipedia.org/wiki/互斥锁)(Mutex) > > 计数信号量具备两种操作动作,称为V(`signal()`)与P(`wait()`)(即部分参考书常称的“PV操作”)。V操作会增加信号标S的数值,P操作会减少它。 > > 运行方式: > > 1. 初始化信号量,给与它一个非负数的整数值。 > 2. 运行P(`wait()`),信号标S的值将被减少。企图进入[临界区](https://zh.m.wikipedia.org/wiki/臨界區段)的进程,需要先运行P(`wait()`)。当信号标S减为负值时,进程会被阻塞住,不能继续;当信号标S不为负值时,进程可以获准进入临界区。 > 3. 运行V(`signal()`),信号标S的值会被增加。结束离开[临界区](https://zh.m.wikipedia.org/wiki/臨界區段)的进程,将会运行V(`signal()`)。当信号标S不为负值时,先前被阻塞住的其他进程,将可获准进入[临界区](https://zh.m.wikipedia.org/wiki/臨界區段)。 我们一般用信号量保护一组资源,**每次获取资源时都会将信号量中的计数器减去对应的数值,在释放时重新加回来。当遇到计数器大于信号量大小时就会进入休眠等待其他线程释放信号**。如果信号量是只有0和1的二进位信号量,那么,它的 P/V 就和互斥锁的...

`SingleFlight`是Go语言`sync`扩展库提供的另一种并发原语,那么`SingleFlight`是用于解决什么问题的呢?官方文档里的解释是: >Package singleflight provides a duplicate function call suppression mechanism. > >翻译过来就是:singleflight包提供了一种抑制重复函数调用的机制。 具体到`Go`程序运行的层面来说,`SingleFlight`的作用是在处理多个`goroutine`同时调用同一个函数的时候,只让一个`goroutine`去实际调用这个函数,等到这个`goroutine`返回结果的时候,再把结果返回给其他几个同时调用了相同函数的`goroutine`,这样可以减少并发调用的数量。在实际应用中也是,它能够在一个服务中减少对下游的并发重复请求。还有一个比较常见的使用场景是用来防止缓存击穿。 `Go`扩展库里用`singleflight.Group`结构体类型提供了`SingleFlight`并发原语的功能。 `singleflight.Group`类型提供了三个方法: ```go func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared...

## 算法思想 滑动窗口算法将一个大的时间窗口分成多个小窗口,每次大窗口向后滑动一个小窗口,并保证大的窗口内流量不会超出最大值,这种实现比固定窗口的流量曲线更加平滑。 普通时间窗口有一个问题,比如窗口期内请求的上限是100,假设有100个请求集中在前1s的后100ms,100个请求集中在后1s的前100ms,其实在这200ms内就已经请求超限了,但是由于时间窗每经过1s就会重置计数,就无法识别到这种请求超限。 对于滑动时间窗口,我们可以把1ms的时间窗口划分成10个小窗口,或者想象窗口有10个时间插槽time slot, 每个time slot统计某个100ms的请求数量。每经过100ms,有一个新的time slot加入窗口,早于当前时间1s的time slot出窗口。窗口内最多维护10个time slot。 ![图片](https://cdn.learnku.com/uploads/images/202012/07/6964/mIzAUxipBX.png) ## 面临的问题 滑动窗口算法是固定窗口的一种改进,但从根本上并没有真正解决固定窗口算法的临界突发流量问题 ## 代码实现 主要就是实现[滑动窗口算法](https://www.zhihu.com/question/314669016),不过滑动窗口算法一般是找出数组中连续k个元素的最大值,这里是已知最大值n (就是请求上限)如果超过最大值就不予通过。 可以参考Bilibili开源的kratos框架里circuit breaker用循环列表保存time slot对象的实现,他们这个实现的好处是不用频繁的创建和销毁time slot对象。下面给出一个简单的基本实现: ``` package main import ( "fmt" "sync"...

### 算法思想 计数器是一种比较简单粗暴的限流算法,其思想是在固定时间窗口内对请求进行计数,与阀值进行比较判断是否需要限流,一旦到了时间临界点,将计数器清零。 ![](https://cdn.learnku.com/uploads/images/202012/07/6964/VImEj0KbQz.jpeg!large) ### 面临的问题 计数器算法存在“时间临界点”缺陷。比如每一分钟限制100个请求,可以在00:00:00-00:00:58秒里面都没有请求,在00:00:59瞬间发送100个请求,这个对于计数器算法来是允许的,然后在00:01:00再次发送100个请求,意味着在短短1s内发送了200个请求,如果量更大呢,系统可能会承受不住瞬间流量,导致系统崩溃。(如下图所示) ![图片](https://cdn.learnku.com/uploads/images/202012/07/6964/KopA1OVk5q.png!large) 所以计数器算法实现限流的问题是没有办法应对突发流量。 ### Go代码实现 ``` type LimitRate struct { rate int //阀值 begin time.Time //计数开始时间 cycle time.Duration //计数周期 count int //收到的请求数 lock sync.Mutex...

Cond 是为等待 / 通知场景下的并发问题提供支持的。它提供了条件变量的三个基本方法 Signal、Broadcast 和 Wait,为并发的 goroutine 提供等待 / 通知机制。在实践中,处理等待 / 通知的场景时,我们常常会使用 Channel 替换 Cond,因为 Channel 类型使用起来更简洁,而且不容易出错。但是对于需要重复调用 Broadcast 的场景,比如 [Kubernetes 的调度队列ScheduleQueue](https://github.com/kubernetes/kubernetes/blob/0599ca2bcfcae7d702f95284f3c2e2c2978c7772/pkg/scheduler/internal/queue/scheduling_queue.go)就是依赖sync.Cond实现的,具体怎么实现的可以看看这篇文章:[Kubernetes调度队列对sync.Cond的使用](https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzU2MA==&mid=2247486447&idx=1&sn=6baa861b54380d2b8e637cbc3b25259b&chksm=fa80da78cdf7536e241d9e9b6aa4fb41326ce22d7eca56fe7670525169879ee5ef41a75fd9ef&token=751471806&lang=zh_CN#rd)。每次往队列中成功增加了元素后就需要调用 Broadcast 通知所有的等待者,使用 Cond 就再合适不过了。使用 Cond 之所以容易出错,就是 Wait 调用需要加锁,以及被唤醒后一定要检查条件是否真的已经满足。...

平时实际应用时不见得非得用Chan替代sync.Mutex,但是用Chan除了实现互斥锁的功能外,还能扩充出`TryLocK`和`LockTimeout`这些扩展功能,主要是面试的时候有可能被问到。 要想使用 chan 实现互斥锁,至少有两种方式。一种方式是先初始化一个 capacity 等于 1 的 Buffered Channel,然后再放入一个元素。这个元素就代表锁,谁取得了这个元素,就相当于获取了这把锁。另一种方式是,先初始化一个 capacity 等于 1 的 Channel,它的“空槽”代表锁,谁能成功地把元素发送到这个 Channel,谁就获取了这把锁。 ``` // 使用chan实现互斥锁 type Mutex struct { ch chan struct{} } // 使用锁需要初始化 func...

今天继续关于`Go`开发经验的分享,这次的主题是关于`Go`的交叉编译和条件编译,伴随着我对自己打不过、惹不起的壕同事小张的碎碎念文字。 ## 交叉编译 **交叉编译是用来在一个平台上生成另一个平台的可执行程序**。比如我工作开发时用的Mac,系统内核是`darwin`,小张用的是外星人,系统内核是`windows` (小张明显比我有钱,我的Mac是公司是发的,人家的外星人是为打游戏自己买的)。 那么假如我编写的代码依赖了系统底层平台或处理器架构特性的`Go`包时,比如说我上周在文章《[Go服务迁到K8s后老抽风重启? 记一次完整的线上问题解决过程](https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzU2MA==&mid=2247485907&idx=1&sn=76e24be0ac7766ec02be149702f13347&chksm=fa80d844cdf75152c054cda3f3d9040ed041986ea0afdecd52a6dd217979dcc987bf44f90f44&token=2095836062&lang=zh_CN#rd)》里写的,为了把`Go`运行时的`panic`错误重定向到日志文件,我用了`syscall.Dup2`这个函数把标准错误原来的文件描述符替换成了自己指定的日志文件的描述符。`syscall.Dup2`是`Go`语言在类`Unix`系统,`X86_64`架构下才有的函数库,在Mac系统上、各种服务器环境上编译都没有问题,但是唯独在像小张这样不用办公电脑的土豪们用的`Windows`系统上编译不过去。 所以在上篇文章说的那个为了追踪在`Kubernetes`上服务老重启的问题,用`syscall.Dup2`重定向标准输出的解决方案是有副作用的,我贴一下之前这个功能的代码。 ```go func RewriteStderrFile() error { if runtime.GOOS == "windows" { return nil } ...... file, err := os.OpenFile(stdErrFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err...

``` package main import ( "fmt" "os" "runtime" "syscall" ) const stdErrFile = "/tmp/go-app1-stderr.log" var stdErrFileHandler *os.File func RewriteStderrFile() error { if runtime.GOOS == "windows" { return nil } file,...