ZetCode

Go 切片

最后修改时间 2024 年 4 月 11 日

在本文中,我们将介绍如何在 Golang 中使用切片。

数组是同一种数据类型的元素的集合。数组包含固定数量的元素,它不能增长或收缩。数组的元素通过索引访问。

一个切片是数组元素的可变大小、灵活的视图。切片可以在底层数组的边界内增长和收缩。切片本身不存储任何数据,它只是描述了数组的一个部分。

Go 声明切片

var s []T

我们声明一个类型为 T 的切片。切片的声明方式与数组类似,只是我们在方括号 [] 中不指定任何大小。

切片字面量

可以使用切片字面量创建切片。

main.go
package main

import "fmt"

func main() {
    var s1 = []int{2, 5, 6, 7, 8}

    s2 := []int{3, 5, 1, 2, 8}

    fmt.Println("s1:", s1)
    fmt.Println("s2:", s2)
}

在代码示例中,我们创建了两个切片。

var s1 = []int{2, 5, 6, 7, 8}

右侧的表达式是切片字面量。

s2 := []int{3, 5, 1, 2, 8}

这里我们有一个简写等价形式。

$ go run main.go
s1: [2 5 6 7 8]
s2: [3 5 1 2 8]

make 函数

我们可以使用 make 内置函数在 Go 中创建新的切片。

func make([]T, len, cap) []T

make 函数接受一个类型、一个长度和一个可选的容量。它会分配一个底层数组,其大小等于给定的容量,并返回一个引用该数组的切片。

main.go
package main

import "fmt"

func main() {

    vals := make([]int, 5)

    fmt.Println("vals: ", vals)

    vals[0] = 1
    vals[1] = 2
    vals[2] = 3
    vals[3] = 4
    vals[4] = 5

    fmt.Println("vals: ", vals)
}

我们使用 make 函数创建了一个大小为 5 的整数切片。最初,切片的元素都为零。然后我们给切片元素赋新值。

$ go run main.go
vals:  [0 0 0 0 0]
vals:  [1 2 3 4 5]

切片长度和容量

len 函数返回切片中的元素数量。cap 函数返回切片的容量。切片的容量是从切片中的第一个元素开始计算的底层数组中的元素数量。

main.go
package main

import "fmt"

func main() {

    vals := make([]int, 5, 10)

    n := len(vals)
    c := cap(vals)

    fmt.Printf("The size is: %d\n", n)
    fmt.Printf("The capacity is: %d\n", c)

    vals2 := vals[0:4]
    n2 := len(vals2)
    c2 := cap(vals2)

    fmt.Printf("The size is: %d\n", n2)
    fmt.Printf("The capacity is: %d\n", c2)
}

在代码示例中,我们打印了两个切片的长度和容量。

$ go run main.go
The size is: 6
The capacity is: 6
The size is: 4
The capacity is: 6

对数组或切片进行切片

我们可以通过对现有数组或切片进行切片来创建切片。

为了形成一个切片,我们指定一个低界和一个高界:a[low:high]。这会选择一个半开区间,包含第一个元素,但不包含最后一个元素。

我们可以省略低界或高界,以使用它们的默认值。低界的默认值是零,高界的默认值是切片的长度。

main.go
package main

import "fmt"

func main() {

    vals := [...]int{1, 2, 3, 4, 5, 6, 7}

    s1 := vals[1:4]
    fmt.Printf("s1: %v, cap: %d\n", s1, cap(s1))

    s2 := vals[5:7]
    fmt.Printf("s2: %v, cap: %d\n", s2, cap(s2))

    s3 := vals[:4]
    fmt.Printf("s3: %v, cap: %d\n", s3, cap(s3))

    s4 := vals[2:]
    fmt.Printf("s4: %v, cap: %d\n", s4, cap(s4))

    s5 := vals[:]
    fmt.Printf("s5: %v, cap: %d\n", s5, cap(s5))
}

我们从一个整数数组创建切片。

vals := [...]int{1, 2, 3, 4, 5, 6, 7}

创建了一个整数数组。使用 ... 运算符,Go 会为我们计算数组的大小。

s2 := vals[5:7]
fmt.Printf("s2: %v, cap: %d\n", s2, cap(s2))

我们从 vals 数组创建了一个切片。结果切片包含从索引 5 开始到索引 7 结束的元素;高界是不包含的。

$ go run main.go
s1: [2 3 4], cap: 6
s2: [6 7], cap: 2
s3: [1 2 3 4], cap: 7
s4: [3 4 5 6 7], cap: 5
s5: [1 2 3 4 5 6 7], cap: 7

切片迭代

使用 for 循环,我们可以在 Go 中迭代切片元素。

main.go
package main

import "fmt"

func main() {

    words := []string{"falcon", "bold", "bear", "sky", "cloud", "ocean"}

    for idx, word := range words {

        fmt.Println(idx, word)
    }
}

在代码示例中,我们使用 for/range 语句迭代一个单词切片。

$ go run main.go
0 falcon
1 bold
2 bear
3 sky
4 cloud
5 ocean

在下面的示例中,我们使用经典的 for 循环迭代一个切片。

main.go
package main

import "fmt"

func main() {

    words := []string{"falcon", "bold", "bear", "sky", "cloud", "ocean"}

    for i := 0; i < len(words); i++ {

        fmt.Println(words[i])
    }
}

我们使用 for 语句迭代一个单词切片。

$ go run main.go
falcon
bold
bear
sky
cloud
ocean

append 函数

内置的 append 函数将新元素添加到切片。

func append(s []T, vs ...T) []T

第一个参数是类型为 T 的切片,其余是附加到切片中的 T 值。

append 的结果值是一个切片,其中包含原始切片的所有元素以及提供的所有值。如果底层数组不够大以容纳所有提供的值,将会分配一个更大的数组。返回的切片将指向新分配的数组。

main.go
package main

import "fmt"

func main() {

    vals := make([]int, 3)

    fmt.Printf("slice: %v; len: %d; cap: %d \n", vals, len(vals), cap(vals))

    fmt.Println("---------------------------")

    vals = append(vals, 1)
    vals = append(vals, 2)
    vals = append(vals, 3)
    vals = append(vals, 4, 5, 6)

    fmt.Printf("slice: %v; len: %d; cap: %d \n", vals, len(vals), cap(vals))
}

在代码示例中,我们将新元素添加到已经有三个元素的切片中。

vals := make([]int, 3)

首先,我们创建一个有三个元素并初始化为 0 的切片。

vals = append(vals, 1)
vals = append(vals, 2)
vals = append(vals, 3)
vals = append(vals, 4, 5, 6)

我们将六个值附加到切片中。一次可以附加多个元素。

$ go run main.go
slice: [0 0 0]; len: 3; cap: 3
---------------------------
slice: [0 0 0 1 2 3 4 5 6]; len: 9; cap: 12

在底层,Go 扩大了底层数组以包含所有新元素。


main.go
package main

import (
    "fmt"
    "strings"
)

func main() {

    words := []string{}
    words = append(words, "an")
    words = append(words, "old")
    words = append(words, "falcon")

    res := strings.Join(words, " ")
    fmt.Println(res)
}

我们从一个空的字符串切片开始。我们将三个单词附加到切片中。然后我们使用 strings.Join 函数将单词连接起来,并在单词之间插入一个空格。

$ go run main.go
an old falcon

copy 函数

内置的 copy 函数复制一个切片。

func copy(dst, src []T) int

该函数返回复制的元素数量。

main.go
package main

import "fmt"

func main() {

    vals := []int{1, 2, 3, 4, 5}

    vals2 := make([]int, len(vals))

    n := copy(vals2, vals)

    fmt.Printf("%d elements copied\n", n)

    fmt.Println("vals:", vals)
    fmt.Println("vals2:", vals2)
}

在代码示例中,我们复制了一个整数切片。

$ go run main.go
5 elements copied
vals: [1 2 3 4 5]
vals2: [1 2 3 4 5]

删除元素

没有内置函数可以从切片中删除项。我们可以使用 append 函数进行删除。

main.go
package main

import (
    "fmt"
)

func main() {

    words := []string{"falcon", "bold", "bear", "sky", "cloud", "ocean"}
    fmt.Println(words)

    words = append(words[1:2], words[2:]...)
    fmt.Println(words)

    words = append(words[:2], words[4:]...)
    fmt.Println(words)
}

在代码示例中,我们从切片中删除一个元素,然后删除两个元素。

words = append(words[1:2], words[2:]...)

这会从切片中删除第一个元素。我们通过附加两个切片并省略要删除的切片来完成删除。

$ go run main.go
[falcon bold bear sky cloud ocean]
[bold bear sky cloud ocean]
[bold bear ocean]

连接切片

slices.Concat 方法创建一个新的切片,连接传入的切片。

main.go
package main

import (
    "fmt"
    "slices"
    "strings"
)

func main() {

    s1 := []string{"an old"}
    s2 := []string{"falcon"}
    s3 := []string{"in the sky"}

    msg := slices.Concat(s1, s2, s3)

    fmt.Println(msg)
    fmt.Println(strings.Join(msg, " "))
}

该示例连接了三个切片。它打印新切片的内容,然后使用 strings.Join 连接切片的所有元素。

$ go run main.go
[an old falcon in the sky]
an old falcon in the sky

唯一元素

在下一个示例中,我们生成一个包含唯一元素的切片。

main.go
package main

import "fmt"

func uniq(vals []int) []int {

    uvals := []int{}
    seen := make(map[int]bool)

    for _, val := range vals {

        if _, in := seen[val]; !in {

            seen[val] = true
            uvals = append(uvals, val)
        }
    }

    return uvals
}


func main() {

    vals := []int{1, 2, 2, 3, 4, 4, 5, 6, 7, 8, 8, 8, 9, 9}
    uvals := uniq(vals)

    fmt.Printf("Original slice: %v\n", vals)
    fmt.Printf("Unique slice: %v\n", uvals)
}

我们有一个包含重复元素的切片。我们创建一个只包含唯一元素的新切片。

seen := make(map[int]bool)

为了完成这项任务,我们创建一个 map,它为我们在切片中遇到的所有值存储布尔值 true。

for _, val := range vals {

    if _, in := seen[val]; !in {

        seen[val] = true
        uvals = append(uvals, val)
    }
}

我们遍历带有重复项的切片元素。如果它不在 seen map 中,我们就将其存储在那里并附加到新的 uvals 切片中。否则,我们就跳过 if 块。

$ go run main.go
Original slice: [1 2 2 3 4 4 5 6 7 8 8 8 9 9]
Unique slice: [1 2 3 4 5 6 7 8 9]

对切片元素进行排序

Go 包含 sort 包来对切片进行排序。

main.go
package main

import (
    "fmt"
    "sort"
)

func main() {

    words := []string{"falcon", "bold", "bear", "sky", "cloud", "ocean"}
    vals := []int{4, 2, 1, 5, 6, 8, 0, -3}

    sort.Strings(words)
    sort.Ints(vals)

    fmt.Println(words)
    fmt.Println(vals)
}

在代码示例中,我们对单词和整数切片进行了排序。

$ go run main.go
[bear bold cloud falcon ocean sky]
[-3 0 1 2 4 5 6 8]

切片初始值

切片的默认零值是 nilnil 切片的长度和容量为 0,并且没有底层数组。

当我们使用 make 函数创建切片时,所有元素都初始化为 0。

main.go
package main

import "fmt"

func main() {

    var vals []int

    if vals == nil {
        fmt.Printf("slice is nil\n")
    }

    fmt.Printf("slice: %v; len: %d; cap: %d \n", vals, len(vals), cap(vals))

    fmt.Println("---------------------------")

    var vals2 = make([]int, 5)
    fmt.Printf("slice: %v; len: %d; cap: %d \n", vals2, len(vals2), cap(vals2))
}

我们创建了两个具有默认零值和 make 函数初始化的元素的切片。

$ go run main.go
slice is nil
slice: []; len: 0; cap: 0
---------------------------
slice: [0 0 0 0 0]; len: 5; cap: 5

切片是引用类型

切片是 Go 中的一种引用类型。这意味着当我们为一个新变量分配一个引用或将一个切片传递给函数时,切片的引用会被复制。

main.go
package main

import "fmt"

func main() {

    vals := []int{ 1, 2, 3, 4, 5, 6 }
    vals2 := vals

    vals2[0] = 11
    vals2[1] = 22

    fmt.Println(vals)
    fmt.Println(vals2)
}

在代码示例中,我们定义了一个切片并将该切片赋给了一个新变量。通过第二个变量所做的更改会反映在原始切片中。

$ go run main.go
[11 22 3 4 5 6]
[11 22 3 4 5 6]

原始切片也被修改了。

切片数组

Go 切片也可以包含其他切片。

main.go
package main

import "fmt"

func main() {

    words := [][]string{
        {"sky", "ocean"},
        {"red", "blue"},
        {"C#", "Go"},
    }

    fmt.Printf("slice: %v; len: %d; cap: %d \n", words, len(words), cap(words))
}

该示例创建了一个切片数组。

$ go run main.go
slice: [[sky ocean] [red blue] [C# Go]]; len: 3; cap: 3

过滤结构体切片

在下一个示例中,我们将过滤 Go 结构体切片。

main.go
package main

import "fmt"

type User struct {
    name       string
    occupation string
    country    string
}

func main() {

    users := []User{

        {"John Doe", "gardener", "USA"},
        {"Roger Roe", "driver", "UK"},
        {"Paul Smith", "programmer", "Canada"},
        {"Lucia Mala", "teacher", "Slovakia"},
        {"Patrick Connor", "shopkeeper", "USA"},
        {"Tim Welson", "programmer", "Canada"},
        {"Tomas Smutny", "programmer", "Slovakia"},
    }

    var programmers []User

    for _, user := range users {

        if isProgrammer(user) {
            programmers = append(programmers, user)
        }
    }

    fmt.Println("Programmers:")
    for _, u := range programmers {

        fmt.Println(u)
    }
}

func isProgrammer(user User) bool {

    return user.occupation == "programmer"
}

在代码示例中,我们定义了一个用户切片。我们创建了一个只包含程序员的新切片。

type User struct {
    name       string
    occupation string
    country    string
}

User 结构体有三个字段。

users := []User{

    {"John Doe", "gardener", "USA"},
    {"Roger Roe", "driver", "UK"},
    {"Paul Smith", "programmer", "Canada"},
    {"Lucia Mala", "teacher", "Slovakia"},
    {"Patrick Connor", "shopkeeper", "USA"},
    {"Tim Welson", "programmer", "Canada"},
    {"Tomas Smutny", "programmer", "Slovakia"},
}

这是原始的 User 结构体切片。

var programmers []User

过滤后的用户/程序员存储在 programmers 切片中。

for _, user := range users {

    if isProgrammer(user) {
        programmers = append(programmers, user)
    }
}

我们遍历 users 切片,如果用户满足 isProgrammer 断言,则将当前用户添加到 programmers 切片中。

func isProgrammer(user User) bool {

    return user.occupation == "programmer"
}

IsProgrammer 断言对于所有 occupation 字段等于 "programmer" 的用户都返回 true。

$ go run main.go
Programmers:
{Paul Smith programmer Canada}
{Tim Welson programmer Canada}
{Tomas Smutny programmer Slovakia}

语法糖

Go 有一种简化的语法,用于将切片传递给另一个函数。

main.go
package main

import "fmt"

func showUsers(users ...string) {

    for _, e := range users {
        fmt.Println(e)
    }
}

func main() {

    showUsers("sky", "tomorrow", "bored", "falcon")
}

users ...string 是一个字符串切片的参数声明。然后我们可以传递逗号分隔的元素:"sky", "tomorrow", "bored", "falcon"

来源

The Go Programming Language Specification

在本文中,我们学习了 Golang 中的切片。

作者

我叫 Jan Bodnar,是一位充满激情的程序员,拥有丰富的编程经验。我自 2007 年以来一直在撰写编程文章。迄今为止,我已撰写了 1400 多篇文章和 8 本电子书。我在教学编程方面拥有十多年的经验。

列出所有 Go 教程