# Slice踩坑日记
# 问题
今天遇到一个这样的问题。背景是这样的,项目需要维护一个多账号的openai Client,我选取了map[string][]*openai.Client这种结构来实现。map用来区分不同标识,slice用来维护同一个标识下面的多个openai Client。代码如下:
// openai config
type OpenAIConfig struct {
Secret map[string][]string `json:"secret"`
Proxy string `json:"proxy"`
}
// packaging openai client
type OpenAIClient struct {
client map[string][]*openai.Client // ori openai client
counter map[string]*uint64 // dispatch counter
}
// new packaging openAI Client
func NewOpenAIClient(c OpenAIConfig) *OpenAIClient {
client := &OpenAIClient{
client: make(map[string][]*openai.Client),
counter: make(map[string]*uint64),
}
for k, sli := range c.Secret {
client.client[k] = make([]*openai.Client, len(sli))
for _, secret := range sli {
cli := openaiClient(c.Proxy, secret)
client.client[k] = append(client.client[k], cli)
}
}
return client
}
// new ori openai Client
func openaiClient(proxy, secret string) *openai.Client {
...
}
代码实现很简单,但是在运行过程中,一直报如下错误:
16:50:37 app | panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x30 pc=0x13cfac9]
goroutine 101 [running]:
16:50:37 app | github.com/sashabaranov/go-openai.(*Client).fullURL(0xc000525af0?, {0x1b9607b, 0x11}, {0xc000525bd8
16:50:37 app | , 0x1, 0x10e3386?
16:50:37 app | })
/Users/fbbyqsyea/go/pkg/mod/github.com/sashabaranov/go-openai@v1.9.4/client.go
16:50:37 app | :105 +0x49
github.com/sashabaranov/go-openai.(*Client).newStreamRequest(
错误显示 内存地址不可用或空指针引用,debug之后发现是在初始化Slice的时候打算指定Slice的容量,但是由于学艺不精指定成了Slice的长度。导致append之后Slice里面有两个空的nil。所以报错了。在这里我们来一起重新简单学习一下golang slice。错误代码如下:
client.client[k] = make([]*openai.Client, len(sli))
# 学习
# 简介
切片是 Go 语言中重要的数据结构之一,它提供了对连续元素序列的动态长度访问。切片是基于数组的封装,具有更灵活的长度和容量。
# 创建
# 声明一个切片变量
var slice []int
# 基于字面量创建切片
slice := []int{1, 2, 3, 4, 5}
# 使用make函数创建
slice := make([]int, 5) // 长度为5,容量为5
slice := make([]int, 0, 10) // 长度为0,容量为10
# 操作
切片支持一系列常见的操作,例如:
# 访问元素:使用索引访问切片中的元素,从0开始:
element := slice[index]
# 修改元素:通过索引可以修改切片中的元素:
slice[index] = newValue
# 切片切片:可以使用切片操作创建新的切片,提取部分元素:
newSlice := slice[startIndex:endIndex] // 提取从 startIndex 到 endIndex-1 的元素
# 切片追加:使用 append 函数向切片末尾追加元素:
slice = append(slice, element1, element2, ...)
# 切片长度和容量:使用 len 和 cap 函数获取切片的长度和容量:
- 切片长度(Length):切片的长度是指切片中当前存储的元素数量。可以使用内置的 len() 函数获取切片的长度。例如,len(slice) 返回切片 slice 的长度。
- 切片容量(Capacity):切片的容量是指底层数组中可以存储的元素数量。切片容量是在创建切片时由运行时系统自动分配的,可以使用内置的 cap() 函数获取切片的容量。例如,cap(slice) 返回切片 slice 的容量。
- 指定长度的切片会被初始化为类型对应的零值。
- 切片的容量超过后会重新分配内存。
length := len(slice)
capacity := cap(slice)
# 特性
切片的一些重要特性需要了解:
- 切片是引用类型:多个切片可以引用相同的底层数组,修改一个切片会影响到其他引用该底层数组的切片。
- 切片扩容:当追加元素时,如果底层数组容量不足,切片会自动扩容,重新分配更大的底层数组。
- 切片与数组的关系:切片是基于数组的封装,底层数组实际存储元素的地方。
- 切片的零值是 nil:未初始化的切片值为 nil,可以通过判断切片是否为 nil 来确定是否已分配底层数组。
# 示例
package main
import "fmt"
func main() {
// 创建切片
slice := []int{1, 2, 3, 4, 5}
// 访问元素
fmt.Println(slice[0]) // 输出: 1
// 修改元素
slice[0] = 10
fmt.Println(slice) // 输出: [10 2 3 4 5]
// 切片切片
newSlice := slice[1:3]
fmt.Println(newSlice) // 输出: [2 3]
// 切片追加
slice = append(slice, 6, 7)
fmt.Println(slice) // 输出: [10 2 3 4 5 6 7]
// 切片长度和容量
fmt.Println(len(slice)) // 输出: 7
fmt.Println(cap(slice)) // 输出: 8
// 切片特性
otherSlice := slice
otherSlice[0] = 20
fmt.Println(slice) // 输出: [20 2 3 4 5 6 7]
fmt.Println(otherSlice) // 输出: [20 2 3 4 5 6 7]
}
# 解答
通过学习,我们知道了go slice中Length和Capacity的区别,所以修改代码为如下即可解决问题:
client.client[k] = make([]*openai.Client, 0, len(sli))
我们需要指定的是切片容量而不是长度。需要注意的是,只有确定slice的大小后采取指定容量。而不是随便的指定容量的大小,这样容易导致内存频繁的重新分配。影响性能问题。