
本文深入探讨了在go语言中如何以惯用的方式预分配和填充包含指针的切片。通过分析`make`函数对切片长度和容量的影响,以及`append`操作的行为,文章指出了常见的误区。我们提供了两种主要的解决方案:一是通过直接索引来填充已预设长度的切片,二是利用`make`函数预设容量并结合`append`操作来高效构建切片。
在Go语言中,切片(slice)是构建动态数组的强大工具。然而,当涉及到预分配并填充包含指针的切片时,如果不理解make函数和append操作的底层机制,可能会导致非预期的结果。本文将详细阐述Go语言中预分配和填充切片的惯用方法。
理解make与append的行为
在Go中,make函数用于创建切片、映射或通道。对于切片,它有以下两种常用形式:
- make([]Type, length): 创建一个长度为length的切片,其所有元素都会被初始化为Type的零值。对于指针类型*Type,零值是nil。切片的容量(capacity)默认与长度相等。
- make([]Type, length, capacity): 创建一个长度为length,容量为capacity的切片。同样,所有元素会被初始化为Type的零值。
append函数用于向切片追加元素。它总是将新元素添加到切片的末尾,并返回一个新的切片(如果底层数组需要重新分配)。append操作会增加切片的长度,但不会改变已存在元素的索引或值。
常见误区示例
考虑以下代码,尝试预分配一个包含5个*UselessStruct指针的切片,然后使用append填充:
立即学习“go语言免费学习笔记(深入)”;
package main
import "fmt"
type UselessStruct struct {
a int
b int
}
func main() {
mySlice := make([]*UselessStruct, 5) // 长度为5,元素为5个nil指针
for i := 0; i != 5; i++ {
mySlice = append(mySlice, &UselessStruct{}) // 追加新元素
}
fmt.Println(mySlice)
}这段代码的输出是:[
如果切片存储的是结构体值而非指针,情况类似:
package main
import "fmt"
type UselessStruct struct {
a int
b int
}
func main() {
mySlice := make([]UselessStruct, 5) // 长度为5,元素为5个零值UselessStruct
for i := 0; i != 5; i++ {
mySlice = append(mySlice, UselessStruct{}) // 追加新元素
}
fmt.Println(mySlice)
}输出将是:[{0 0} {0 0} {0 0} {0 0} {0 0} {0 0} {0 0} {0 0} {0 0} {0 0}]。 同样,前5个元素是零值结构体,后续追加的才是新的结构体。
这表明,当使用make([]Type, length)创建一个具有非零长度的切片时,切片已经包含了length个元素(零值)。append操作只会在此基础上增加新元素,而不会覆盖已存在的元素。
惯用解决方案
Go语言提供了两种惯用的方法来正确地预分配和填充切片,具体取决于你的需求:
方案一:直接索引填充(适用于已知固定长度)
当你事先知道切片的最终长度,并且希望填充所有元素时,最直接和惯用的方法是使用make([]Type, length)创建切片,然后通过索引直接为每个位置赋值。
package main
import "fmt"
type UselessStruct struct {
a int
b int
}
func main() {
const size = 5
mySlice := make([]*UselessStruct, size) // 创建一个长度为5的切片,元素为nil指针
// 通过索引直接填充每个位置
for i := range mySlice {
mySlice[i] = new(UselessStruct) // 为每个位置分配并赋值一个新的UselessStruct指针
// 或者使用 &UselessStruct{} 也可以
// mySlice[i] = &UselessStruct{}
}
fmt.Println(mySlice)
// 预期输出:[0xc0... 0xc0... 0xc0... 0xc0... 0xc0...] (5个不同的指针)
}在这个例子中:
- make([]*UselessStruct, size)创建了一个长度为size的切片,其中包含size个nil指针。
- for i := range mySlice遍历切片的索引。
- mySlice[i] = new(UselessStruct)在每个索引位置上创建了一个新的UselessStruct实例,并将其地址赋值给mySlice[i]。new(UselessStruct)和&UselessStruct{}都用于创建结构体实例的指针,它们在大多数情况下是等效的。
这种方法简洁明了,直接表达了“我需要一个固定大小的切片,并用特定值填充它”的意图。
方案二:预设容量与append结合(适用于动态增长但需优化性能)
当你需要动态地向切片添加元素,但又希望通过预分配内存来避免频繁的底层数组重新分配(这会带来性能开销)时,可以使用make([]Type, 0, capacity)来创建切片。这会创建一个长度为0但预留了capacity空间(底层数组)的切片。
package main
import "fmt"
type UselessStruct struct {
a int
b int
}
func main() {
const capacity = 5
// 创建一个长度为0,但容量为5的切片
mySlice := make([]*UselessStruct, 0, capacity)
// 使用append追加元素
for i := 0; i < capacity; i++ {
mySlice = append(mySlice, &UselessStruct{}) // 追加新的UselessStruct指针
}
fmt.Println(mySlice)
// 预期输出:[0xc0... 0xc0... 0xc0... 0xc0... 0xc0...] (5个不同的指针)
}在这个例子中:
- make([]*UselessStruct, 0, capacity)创建了一个空切片,但底层数组已分配了容纳capacity个元素的空间。
- append操作会将新元素添加到切片的末尾。由于切片有足够的容量,前capacity次append操作不会导致底层数组的重新分配,从而提高了效率。
- 每次append都会增加切片的长度。
这种方法适用于你不知道最终长度,但可以预估一个大致容量的场景,以优化性能。
总结与注意事项
- make([]Type, length):创建一个包含length个零值元素的切片。如果你想填充所有这些元素,应该使用索引mySlice[i] = value来赋值。
- make([]Type, 0, capacity):创建一个空切片,但预留了capacity的内存空间。如果你打算通过append动态添加元素,并希望优化性能,这是首选方式。
- append操作:append总是将元素添加到切片的末尾,并增加切片的长度。它不会覆盖或替换切片中已存在的元素。
- 指针类型与值类型:无论是切片指针还是切片值,上述原则都适用。对于指针切片,你通常会分配新的对象并获取其地址(如new(Type)或&Type{});对于值切片,你直接提供值即可。
理解这些基本概念是编写高效、惯用Go代码的关键。选择哪种方法取决于你的具体需求:如果长度固定且已知,直接索引填充更简洁;如果长度动态且需要性能优化,预设容量并结合append是更好的选择。










