细说Go语言切片

  目录
  
  内部实现
  
  声明切片
  
  初始化数组
  
  切片内存结构
  
  使用切片
  
  迭代切片
  
  切片重组
  
  在函数间传递切片
  
  new()和make()的区别
  
  字符串、数组和切片的应用
  
  从字符串生成字节切片
  
  字符串和切片的内存结构
  
  修改字符串中的某个字符
  
  在上一篇文章中已经了解了数组,数组有特定的用处,但是却有一些呆板(数组长度固定不可变),所以在 Go 语言的代码里并不是特别常见。接下来聊聊切片(slice),相对的,切片却是随处可见的,Go语言切片是一种建立在数组类型之上的抽象,它构建在数组之上并且提供更强大的能力和便捷。
  
  内部实现
  
  切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。
  
  切片是可索引的,并且可以由 len() 函数获取长度。
  
  给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修
  
  改,最小为 0 最大为相关数组的长度:切片是一个 长度可变的数组。
  
  切片对象非常小,是因为它是只有3个字段的数据结构:一个是指向底层数组的指针,一个是切片的长度,一个是切片的容量。这3个字段,就是Go语言操作底层数组的元数据,有了它们,我们就可以任意的操作切片了。
  
  切片提供了计算容量的函数 cap() 可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。如果 s 是一个切片, cap(s) 就是从 s[0] 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于 切片 s 来说该不等式永远成立: 0 <= len(s) <= cap(s) 。
  
  多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。
  
  优点 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。
  
  声明切片
  
  声明切片的方式和声明数组的方式差不都
  
  //   变量名            变量类型
  
  var variable_name = []var_type
  
  如果切片只声明而没有初始化,那么这个切片的默认值为nil,长度为 0。
  
  package main
  
  import "fmt"
  
  func main() {
  
  var sli []int
  
  if sli == nil {
  
  fmt.Println("sli和nil相等")
  
  } else {
  
  fmt.Println("sli和nil不相等")
  
  }
  
  }
  
  初始化数组
  
  make方式创建切片,对应用类型的数据都可以使用make函数创建,该函数会返回该类型的引用。
  
  slice := make([]int, 10, 20)
  
  这里我们创建了一个类型为[]int,长度为10,容量为20的切片,如果不指定切片的容量,例如slice := make([]int, 10),那么该切片的容量和长度相等。
  
  因为切片的底层是数组,所以创建切片时,如果不指定字面值的话,默认值就是数组的元素的零值。这里我们所以指定了容量是20,但是我们职能访问10个元素,因为切片的长度是10,剩下的10个元素,需要切片扩充后才可以访问。
  
  容量必须>=长度,我们是不能创建长度大于容量的切片的。
  
  还有一种创建切片的方式,是使用字面量,就是指定初始化的值。
  
  slice := []int{1,2,3,4,5}
  
  通过字面量创建切片和创建数组的方式非常像,只不过不用指定[]中的值([]里面没有...),这时候切片的长度和容量是相等的,并且会根据我们指定的字面量推导出来。当然我们也可以像数组一样,只初始化某个索引的值:
  
  slice := []int{4:1}
  
  这是指定了第5个元素为1,其他元素都是默认值0。这时候切片的长度和容量也是一样的。这里再次强调一下切片和数组的微小差别。
  
  //数组
  
  array := [...]int{4:1}
  
  //切片
  
  slice := []int{4:1}
  
  切片还有nil切片和空切片,它们的长度和容量都是0,但是它们指向底层数组的指针不一样,nil切片意味着指向底层数组的指针为nil,而空切片对应的指针是个地址。
  
  //nil切片
  
  var slice []int
  
  //空切片
  
  slice:=[]int{}
  
  nil切片表示不存在的切片,而空切片表示一个空集合,它们各有用处。
  
  切片另外一个用处比较多的创建是基于现有的数组或者切片创建。
  
  slice := []int{1, 2, 3, 4, 5}
  
  slice1 := slice[:]
  
  slice2 := slice[0:]
  
  slice3 := slice[:5]
  
  fmt.Println(slice1)
  
  fmt.Println(slice2)
  
  fmt.Println(slice3)
  
  基于现有的切片或者数组创建,使用[i:j]这样的操作符即可,她表示以i索引开始,到j索引结束,截取原数组或者切片,创建而成的新切片,新切片的值包含原切片的i索引,但是不包含j索引。
  
  i如果省略,默认是0;j如果省略默认是原数组或者切片的长度,所以例子中的三个新切片的值是一样的。这里注意的是i和j都不能超过原切片或者数组的索引。
  
  slice := []int{1, 2, 3, 4, 5}
  
  newSlice := slice[1:3]
  
  newSlice[0] = 10
  
  fmt.Println(slice)
  
  fmt.Println(newSlice)
  
  fmt.Printf("%p\n", &slice[1])
  
  fmt.Printf("%p\n", &newSlice[0])
  
  这个例子证明了,新的切片和原切片共用的是一个底层数组,所以当修改的时候,底层数组的值就会被改变,所以原切片的值也改变了。当然对于基于数组的切片也一样的。
  
  我们基于原数组或者切片创建一个新的切片后,那么新的切片的大小和容量是多少呢?这里有个公式:
  
  对于底层数组容量是k的切片slice[i:j]来说
  
  长度:j-i
  
  容量:k-i
  
  比如我们上面的例子slice[1:3],长度就是3-1=2,容量是5-1=4。不过代码中我们计算的时候不用这么麻烦,因为Go语言为我们提供了内置的len和cap函数来计算切片的长度和容量。
  
  slice := []int{1, 2, 3, 4, 5}
  
  newSlice := slice[1:3]
  
  fmt.Printf("newSlice长度:%d,容量:%d",len(newSlice),cap(newSlice))
  
  以上基于一个数组或者切片使用2个索引创建新切片的方法,此外还有一种3个索引的方法,第3个用来限定新切片的容量,其用法为slice[i:j:k]。
  
  slice := []int{1, 2, 3, 4, 5}
  
  newSlice := slice[1:2:3]
  
  这样我们就创建了一个长度为2-1=1,容量为3-1=2的新切片,不过第三个索引,不能超过原切片的最大索引值5。
  
  切片内存结构
  
  var slice = []int{1, 2, 3, 4, 5}这样就创建了一个长度为5的数组并且创建了一个相关切片。
  
  我们可以分析一下,上图中的切片在内存中结构
  
  切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。
  
  注意 绝对不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!!
  
  正因为这样,所以我们数组首地址需要取地址符(&),但是打印一个切片的地址的时候就不要加取地址符了。
  
  func main() {
  
  arr := [...]int{1, 2, 3, 4 ,5}
  
  slice := []int{1, 2, 3, 4, 5}
  
  fmt.Printf("arr的首地址为: %p\n", &arr)
  
  fmt.Printf("slice的首地址为: %p\n", slice)
  
  }
  
  使用切片
  
  使用切片,和使用数组一样,通过索引就可以获取切片对应元素的值,同样也可以修改对应元素的值。
  
  slice := []int{1, 2, 3, 4, 5}
  
  fmt.Println(slice[2]) //获取值
  
  slice[2] = 10 //修改值
  
  fmt.Println(slice[2]) //输出10
  
  切片只能访问到其长度内的元素,访问超过长度外的元素,会导致运行时异常,与切片容量关联的元素只能用于切片增长。
  
  我们前面讲了,切片算是一个动态数组,所以它可以按需增长,我们使用内置append函数即可。append函数可以为一个切片追加一个元素,至于如何增加、返回的是原切片还是一个新切片、长度和容量如何改变这些细节,append函数都会帮我们自动处理。
  
  slice := []int{1, 2, 3, 4, 5}
  
  newSlice := slice[1:3]
  
  newSlice=append(newSlice,10)
  
  fmt.Println(www.feifanyule.cn newSlice)
  
  fmt.Println(www.haom178.com slice)
  
  //Output
  
  [2 3 10]
  
  [1 2 3 10 5]
  
  例子中,通过append函数为新创建的切片newSlice,追加了一个元素10,我们发现打印的输出,原切片slice的第4个值也被改变了,变成了10。引起这种结果的原因是因为newSlice有可用的容量,不会创建新的切片来满足追加,所以直接在newSlice后追加了一个元素10,因为newSlice和slice切片共用一个底层数组,所以切片slice的对应的元素值也被改变了。
  
  这里newSlice新追加的第3个元素,其实对应的是slice的第4个元素,所以这里的追加其实是把底层数组的第4个元素修改为10,然后把newSlice长度调整为3。
  
  如果切片的底层数组,没有足够的容量时,就会新建一个底层数组,把原来数组的值复制到新底层数组里,再追加新值,这时候就不会影响原来的底层数组了。
  
  所以一般我们在创建新切片的时候,最好要让新切片的长度和容量一样,这样我们在追加操作的时候就会生成新的底层数组,和原有数组分离,就不会因为共用底层数组而引起奇怪问题,因为共用数组的时候修改内容,会影响多个切片。
  
  append函数会智能的增长底层数组的容量,目前的算法是:容量小于1000个时,总是成倍的增长,一旦容量超过1000个,增长因子设为1.25,也就是说每次会增加25%的容量。
  
  内置的append也是一个可变参数的函数,所以我们可以同时追加好几个值。
  
  newSlice=append(newSlice,10,20,30)
  
  此外,我们还可以通过...操作符,把一个切片追加到另一个切片里。
  
  slice := []int{1, 2, 3, 4, 5}
  
  newSlice := slice[www.michenggw.com 1:2:3]
  
  newSlice=append(newSlice,slice...)
  
  fmt.Println(newSlice)
  
  fmt.Println(slice)
  
  迭代切片
  
  切片是一个集合,我们可以使用 for range 循环来迭代它,打印其中的每个元素以及对应的索引。
  
  slice := []int{1, 2, 3, 4, 5}
  
  for i,v:=range slice{
  
  fmt.Printf("索引:%d,值:%d\n",i,v)
  
  }
  
  如果我们不想要索引,可以使用_来忽略它,这是Go语言的用法,很多不需要的函数等返回值,都可以忽略。
  
  slice := [www.mhylpt.com]int{1, 2, 3, 4, 5}
  
  for _,v:=range slice{
  
  fmt.Printf("值:%d\n",v)
  
  }
  
  这里需要说明的是range返回的是切片元素的复制,而不是元素的引用,所以这里我们修改v的值并不会改变slice切片里的值。
  
  除了for range循环外,我们也可以使用传统的for循环,配合内置的len函数进行迭代。
  
  slice := []int{1, 2, 3, 4, 5}
  
  for i := 0; i < len(slice); i++ {
  
  fmt.Printf("值:%d\n", slice[i])
  
  }
  
  切片重组
  
  我们已经知道切片创建的时候通常比相关数组小,例如:
  
  slice1 := make([www.furggw.com]type, start_length, capacity)
  
  其中 start_length 作为切片初始长度而 capacity 作为相关数组的长度。
  
  这么做的好处是我们的切片在达到容量上限后可以扩容。改变切片长度的过程称之为切片重组 reslicing,做法如下: slice1 = slice1[0:end] ,其中 end 是新的末尾索引(即长度)。
  
  将切片扩展 1 位可以这么做:
  
  slice = slice[0:len(slice)+1]
  
  切片可以反复扩展直到占据整个相关数组。
  
  在函数间传递切片
  
  其实无论是值类型还是引用类型,函数间的传递都是值传递,只不过值类型的数据传递是传递的是变量的值,而引用类型在函数间的传递的是变量的地址,然而这个地址其实也是一个值。
  
  package main
  
  import "fmt"
  
  func main(www.gcyl152.com) {
  
  slice := []int{1, 2, 3, 4, 5}
  
  fmt.Printf("%p\n", slice)
  
  modify(slice)
  
  fmt.Println(slice)
  
  }
  
  func modify(slice []int) {
  
  fmt.Printf("%p\n", slice)
  
  slice[1] = 10
  
  }
  
  打印的输出如下:
  
  0xc0000180c0
  
  0xc0000180c0
  
  [1 10 3 4 5]
  
  仔细看,这两个切片的地址是一样的,所以这两个切片指向同一个内存地址。因此我们修改一个索引的值后,发现原切片的值也被修改了,说明它们共用一个底层数组。
  
  new()和make()的区别
  
  看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
  
  new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。
  
  make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片、map 和 channel
  
  字符串、数组和切片的应用
  
  从字符串生成字节切片
  
  假设 s 是一个字符串(本质上是一个字节数组),那么就可以直接通过 c := []bytes(s) 来获取一个字节的切片 c。另外,您还可以通过 copy 函数来达到相同的目的: copy(dst []byte, src string) 。
  
  同样的,还可以使用 for-range 来获得每个元素
  
  package main
  
  import "fmt"
  
  func main() {
  
  s := "我爱你中国"
  
  for _, value := range s {
  
  fmt.Printf("%c ", value)
  
  }
  
  }
  
  输出:
  
  我 爱 你 中 国
  
  我们知道,Unicode 字符会占用 2 个字节,有些甚至需要 3 个或者 4 个字节来进行表示。如果发现错误的 UTF8 字符,则该字符会被设置为 U+FFFD 并且索引向前移动一个字节。和字符串转换一样,您同样可以使用 c := []int(s) 语法,这样切片中的每个 int 都会包含对应的 Unicode 代码,因为字符串中的每次字符都会对应一个整数。类似的,您也可以将字符串转换为元素类型为 rune 的切片: r := []rune(s) 。
  
  可以通过代码 len([]rune(s)) 来获得字符串中字符的数量,但使用 utf8.RuneCountInString(s) 效率会更高一点。
  
  您还可以将一个字符串追加到某一个字符数组的尾部:
  
  import "fmt"
  
  func main() {
  
  var r []byte
  
  //比较适合ascii字符串,用汉字的话输出会乱码
  
  var s string = "I love you"
  
  r = append(r, s...)
  
  for _, v := range r {
  
  fmt.Printf("%c ", v)
  
  }
  
  }
  
  字符串和切片的内存结构
  
  在内存中,一个字符串实际上是一个双字结构,即一个指向实际数据的指针和记录字符串长度的整数。因为指针对用户来说是完全不可见,因此我们可以依旧把字符串看做是一个值类型,也就是一个字符数组。
  
  字符串 string s = "hello" 和子字符串 t = s[2:3] 在内存中的结构可以用下图表示:
  
  修改字符串中的某个字符
  
  Go 语言中的字符串是不可变的,也就是说 str[index] 这样的表达式是不可以被放在等号左侧的。如果尝试运行 str[i] = 'D' 会得到错误: cannot assign to str[i] 。
  
  因此,您必须先将字符串转换成字节数组,然后再通过修改数组中的元素值来达到修改字符串的目的,最后将字节数组转换回字符串格式。
  
  例如,将字符串 "hello" 转换为 "cello":
  
  s := "hello"
  
  c := []byte(s)
  
  c[0] = 'c'
  
  s2 := string(c) //s2 == "cello"
  
  所以,您可以通过操作切片来完成对字符串的操作。

猜你喜欢

转载自blog.csdn.net/li123128/article/details/84620169