Golang中`var foo []int`与`foo := []int{}`或`foo := make([]int, 0)`的使用场景区别

Golang中var foo []intfoo := []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

2 回复

我想知道为什么有一种声明切片的方式不需要底层数组支持。我也不完全确定值为 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 == nillen(slice) == 0 时做不同处理的情况下才看到这有用(也许是 JSON 序列化?)。

我个人的习惯是:

  1. 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,请预先设置容量。有些人称此为微优化,但我称之为不浪费资源 🤷。

  2. 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 []intfoo := []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
}

关键区别

  1. nil检查
var s1 []int        // s1 == nil 为 true
s2 := []int{}       // s2 == nil 为 false
s3 := make([]int, 0) // s3 == nil 为 false
  1. 反射信息
var s1 []int
s2 := []int{}
fmt.Println(reflect.ValueOf(s1).IsNil())  // true
fmt.Println(reflect.ValueOf(s2).IsNil())  // false
  1. 序列化差异(如前所述)

性能考虑

对于大多数情况,三种方式的性能差异可以忽略。但在高频循环中,预分配容量(make([]T, 0, capacity))可以显著减少内存分配。

最佳实践建议

  • 使用var s []T当切片可能为nil状态有意义时
  • 使用s := []T{}当需要明确空集合时
  • 使用make([]T, 0, capacity)当知道大概容量时
  • 在函数返回切片时,如果可能没有数据,返回nil切片通常更符合Go的惯用法

这三种方式在len()cap()上表现一致,主要区别在于语义和特定场景下的行为。

回到顶部