Go 语言进阶 - 工程进阶 | 青训营笔记

68 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

语言进阶

协程:用户态,轻量级线程,栈空间KB级别,切换开销小 线程:内核太,一个线程可以运行多个协程,栈空间MB级别

启动一个协程

在go里面启动一个协程非常方便简单,只需要在调用函数前添加go关键字

func sayHello(i int) {
   fmt.Println("hello :", i)
}

func Goroutine() {
   for i := 0; i < 5; i++ {
      go func(j int) {
         sayHello(j)
      }(i)//匿名函数后跟括号,表示立即执行该函数,括号内是匿名函数对应的需要的参数
   }
   time.Sleep(time.Second)
}

可能的运行结果:

hello : 3
hello : 0
hello : 1
hello : 4
hello : 2

什么是Channel

channel是Go语言中的一个核心类型,可以把它看成管道。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。
channel是一个数据类型,主要用来解决go程的同步问题以及协程之间数据共享(数据传递)的问题。
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存,而不是共享内存来通信。引用类型 channel可用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。

创建一个Channel

make(chan 元素类型, [缓冲大小])

  • 无缓冲通道 make(chan int)
  • 有缓冲通道 make(char int, 2)

无缓冲通道:当读端正在使用时,写端被阻塞,所以一次只能传输一个数据
无缓冲通道:当缓冲区被读空时,才会被阻塞;当缓冲区被填满后,写端才会阻塞

下面这个例子中通过生产者协程发送0~9的数字
再通过消费者协程计算输入数字的平方
最后再主协程中输出结果

func CalSquare() {
   src := make(chan int)
   dest := make(chan int, 3)
   go func() { //生产者
      defer close(src)//关闭channel
      for i := 0; i < 10; i++ {
         src <- i
      }
   }()
   go func() { //消费者
      defer close(dest)
      for i := range src {
         dest <- i * i
      }
   }()
   for i := range dest {
      println(i)
   }
}

并发安全

控制并发安全,我们一般都是使用的mutex来对共享内存进行加锁访问
下面让我们看一下go中如何实现

var (
   x    int64
   lock sync.Mutex
)

func addWithLock() {
   for i := 0; i < 2000; i++ {
      lock.Lock()
      x += 1
      lock.Unlock()
   }
}

func addWithoutLock() {
   for i := 0; i < 2000; i++ {
      x += 1
   }
}

func Add() {
   x = 0
   for i := 0; i < 5; i++ {
      go addWithoutLock()
   }
   time.Sleep(time.Second)
   println("without lock:", x)

   x = 0
   for i := 0; i < 5; i++ {
      go addWithoutLock()
   }
   time.Sleep(time.Second)
   println("with lock:", x)
}

可能的结果:

without lock: 7489
with lock: 10000

WaitGroup

我们常使用WaitGroup来阻塞主协程,直到我期待的子协程全部执行完毕

func GoWaitGroup() {
   var wg sync.WaitGroup
   goroutineCnt := 5
   wg.Add(goroutineCnt)//初始化一个计数器,如果为负数,触发panic
   for i := 0; i < goroutineCnt; i++ {
      go func(j int) {
         defer wg.Done()//完成一个任务,将计数值减一
         sayHello(j)
      }(i)
   }
   wg.Wait()//等待所有子协程结束
   fmt.Println("main goroutine is end!!!")
}

可能的结果:

hello : 4
hello : 1
hello : 2
hello : 3
hello : 0
main goroutine is end!!!

依赖管理

依赖:即他人已经开发和封装好的包,在复杂项目中,我们需要引入各种依赖

Go依赖管理的发展

graph LR
GOPATH --> Go_Vender  --> Go_Module

现如今我们多用GO Module进行包管理

graph LR
go_mod --> init:初始化go.mod文件 & download:下载模块到本地 & tidy:整理依赖删除不需要的

测试

测试是软件开发流程中的最后一道坎,同时也是最重要的一个流程。

单元测试-规则

  • 测试文件以 _test.go 结尾
  • 测试函数以 Test 开头,且参数必须为*testing.T(如TestXxx(t *testing.T))
  • 初始化逻辑放到TestMain中
func TestMain(m *testing.M){
   // 数据初始化
   code := m.Run()
   // 资源回收
   os.Exit(code)
}
package hello

func HelloTom() string {
   return "Jerry"
}
package hello

import (
   "github.com/stretchr/testify/assert"
   "testing"
)

func TestHelloTom(t *testing.T) {
   output := HelloTom()
   expectOutput := "Tom"
   //if output != expectOutput {
   // t.Errorf("Expected %s do not match actual %s", expectOutput, output)
   //}
   assert.Equal(t, expectOutput, output)//两种方式进行判断
}

image.png 如果我们对返回值修改成Tom,则测试通过

image.png

单元测试-覆盖率

package passline

func PassLine(score int16) bool {
   if score >= 60 {
      return true
   }
   return false
}
package passline

import (
   "github.com/stretchr/testify/assert"
   "testing"
)

func TestPassLineTrue(t *testing.T) {
   isPass := PassLine(70)
   assert.Equal(t, true, isPass)
}

func TestPassLineFail(t *testing.T) {
   isPass := PassLine(50)
   assert.Equal(t, false, isPass)
}

控制台输入:

go test PassLine_test.go PassLine.go --cover

输出:

image.png

性能测试 Benchmark

  • 函数名以 Benchmark 开头

image.png

image.png