青训营笔记 | 第二天

80 阅读4分钟

1.语言进阶

并发编程

并发:多线程程序在一个的核cpu上运行。

并行:多线程程序在多个核的cpu上运行。

Go可以充分发挥多核优势,高效运行。

线程:系统中比较昂贵的系统资源,属于内核态 栈MB级别。

协程:属于轻量级的线程,属于用户态,栈KB级别。

创建协程的函数:

go func(){

}()

例子:

import (  
   "fmt"  
   "time")  
  
func hello(i int) {  
   println("hello goroutine:" + fmt.Sprint(i))  
  
}  
  
func HelloGoRoutine() {  
   for i := 0; i < 5; i++ {  
      go func(j int) {  
         hello(j)  
      }(i)  
   }  
   time.Sleep(time.Second)  
}  
  
func main() {  
   HelloGoRoutine()  
}

通信

Go提倡通过通信共享内存,而不是通过共享内存实现通信。

共享内存实现通信:

优点:提高程序运行效率。

缺点:会导致数据不一致、数据竞态等问题。

通过通信共享内存:

优点:channel通道让协程进行连接,保证收发顺序,不会有数据不一致等问题。

Channel

channel通过make关键字创建 可分为有缓冲通道 和 无缓冲通道

区别:
无缓冲被称为同步通道。
有缓冲通道会阻塞发送,直到有人取走缓冲内的数据

func CalSquare() {  
   src := make(chan int)  
   dest := make(chan int, 3)  
   go func() {  
      defer close(src)  
      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)  
   }  
}  
  
func main() {  
   CalSquare()  
}

并发安全Lock

暴力加锁:

import (  
   "sync"  
   "time")  
  
var (  
   x    int64  
   lock sync.Mutex  
)  
  
func addWithLock() {  
   for i := 0; i < 10000; i++ {  
      lock.Lock()  
      x += 1  
      lock.Unlock()  
   }  
}  
  
func addWithoutLock() {  
   for i := 0; i < 10000; i++ {  
      x += 1  
   }  
}  
  
func add() {  
   x = 0  
   for i := 0; i < 10; i++ {  
      go addWithLock()  
   }  
   time.Sleep(time.Second)  
   println("WithLock", x)  
  
   x = 0  
   for i := 0; i < 10; i++ {  
      go addWithoutLock()  
   }  
   time.Sleep(time.Second)  
   println("WithOutLock", x)  
  
}  
  
func main() {  
   add()  
  
}

通过WaitGroup实现协程的同步阻塞:

因为已知5个协程,所以add对计数器+5,每个协程执行完毕后,通过done对计数器减少1,最后wait阻塞主协程,计数器为0退出主协程

func HelloGoRoutine() {  
   var wg sync.WaitGroup  
   wg.Add(5)  
   for i := 0; i < 5; i++ {  
      go func(j int) {  
         defer wg.Done()  
         hello(j)  
      }(i)  
   }  
   wg.Wait()  
}

2.依赖管理

实际项目工程不会基于标准库搭建,会更多的关注业务逻辑的实现,所以其他涉及框架、日志、driver和collection等一系列依赖都会通过sdk的方式引入。

依赖管理的三个阶段:

GOPATH -> GO Vendor -> Go Module

GOPATH

环境变量$GOPATH:

1.项目代码直接依赖src下的代码

2.go get下载最新版本的包到src的目录下

弊端:

无法实现package的多版本控制


Go Vendor

在项目目录下增加了vendor文件,所有依赖包以副本形式放在$ProjectRoot/vendor。

依赖寻址方式:vendor => GOPATH。

解决了多个项目需要同一个package依赖的冲突问题。

弊端: 可能出现依赖冲突,导致编译错误。


Go Module

go语言官方推出的依赖管理系统

依赖管理三要素:
1.配置文件,描述依赖了哪些包和包的定位 go.mod
2.中心仓库管理依赖库 Proxy
3.本地工具 go get/mod

间接依赖在配置文件中用indirect标识

Go Modules系统中定义的依赖,可以对应到多版本代码管理系统中某一项目的特定提交或版本,所以对go.mod中定义的依赖可以直接从对应仓库中下载指定软件依赖。

但直接使用版本管理仓库下载依赖有3个问题:

1.无法保证构建稳定性 :作者增加/删除/修改 软件版本

2.无法保证依赖可用性 :依赖软件作者可以直接删除软件,导致依赖不可用

3.大幅增加第三方代码托管平台的压力


所以
Go Proxy诞生了
Go Proxy是一个服务站点,他会缓源站中的软件内容,缓存的版本不会改变,而且在源站软件删除后依然可用。

GOPROXY="proxy1.cn,https://proxy2.cn,…"

变量GOPROXY中,依赖的寻址路径会优先从proxy1下载,如果proxy1中不存在,会在proxy2中寻找,如果proxy2中不存在,则会直接回到源站下载依赖。

go mod init 初始化,创建go.mod文件
go mod tidy 增加需要的依赖,删除不需要的依赖 (常用)


3.测试

测试

测试是避免事故的最后一道屏障。 测试包含了以下三个:
回归测试
集成测试
单元测试
从上到下,覆盖率逐渐变大,但成本却逐渐降低。

单元测试-规则:
1.所有测试文件以_test.go结尾

2.测试函数命名规范:
func TesXxx(*testting.T)

3.初始化逻辑放到TestMain中

例子:

func TestHelloTom(t *testing.T) {  
   output := HelloTom()  
   expect := "tom"  
   if output != expect {  
      t.Errorf("Expect is %s do not match actual %s", expect, output)  
   }  
}

代码覆盖率测试
例子:

func JudgePassLine(score int64) bool {  
   if score >= 60 {  
      return true  
   }  
   return false  
}
import (  
   "github.com/stretchr/testify/assert"  
   "testing")  
  
func TestJudgePassLineTrue(t *testing.T) {  
   ispass := JudgePassLine(70)  
   assert.Equal(t, true, ispass)  
}  
  
func TestJudgePassLineFalse(t *testing.T) {  
   notpass := JudgePassLine(40)  
   assert.Equal(t, false, notpass)  
}

屏幕截图 2023-05-14 164743.png 一般覆盖率:50%~60%,较高覆盖率80%+,覆盖率越高成本越大。
测试分支相互独立,全面覆盖。
测试单元力度足够小,函数单一职责。

mock测试
对函数进行mock,屏蔽对于文件的依赖。(个人理解和回调函数差不多?)

基准测试
基准测试以Benchmark开头,入参是testing.B,用b中的N值反复递增循环测试。