
文章目录1. 数组和切片2. 数组2.1 初始化2.2 遍历2.3 数组值传递2.4 指针传递3. 切片3.1 初始化3.2 空切片和 nil 切片3.3 切片传参3.4 切片扩容机制3.5 new 和 make 创建切片3.6 切片重组3.7 切片复制3.8 字符串切片4. 小结本系列文章Java 转 go 学习 - 项目管理Java 转 go 学习 - 基本语法Java 转 go 学习 - 类型转换Java 转 go 学习 - 流程控制结构上面几篇文章学习了一些基本的语法这篇文章就来看下数组和切片类比 Java 的数组和 ArrayList。1. 数组和切片下面先来对比下 Java 数组、Go 数组和 Go 切片特性Java 数组Go 数组Go 切片类型本质引用类型值类型引用类型底层指针长度固定固定是类型的一部分动态可变扩容需手动或 ArrayList不支持自动append函数传参引用传递值拷贝引用传递零值null元素零值填充nil2. 数组2.1 初始化Go 的数组初始化和 Java 不一样有下面几种方式指定数组长度初始化数组长度和下标值只传入数组长度编译器推断长度指定下标初始化packagemainimportfmtfunctest1(){// 1. 指定长度vararr1[5]int// 2. 传入初始化值arr2:[3]int{1,2,3}// 方式3编译器推断长度arr3:[...]int{1,2,3,4,5}// 方式4指定索引初始化arr4:[5]int{0:10,2:30}fmt.Println(arr1)// [0 0 0 0 0]fmt.Println(arr2)// [1 2 3]fmt.Println(arr3)// [1 2 3 4 5]fmt.Println(arr4)// [10 0 30 0 0]}funcmain(){test1()}2.2 遍历数组的遍历比较简单可以通过下标和 range 遍历functest2(){arr:[5]int{1,2,3,4,5}// 下标遍历fori:0;ilen(arr);i{fmt.Println(arr[i])}// range 遍历for_,value:rangearr{fmt.Println(value)}}funcmain(){test2()}2.3 数组值传递Java 中的数组是对象类型传递数组是引用传递但是 Go 中的数组是值类型也就是传递是值传递赋值 / 传参时会复制整个数组修改副本不影响原数组下面来看下示例值复制functest3(){arr1:[5]int{1,2,3,4,5}arr2:arr1 arr2[0]100fmt.Println(arr1)// [1 2 3 4 5]fmt.Println(arr2)// [100 2 3 4 5]}上面例子中是把 arr1 的 3 个元素复制到 arr2 的内存空间了两者是完全不同的数组这里是最简单的赋值那么对于函数传参也是一样的函数接收数组参数时同样复制整个数组在函数内修改数组的值是不会影响原数组的funcmodifyArr(arr[3]int){arr[0]999fmt.Println(arr)// [999 2 3]}functest4(){arr:[3]int{1,2,3}modifyArr(arr)fmt.Println(arr)// [1 2 3]}如果是 Java 将数组传入一个方法去修改那么原数组也会被修改因为 Java 的数组是对象传参属于引用传递由此就有一个注意事项了⚠️如果数组元素很多比如 [100000]int值传递会复制所有元素导致内存占用翻倍、性能下降。2.4 指针传递既然直接将数组传入会导致元素复制那么有没有办法将原始数组传进函数呢指针我们知道指针传递的其实激就是原始数组的地址那么将数组指针传进方法本质上就是将数组的地址传进去这种情况下不需要复制整个数组而是只复制数组的地址8 字节64 位系统开销大大减少。有了数组地址方法内通过指针就能访问到原始数组对原始数组进行修改了下面是一个例子// 指针传递funcmodifyArrPointer(arr*[3]int){arr[0]999fmt.Println(*arr)// [999 2 3]}functest5(){arr:[3]int{1,2,3}modifyArrPointer(arr)fmt.Println(arr)// [999 2 3]}除了数组指针下面要讲到的切片不需要用指针的方式传递也可以因为切片(int[])属于引用类型传参仅仅复制切片头指针、长度、容量开销24字节下面就来看看切片是什么。3. 切片3.1 初始化切片可以看成是数组一个连续片段的引用同样的既然是一段子数组那么也可以通过len函数和for去遍历不过先看写如何创建一个切片。首先切片的声明格式是var identifier []type不需要写明长度可以动态扩容没有初始化之前默认就是nil容量为0。其次是初始化格式是var slice []type arr[start:end]这种方式就是利用数组来创建一个切片默认范围是[start, end)如果想要截取全部数组元素可以用var slice []type arr[:]下面来看下例子functest6(){// 原数组arr:[5]int{10,20,30,40,50}// 截取 [1, 2] 范围的数据s:arr[1:3]fmt.Printf(切片: %v, 长度: %d, 容量: %d\n,s,len(s),cap(s))// 切片: [20 30], 长度: 2, 容量: 4// 修改切片, 由于切片和数组共享内存地址, 所以下面切片修改数组也会修改s[0]200fmt.Println(修改后的切片: ,s)// [200 30]fmt.Println(修改后的数组: ,arr)// [10 200 30 40 50]}下面是字面量创建语法格式切片变量 : []类型{元素1, 元素2, ...}这样的好处就是不需要指定长度直接根据元素值来初始化。functest7(){// 1. int 类型s1:[]int{1,2,3}fmt.Printf(内容: %v, 长度: %d, 容量: %d\n,s1,len(s1),cap(s1))// 内容: [1 2 3], 长度: 3, 容量: 3// 2. string 类型s2:[]string{张三,李四}fmt.Printf(内容: %v, 长度%d, 容量: %d\n,s2,len(s2),cap(s2))// 内容: [张三 李四], 长度2, 容量: 2// 3. 空字面量切片s3:[]int{}fmt.Printf(内容: %v, 是否 nil: %t\n,s3,s3nil)// 内容: [], 是否 nil: false}然后是使用make创建这样的创建方式可以指定长度和容量预分配容量避免频繁扩容。functest8(){// 指定长度s1:make([]int,3)fmt.Printf(s1: %v, len: %d, cap: %d\n,s1,len(s1),cap(s1))// s1: [0 0 0], len: 3, cap: 3// 指定长度 容量, 长度是当前数组长度, 容量是最大容量, 扩容不会超过最大容量s2:make([]string,3,5)// s2: [ ], len: 3, cap: 5fmt.Printf(s2: %v, len: %d, cap: %d\n,s2,len(s2),cap(s2))// 仅能给长度范围内的索引赋值s2[0]hellos2[1]world// s2[4] 123 报错 index out of range [4] with length 3fmt.Printf(s2[0]: %s, s2[1]: %s, s2[2]: %t,s2[0],s2[1],s2[2])// s2[0]: hello, s2[1]: world, s2[2]: true}最后我们也可以基于现有切片来创建一个新的切片这个新的切片的容量是范围长度end - start 1且这个新的切片和原来的切片共享底层数组下面是例子functest9(){// 原切片s1:[]int{1,2,3,4,5}arr:append(s1,2)fmt.Println(arr)// [1 2 3 4 5 2]// 基于切片创建切片 [2, 3], 容量: 3s2:s1[2:4]// 容量: 5s3:s1[:]fmt.Printf(s2: %v, len: %d, cap: %d\n,s2,len(s2),cap(s2))// s2: [3 4], len: 2, cap: 3fmt.Printf(s3: %v, len: %d, cap: %d\n,s3,len(s3),cap(s3))// s3: [1 2 3 4 5], len: 5, cap: 5// 修改切片s2[0]300fmt.Println(s1: ,s1)// s1: [1 2 300 4 5]fmt.Println(s2: ,s2)// s2: [300 4]}上面这种基于切片创建的例子如果想要不共享内存可以使用copy函数。functest11(){s1:[]int{1,2,3}s2:make([]int,len(s1))// 将 s1 拷贝 s2copy(s2,s1)s2[0]100fmt.Println(s2)// [100 2 3]fmt.Println(s1)// [1 2 3]}3.2 空切片和 nil 切片下面再来看下空切片和nil 切片的对比nil 切片仅声明未初始化无底层数组len0、cap0 nil为true语法var nilSlice []int。空切片: 字面量[]T{}或者make([]T, 0)创建出底层数组只不过len0、cap0、 nil为false。下面是例子functest10(){// nil 切片varnilSlice[]intfmt.Printf(nilSlice: %v, is nil: %t\n,nilSlice,nilSlicenil)// nilSlice: [], is nil: true// 空切片emptySlice1:[]int{}fmt.Printf(emptySlice1: %v, is nil: %t\n,emptySlice1,emptySlice1nil)// emptySlice1: [], is nil: falseemptySlice2:make([]int,0)fmt.Printf(emptySlice2: %v, is nil: %t\n,emptySlice2,emptySlice2nil)// emptySlice2: [], is nil: false}3.3 切片传参切片传参属于引用传参跟数组不一样比如下面的图图片来自数组与切片。引用传参就意味着可以直接在函数里面修改切片值例子如下// 函数修改funcmodifyArray(arr[3]int){arr[0]999}// 切片修改funcmodifySlice(s[]int){s[0]999}functest12(){// 数组传参 - 值拷贝arr:[3]int{1,2,3}modifyArray(arr)fmt.Println(数组:,arr)// 数组: [1 2 3]// 切片传参 - 引用传递s:[]int{1,2,3}modifySlice(s)fmt.Println(切片:,s)// 切片: [999 2 3]}3.4 切片扩容机制当切片长超过cap的时候自动分配新的底层数组然后将原元素赋值到新数组切片指针指向新数组Go 1.18前无论容量大小统一按照容量 * 2 来扩容。在Go 1,18之后切片的扩容规则如下小容量快速翻倍(Cap ≤ 256)如果原来的容量 256新容量就是原来容量的2倍目的就是在容量比较小的时候减少频繁的内存分配malloc操作对比Java的1.5倍比较激进。当原来的容量cap 256时进入平滑增长模式新容量约为原容量的1.25倍计算结果向上取整同时会经过内存对齐优化。但是在极端情况如果一次性append了大量数据跳过上面的计算公式新容量 当前容量 新增的元素数量。同时为了按照内存页 / 块对齐较少内存碎片和提高CPU 缓存命中率Go 的内存分配器会将申请的内存大小对齐到特定的Size Class或内存页边界这样就会导致最终分配的容量比计算出来的略大。下面来看下例子functest13(){// 创建一个初始容量很小的切片s:make([]int,0,1)fmt.Println(测试扩容...)fmt.Printf(初始: len%d, cap%d\n,len(s),cap(s))// 循环 append 直到容量超过 256观察变化fori:0;i1000;i{oldCap:cap(s)sappend(s,i)newCap:cap(s)// 如果新容量和旧容量不一样, 发生扩容了, 打印ifnewCap!oldCap{ratio:float64(newCap)/float64(oldCap)fmt.Printf(索引 %d: 容量从 %d - %d (倍数: %.2f)\n,i,oldCap,newCap,ratio)}}// 测试扩容...// 初始: len0, cap1// 索引 1: 容量从 1 - 2 (倍数: 2.00)// 索引 2: 容量从 2 - 4 (倍数: 2.00)// 索引 4: 容量从 4 - 8 (倍数: 2.00)// 索引 8: 容量从 8 - 16 (倍数: 2.00)// 索引 16: 容量从 16 - 32 (倍数: 2.00)// 索引 32: 容量从 32 - 64 (倍数: 2.00)// 索引 64: 容量从 64 - 128 (倍数: 2.00)// 索引 128: 容量从 128 - 256 (倍数: 2.00)// 索引 256: 容量从 256 - 512 (倍数: 2.00)// 索引 512: 容量从 512 - 848 (倍数: 1.66)// 索引 848: 容量从 848 - 1280 (倍数: 1.51)}runtime/malloc.go中定义了一系列的内存档位这里先不去探讨这里是为什么倍数不对跟 go 的内存分配有关以后会分析现在还是先来看下基本功能吧。3.5 new 和 make 创建切片上面3.1也看到可以用make创建一个切片创建的语法为func make([]T, len, cap)也可以写成slice : make([]type, len)如果是这样不设置cap容量的这种情况下创建出来的消息len cap。functest14(){slice:make([]int,50)fmt.Printf(len: %d, cap: %d\n,len(slice),cap(slice))// len: 50, cap: 50}而通过new也可以创建切片语法如下functest15(){slice:new([100]int)[0:50]fmt.Printf(len: %d, cap: %d\n,len(slice),cap(slice))// len: 50, cap: 100}但是如果仅仅是通过slice : new([]int)创建的切片不能直接使用还需要初始化new(T)为每个新的类型 T 分配内存然后初始化为 0并不会分配底层数组返回的 slice 是*T类型的内存地址相当于T。make(T)返回一个类型为 T 的初始值不需要初始化可以直接使用下面来看例子functest16(){s1:new([]int)fmt.Printf(s1 类型: %T\n,s1)// s1 类型: *[]intfmt.Printf(s1 值: %v\n,*s1)// s1 值: []fmt.Printf(s1 nil: %v\n,*s1nil)// s1 nil: true// 手动初始化*s1make([]int,5)// 下面两种初始化// *s1 []int{1, 2, 3}// *s1 []int{}// 初始化之后可以使用(*s1)[0]100fmt.Println(*s1)// [100 0 0 0 0]}可以看到new创建出来的切片默认指向的数组是nil也就是还没有初始化需要我们手动初始化才行这两种方式创建出来的内存地址如下所示一般来说很少用new 手动初始化的都是用 makenew 比较适合值类型比如数组。3.6 切片重组切片重组就是基于现有切片进行重新切片的操作本质上就是基于同一个底层数组创建新的切片视图下面是例子functest17(){// 原始切片s:[]int{1,2,3,4,5}// 切片重组s2:s[2:4]// 下标 [2, 4)s3:s[:3]// 下标 [0, 3)s4:s[1:]// 下标 [1, 5)fmt.Println(s2)// [3 4]fmt.Println(s3)// [1 2 3]fmt.Println(s4)// [2 3 4 5]}内存结构如下3.7 切片复制切片复制可以使用copy函数来复制复制的元素个数是from和to切片的最小值来看下面例子functest18(){slice_from:[]int{1,2,3,4,5}slice_to1:make([]int,len(slice_from))slice_to2:make([]int,len(slice_from)1)slice_to3:make([]int,len(slice_from)-1)// 将 slice_from 切片的数据拷贝到 slice_ton1:copy(slice_to1,slice_from)n2:copy(slice_to2,slice_from)n3:copy(slice_to3,slice_from)fmt.Println(slice_to1)fmt.Println(slice_from)fmt.Println(拷贝的元素数: ,n1)// [1 2 3 4 5]// [1 2 3 4 5]// 拷贝的元素数: 5fmt.Println(slice_to2)fmt.Println(slice_from)fmt.Println(拷贝的元素数: ,n2)// [1 2 3 4 5 0]// [1 2 3 4 5]// 拷贝的元素数: 5fmt.Println(slice_to3)fmt.Println(slice_from)fmt.Println(拷贝的元素数: ,n3)// [1 2 3 4]// [1 2 3 4 5]// 拷贝的元素数: 4}3.8 字符串切片字符串本质上就是字节数组所以可以先转成字节数组[]byte然后再获取切片如下例子所示functest19(){str:helloslice:[]byte(str)[2:4]// llfmt.Println(string(slice))}4. 小结本文学习了 go 语言的切片和数组的不同和相关用法下一篇文章就来看下 map 的相关用法。如有错误欢迎指出