关于go:切片有哪些注意事项是一定要知道的呢

7次阅读

共计 4842 个字符,预计需要花费 13 分钟才能阅读完成。

1. 引言

在之前我写了一篇 切片比数组好用在哪 的文章,认真介绍了切片相比于数组的长处。但切片事实上也暗藏着一些潜在的陷阱和须要留神的细节,理解和把握切片的应用注意事项,能够防止意外的程序行为。本文将深入探讨 Go 语言切片常见的注意事项,从而可能更好得应用切片。

2. 注意事项

2.1 留神一个数组能够同时被多个切片援用

当创立一个切片时,它实际上是对一个底层数组的援用。这意味着对切片的批改会间接影响到底层数组以及其余援用该数组的切片。这种援用关系可能导致一些意想不到的后果,上面是一个示例代码来阐明这个问题:

package main

import "fmt"

func main() {array := [5]int{1, 2, 3, 4, 5}
        firstSlice := array[1:4] // 创立一个切片,援用了底层数组的索引 1 到 3 的元素
        secondSlice := array[1:3]
        fmt.Println("Original array:", firstSlice)  // 输入第一个切片 [2 3 4]
        fmt.Println("Original slice:", secondSlice) // 输入第二个切片 [2 3]

        // 批改切片的第一个元素
        firstSlice[0] = 10

        fmt.Println("Modified array:", firstSlice)  // 输入第一个切片 [10 3 4]
        fmt.Println("Modified slice:", secondSlice) // 输入第二个切片 [10 3]
}

在上述代码中,咱们创立了一个长度为 5 的数组 array 和两个援用该数组的切片 firstSlicesecondSlice。当咱们批改第一个切片的第一个元素为 10 时,底层数组的对应地位的元素也被批改了。这里导致了数组和其余援用该数组的切片的内容也会受到影响。

如果咱们有多个切片同时援用了同一个底层数组,同时咱们并不想因为对某个切片的批改,影响到另外一个切片的数据,此时咱们能够新创建一个切片,应用内置的 copy 函数来复制原切片元素的值。示例代码如下:

package main

import "fmt"

func main() {array := [5]int{1, 2, 3, 4, 5}
        slice := array[1:4]

        // 复制切片创立一个独立的底层数组
        newSlice := make([]int, len(slice))
        copy(newSlice, slice)

        fmt.Println("Original array:", array) // 输入原始数组 [1 2 3 4 5]
        fmt.Println("Original slice:", slice) // 输入初始切片 [2 3 4]
        fmt.Println("New slice:", newSlice)  // 输入新创建的切片 [2 3 4]
        
        // 批改 newSlice 的第一个元素
        newSlice[0] = 10

        fmt.Println("Modified array:", array)// 输入批改后的数组 [1 2 3 4 5]
        fmt.Println("Original slice:", slice)// 输入初始切片 [2 3 4]
        fmt.Println("New slice:", newSlice)// 输入批改后的切片 [10 3 4]
}

通过创立了一个新的切片 newSlice,它领有独立的底层数组,同时应用copy 函数复制原切片的值,咱们当初批改 newSlice 不会影响原始数组或原始切片。

2.2 留神主动扩容可能带来的性能问题

在 Go 语言中,切片的容量是指底层数组的大小,而长度是切片以后蕴含的元素数量。当切片的长度超过容量时,Go 语言会主动扩容切片。扩容操作波及到重新分配底层数组,并将原有数据复制到新的数组中。上面先通过一个示例代码,演示切片的主动扩容机制:

package main

import "fmt"

func main() {slice := make([]int, 3, 5) // 创立一个初始长度为 3,容量为 5 的切片

        fmt.Println("Initial slice:", slice)        // 输入初始切片 [0 0 0]
        fmt.Println("Length:", len(slice))          // 输入切片长度 3
        fmt.Println("Capacity:", cap(slice))        // 输入切片容量 5

        slice = append(slice, 1, 2, 3)              // 增加 3 个元素到切片,长度超过容量

        fmt.Println("After appending:", slice)      // 输入扩容后的切片 [0 0 0 1 2 3]
        fmt.Println("Length:", len(slice))          // 输入切片长度 6
        fmt.Println("Capacity:", cap(slice))        // 输入切片容量 10
}

在上述代码中,咱们应用 make 函数创立了一个初始长度为 3,容量为 5 的切片 slice。而后,咱们通过append 函数增加了 3 个元素到切片,导致切片的长度超过了容量。此时,Go 语言会主动扩容切片,创立一个新的底层数组,并将原有数据复制到新的数组中。最终,切片的长度变为 6,容量变为 10。

然而切片的主动扩容机制,其实是存在性能开销的,须要创立一个新的数组,同时将数据全副拷贝到新数组中,切片再援用新的数组。上面先通过基准测试,展现没有设置初始容量和设置了初始容量两种状况下的性能差距:

package main

import (
        "fmt"
        "testing"
)

func BenchmarkSliceAppendNoCapacity(b *testing.B) {
        for i := 0; i < b.N; i++ {var slice []int
                for j := 0; j < 1000; j++ {slice = append(slice, j)
                }
        }
}

func BenchmarkSliceAppendWithCapacity(b *testing.B) {
        for i := 0; i < b.N; i++ {slice := make([]int, 0, 1000)
                for j := 0; j < 1000; j++ {slice = append(slice, j)
                }
        }
}

在上述代码中,咱们定义了两个基准测试函数:BenchmarkSliceAppendNoCapacityBenchmarkSliceAppendWithCapacity。其中,BenchmarkSliceAppendNoCapacity 测试了在没有设置初始容量的状况下,循环追加元素到切片的性能;BenchmarkSliceAppendWithCapacity测试了在设置了初始容量的状况下,循环追加元素到切片的性能。基准测试后果如下:

BenchmarkSliceAppendNoCapacity-4          280983              4153 ns/op           25208 B/op         12 allocs/op
BenchmarkSliceAppendWithCapacity-4       1621177              712.2 ns/op              0 B/op          0 allocs/op

其中ns/op 示意每次操作的均匀执行工夫,即函数执行的耗时。B/op 示意每次操作的均匀内存调配量,即每次操作调配的内存大小。allocs/op 示意每次操作的均匀内存调配次数。

能够看到,在设置了初始容量的状况下,性能要显著优于没有设置初始容量的状况。循环追加 1000 个元素到切片时,设置了初始容量的状况下均匀每次操作耗时约为 712.2 纳秒,而没有设置初始容量的状况下均匀每次操作耗时约为 4153 纳秒。这是因为设置了初始容量防止了频繁的扩容操作,进步了性能。

所以,尽管切片的主动扩容好用,然而其也是存在代价的。更好得应用切片,应该防止频繁的扩容操作,这里能够在创立切片时预估所需的容量,并提前指定切片的容量,这样能够缩小扩容次数,进步性能。须要留神的是,如果你不晓得切片须要多大的容量,能够应用适当的初始容量,而后依据须要动静扩容。

2.3 留神切片参数批改原始数据的陷阱

在 Go 语言中,切片是援用类型。当将切片作为参数传递给函数时,实际上是传递了底层数组的援用。这意味着在函数外部批改切片的元素会影响到原始切片。上面是一个示例代码来阐明这个问题:

package main

import "fmt"

func modifySlice(slice []int) {slice[0] = 10
     fmt.Println("Modified slice inside function:", slice)
}

func main() {originalSlice := []int{1, 2, 3}
     fmt.Println("Original slice:", originalSlice)
     modifySlice(originalSlice)
     fmt.Println("Original slice after function call:", originalSlice)
}

在上述代码中,咱们定义了一个 modifySlice 函数,它接管一个切片作为参数,并在函数外部批改了切片的第一个元素,并追加了一个新元素。而后,在 main 函数中,咱们创立了一个初始切片 originalSlice,并将其作为参数传递给modifySlice 函数。当咱们运行代码时,输入如下:

Original slice: [1 2 3]
Modified slice inside function: [10 2 3]
Original slice after function call: [10 2 3]

能够看到,在 modifySlice 函数外部,咱们批改了切片的第一个元素并追加了一个新元素。这导致了函数外部切片的变动。然而,当函数返回后,原始切片 originalSlice 数据也受到影响。

如果咱们心愿函数外部的批改不影响原始切片,能够通过复制切片来解决。批改示例代码如下:

package main

import "fmt"

func modifySlice(slice []int) {newSlice := make([]int, len(slice))
        copy(newSlice, slice)

        newSlice[0] = 10
        fmt.Println("Modified slice inside function:", newSlice)
}

func main() {originalSlice := []int{1, 2, 3}
        fmt.Println("Original slice:", originalSlice)
        modifySlice(originalSlice)
        fmt.Println("Original slice after function call:", originalSlice)
}

通过应用 make 函数创立一个新的切片 newSlice,并应用copy 函数将原始切片复制到新切片中,咱们确保了函数外部操作的是新切片的正本。这样,在批改新切片时不会影响原始切片的值。当咱们运行批改后的代码时,输入如下:

Original slice: [1 2 3]
Modified slice inside function: [10 2 3]
Original slice after function call: [1 2 3]

能够看到,原始切片放弃了不变,函数外部的批改只影响了复制的切片。这样咱们能够防止在函数间传递切片时对原始切片造成意外批改。

3. 总结

本文深入探讨了 Go 语言切片的一些注意事项,旨在帮忙读者更好地应用切片。

首先,切片是对底层数组的援用。批改切片的元素会间接影响到底层数组以及其余援用该数组的切片。如果须要防止批改一个切片影响其余切片或底层数组,能够应用 copy 函数创立一个独立的底层数组。

其次,切片的主动扩容可能带来性能问题。当切片的长度超过容量时,Go 语言会主动扩容切片,须要重新分配底层数组并复制数据。为了防止频繁的扩容操作,能够在创立切片时预估所需的容量,并提前指定切片的容量。

最初,须要留神切片作为参数传递给函数时,函数外部的批改会影响到原始切片。如果心愿函数外部的批改不影响原始切片,能够通过复制切片来解决。

理解和把握这些切片的注意事项和技巧,能够防止意外的程序行为。

正文完
 0