# 当开源项目 Issue 遇到了 DevChat - 每天5分钟玩转 GPT 编程系列(11)


## 1. 概述

没错，又有人给 [GoPool](https://github.com/devchat-ai/gopool) 项目提 issue 了：

![](a.png)

和往常一样，我借着 DevChat 提供的 GPT-4 能力来解这个 issue。今天我准备以这个 issue 为例，和大家介绍下使用 DevChat 编程的常用操作。

## 2. Bug 分析与复现

用户写了这样一段代码：

```go
package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"

	"github.com/devchat-ai/gopool"
)

func main() {
	var m sync.Map
	pool := gopool.NewGoPool(
		20,
		gopool.WithMinWorkers(3),
	)
	defer pool.Release()
	var i = 0
	for {
		i += 1
		if _, ok := m.Load(i); !ok {
			pool.AddTask(func() (interface{}, error) {
				m.Store(i, 1)
				task(i)
				m.Delete(i)
				return nil, nil
			})
		}
		if i > 20 {
			break
		}
	}
	pool.Wait()
}

func task(t int) error {
	i := rand.Intn(20)
	time.Sleep(time.Second * time.Duration(i))
	fmt.Printf("> %d -- %d \n", t, i)
	return nil
}
```

然后发现这个程序执行后会遇到这样的报错：

```bash
$ go run main.go
> 21 -- 0 
> 21 -- 0 
> 21 -- 5 
> 21 -- 0 
> 21 -- 0 
panic: runtime error: slice bounds out of range [:-4]

goroutine 52 [running]:
github.com/devchat-ai/gopool.(*goPool).adjustWorkers(0x14000108000)
        /Users/danielhu/go/pkg/mod/github.com/devchat-ai/gopool@v0.6.0/gopool.go:173 +0x1c4
created by github.com/devchat-ai/gopool.NewGoPool
        /Users/danielhu/go/pkg/mod/github.com/devchat-ai/gopool@v0.6.0/gopool.go:95 +0x3e8
```

我试了下可以稳定复现，上面这段日志就是在我本地执行的输出内容。每次执行报错的前几行可能内容不一样，不过下面抛异常的那一行是确定的，也就是 `gopool.go:173`。

好吧，那就先把这一段代码扒出来看看。第173行代码是：

```go
p.workerStack = p.workerStack[:len(p.workerStack)-removeWorkers]
```

这行代码在下面这个函数里面：

```go
// adjustWorkers adjusts the number of workers according to the number of tasks in the queue.
func (p *goPool) adjustWorkers() {
	ticker := time.NewTicker(p.adjustInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			p.cond.L.Lock()
			if len(p.taskQueue) > len(p.workers)*3/4 && len(p.workers) < p.maxWorkers {
				// Double the number of workers until it reaches the maximum
				newWorkers := min(len(p.workers)*2, p.maxWorkers) - len(p.workers)
				for i := 0; i < newWorkers; i++ {
					worker := newWorker()
					p.workers = append(p.workers, worker)
					p.workerStack = append(p.workerStack, len(p.workers)-1)
					worker.start(p, len(p.workers)-1)
				}
			} else if len(p.taskQueue) == 0 && len(p.workers) > p.minWorkers {
				// Halve the number of workers until it reaches the minimum
				removeWorkers := max((len(p.workers)-p.minWorkers)/2, p.minWorkers)
				p.workers = p.workers[:len(p.workers)-removeWorkers]
				p.workerStack = p.workerStack[:len(p.workerStack)-removeWorkers]
			}
			p.cond.L.Unlock()
		case <-p.ctx.Done():
			return
		}
	}
}
```

你也可以在 GitHub 上的[这个地址](https://github.com/devchat-ai/gopool/blob/0af60423ee5543caae18df18c04ad57dc3552a61/gopool.go#L173C4-L173C4)找到这行代码。

## 3. Bug 定位与修复

可能你对这段代码不太熟悉，感觉看起来费劲。没关系，我看起来也费劲，毕竟一来这段代码主要是 GPT 写的，二来也有些天了，我也忘得差不多了。

第173行出错的话，不难想到大概率是 removeWorkers 的值计算有问题，也就是 171 行的：

```go
removeWorkers := max((len(p.workers)-p.minWorkers)/2, p.minWorkers)
```

缩容，具体怎么缩的我和你一样记不清，不过没关系，我们知道缩容要干的事情是“当 Pool 空闲的时候，将 workers 的数量缩减到最小数量”就行。那么 `max((len(p.workers)-p.minWorkers)/2, p.minWorkers)` 这个算式的逻辑大概率就不对了。假如当前 workers 数量是9，minWorkers 值是5，这个算式的结果是 `max((9-5)/2, 5)`，也就是 `max(2, 5)`，结果是5，9-5=4，4!=5，确实不对。

但是应该怎么改呢？，于是我就把这个函数丢给 DevChat，告诉它我想要的结果，看下 DevChat 能不能将其修复：

![](1.png)

如图所示，首先我们需要将这个函数作为 Context 告诉 DevChat，然后基于这段 Context 来提问。

DevChat 说 `removeWorkers := max((len(p.workers)-p.minWorkers)/2, p.minWorkers)` 这一行是错的，应该改成 `removeWorkers := (len(p.workers) - p.minWorkers + 1) / 2`：

![](2.png)

当 DevChat 给出新代码之后，你可以用上图这种方式，通过 diff 视图来“预览” GPT 到底改了哪些行。

看起来只有一行，不过数学运算，边界考虑啥的，还是有点烧我的“CPU”。我没法一眼看出来这个新的等式对不对，于是，掰手指吧：

假设当前 workers 是 10，minWorkers 是 5，那么：

1. 第一轮 removeWorkers 应该是 `(10 - 5 + 1) / 2 = 3`，也就是说要 remove 掉 3 个 workers，剩下 7 个；
2. 第二轮 removeWorkers 应该是 `(7 - 5 + 1) / 2 = 1`，也就是说要 remove 掉 1 个 workers，剩下 6 个；
3. 第三轮 removeWorkers 应该是 `(6 - 5 + 1) / 2 = 1`，也就是说要 remove 掉 1 个 workers，剩下 5 个；

至此 workers 和 minWorkers 相等，任务完成。行吧，看起来没毛病。

## 4. 代码测试

开头那段代码重新跑一遍，发现还是报错，我的嘴巴半分钟没合拢……

细看目前这段缩容逻辑：

```go
if len(p.taskQueue) == 0 && len(p.workers) > p.minWorkers {
	// Halve the number of workers until it reaches the minimum
	removeWorkers := max((len(p.workers)-p.minWorkers)/2, pminWorkers)
	p.workers = p.workers[:len(p.workers)-removeWorkers]
	p.workerStack = p.workerStack[:len(p.workerStack)-removeWorkers]
}
```

哦，当 taskQueue 为空的时候，只是任务队列里没有新任务了，但是 workers 可能还在工作中，这时候执行缩容，相当于有一定概率 kill 掉了正在工作的 workers…… 不过这个问题是偶现的，因为缩容动作1秒才触发一次，而之前在 UT 里覆盖的场景是1秒内 workers 已经全部执行完，进入 idle 状态，所以缩容不会出错。但是恰好这次用户的 task 里面有一个 `time.Sleep(time.Second * time.Duration(i))` 的逻辑，也就是随机 Sleep。好吧，我服。

另外在 GoPool 里 worker 的 push 和 pop 逻辑是这样的：

```go
func (p *goPool) popWorker() int {
	p.lock.Lock()
	workerIndex := p.workerStack[len(p.workerStack)-1]
	p.workerStack = p.workerStack[:len(p.workerStack)-1]
	p.lock.Unlock()
	return workerIndex
}

func (p *goPool) pushWorker(workerIndex int) {
	p.lock.Lock()
	p.workerStack = append(p.workerStack, workerIndex)
	p.lock.Unlock()
	p.cond.Signal()
}
```

也就是说当有 worker 在工作的时候，workerStack 里保存的 index 会被 pop 出去，因此可以得到2个结论：

1. 当 len(workerStack) == len(workers) 的时候表示 Pool 空闲；
2. 当 Pool 繁忙时执行扩容，会导致 workerStack 中的 index 无序。（比如 [1,2,3] 这个初始 index 切片在工作中可能变成了 [1,2]，然后扩容后会变成 [1,2,4,5]，接着空闲了又变成 [1,2,4,5,3]）

复杂度又上来了一点。首先 if 的条件需要加上“空闲判断”，于是就得这样写了：

```go
if len(p.taskQueue) == 0 && len(p.workerStack) == len(p.workers) && len(p.workers) > p.minWorkers
```

接着要解决无序问题，加个排序。我也不记得排序应该调用哪个库了，另外上面这段分析我也不确定是不是一定正确，还是发给 GPT 吧：

![](7.png)

最后 DevChat 给出的新代码里这个 if 长这样：

```go
if len(p.taskQueue) == 0 && len(p.workerStack) == len(p.workers) && len(p.workers) > p.minWorkers {
	// Halve the number of workers until it reaches the minimum
	removeWorkers := (len(p.workers) - p.minWorkers + 1) / 2
	// Sort the workerStack before removing workers
	sort.Ints(p.workerStack)
	p.workers = p.workers[:len(p.workers)-removeWorkers]
	p.workerStack = p.workerStack[:len(p.workerStack)-removeWorkers]
}
```

再测一次开头那段代码，ok 了。

## 5. 文档更新

写文档还是挺让程序员头疼，写英文文档更加头疼。还好现在有 GPT 了，咱继续将需要更新的文档作为上下文，发给 DevChat：

![](3.png)

DevChat 插件提供了一个“响应内容一键复制”按钮，点一下就能将 GPT 生成的原始 Markdown 格式文本复制到剪切板，这样就能轻松地将包含代码块的文档内容插入到 `README.md` 文件里了。

## 6. 提交 Commit

别再写只有一个“update”的 commit message 了，在 DevChat 里可以直接将 git diff 内容发送给 GPT，然后让 GPT 根据当前修改总结一个 commit message 出来：

1. 执行 git add，然后将 diff 内容加入 Context：

![](4.png)

2. 执行 commit_message 命令：

![](5.png)

3. 获取 GPT 总结的 commit message，可以直接在 DevChat 页面 commit，也可以复制后手动执行 git commit：

![](6.png)

## 7. 总结

要想让 GPT 输出令人满意的答案，一定要给它精准的 Context（上下文）。如果使用 DevChat 你可以灵活地选择一段代码、几段代码或者几个源文件加入 Context，然后基于这些选中的 Context 向 GPT 提问。

最后，别忘了 DevChat 的注册地址：[www.devchat.ai](https://www.devchat.ai/?utm_medium=website&utm_source=hutao&utm_content=issue-devchat&utm_campaign=5-minutes)

