# 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的大小后采取指定容量。而不是随便的指定容量的大小,这样容易导致内存频繁的重新分配。影响性能问题。