go语言上手-工程实践|青训营笔记

112 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第 2 篇笔记

一、本堂课重点内容

1. go语言进阶

  1. go并发编程,go语言的突出点在于它自身支持的并发,而且通过go关键字创建的协程相较线程更加轻量级,协程之间的运行是串行的,同一时刻只有一个协程运行
  2. go不同协程之间的通信使用信道channel来实现,go推荐通信方式来共享内存而不是共享内存的方式来实现通信
  3. 因为协程的创建几乎不需要时间,协程运行时可能主线程已经结束,所以需要使用到WaitGroup协程计数器来保证协程结束之后再结束主线程

2. go工程依赖管理

  1. 最初的依赖管理是由$GOPATH进行的,即工程的环境变量,项目全部依赖于src下的代码,所有下载的依赖都和自己编写的一样放在src下,这种方式无法实现依赖包的多版本控制
  2. Go Vendor机制是将依赖副本放在vendor目录下,vendor找不到的才回去src找,这样就可以依赖多个版本,但是这种方式一定程度上解决了GoPATH的无法使用多个版本的问题,但是本质上还是依赖源码,没有多版本控制的概念
  3. go.mod类似于Java中的maven,是一个多版本依赖管理工具,通过这个工具就可以很好的控制多版本的依赖

3. go单元测试

  1. go的测试文件一般和源文件放在一起,以_test结尾,测试函数以Test开头
  2. go的mock测试用于在rpc连接时对远程接口的定义,提前依赖固定的数据,和前端的mock测试是一样的
  3. 基准测试可以获得代码的执行时间等性能参数,用于定位和解决性能瓶颈,测试方法以Benchmark开头

4. 简单web工程需求实践

  1. 实现一个简单的帖子评论的需求

二、详细知识点介绍:

go并发编程

1. 并发与并行:

1)并发是指在单核上多线程程序通过切片的方式交替执行,旨在一个时间段内多个程序执行
2)并行则时指在多核的计算机上,多线程同一时刻一起执行

2. go创建协程:

1)协程:用户态的,轻量级线程,栈KB级别
2)线程:内核态的,一个线程跑多个协程,栈MB级别
3go语言实现并发是通过协程的形式,这种方式非常适合多核计算机,这也是go非常适合高并发的原因
4go语言开启协程的方式:在函数前面使用go关键字,这样后面的函数就会创建一个协程来进行  

3. 协程通信

1go中具有一个数据类型channel,信道,这个就是为了协程之间互相的通信,信道类似于一个队列
2)无缓冲信道:创建信道使用make的方式,如果make的时候没有指定大小,那么创建的就是无缓冲信
   道,如果没有这个信道生产和消费是同步的,也就是说如果消费端没有准备好消费,生产端就不能
   发送信息
31缓冲信道:创建信道时指定容量为1,则该信道最多存储一个数据,这个特性就可以用来实现一个锁,
   即在加锁代码前面往信道中添加数据,添加成功后执行代码,代码结束后消费信道的数据,这样就能
   保证代码加锁执行
4)多缓冲信道:创建信道指定多个容量,主要用于协程之间的通信,协程之间通过这种方式通信来共享
   他们的内存,而不推荐通过共享内存的方式(即创建一个所有协程都能访问的空间)来通信
5)信道的存取值使用符号“<-”来实现,如chan <- 1表示将1 加入信道;temp <- chan表示从信道取出
   一个数据
6)信道可以通过for-range来遍历信道中的数据

4. 并发安全

1)在多个协程对一个全局变量进行操作的时候就需要考虑到并发安全问题,在sync包下就有各种锁对象
2)如sync.Mutex类型是一个互斥锁,同时只能有一个协程对其加锁、sync.RWMutex是一个读写互斥锁
   可以有多个读访问和唯一的一个写访问

5. 线程与子协程同步

1)因为子协程创建完成之后,线程立刻执行下面的代码而不会等待协程执行完毕,所以可能协程没有执
   完毕线程就结束了,使用协程计数就可以解决这一问题
2)同在sync包下,sync.WaitGroup像一个协程计数器,在创建协程的时候这个计数器+1,一个协程及执
   执行结束后通过Done函数将计数器-1,在线程最后调用Wait函数进行阻塞,等待所有协程执行完毕

go项目依赖管理-gomod

1)依赖管理三要素:依赖描述的配置文件:go.mod(pom.xml);中心仓库管理依赖库:proxy(mavenrepo)
   本地管理工具:go get/mod(mvn)
2)依赖配置文件:依赖管理基本单元(module+当前模块的路径);go原生标准库版本;单元依赖,使用
   require括起来,每一个依赖标识为:依赖的模块路径+版本号+标识关键字,标识关键字如:
   // indirect表示当前模块对这个依赖没有直接依赖,而是通过别的依赖间接依赖的;+
3)版本格式规则:${MAJOR}.${MINOR}.${PATCH},MAJOR表示前后不兼容的大版本,MINOR表示前后兼容
   的小版本,PATCH表示小的bug修复
4)依赖分发:由github、svn等中央仓库分发到proxy,再由proxy到本地项目,类似于maven的本地仓库
   配置GOPROXY="https://proxy1.cn,https://proxy2,direct" 表示拉取依赖的站点direct表示源站
5)go get工具:@update表示拉取默认最新的版本;@none表示删除该依赖;@v1.1.2表示指定语义版本
   @commit标识表示拉取特定的commit;@分支表示拉取特定的分支
6)go mod工具:init表示初始化创建一个go.mod文件;download表示下载模块到本地缓存;tidy表示
   清理项目依赖,删除不需要的,下载需要的,推荐项目打包前都执行一次

测试

单元测试

go标准库的testing包下提供了单元测试的函数,在执行go test指令的时候执行所有的以Test开头的函
数,不同于Java的单元测试需要借助于junit库,go的单元测试标准库就具备

mock测试

1)在可能涉及到网络连接,如请求db、cache、file等时,直接使用单元测试去调用是不稳定的,所以
   就需要用到mock测试,来模拟数据的返回
2)mock测试需要依赖其他库,比如https://github.com/bouk/monkey 通过Patch函数对原函数进行打
   桩,再通过UnPatch进行取消;在测试的时候就调用打桩函数里面的数据

基准测试

1)在实际项目中可能遇到代码性能问题,对代码性能进行观测就是基准测试做的事情
2)通过go test -bentch flag可以运行所有Benchmark开头的函数,执行之后可以看到cpu的耗时

三、实践练习例子:

1. go并发编程

协程使用

package main

import (
	"fmt"
	"sync"
)

func one(num int){
	fmt.Println("这是协程",num)
}
func main(){
	//声明一个WaitGroup变量
	var wait sync.WaitGroup
	//计数添加10个协程,表示将有10个协程存在
	wait.Add(10)
	//for循环创建多个协程
	for i := 0 ; i < 10 ; i++ {
		//创建一个匿名函数调用one函数
		go func(i int){
			one(i)
			//每个协程执行结束之后计数器-1
			wait.Done()
		}(i)
	}
	//线程阻塞等待协程计数器为0
	wait.Wait()
}

协程通信

package main

import "fmt"

func main(){
	//创建两个容量为10的信道
	var src = make(chan int,10)
	var dest = make(chan int,10)

	//创建一个协程向src信道添加十个数字
	go func ()  {
		//执行结束后关闭src信道
		defer close(src)
		for i:=0;i<10;i++ {
			src <- i
		}
	}()
	
	//从src信道取出数字平方计算后存入dest信道中
	go func ()  {
		//执行结束后关闭dest信道,关闭后不能再往信道中存数据
		defer close(dest)
		for i := range src {
			dest <- i*i
		}	
	}()

	//向dest信道去除数字并打印
	for i := range dest {
		fmt.Println(i)
	}
}

加锁

package main

import (
	"fmt"
	"sync"
)
var (
	lock sync.Mutex
	x int
	wait sync.WaitGroup
)
//带有锁的方法
func withLock(){
	//计数10个协程
	wait.Add(10)
	for i:=0;i<10;i++ {
		go func (i int)  {
			defer wait.Done()
			lock.Lock()
			x ++;
			lock.Unlock()
		}(i)
	}
}
//不带锁的方法
func withoutLock(){
	//计数10个协程
	wait.Add(10)
	for i:=0;i<10;i++ {
		defer wait.Done()
		go func (i int)  {
			x ++;
		}(i)
	}
}
func main(){
	fmt.Println(x)
	// withoutLock()
	withLock()
	wait.Wait()
	//在协程执行完毕后执行输出x
	//结果是带有锁的最后x是10,而不带锁的每次x都不一致,
	fmt.Println(x)
}

单元测试

package unit

func Hello()  string{
	return "hello"
}
package unit

import (
	"testing"
	"fmt"
)
func TestHello(t *testing.T)  {
	output := Hello()
	fmt.Println(output)
}

四、课后个人总结:

  1. go语言并发编程是通过协程来进行的,Java中的是通过线程,一个线程可以有多个子协程,协程更加轻量级,这就是go更加适合高并发的原因
  2. go的协程在使用过程中需要格外的注意并发问题,对于公共变量需要加锁访问,以免一致性出现问题

五、引用参考:

Go语言标准库

进程协程与线程之间的关系

详解信道