1.问题
先说一下自己最近写一个小玩意儿遇见的问题吧,刚刚把Go学完,对切片的认知不到位,导致自己费了一段精力去解决这个问题.
func (d Ding) DingGetUsersInformation(useridListResult []dingOfficialModel.UserIdListResultByDeptId, accessToken string) (err error, usersInformationResult []dingOfficialModel.UsersInformationResult) {
var response *http.Response
//得到Token
url := fmt.Sprintf("https://oapi.dingtalk.com/topapi/v2/user/list?access_token=%s", accessToken)
var usersInformation dingOfficialModel.UsersInformationResult
for i := 0; i < len(useridListResult); i++ {
deptId := useridListResult[i].DeptId //部门id
//封装请求参数
userParams := struct {
DeptId int64 json:"dept_id"
Cursor uint8 json:"cursor"
Size uint32 json:"size"
}{
DeptId: deptId,
Cursor: 0,
//len(useridListResult[i].Result.UseridList) 为每一个部门下相应的人数
Size: uint32(len(useridListResult[i].Result.UseridList)),
}
//将请求参数转化为JSON字符串
userParamsJson, err := json.Marshal(&userParams)
if err != nil {
zap.L().Error("json.Marshal(&userParams) is failed", zap.Error(err))
return err, usersInformationResult
}
reqBody := bytes.NewBuffer([]byte(userParamsJson))
response, err = http.Post(url, "application/json", reqBody)
if err = json.NewDecoder(response.Body).Decode(&usersInformation); err != nil {
zap.L().Error("json.NewDecoder(response.Body).Decode(&useridListResult)", zap.Error(err))
return err, usersInformationResult
}
//fmt.Println("@@@@@@@@@", usersInformation)
usersInformation.DeptId = deptId
//fmt.Print(usersInformation)
usersInformationResult = append(usersInformationResult, usersInformation)
}
return err, usersInformationResult
}
各位可以自己观察上面代码会产生什么问题
上面代码中存在着一个严重的问题,导致返回的切片 usersInformationResult
中包含的都是对同一个 usersInformation
结构的引用。这意味着在循环中不断修改同一个结构,最终导致 usersInformationResult
中所有元素都相同。
要解决这个问题,需要在每次循环迭代中创建一个新的 usersInformation
结构,而不是在循环外部创建一个。这样,每个部门的数据将被单独保存,而不会被不断覆盖。
2.切片基础内容
2.1 初始化方式
(1). 变量声明
var s []int //s的值为切片的零值 即为nil
此种方式声明的切片长度和容量都为0,并且不需要内存分配
(2). 字面量
s1 := []int{} //空切片 , 需要内存分配 切片的长度为0 , 但值并不是nil
s2 := []int{1,2,3) //声明一个长度为3的切片
(3). 内置函数make()
s1 := make([]int , len , cap) //其中len为切片的初始长度,cap为切片的容量
使用make创建切片,切片中的元素均初始化为对应类型的零值.
(4).从切片或数组中切取
array := [5]int{1,2,3,4,5}
s1 := array[0:2] // 1,2
也可以从另一个切片中使用切片表达式来切取
切片表达式[low:high] 为左开右闭,并且切片表达式可以切取字符串,但是使用切片表达式切取字符串时,返回值依然是字符串,而不是切片.
完整的切片表达式:
[low:high:max]
max用于限制新生成的切片的容量 cap = max - low 并且在完整的切片表达式中只有low可以省略(默认值为0)
array := [5]int{1,2,3,4,5}
s1 := array[1:3:5] //2,3 len(s1) = 2 cap(s1) = 4
使用切片表达式切取的切片,切片与原数组或切片共享底层数组,修改切片的值,可能会影响到原数组或切片的值.
2.2 append 添加操作
s1 := make([]int , 0)
s1 = append(s1 , 1) //1
s1 = append(s1 , 2) //1 , 2
当切片容量不足时,append会先新创建一个大容量的切片,添加完新元素之后,再返回新的切片.
3.切片原理
切片的本质就是对底层数组的封装,是一个名字叫slice的结构体
3.1切片的数据结构
type slice struct {
array unsafe.Pointer
len int
cap int
}
可以看到结构体中直接包含了切片的长度(len) 和 容量(cap) 因此使用len获取切片长度和使用cap()获取切片容量的时间复杂度均为O(1)
3.2使用make创建切片
s1 := make([]int , 2 , 5)
创建完切片之后,底层会分配一个数组,数组的长度就是切片的容量.
s1 的长度为2,即可以使用s1[0]和s1[2]操作切片里面的元素,容量为5表示,后面向切片中添加元素时,除非预留容量已满,否则添加操作是不需要在重新分配内存的,使用预留的内存即可.
3.3.使用切片表达式切取数组创建切片
使用该方法创建切片时,切片将会与原数组共用一部分内存
数组从low开始以后的内存,都是为新产生的切片预留的内存,新切片和数组共享底层存储空间
array := [5]int{1,2,3,4,5}
s1 := array[1:3] //len = 2 , cap = 4
3.4 扩容
当切片的容量不足时,会先重新分配一块更大的内存,将原切片的数据拷贝到新切片之后,再返回新切片,扩容之后在将新数据追加进去.
扩容的原则:
1.当切片容量小于1024时,每一次扩容都会容量都会变为原来的两倍.
2.当切片容量大于1024时,新切片的容量将为原来的1.25倍.
例如向s1中在追加5,6,7,三个元素最终结果如下:
array := [5]int{1, 2, 3, 4, 5}
s1 := array[1:3]
s1 = append(s1, 5)
s1 = append(s1, 6)
s1 = append(s1, 7)
fmt.Println(s1, array) //[2 3 5 6 7] [1 2 3 5 6]
切片原容量为4,因此前两次追加操作不会发生扩容操作,但是因为和切片数组共享内存空间,修改切片的值,会影响到原数组中后面的两个值.
当切片容量满了之后,先分配内存,将原数据拷贝到新切片,将新切片返回,最后在执行追加操作.
3.5 切片的拷贝
切片也属于引用类型的变量,如果直接赋值,属于浅拷贝,两个切片共享同一块内存地址.修改赋值产生切片会影响到原切片中的数据
但是Go语言内置的Copy()函数可以迅速的将一个切片的数据复制到另一个切片空间中
// copy()复制切片
s1 := []int{1, 2, 3, 4, 5}
s2 := make([]int, 5, 5)
copy(s2, s1) //使用copy()函数将切片s1中的元素复制到切片s2
fmt.Println(s1) //[1 2 3 4 5]
fmt.Println(s2) //[1 2 3 4 5]
s2[0] = 1000
fmt.Println(s1) //[1 2 3 4 5]
fmt.Println(s2) //[1000 2 3 4 5]