GO语言学习心得day02| 青训营笔记

98 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第二天。今天学习了go语言的进阶、依赖管理和项目测试,经过前一天的练习,已经慢慢习惯了go语言的语法规则,也感觉到了自己有明显的编程思维上的转变。

数组、切片、map

 与java语言一样go语言中的数组、切片(动态数组)、map也是用来存储数据的工具,不一样的是,java中三者在函数中是引用传递,而在go语言中则为值传递,但go语言的开发者们通过巧妙的设计使得切片及map也可以达到类似于引用传递的效果,通过改变形参达到改变实参的效果,这种设计令我惊讶。

数组

 数组的声明类似于普通变量的声明:

var arr1 =[5]int{}
var arr2 =[5]int{1,2,3,4,5}
var arr3 [5]int 
arr4:=[5]int{1,2,4,5,6}

声明时数组的长度一定要输入,不然声明的就不是数组了而是切片,可以咋在声明时赋值,也可以不赋值,不赋值的话默认为0或者空字符串。还需要注意的是不同长度的数组类型是不同的不像java那样都为【类型或者【【类型,而是不同长度分别对应一种类型:

fmt.Printf("%T", arr1)//[5]int

切片

 切片即为可变长数组,顾名思义他是可以扩容的,类似于java中的集合,切片的声明:

var arr1 = []int{}
var arr2 = []int{1, 2, 3, 4, 5}
var arr3 = make([]int, 5, 5)
arr4 := make([]int, 5)

切片有两种声明方式,不指定数组长度及使用make方法。make方法是专门为切片、map、channel分配内存的方法,第一个参数为类型,第二个参数为int类型的可变长参数,一般为两个,第一个为切片的初始大小,第二个参数为初始容量,如为指定容量,则以初始长度作为容量。

切片的追加及扩容

可以使用append方法进行元素添加,该方法返回添加元素后的切片,所以应使用原切片进行接收:

arr4 = append(arr4, 1)

可以使用len()方法与cap()方法获取切片的长度和容量:

arr4 = append(arr4, 1)
arr4 = append(arr4, 1)
arr4 = append(arr4, 1)
fmt.Println("-----")
fmt.Print("len:", len(arr4), ",cap:", cap(arr4)) //len:8,cap:10

当长度等于容量时进行追加操作会触发切片的自动扩容,针对于常用数据类型,数据量小于1024时容量会扩大到原来的二倍,当需要的容量超过原切片容量的两倍时,会使用需要的容量作为新容量,并将数据拷贝到新的切片中。

切片的截取及拷贝

切片截取的语法:

arr5 := arr3[0:2]
arr6 := arr3[:2]
arr7 := arr3[0:]

切片用一个指针。两个参数来维护数组,第一个指针指向第一个元素,代表切片地址,第一个参数表示长度,最后一个表示最大容量,arr5,arr6,arr7只是将第一个指针指向了原切片的目标元素上,并没有申请新的空间来存放新切片,在原来的切片上继续进行维护,当arr3不在指向原切片时,由于arr5等切片的存在,原切片仍然属于可达对象,所以空间并不会释放, 切片拷贝的语法:


var arr8 []int 
copy(arr8, arr2)

将arr2中的值依次拷贝到arr8中,由于arr8是新申请的空间所以原切片可以释放空间。

map

 mapd的声明方式:

var map1 = map[string]int{}
var map2 = map[string]int{
   "name":1,
}
var map3=make(map[string]int,5)

与java类似map是无序的k,v键值对,并且同名key会覆盖,同时可像Python一样用range关键字来获取k,v配合for循环进行遍历。可以使用delete方法删除指定的key。map同样可以做到修改形参来改变实参。

并发编程

 go语言的优势就是并发能力突出,这都要归功于go语言引入了协程,虽然协程与线程在功能上相似,但两者对资源的消耗却天差地别,语言本身没有申请线程的能力,线程都是由操作系统创建的,而针对线程的操作非常消耗资源,协程解决了这个问题,协程之所以被称为用户态线程,是因为其可以由语言自己来生成,并将多个协程同时运行在主线程上,实现并行的目的。
 协程由go关键字创建:

func main() {
   m := func(i int) {
      fmt.Println(i)
   }
   for i := 0; i < 5; i++ {
      go m(i)
   }
   time.Sleep(time.Second)
}

 协程通过channel进行通信,channel的声明:

chan1 := make(chan int)
chan2 := make(chan int,5)

chan1为无缓冲通道,chan2为有缓冲通道,当协程向channel中添加数据时如果达到了缓冲上限则会阻塞该协程的添加操作,使用←符号进行数据添加。
 sync.Mutex包下提供了保证并发放安全的工具Lock,类似于java中的Lock类,其具有两个方法Lock和unLock方法,为一段代码上锁保证对同一数据操作的安全性。
 sync包下提供了一个辅助工具WaitGroup,它类似于JUC中的CountDownLatch,设置初始值后每次调用countDown方法会使值减一,知直到值为零该线程才会被唤醒,否则会被await方法阻塞,WaitGroup也提供了这三个方法Add()设置计数器的值,Down()计数器减一,Wait()阻塞该协程,通过该工具可以改进之前的代码为:

func main() { 
    var wg sync.WaitGroup 	
    wg.Add(5) 	
    m := func(i int) { 		
        defer wg.Done() 		
        fmt.Println(i) 	
        } 	
    for i := 0; i < 5; i++ { 		
        go m(i) 	
        } 	
    wg.Wait() 
 }

确保五个协程执行完成之后主线程在结束。

依赖管理

 go语言的依赖管理主要经过了三个版本的迭代GOPATH->GO VANDOR->GO MODULE,不同的版本使用的依赖不同。

GOPATH

 在GOPATH路径下有三个文件夹分别为src、pkg、bin,bin目录下主要存放项目编译产生的二进制文件,pkg下主要存放一些项目编译的中间产物,加速编译,src下存放项目的源码,我们通过go get下载到的依赖也都存放于此。
 其弊端也很明显,当我们两个项目同时依赖同一package的不同版本时,由于只能存在同一package的一个版本,所以会导致其中一个项目无法编译。

GO VANDOR

 在项目目录下建立一个vandor文件夹,存放该项目要使用的package,每个项目有每个项目的依赖文件夹,解决了项目间依赖版本的冲突问题,但新的问题又出现了,无法解决同一项目不同包之前的依赖版本问题。

GO MODULE

 GO MODUL很好的解决了这些问题,通过go mod指令来初始化项目,为项目生成go.mod文件,在该文件中管理go的版本和所有依赖,引入该文件的目的是:定义版本规则和管理项目依赖关系,如果项目子包想被单独引用,则需要通过单独的go init。

项目测试

 项目测试是代码开发非常重要的一环,通过测试来模拟项目上线后的种种突发情况来排除代码漏洞,避免上线后造成损失。测试大体分为单元测试,集成测试,回归测试,覆盖率依次增大,成本依次降低。

单元测试

 单元测试主要包括输入,输出,测试单元,校对,测试单元为我们想要测试的对象,它可以是函数,也可以是模块,校对则是用来比较输入与输出是否一致。测试文件要以_test.go结尾,测试方法要以Testxxx命名,并且参数为*testing.T,初始化逻辑要放在TestMain中。导入assert包后可以使用assert测试,使用assert.Equal方法,可以将输出和输入作为参数判断二者是否相同。代码覆盖率为使用给定的输入可以运行被参测试方法的行数占比,对于一个方法应多写几个测试方法,使用不同的输入进行测试,确保每种情况都覆盖。