数组

数组介绍

数组可以存放多个同一类型数据。数组也是一种数据类型,在 Go 中,数组是值类型。

数组的快速入门

  1. //使用数组的方式来解决问题
  2. //1.定义一个数组
  3. var hens [7]float64
  4. //2.给数组的每个元素赋值, 元素的下标是从0开始的 0-5
  5. hens[0] = 3.0 //hens数组的第一个元素 hens[0]
  6. hens[1] = 5.0 //hens数组的第2个元素 hens[1]
  7. hens[2] = 1.0
  8. hens[3] = 3.4
  9. hens[4] = 2.0
  10. hens[5] = 50.0
  11. hens[6] = 150.0 //增加一只鸡
  12. //3.遍历数组求出总体重
  13. totalWeight2 := 0.0
  14. for i := 0; i < len(hens); i++ {
  15. totalWeight2 += hens[i]
  16. }
  17. //4.求出平均体重
  18. avgWeight2 := fmt.Sprintf("%.2f", totalWeight2 / float64(len(hens)))
  19. fmt.Printf("totalWeight2=%v avgWeight2=%v", totalWeight2, avgWeight2)

数组定义和内存布局

数组的定义

var    数组名 [数组大小]数据类型
var a [5]int
//赋初值 
a[0] = 1 a[1] = 30 ....

💡数组在内存布局

    var intArr [3]int //int占8个字节
    //当我们定义完数组后,其实数组的各个元素有默认值 0
    fmt.Println(intArr)
    intArr[0] = 10
    intArr[1] = 20
    intArr[2] = 30
    fmt.Println(intArr)
    fmt.Printf("intArr的地址=%p intArr[0] 地址%p intArr[1] 地址%p intArr[2] 地址%p\n", 
        &intArr, &intArr[0], &intArr[1], &intArr[2])

输出

[0 0 0]
[10 20 30]
intArr的地址=0xc000014168 intArr[0] 地址0xc000014168 intArr[1] 地址0xc000014170 intArr[2] 地址0xc000014178

数组与切片 - 图1
对上图的说明

  1. 数组的地址可以通过数组名来获取 &intArr
  2. 数组的第一个元素的地址,就是数组的首地址
  3. 数组的各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8字节、int32->4字节…

    数组的使用

    访问数组元素
    数组名[下标] 比如:你要使用 a 数组的第三个元素 a[2]

     //从终端循环输入5个成绩,保存到float64数组,并输出.
     var score [5]float64
    
     for i := 0; i < len(score); i++ {
         fmt.Printf("请输入第%d个元素的值\n", i+1)
         fmt.Scanln(&score[i])
     }
    
     //变量数组打印
     for i := 0; i < len(score); i++ {
         fmt.Printf("score[%d]=%v\n", i, score[i])
     }
    

    几种初始化数组的方式

     var numArr01 [3]int = [3]int{1, 2, 3}
     fmt.Println("numArr01=", numArr01)
    
     var numArr02 = [3]int{5, 6, 7}
     fmt.Println("numArr02=", numArr02)
     //这里的 [...] 是规定的写法
     var numArr03 = [...]int{8, 9, 10}
     fmt.Println("numArr03=", numArr03)
    
     var numArr04 = [...]int{1: 800, 0: 900, 2:999}
     fmt.Println("numArr04=", numArr04)
     //这里的 numArr05 是一个切片
     var numArr05 = []int{11,22,33}
     fmt.Println("numArr05=", numArr05)
    
     //类型推导
     strArr05 := [...]string{1: "tom", 0: "jack", 2:"mary"}
     fmt.Println("strArr05=", strArr05)
    

    输出

    numArr01= [1 2 3]
    numArr02= [5 6 7]
    numArr03= [8 9 10]
    numArr04= [900 800 999]
    numArr05= [11 22 33]
    strArr05= [jack tom mary]
    

    数组的遍历

    方式 1-常规遍历

     score := []int{1,2,3}
     for i := 0; i < len(score); i++ {
         fmt.Printf("score[%d]=%v\n", i, score[i])
     }
    

    方式 2-for-range 结构遍历

    //● 第一个返回值index是数组下标
    //● 第二个value是在该下标位置的值
    //● 他们都是仅在for循环内部可见的局部变量
    //● 遍历数组元素的时候,如果不想使用下标index,可以直接把下标index标为下划线_
    //● index和value的名称不是固定的,即程序员可以自行指定,一般命名为index和value
    for index,value := range array{
     ...
    }
    

    for-range 的案例

     //演示for-range遍历数组
      heroes  := [...]string{"宋江", "吴用", "卢俊义"}
    
     for i, v := range heroes {
         fmt.Printf("i=%v v=%v\n", i , v)
     }
    

    输出

    i=0 v=宋江
    i=1 v=吴用
    i=2 v=卢俊义
    

    数组使用的注意事项和细节

  4. 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化

  5. var arr []int 这时 arr 就是一个 slice 切片
  6. 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用
  7. 数组创建后,如果没有赋值,有默认值(零值)

     //数组创建后,如果没有赋值,有默认值(零值)
     //1. 数值(整数系列, 浮点数系列) =>0
     //2. 字符串 ==> ""
     //3. 布尔类型 ==> false
    
     var arr01 [3]float32
     var arr02 [3]string
     var arr03 [3]bool
     fmt.Printf("arr01=%v arr02=%v arr03=%v\n", arr01, arr02, arr03)//arr01=[0 0 0] arr02=[  ] arr03=[false false false]
    
  8. 使用数组的步骤

    1. 声明数组并开辟空间
    2. 给数组各个元素赋值(默认零值)
    3. 使用数组
  9. 数组的下标是从 0 开始的
  10. 数组下标必须在指定范围内使用,否则报 panic:数组越界,比如var arr [5]int则有效下标为 0-4
  11. Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响 ```go //值传递方式 func test01(arr [3]int) { arr[0] = 88 fmt.Printf(“test01 arr地址=%p\n”, &arr) fmt.Println(“test01 arr=”, arr) }

func main() { //Go的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响 arr := [3]int{11, 22, 33} test01(arr) fmt.Printf(“main arr地址=%p\n”, &arr) fmt.Println(“main arr=”, arr) }

输出
```go
test01 arr地址=0xc0000ae090
test01 arr= [88 22 33]
main arr地址=0xc0000ae078
main arr= [11 22 33]

数组与切片 - 图2

  1. 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式) ```go //引用传递方式 func test02(arr [3]int) { arr[0] = 88 fmt.Printf(“test02 arr地址=%p\n”, &arr) fmt.Printf(“test02 arr指针指向地址=%p\n”, &(arr)) fmt.Println(“test02 arr=”, *arr) }

func main() { arr := [3]int{11, 22, 33} test02(&arr) fmt.Printf(“main arr地址=%p\n”, &arr) fmt.Println(“main arr=”, arr) }

输出
```go
test02 arr地址=0xc000006030
test02 arr指针指向地址=0xc000014168
test02 arr= [88 22 33]
main arr地址=0xc000014168
main arr= [88 22 33]

数组与切片 - 图3

  1. 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度

    二维数组

    使用方式 1: 先声明/定义,再赋值

    语法:var 数组名 [大小][大小]类型比如: var arr [2][3]int, 再赋值。

    var arr2 [2][3]int //以这个为例来分析arr2在内存的布局!!
    arr2[1][1] = 10
    fmt.Println(arr2)
    
    fmt.Printf("arr2[0]的地址%p\n", &arr2[0])
    fmt.Printf("arr2[1]的地址%p\n", &arr2[1])
    
    fmt.Printf("arr2[0][0]的地址%p\n", &arr2[0][0])
    fmt.Printf("arr2[1][0]的地址%p\n", &arr2[1][0])
    

    输出

    [[0 0 0] [0 10 0]]       
    arr2[0]的地址0xc0000c0060
    arr2[1]的地址0xc0000c0078
    arr2[0][0]的地址0xc0000c0060
    arr2[1][0]的地址0xc0000c0078
    

    数组与切片 - 图4

    使用方式 2: 直接初始化

    声明:var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}

    arr3  := [2][3]int{{1,2,3}, {4,5,6}}
    fmt.Println("arr3=", arr3)
    

    二维数组在声明/定义时也对应有四种写法[和一维数组类似]

  • var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}
  • var 数组名 [大小][大小]类型 = […][大小]类型{{初值..},{初值..}}
  • var 数组名 = [大小][大小]类型{{初值..},{初值..}}
  • var 数组名 = […][大小]类型{{初值..},{初值..}}

    二维数组的遍历

    双层 for 循环完成遍历
    for-range 方式完成遍历

    func main() {
    
      //演示二维数组的遍历
      var arr3  = [2][3]int{{1,2,3}, {4,5,6}}
    
      //for循环来遍历
      for i := 0; i < len(arr3); i++ {
          for j := 0; j < len(arr3[i]); j++ {
              fmt.Printf("%v\t", arr3[i][j])
          }
          fmt.Println()
      }
    
      //for-range来遍历二维数组
      for i, v := range arr3 {
          for j, v2 := range v {
              fmt.Printf("arr3[%v][%v]=%v \t",i, j, v2)
          }
          fmt.Println()    
      }
    }
    

    输出

    1       2       3
    4       5       6
    arr3[0][0]=1    arr3[0][1]=2    arr3[0][2]=3
    arr3[1][0]=4    arr3[1][1]=5    arr3[1][2]=6
    

    切片

    切片的基本介绍

  1. 切片的英文是 slice
  2. 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
  3. 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
  4. 切片的长度是可以变化的,因此切片是一个可以动态变化数组
  5. 切片定义的基本语法:var 切片名 []类型比如:var a [] int

    快速入门

    func main() {
    
     //演示切片的基本使用
     var intArr [5]int = [...]int{11, 22, 33, 44, 55}
     //声明/定义一个切片
     //slice := intArr[1:3]
     //1. slice 就是切片名
     //2. intArr[1:3] 表示 slice 引用到intArr这个数组 
     //3. 引用intArr数组的起始下标为 1 , 最后的下标为3(但是不包含3)    
     slice := intArr[1:3] 
     fmt.Println("intArr=", intArr)
     fmt.Println("slice 的元素是 =", slice) //  22, 33
     fmt.Println("slice 的元素个数 =", len(slice)) // 2
     fmt.Println("slice 的最大容量 =", cap(slice)) // 切片的容量是可以动态变化 一般len<=cap 
    }
    

    输出

    intArr= [11 22 33 44 55]
    slice 的元素是 = [22 33]
    slice 的元素个数 = 2
    slice 的最大容量 = 4
    

    💡切片在内存布局

    func main() {
     var intArr [5]int = [...]int{11, 22, 33, 44, 55}
     slice := intArr[1:3] 
    
     fmt.Printf("slice的类型=%T intArr的类型=%T\n", slice, intArr)
     fmt.Printf("intArr[0]的地址=%p intArr[1]的地址=%p\n", &intArr[0], &intArr[1])
     fmt.Printf("slice[0]的地址=%p slice[0==%v]\n", &slice[0], slice[0])
    
     slice[0] = 66//修改元素值
     fmt.Println("intArr=", intArr)
     fmt.Println("slice 的元素是 =", slice)
    }
    

    输出

    slice的类型=[]int intArr的类型=[5]int
    intArr[0]的地址=0xc0000c0060 intArr[1]的地址=0xc0000c0068
    slice[0]的地址=0xc0000c0068 slice[0==22]
    intArr= [11 66 33 44 55]
    slice 的元素是 = [66 33]
    

    对应的内存模型
    数组与切片 - 图5
    对上图的几点说明

  • slice 的确是一个引用类型
  • slice 从底层来说,其实就是一个数据结构(struct 结构体)
    // src/runtime/slice.go
    type slice struct {
      array unsafe.Pointer // 元素指针
      len   int // 长度 
      cap   int // 容量
    }
    

    切片的使用

  1. 第一种方式:定义一个切片,然后让切片去引用一个已经创建好的数组,比如前面的案例就是这样的。
  2. 第二种方式:通过 make 来创建切片
    1. 基本语法:var 切片名 []type = make([]type, len, [cap])
    2. 参数说明: type: 就是数据类型 len : 大小 cap :指定切片容量,可选, 如果你分配了 cap,则要求 cap>=len.
      //演示切片的使用 make
      var slice []float64 = make([]float64, 5, 10)
      slice[1] = 10
      slice[3] = 20
      //对于切片,必须make使用.
      fmt.Println(slice)
      fmt.Println("slice的size=", len(slice))
      fmt.Println("slice的cap=", cap(slice))
      
      输出
      [0 10 0 20 0]
      slice的size= 5
      slice的cap= 10
      
      对上面代码的小结:
  • 通过 make 方式创建切片可以指定切片的大小和容量
  • 如果没有给切片的各个元素赋值,那么就会使用默认值
  • 通过 make 方式创建的切片对应的数组是由 make 底层维护,对外不可见,即只能通过 slice 去访问各个元素
  1. 第 3 种方式:定义一个切片,直接就指定具体数组,使用原理类似 make 的方式

     //第3种方式:定义一个切片,直接就指定具体数组,使用原理类似make的方式
     var strSlice []string = []string{"tom", "jack", "mary"}
     fmt.Println("strSlice=", strSlice)
     fmt.Println("strSlice size=", len(strSlice)) 
     fmt.Println("strSlice cap=", cap(strSlice))
    

    输出

    strSlice= [tom jack mary]
    strSlice size= 3
    strSlice cap= 3
    

    切片的遍历

    切片的遍历和数组一样,也有两种方式

  2. for 循环常规方式遍历

  3. for-range 结构遍历切片

    func main() {
    
     //使用常规的for循环遍历切片
     var arr [5]int = [...]int{10, 20, 30, 40, 50}
     slice := arr[1:4] // 20, 30, 40
     for i := 0; i < len(slice); i++ {
         fmt.Printf("slice[%v]=%v ", i, slice[i])
     }
    
     fmt.Println()
     //使用for--range 方式遍历切片
     for i, v := range slice {
         fmt.Printf("i=%v v=%v \n", i, v)
     }
    }
    

    切片的使用的注意事项和细节讨论

  4. 切片初始化时var slice = arr[startIndex:endIndex]说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])

  5. 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长.
    1. var slice = arr[0:end]可以简写 var slice = arr[:end]
    2. var slice = arr[start:len(arr)] 可以简写: var slice = arr[start:]
    3. var slice = arr[0:len(arr)] 可以简写: var slice = arr[:]
  6. cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素
  7. 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用
  8. 切片可以继续切片 ```go func main() {

    //使用常规的for循环遍历切片 var arr [5]int = […]int{10, 20, 30, 40, 50} slice := arr[1:4] // 20, 30, 40 slice2 := slice[1:2] // slice [ 20, 30, 40] [30] slice2[0] = 100 // 因为arr , slice 和slice2 指向的数据空间是同一个,因此slice2[0]=100,其它的都变化

    fmt.Println(“slice2=”, slice2) fmt.Println(“slice=”, slice) fmt.Println(“arr=”, arr)

}

输出
```go
slice2= [100]
slice= [20 100 40]
arr= [10 20 100 40 50]
  1. 用 append 内置函数,可以对切片进行动态追加

     //用append内置函数,可以对切片进行动态追加
     var slice3 []int = []int{100, 200, 300}
     //通过append直接给slice3追加具体的元素
     slice3 = append(slice3, 400, 500, 600)
     fmt.Println("slice3", slice3) //100, 200, 300,400, 500, 600
    
     //通过append将切片slice3追加给slice3
     //slice3... 表示将strss3的元素打散一个个append进slice3中
     slice3 = append(slice3, slice3...) // 100, 200, 300,400, 500, 600 100, 200, 300,400, 500, 600
     fmt.Println("slice3", slice3)
    

    输出

    slice3 [100 200 300 400 500 600]
    slice3 [100 200 300 400 500 600 100 200 300 400 500 600]
    

    数组与切片 - 图6
    💡切片 append 操作的底层原理分析

  • 切片 append 操作的本质就是对数组扩容
  • go 底层会创建一下新的数组 newArr(安装扩容后大小)
  • 将 slice 原来包含的元素拷贝到新的数组 newArr
  • slice 重新引用到 newArr
  • 注意 newArr 是在底层来维护的,程序员不可见.
  1. 切片的拷贝操作
     //切片使用copy内置函数完成拷贝,举例说明
     var slice4 []int = []int{1, 2, 3, 4, 5}
     var slice5 = make([]int, 10)
     copy(slice5, slice4)
     fmt.Println("slice4=", slice4)// 1, 2, 3, 4, 5
     fmt.Println("slice5=", slice5) // 1, 2, 3, 4, 5, 0 , 0 ,0,0,0
    
    对上面代码的说明:
  • copy(para1, para2) 参数的数据类型是切片
  • 按照上面的代码来看, slice4 和 slice5 的数据空间是独立,相互不影响,也就是说 slice4[0]= 999,slice5[0] 仍然是 1
  1. 关于拷贝的注意事项

    var a []int = []int{1,2,3}
    var slice = make([]int,1)
    fmt.Println(slice)//[0]
    //不会报错 可以运行,  最后输出的是 [1]
    copy(slice,a)
    fmt.Println(slice)//[1]
    
  2. 切片是引用类型,所以在传递时,遵守引用传递机制。 ```go

func test(slice []int) { slice[0] = 100 //这里修改slice[0],会改变实参 }

func main() { var slice = []int {1, 2, 3, 4} fmt.Println(“slice=”, slice) // [1,2,3,4] test(slice) fmt.Println(“slice=”, slice) // [100, 2, 3, 4] }

<a name="yXrpS"></a>
## string 和 slice

1. string 底层是一个 byte 数组,因此 string 也可以进行切片处理
```go
    //string底层是一个byte数组,因此string也可以进行切片处理
    str := "hello@abc"
    //使用切片获取到 abc
    slice := str[6:] 
    fmt.Println("slice=", slice)//slice= abc
  1. string 和切片在内存的形式,以 “abcd” 画出内存示意图

数组与切片 - 图7

  1. string 是不可变的,也就说不能通过str[0] = 'z' 方式来修改字符串

    str := "hello@abc"    
    str[0] = 'z' //[编译不会通过,报错,原因是string是不可变]
    
  2. 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string

     //如果需要修改字符串,可以先将string -> []byte / 或者 []rune -> 修改 -> 重写转成string
     // "hello@abc" =>改成 "zello@abc"
      str := "hello@abc"
      arr1 := []byte(str) 
      arr1[0] = 'z'
      str = string(arr1)
      fmt.Println("str=", str)//str= zello@abc
    
     // 细节,我们转成[]byte后,可以处理英文和数字,但是不能处理中文
     // 原因是 []byte 字节来处理 ,而一个汉字,是3个字节,因此就会出现乱码
     // 解决方法是 将  string 转成 []rune 即可, 因为 []rune是按字符处理,兼容汉字
    
     arr2 := []rune(str) 
     arr2[0] = '中'
     str = string(arr2)
     fmt.Println("str=", str)//str= 中ello@abc