Golang中`var foo []int`与`foo := []int{}`或`foo := make([]int, 0)`的使用场景区别
Golang中var foo []int与foo := []int{}或foo := make([]int, 0)的使用场景区别
基于这个问题(及其答案):如何在Go中定义一个空切片? - Stack Overflow
我想知道为什么有一种方法可以声明一个没有底层数组支持的切片。我也不完全确定值为nil的切片与有值的切片之间有什么区别,尽管这三种方法创建的切片其长度和容量都是0。
我很难准确地用语言表达我的困惑(这就是我如此困惑的原因!),所以我希望就这个话题进行讨论,而不是在Stack Overflow上发帖,结果被踩到无人问津。
谢谢!
更多关于Golang中`var foo []int`与`foo := []int{}`或`foo := make([]int, 0)`的使用场景区别的实战教程也可以访问 https://www.itying.com/category-94-b0.html
我想知道为什么有一种声明切片的方式不需要底层数组支持。我也不完全确定值为 nil 的切片与有值的切片之间有什么区别,尽管这三种方法创建的切片其长度和容量都是 0。
切片本质上是一个包含 3 个字段的结构:
type slice[T any] struct {
data *T
len int
cap int
}
一个 nil 切片本质上是 var a = slice[int]{nil, 0, 0},但 make([]int, 0) 实际上会用一个非 nil 指针来填充数据指针。我只有在你的代码需要在 slice == nil 和 len(slice) == 0 时做不同处理的情况下才看到这有用(也许是 JSON 序列化?)。
我个人的习惯是:
-
a := make([]int, 0, capacity),其中在大多数情况下 capacity >= 4。目前,使用默认的 Go 实现,
make([]int, 0)会将数据指针设置为所有类型共享的 0 大小内存位置(即,多次调用make([]int, 0)甚至make([]string, 0)将导致切片的数据指针相同,但在应用程序的不同执行之间会有所不同)。Go 的
append函数的工作原理是将当前切片容量加倍(直到达到非常大的容量,或者如果切片容量为零,则增加到 1),因此你的前 3 次追加将导致重新分配(容量从 0 → 1,1 → 2,2 → 4),所以如果你知道容量可能超过 4,请预先设置容量。有些人称此为微优化,但我称之为不浪费资源 🤷。 -
var a []int如果初始化取决于某个条件,例如:var a []int if something { a = doSomething() } else { a = doSomethingElse() }喜欢代码“干净”的人会争辩说,
something检查应该放在它自己的函数中:a := doSomethingOrSomethingElse(something) // .... func doSomethingOrSomethingElse(something bool) { if something { return doSomething() } return doSomethingElse() }如果你愿意在任何地方都这样做,那么你可能不需要使用
var a []int这种声明切片的方式。
更多关于Golang中`var foo []int`与`foo := []int{}`或`foo := make([]int, 0)`的使用场景区别的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在Go语言中,var foo []int、foo := []int{}和foo := make([]int, 0)这三种方式虽然都创建了长度和容量为0的切片,但在底层实现和使用场景上存在关键区别。
1. var foo []int:nil切片
这种方式声明了一个nil切片,其底层数组指针为nil,长度和容量均为0。这是最节省内存的声明方式,因为不会分配任何底层数组。
var foo []int
fmt.Println(foo == nil) // true
fmt.Println(len(foo)) // 0
fmt.Println(cap(foo)) // 0
使用场景:当切片可能不会被使用时,或者作为函数的返回值表示“无数据”时。例如:
func findEvenNumbers(data []int) []int {
var result []int
for _, v := range data {
if v%2 == 0 {
result = append(result, v)
}
}
return result // 如果没有偶数,返回nil切片
}
2. foo := []int{}:空切片
这种方式创建了一个非nil的空切片,底层分配了一个零长度的数组(具体实现可能优化)。
foo := []int{}
fmt.Println(foo == nil) // false
fmt.Println(len(foo)) // 0
fmt.Println(cap(foo)) // 0
使用场景:当需要明确表示“空集合”而非“无集合”时,或者与JSON序列化相关时:
// JSON序列化时,nil切片可能被编码为null,而空切片编码为[]
var nilSlice []int
emptySlice := []int{}
json1, _ := json.Marshal(nilSlice) // "null"
json2, _ := json.Marshal(emptySlice) // "[]"
3. foo := make([]int, 0):预分配切片
使用make可以指定初始长度和容量,这里创建的是长度为0、容量为0的非nil切片。
foo := make([]int, 0)
fmt.Println(foo == nil) // false
fmt.Println(len(foo)) // 0
fmt.Println(cap(foo)) // 0
// 也可以预分配容量
bar := make([]int, 0, 10)
fmt.Println(len(bar)) // 0
fmt.Println(cap(bar)) // 10
使用场景:当你知道需要多少容量时,可以预分配以提高性能:
// 预分配容量可以减少append时的内存分配
func processItems(items []string) []string {
result := make([]string, 0, len(items))
for _, item := range items {
if isValid(item) {
result = append(result, item)
}
}
return result
}
关键区别
- nil检查:
var s1 []int // s1 == nil 为 true
s2 := []int{} // s2 == nil 为 false
s3 := make([]int, 0) // s3 == nil 为 false
- 反射信息:
var s1 []int
s2 := []int{}
fmt.Println(reflect.ValueOf(s1).IsNil()) // true
fmt.Println(reflect.ValueOf(s2).IsNil()) // false
- 序列化差异(如前所述)
性能考虑
对于大多数情况,三种方式的性能差异可以忽略。但在高频循环中,预分配容量(make([]T, 0, capacity))可以显著减少内存分配。
最佳实践建议
- 使用
var s []T当切片可能为nil状态有意义时 - 使用
s := []T{}当需要明确空集合时 - 使用
make([]T, 0, capacity)当知道大概容量时 - 在函数返回切片时,如果可能没有数据,返回nil切片通常更符合Go的惯用法
这三种方式在len()和cap()上表现一致,主要区别在于语义和特定场景下的行为。

