前言
- 我是一名多年Java开发人员,因为工作需要现在要学习go语言,Go语言之路是一个系列,记录着我从0开始接触Go,到后面能正常完成工作上的业务开发的过程,如果你也是个小白或者转Go语言的,希望我这篇文章对你有所帮助。
- 有关go其他基础的内容的文章大家可以查看我的主页,接下来主要就是把这个系列更完,更完之后我会在每篇文章中挂上连接,方便大家跳转和复习。
在java中,大家最常用的就是list和map,而在go中,最常用的就是切片和map,下面我就一一介绍一下它们的用法。
一、数组
-
用var定义一个数组,var后面跟数组名字,后面中括号代表数组容量(容量一定要是常量),最后面代表数组值的数据类型
var array [5]int
-
初始化数组
其实在第一步中,我们用var声明的时候,已经相当于初始化了数组,这样声明出来的数组,里面数据全是0,容量为5
我们还可以用值等的方式去初始化:func main() { array := [5]int{1, 2, 3, 4, 5} fmt.Println(array) } //输出:[1 2 3 4 5]
-
获取数组长度 、容量
获取长度,我们需要用到len函数,容量需要用到cap函数,不过数组的长度和容量相等的,容量对切片才有意义。fmt.Println(len(array)) fmt.Println(cap(array)) //输出:5
-
根据下标操作元素
func main() { array := [5]int{1, 2, 3, 4, 5} fmt.Println(array[1]) array[1] = 11 fmt.Println(array[1]) }
-
遍历
数组的遍历,和下面切片的遍历,都可以用上篇文章我们讲的for range来实现func main() { array := [5]int{1, 2, 3, 4, 5} for _, value := range array { fmt.Println(value) } }
-
数组的切割
数组可以根据下标去切割,区间为左闭右开,切割后,就变成了切片func main() { array := [5]int{1, 2, 3, 4, 5} array1 := array[1:3] fmt.Println(array1) } //输出:[2 3]
上面示例中,经过我们切割后的array1,就是一个切片,我们来打印一下数据类型:
fmt.Printf("%T", array) fmt.Printf("%T", array1) //输出:[5]int []int
[5]int是一个数组,[]int是一个切片
要注意,这时候的切片array1和数组array指向的是同一片内存,我们修改切片中的内容,也会修改到数组中的值,看代码:func main() { array := [5]int{1, 2, 3, 4, 5} slice := array[1:3] fmt.Println(array[1]) slice[0] = 88 fmt.Println(array[1]) } //输出: 2 88
如果我们要对切片做更改怎么办,可以用slices的Clone函数,我们拷贝一个新的切片出来,避免修改到同一个内存中的数据
func main() { array := [5]int{1, 2, 3, 4, 5} array1 := array[1:3] clone := slices.Clone(array1) clone[0] = 88 fmt.Println(array[1]) } //输出:2
数组是值类型,并且不能扩容
二、切片
定义:和数组几乎一模一样,但是又有着很明显的区别,那就是切片可以动态扩展,而数组当你定义好长度后,就不能动态去扩展了。而且数组是值类型,切片是引用类型,切片的底层实现依旧是数组,可以简单理解为是指向底层数组的指针。切片我们在实际开发中用的最多,所以我们就详细来讲一下,用法和数组基本一致,会操作切片了,也就会操作数组了。
-
定义一个切片,有如下4几种方法
var slice[]int // 值 slice := []int{1, 2, 3} // 值 slice := make([]int, 0, 0) // 值 slice := new([]int) // 指针
通过上面数组的第六点和现在的代码,我们可以看到切片和数组定义几乎一模一样,只是没有在中括号中去声明数组的长度
-
make函数
通常情况下,我们用make函数来定义一个切片,下面要讲的map也是,都用make函数,make函数接受3个参数,第一个参数代表定义的数据类型,第二个参数代表长度,第三个参数代表的是容量func main() { slice := make([]int, 0, 0) // 值 slice2 := make([]int, 4, 40) // 值 fmt.Println(len(slice), cap(slice)) fmt.Println(len(slice2), cap(slice2)) } //输出: 0 0 4 40
怎么去理解长度和容量呢,长度就是代表着切片当前的数据长度,而容量代表着切片允许最大的数据长度,一旦超过容量,就要进行扩容,就像水库蓄水一样。
-
new关键词
既然这里提到了,就简单说一下,new关键词跟java不一样,不是创建一个新的对象出来,而是创建一个指针,后面会详细说指针。 -
append函数向尾部添加元素
切片的下标读取、修改、for循环遍历等和数组完全一样,这里就不写重复代码了,重点提一下他们的区别,如何动态扩容:append函数
append向末尾添加新元素,并且返回一个切片func main() { slice := make([]int, 0, 0) // 值 slice = append(slice, 1) fmt.Println(slice, len(slice), cap(slice)) } //输出:[1] 1 1
我们即使定义一个长度和容量都为0的切片,用append添加一个元素后,会动态扩容为1的切片。这里再简单提一下返回的新切片的扩容策略:在 1.18 版本更新之后,slice 的扩容策略变为了: 当原 slice 容量(oldcap)小于 256 的时候,新 slice(newcap)容量为原来的 2 倍;原 slice 容量超过 256,新 slice 容量 newcap = oldcap+(oldcap+3*256)/4,这个大家了解一下就行了,一般只有面试才能用到。
因为这个扩容机制,所以才会出现长度和容量不一致的情况,这都是正常的内部算法。 -
append插入元素
从头部插入元素func main() { slice := []int{1, 2, 3, 4, 5} slice = append([]int{-1, 0}, slice...) fmt.Println(slice) }
从中间插入元素,比如我们是从下标为3开始插入
func main() { slice := []int{1, 2, 3, 4, 5} slice = append(slice[:3+1], append([]int{999, 999}, slice[3+1:]...)...) fmt.Println(slice) }
尾部插入
尾部插入就是我们上面已经使用过的,直接调用就好,可以支持一次性插入多个func main() { slice := []int{1, 2, 3, 4, 5} slice = append(slice, 6, 7, 8) fmt.Println(slice) }
我们如果仔细去阅读一下头部插入和中间插入的代码,无非就是定义一个新的切片,然后组装老的切片,以实现插入的效果。
-
删除元素
定义以下切片slice := []int{1, 2, 3, 4, 5}
-
删除头部和尾部,直接用上面讲到的数据切割的知识点就行了。
//删除头部2个元素 slice = slice[2:] //删除尾部2个元素 slice = slice[:3] //输出分别为:[3 4 5] 和[1 2 3]
-
从中间指定下标 i 位置开始删除 n 个元素
这个就要借助append函数,本质上还是和插入一样,新切片的组装,这点就是要吐槽的一点,没有官方的api封装,这个使用起来很是麻烦。
slice = append(slice[:i], slice[i+n:]...)
-
-
拷贝
从一个切片拷贝到另一个切片,切记目标切片的长度一定要大于等于源切片func main() { source := []int{1, 2, 3, 4, 5} target := make([]int, 5, 10) copy(target, source) fmt.Println(target) }
就像这个例子,我们定义target的时候,它的长度一定要大于等于5,才能拷贝全,如果我们定义长度为4,那么拷贝后就是[1,2,3,4],5拷贝不进来。
-
遍历
上面讲数组说过了,主要常用两种方式,fori和for range,直接看代码:source := []int{1, 2, 3, 4, 5} for i := 0; i < len(source); i++ { fmt.Println(source[i]) } for _, v := range source { fmt.Println(v) }
-
清空数组或者切片
clear函数source := []int{1, 2, 3, 4, 5} clear(source)
-
二维数组或者切片
这里简单提一下,其实和java差不多,只是要结合go来使用
分别定义一个二维的数组和二维切片func main() { aa := [5][5]int{} fmt.Println(aa) bb := make([][]int, 5) fmt.Println(bb) } //输出: [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]] [[] [] [] [] []]
通过这里大家也看到了,数组长度是固定的,所以定义出来就是好的,但是切片不一样,所以切片的多维我们要循环单独定义
func main() { bb := make([][]int, 5) for i := range bb { bb[i] = make([]int, 5) } fmt.Println(bb) } //输出: [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]
三、map
go中map的术语叫做映射,它和java的map基本一致,都是k,v结构,只是用法有细微的差别,我给你简单举几个例,相信你很快就能掌握它
-
我们先来用var和make函数定义一个映射
//定义一个key是string类型,value是string类型的一个映射 var a map[string]string //定义一个key是string类型,value是int类型的一个映射 a := make(map[string]int, 5)
可以看到,map的语法:map[keyType]valueType{},然后后面跟一个初始容量,默认是0
-
map的添加和读取
map的添加和读取都是通过key索引来做的,就跟数组一样,非常简单func main() { a := make(map[string]int, 5) a["one"] = 1 a["two"] = 2 fmt.Println(a) i := a["one"] fmt.Println(i) } //输出: map[one:1 two:2] 1
map的访问是有返回的,可以用来判断是否存在exist,如果不存在就返回false和默认值,int默认值是0
func main() { a := make(map[string]int, 5) a["one"] = 1 val, exist := a["f"] fmt.Println(exist,val) } //输出:false,0
-
map的删除和清空
使用go的两个内置函数,delete和clearfunc main() { a := make(map[string]int, 5) a["one"] = 1 a["two"] = 2 delete(a,"one") clear(a) }
-
map的遍历
也是我们的老朋友,for range,range迭代两个参数,一个k一个vfunc main() { a := make(map[string]int, 5) a["one"] = 1 a["two"] = 2 for k, v := range a { fmt.Println(k, v) } }
map不是并发安全的,go中有sync.Map来做并发安全的map,类似于java的ConcurrentHashmap
好了,以上就是本节全部内容了,下一篇,我们将开始了解go中的指针和结构体的知识点。