6.切片

Tuesday, July 18, 2023

切片是一个可以通过增长来保存额外数据的集合类型。

切片

切片实际上是一个go的数据结构,与数组相同的是,切片由多个相同类型的元素构成,不同的是,切片允许在结尾追加更多的元素。

声明一个保存切片的变量,使用一对空方括号,后面跟着这个切片所保存的元素类型 : var mySlice []string

除了不指定大小,与声明一个数组变量的语法完全相同。

与数组变量不同,声明切片变量并不会自动创建一个切片,可以调用内建的make函数,传递给make想要创建的切片类型(这个类型与想要赋值的变量的类型相同)和需要创建的切片的长度。

var notes []string   //声明一个切片变量
notes = make([]string, 7)  //创建7个字符串的切片

当切片被创建后,切片中的元素的赋值和取值操作语法与数组相同。

无需将变量声明和创建切片分成两步,使用一个短变量声明的make会自动推导出变量类型。

primes := make([]int, 5)
primes[0] = 2
primes[1] = 3
fmt.Println(primes[0])

内建的函数len对切片和数组有相同效果,将一个切片变量传入len,会返回一个整型的长度值。

notes := make([]string, 7)
primes := make([]int, 5)
fmt.Println(len(notes))
fmt.Println(len(primes))

forfor...range数组与切片操作也相同。

letters := []string{"a", "b", "c"}
for i := 0; i < len(letters); i++ {
       fmt.Println(letters[i])
}
for _, letter := range letters {
       fmt.Println(letter)
}

输出:

a
b
c
a
b
c

切片字面量

与数组相同,若最初知道切片有哪些值,可以使用切片字面量来通过这些值初始化切片。

切片字面量方括号是空的,后跟切片存储的元素的类型,再跟一个花括号的列表,列表中是每个元素的初始值。
[]int{9, 18, 27}

例如:

notes := []string{"do", "re", "mi", "fa", "so", "la", "ti"}
fmt.Println(notes[3], notes[6], notes[0])
primes := []int{
       2,
       3,
       5,
}
fmt.Println(primes[0], primes[1], primes[2])

输出:

fa ti do
2 3 5

切片运算符

每个切片都构建在一个底层的数组之上,实际上是底层的数组存储了切片的数据;切片仅仅是数组中的一部分(或者所有)元素的视图。

当使用 make 函数或者切片字面量创建一个切片的时候,底层的数组会自动创建出来(只有通过切片,才能访问它),但也可以创建一个数组,然后再基于数组通过切片运算符创建一个切片。 mySlice := myArray[1:3]

切片运算符有两个索引:其中一个标识切片开始的位置,另一个标识切片在此位置之前结束。

underlyingArray := [5]string{"a", "b", "c", "d", "e"}
slicel := underlyingArray[0:3]
fmt.Println(slicel)

输出: [a b c]

注意:切片需要在第二个位置之前结束。 比如 underlyingArray[i:j] ,生成的切片从 underlyingArray[i] 到 underlyingArray[j-1]。

若想要一个包含了底层数组最后一个元素的切片,需要在运算符中指定越过数组结尾的第二个索引。

underlyingArray := [5]string{"a", "b", "c", "d", "e"}
slice3 := underlyingArray[2:5]
fmt.Println(slice3)

输出: [c d e]

切片运算符默认需要两个索引,若忽略第一个索引,第一个元素会被使用。

underlyingArray := [5]string{"a", "b", "c", "d", "e"}
slice4 := underlyingArray[:3]
fmt.Println(slice4)

输出:[a b c]

若忽略了结束的索引,从底层数组的开始索引到数组结尾之间的所有元素都会被包含到结果切片中。

underlyingArray := [5]string{"a", "b", "c", "d", "e"}
slice5 := underlyingArray[1:]
fmt.Println(slice5)

输出:[b c d e]

底层数组

切片并不会自己保存任何数据,仅仅是底层数组元素的视图。当使用切片的时候,仅仅可以操作通过切片可见的部分。

甚至可以有多个切片都指向相同的底层数组,每一个切片会是一个指向数组元素的子集的视图,切片甚至可以重叠。

array3 := [5]string{"a", "b", "c", "d", "e"}
slice3 := array3[0:3]
slice4 := array4[2:5]
fmt.Println(slice3, slice4)

修改底层数组,修改切片

注意:由于切片只是底层数组内容的视图,若修改底层数组,这些变化也会反映到切片。

给切片的一个元素赋一个新值,也会修改底层数组相应的元素。

若有多个切片指向了同一个底层数组,数组的元素修改会反映给所有的切片。

由于这些问题,通常我们会使用 make切片字面量 来创建切片,而不是创建一个数组,再用一个切片在上面操作。 使用了 make 和 切片字面量 就不用关心底层数组了。

使用"append"函数在切片上添加数据

go 提供一个内建的函数 append 来将一个或多个值追加到切片的末尾,它返回一个与原始切片元素完全相同的并且在尾部追加了新元素的新的更大的切片。

slice := []string{"a", "b"}
fmt.Println(slice, len(slice))
slice = append(slice, "c")      //在切片末尾追加一个元素,并将append返回值赋回相同的切片变量
fmt.Println(slice, len(slice))
slice = append(slice, "d", "e")
fmt.Println(slice, len(slice))

输出:

[a b] 2
[a b c] 3
[a b c d e] 5

无需记住需要追加到尾部的新值的索引,仅仅调用append函数并且传入切片和需要追加到末尾的值,就会得到一个新的更长的切片。

注意需要确保将append返回的值重新赋给传递给append的那个变量,这是为了避免append返回的切片中的一些不一致行为。

切片的底层数组不能增长大小,若数组没有足够的空间来保存新的元素,所有的元素会被拷贝到一个新的更大的数组中,且切片会被更新为引用这个新的数组,但由于这些场景都发生在append函数内部,无法知道返回的切片与传入append函数的切片是否具有相同的底层数组,若保留了两个切片,会导致一些非预期的错误。

调用append函数,惯例是将函数的返回值赋给传入的那个切片变量,若只保存一个切片,就无需考虑多个切片是否共享同一个底层数组。

切片和零值

与数组一样,若访问了一个切片中没有赋值的元素,会得到那个元素类型的零值。

floatSlice := make([]float64, 10)
boolSlice := make([]bool, 10)    //创建元素没有赋值的切片
fmt.Println(floatSlice[9], boolSlice[5])

输出: 0 false

与数组不同,切片变量有零值:nil,一个没有赋值的切片变量值为 nil。

var intSlice []int
var stringSlice []string
fmt.Printf("intSlice: %#v, stringSlice: %#v\n", intSlice, stringSlice)   //"%#v"把值格式化为它在go代码中呈现的样子

输出:

intSlice: []int(nil), stringSlice: []string(nil)

在其他语言中,需要在使用切片变量前先测试是否包含切片,但在go中,函数有意被写成对待nil的切片就像它是一个空切片一样。

fmt.Println(len(intSlice))  //把nil切片传递给len函数,返回0,就像传入一个空的切片

append函数也会把nil切片看作是一个空的切片,若append传入了空的切片,会在切片里增加一个元素并返回只有一个元素的切片。 若传入nil切片,也会得到只有一个元素的切片。实际上并没有一个切片来追加元素,append函数会在幕后创建一个切片。

intSlice = append(intSlice, 27)   //向append传入一个nil切片
fmt.Printf("intSlice: %#v\n", intSlice)

输出:

stringSlice: []string(27)

意味着通常无需担心切片是nil还是空的。

var slice []string   //变量值是nil
if len(slice) == 0 {   //len函数返回0
       slice = append(slice, "first item")   //append函数会返回一个元素的切片
}
fmt.Printf("%#v\n", slice)

输出:

[]string{"first item"}

使用切片和"append"读取额外的文件行

func GetFloats(fileName string) ([]float64, error) {    //返回一个切片
       var numbers []float64     //该变量默认为nil,append处理nil的行为与处理空切片一样
       file, err := os.Open(fileName)
       if err != nil {
              return numbers, err
       }
       scanner := bufio.NewScanner(file)
       for scanner.Scan() {
              number, err := strconv.ParseFloat(scanner.Text(), 64)   //将string转换为float64且赋值给一个临时变量
	      if err != nil {
                     return numbers, err
	      }
              numbers = append(numbers, number)   //追加新的数字给切片
       }
       err = file.Close()
       if err != nil {
              return numbers, err
       }
       if scanner.Err() != nil {
              return numbers, scanner.Err()
       }
       return numbers, nil
}

不必修改主程序中任何代码,因为使用:=短变量声明,来将GetFloats函数的返回值赋给一个变量,numbers变量自动从[3]float64 (数组类型) 切换到 []float64 (切片类型),并且 for…range 循环和len函数对于数组和切片的行为是一致的。

package main

import (
	"fmt"
	"log"
	"xxx/xxx/datafile"
)

func main() {
	numbers, err := datafile.GetFloats("data.txt")  //自动获得一个[]float64来代替[3]float64
	if err != nil {
		log.Fatal(err)
	}
	var sum float64 = 0
	for _, number := range numbers {
		sum += number
	}
	sampleCount := float64(len(numbers))
	fmt.Printf("Average: %0.2f\n", sum/sampleCount)
}

出错时返回nil值切片

进一步优化Getfloats函数,当前,即使出现错误也会返回一个numbers切片,这意味着会返回一个包含无效数据的切片。

numbers, err := strconv.ParseFloat(scanner.Text(), 64)
if err != nil {
return numbers, err  //返回了一个不该被使用的无效数据
}

调用GetFloats的代码应该检查返回的错误值,若不是nil则需要忽略返回的切片。那为啥还要多此一举返回一个包含无效数据的切片?接下来修改GetFloats函数,让函数出错时返回一个nil代替之前的包含无效数据的切片。

func GetFloats(fileName string) ([]float64, error) {
       var numbers []float64
       file, err := os.Open(fileName)
       if err != nil {
              return nil, err   //返回nil而不是切片,切片在此处的值也是nil,但修改使之更显而易见
       }
       scanner := bufio.NewScanner(file)
       for scanner.Scan() {
              number, err := strconv.ParseFloat(scanner.Text(), 64)
	      if err != nil {
                     return nil, err   //返回nil而不是切片
	      }
              numbers = append(numbers, number)
       }
       err = file.Close()
       if err != nil {
              return nil, err  //返回nil而不是切片
       }
       if scanner.Err() != nil {
              return nil, scanner.Err()  //返回nil而不是切片
       }
       return numbers, nil
}

命令行参数

每当要计算一个新的平均数时,都要修改data.txt,有一种替代方案:用户把值作为命令行参数传递给程序。

就像通过传入不同的参数来控制函数的行为一样,在终端传递参数给程序,这叫做命令行接口

从"os.Args"切片获取命令行参数

os 包有一个包级别的变量 os.Args , 是一个字符串的切片,代表了当前执行程序的命令行参数。

package main

import (
       "fmt"
       "os"
)

func main() {
       fmt.Println(os.Args)
}

比如将上述代码编译后,运行:

./xxx a b c d e

输出:
[./xxx a b c d e]

切片运算符可用于其他切片

上述 ./xxx 是程序的名字,我们并不想要,解决:

package main

import (
       "fmt"
       "os"
)

func main() {
       fmt.Println(os.Args[1:])    //获取一个包含os.Args的从第二个元素到最后一个元素的新的切片
}

这样就避免了上述问题。

使用命令行参数

在 os.Args 上使用切片运算符来忽略程序名称,并把返回切片赋给一个 arguments 变量,设置一个 sum 变量来保存所有输入数字的和,然后使用 for…range 循环来处理 arguments 的元素(使用 _ 空白标识符来忽略元素索引)。使用strconv.ParseFloat 来将参数从字符串转换为 float64 ,若出现错误,输出并退出,若无错误,将数字累加到 sum。 当循环处理完所有参数时,用 len(arguments) 来确定样本个数,然后用 sum 除以样本个数得到平均值。

package main

import (
	"fmt"
	"log"
	"os"
	"strconv"
)

func main() {
	arguments := os.Args[1:]             //通过os.Args获取一个不包含首元素的字符串切片
	var sum float64 = 0                  //定义一个保存数字的累加值
	for _, argument := range arguments { //处理命令行的每一个参数
		number, err := strconv.ParseFloat(argument, 64) //将字符串转换为float64
		if err != nil {
			log.Fatal(err)
		} //若转换函数出现问题则输出log并退出
		sum += number //累加数字
	}
	sampleCount := float64(len(arguments))          //参数的长度可以用作样本的个数
	fmt.Printf("Average: %0.2f\n", sum/sampleCount) //计算平均值并进行打印
}

可变长参数函数

一些函数调用可以获取任何个数的参数,例如 fmt.Println 或 append 。但不要直接在任何函数中使用,定义的所有函数的参数个数与函数调用的参数个数要严格匹配,否则会导致编译错误。

那么 Println 与 append 是如何做到的呢?因为它定义了一个可变长参数函数,一个可变长参数函数可以以多种参数个数来调用,为了让函数的参数可变长,在函数声明中的最后的(或仅有的)参数类型前使用省略号...

语法:

func myFunc(param1 int, param2 ...string) {
       //function code here
}

可变长参数函数的最后一个参数接收一个切片类型的变长参数,这个切片可以被函数当作普通切片来处理。

func severalInts(numbers ...int) {
       fmt.Println(numbers)  //numbers变量被存储在作为参数的切片中
}

func main() {
       severalInts(1)
       severalInts(1, 2, 3)
}

有一个带有字符串参数的函数,若不提供变长参数,不会返回错误,会收到一个空切片。

func severalStrings(strings ...string) {  //strings变量保存了一个所有参数的切片
       fmt.Println(strings)
}

func main() {
       severalStrings("a", "b")
       severalStrings("a", "b", "c", "d", "e")
       severalStrings()   //若没有参数,会收到一个空的切片
}

输出:

[a b]
[a b c d e]
[]

函数也可以接收一个或多个非可变长参数,即使一个函数调用可以忽略可变长参数,其中的非可变长参数是不可忽略的,若忽略会导致编译失败。

func mix(num int, flag bool, strings ...string) {  //首先需要一个int参数,其次需要一个boolean参数,其余剩下的参数必须是string且会被保存成切片
	fmt.Println(num, flag, strings)
}

func main() {
	mix(1, true, "a", "b")
	mix(2, false, "a", "b", "c", "d", "e")
}

输出:

1 true [a b]
2 false [a b c d e]

只有函数定义中的最后一个参数是可变长参数 ,不能放到其他必需参数前。

使用可变长参数函数

maximum 函数会接收任意个数的float64参数并会返回其中的最大值,这个函数的所有参数被保存在一个切片类型的参数 numbers 中,初始设置当前最大值为 -Inf ,一个代表了负无穷的特殊值,通过调用math.Inf获得。(可以使用当前最大值0,但这个最大值可能是负数),然后使用 for…range 来处理 numbers 上的每个值,将其与最大值比较。若大于最大值,将其设置为最大值,处理完所有参数后剩下的最大值就是要返回的。

package main

import (
	"fmt"
	"math"
)

func maximum(numbers ...float64) float64 {  //接收任何个数的float64参数
	max := math.Inf(-1)   //以一个很小的值开始
	for _, number := range numbers {
		if number > max {
			max = number   //找到参数中的最大值
		}
	}   //处理变长参数的每一个值
	return max
}

func main() {
	fmt.Println(maximum(71.8, 56.2, 89.5))
	fmt.Println(maximum(90.7, 89.7, 98.5, 92.3))
}

输出:

89.5
98.5

“inRange"函数,接收一个最小值,一个最大值和任何个数的float64参数,丢弃在给定最小值和给定最大值范围之外的参数,返回一个在范围之内的参数值的切片。

package main

import "fmt"

func inRange(min float64, max float64, numbers ...float64) []float64 {
	var result []float64   //这个切片会保存范围内的值
	for _, number := range numbers {
		if number >= min && number <= max {
			result = append(result, number)
		}
	}
	return result
}

func main() {
	fmt.Println(inRange(1, 100, -12.5, 3.2, 0, 50, 103.5))   //寻找>=1且<=100的参数
	fmt.Println(inRange(-10, 10, 4.1, 12, -12, -5.2))   //寻找>=-10且<=10的参数
}

输出:

[3.2 50]
[4.1 -5.2]

使用可变长参数函数计算平均值

来创造一个可以接收任意多个float64类型参数,并返回它们的平均值的可变长参数函数。

package main

import "fmt"

func average(numbers ...float64) float64 {  //获取任意个数的float64参数
	var sum float64 = 0   //定义一个变量来保存参数的总和
	for _, number := range numbers {
		sum += number
	}
	return sum / float64(len(numbers))
}

func main() {
	fmt.Println(average(100, 50))
	fmt.Println(average(90.7, 89.7, 98.5, 92.3))
}

向可变长参数函数传递一个切片

将numbers切片传递给average函数。

当调用一个可变长参数函数时,在传入的切片变量后增加省略号...

func severalInts(numbers ...int) {
       fmt. Println(numbers)
}
func mix(num int, flag bool, strings ...string) {
       fmt.Println(num, flag, strings)
}
func main() {
       intSlice := []int{1, 2, 3}
       severalInts(intSlice...)   //使用int切片代替可变参数
       stringsSlice := []string{"a", "b", "c", "d"}
       mix(1, true, stringsSlice...)   //使用string切片代替可变参数
}
package main

import (
       "fmt"
       "log"
       "os"
       "strconv"
)

func average(numbers ...float64) float64 {
	var sum float64 = 0
	for _, number := range numbers {
		sum += number
	}
	return sum / float64(len(numbers))
}

func main() {
        arguments := os.Args[1:]
        var numbers []float64  //该切片保存了将要被计算平均值的数字
        for _, argument := range arguments {
               number, err := strconv.ParseFloat(argument, 64)
               if err != nil {
                      log.Fatal(err)
               }
               numbers = append(numbers, number)
        }
        fmt.Printf("Average: %0.2f\n", average(numbers...))  //向可变参数函数传入切片
}

会把命令行参数转换为float64的切片,然后传递给可变参数的average函数。

Golang打怪升级

7.数据标签:映射

5.数组

comments powered by Disqus