语言进阶与依赖管理|青训营

108 阅读6分钟

1. 语言进阶

并发编程
并发:时间片切换 并行:多核,多线程同时运行 go语言就是为并发而生的

1.1 goroutine 协程

线程:系统昂贵的资源,内核态,比较消耗资源 栈MB级别   可并发跑多个协程 协程:用户态,轻量级线程, 栈 KB级别 创建和调度由go语言本身完成 go语言一次可以创建上万个协程

开启协程

目标:快速打印 hello goroutine 在调用函数的时候在前面加上go关键字,就能为一个函数创建一个协程来运行 time.Sleep(time.Second):保证子协程执行完之前,主线程不退出

package main

import (
	"fmt"
	//"sync"
	"time"
)

func hello(i int) {
	println("hello world : " + fmt.Sprint(i))
}

func main() {
	//var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		//wg.Add(1)
		go func(j int) {
			//defer wg.Done()
			hello(j)
		}(i)
	}
	//wg.Wait()
	time.Sleep(time.Second)
}

输出如下:并非按顺序的,说明是并行的输出 输出

1.2 CSP(communicating sequential processes)

协程通信
提倡通过通信共享内存,而不是通过共享内存而实现通信
通过通信共享内存
通道Channel
通道把协程连接,像是一个通信队列,先入先出,保证收发数据的顺序
通过共享内存实现通信:
临界区

1.3 channel 通道

一种引用类型
###通过make创建 make(chan 元素类型,[缓冲大小])
无缓冲通道:make(chan int)
也被称为同步通道
有缓冲通道:make(chan int,,2)
类似菜鸟驿站:货架容量有限,当放满了,会阻塞,直到有人取走一个包裹,才能继续放进去

例子(channel的具体使用)

要求:
A子协程生产数字
B子协程接受A的数字,并对数字进行平方的计算
主协程接受B子协程的结果,输出最后的平方数

func CalSquare() {
//无缓冲
	src := make(chan int)
//有缓冲
	dest := make(chan int, 3)
//A
	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
//把数字发送到src
			src <- i
		}
	}()
//B
	go func() {
		defer close(dest)
//通过src这个channel实现了A协程和B协程的通信
		for i := range src {
			dest <- i * i
		}
	}()
//M
	for i := range dest {
     //也可以是其他复杂操作
//dest选择有缓冲就是考虑到消费者(这里的M)的消费速度可能比较慢,有一些复杂操作,比较耗时。
//选择有缓冲的通道,就不会因为消费者的问题影响生产者的执行效率
		println(i)
	}
}

最终输出会按照顺序,即并发安全

1.4 并发安全Lock

上锁

1.5 WaitGroup

之前用sleep,但是不能确定sleep的时间 现在用wait group实现并发任务的同步
waitgorup 内部维护了一个计数器,并且公开了有三个方法:

  1. add 开启协程 +1;
  2. done 执行结束 -1;
  3. wait 主协程阻塞直到计数器为0(表明所有并发任务都完成了)
    现在对快速打印hello goroutine的例子进行代码优化,用waitgroup代替sleep
import (
	"fmt"
	"sync"
)

func hello(i int) {
	println("hello world : " + fmt.Sprint(i))
}

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

2. 依赖管理

依赖,指用各种开发包
对于单体函数,只需要使用原本的库就可以
但在实际开发中,项目复杂,可以用其他依赖包

2.1 go依赖管理演进

2.1.1 GOPATH

环境变量,一个路径,通到go项目的工作区
这个目录下面的三个分支:

  1. bin:项目编译的二进制文件
  2. pkg:(package)项目编译的中间产物,加速编译
  3. src:项目源码,项目代码直接依赖src下的代码
    通过go get 下载最新版本的包到src目录下

弊端:无法实现package的多版本控制

本地有2个项目A和B,两个项目依赖同一个package,但是是同一个package的不同版本。
假设项目A依赖于pkg v1,项目B依赖于pkg v2(属于是v1的升级版) v1实现了func A(), v2实现了func B()。
由于v2没有实现兼容,也就是说,v2里面没有func A(),把它删掉了
由于都是依赖同一个src的源码,所以项目A和项目B不能同时构建成功。

2.1.2 go vendor

在项目目录下增加一个vendor文件夹,存放所有依赖包的副本
项目的依赖会优先从vendor目录下获取,若vendor里面没有,再去gopath找
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
对于上面的两个项目,A项目的vendor下是v1版本,B项目的vendor下是v2版本

弊端

项目A依赖了package B和package C,package B依赖了package D的v1版本,package C依赖了package D的v2版本,并且package D 的v1,v2不兼容
无法控制依赖的版本
更新项目有可能出现依赖冲突,导致编译出错
它依赖的是源码,而不能标识版本

2.1.3 Go Module

依赖管理系统
通过go.mod 文件管理依赖包版本
通过go get/go mod指令工具管理依赖包
最终通过go mod定义版本规则,也可以通过工具管理项目依赖关系

2.2 依赖管理三要素

2.3.1. 配置文件,描述依赖

文件描述依赖了哪些包,包如何定位
go.mod 文件

  1. 模块路径 标识了一个模块,知道从哪里可以找到这个模块
  2. 原生库的版本号 不同项目需要的版本可能不一样
  3. 描述单元依赖 module path + 版本号 这样就唯一地定位某一个仓库的某一个版本

2.3.2 依赖配置-version

go path 和 go vendor 没有版本 go module才有版本定义

语义化版本

${MAJOR}.${MINOR}.${PATCH}
如V1.3.0 ,V2.3.0
MAJOR:大版本,不同的major版本可以不兼容(代码隔离)
MINOR:新增函数/功能,要在同一个major下前后兼容
PATCH:代码bug的修复

基于commit 伪版本

vX.0.0-yyyymmddhhmmss-abcdefgh1234

  1. 版本前缀,同语义化版本
  2. 时间戳。提交某次commit的时间戳
  3. 提交某次commit的哈希码前缀

2.3.3 依赖配置-indirect 关键字

A依赖了B,B依赖了C,记作 A->B->C
A->B:直接依赖
A->C:间接依赖
在go.mod中,间接依赖会用indirect标识出来

2.3.4 依赖配置-incompatible(不兼容)

主版本(MAJOR)2+的模块要在模块路径增加/vN的后缀,如example/lib5/v3 v3.0.2
对于没有go.mod文件并且主版本2+的依赖,要在版本号后面+incompatible 。如example/lib6 v3.2.0+incompatible ,标识出来可能存在不兼容的代码逻辑

2.3.4 依赖配置-依赖图

依赖图如下: 依赖图 项目X依赖了A、B两个项目,且A、B分别依赖了C项目的v1.3 和 v1.4版本,最终编译时所使用的C项目的版本为v1.4(选择最低的兼容版本
即使C有1.5,也是之选1.4版本

2.3.5 依赖分发

回源

依赖去哪里下载,如何下载 直接从代码托管平台(如GitHub)下载对应版本的依赖,存在以下问题:

  1. 无法保证构建稳定性
    软件的作者可以直接在代码托管平台 增加/修改/删除软件版本,下次去构建项目的时候,之前依赖的版本可能找不到了
  2. 无法保证依赖可用性
    作者可以对它的仓库 删除软件
  3. 增加第三方压力
    代码托管平台负载问题

proxy

一个服务站点
会缓存源站中的软件内容,缓存的软件版本不会改变
实现了稳定、可靠的依赖分发
使用了go proxy,以后就直接从proxy直接拉取依赖
没有问题是proxy解决不了的,解决不了就两层proxy 2. 中心仓库管理依赖库 gomodule里面的Proxy

2.3.6 依赖分发-变量 GOPROXY

GOPROXY是环境变量,url列表,用逗号分割 例:GOPROXY = "https://proxy1.cn,https://proxy2.cn,direct" 先在proxy1 中找依赖,proxy1中没有就去proxy2
最后的direct表示源站:若前面的站点都没有依赖就回源到第三方代码平台上去

2.3.7 工具-go get

直接go get example.org/pkg,默认拉取MAJOR的最新版本的提交 若加 @update 默认,同上 @none 删除依赖 @v1.1.2 tag版本,语义版本 @23dfdd5 特定的commit @master 指定分支,拉取分支的最新commit 3. 本地工具
go get 和 go mod

2.3.8 工具-go mod

go mod init 初始化,创建go.mod文件(在初始化项目的时候用) go mod download 下载模块到本地缓存(把所有的依赖拉下来) go mod tidy 增加需要的依赖,删除不需要的依赖(在go.mod文件里,之前用过某些依赖,但经过代码的修改,现在已经不需要了,通过tidy 就可以把这些非必要的依赖删除,减少构建整个项目的时间

3. 测试

事故

  1. 营销配置错误,导致非预期用户享受权益
  2. 代码逻辑错误,广告位被占
  3. 代码指针使用错误,导致app无法使用

分类

  1. 回归测试:
    质量保证,手动通过终端回归一些固定的主流场景。如:刷抖音,看评论等。
  2. 集成测试:
    对系统功能的一种测试验证。 通过服务暴露的某个接口,进行一些自动化的回归测试
    集成一个功能
  3. 单元测试:
    测试开发阶段,开发者对单独的函数、模块做功能验证 从上至下,覆盖率逐层变大,成本却逐层降低 单元测试的覆盖率在一定程度上决定了代码的质量

3.1 单元测试

输入->测试单元->输出(和期望输出校对,验证代码的正确性)
这里的测试单元可能是函数、模块、一些聚合的大的函数等等
可以保证质量,提高效率

3.1.1 单元测试-规则

  1. 所有文件以_test.go结尾
    通过展开目录,可以分出哪些是源代码,哪些是测试代码
  2. func TestXxx(*testing.T)
    测试函数命名规范 Test开始,Xxx指的是大写首字母 如func TestPublishPost(t *testing.T)
  3. 初始化逻辑放到TestMain中
func TestMaiin(m *testing.M){
  //测试前:数据装载、配置初始化等前置工作  
  code := m.Run()//跑这个package下的所有单元测
  //测试后:释放资源等收尾工作 
  os.Exit(code)
}

3.1.5 单元测试-覆盖率

go test 文件名 --cover cover参数,计算覆盖率
对各分支分别测试
一般覆盖率:50%~60% 主流程 较高覆盖率:80%(资金类的操作,要求比较高)
测试分支相互独立、全面覆盖(不重不漏)
测试单元粒度足够小,函数单一职责

3.2 单元测试-依赖

单元可能依赖某个本地文件,数据库DB,或者是cache 幂等:每次测试的结果一样
稳定:测试能够相互隔离,单元测试能在任何时间,任何函数独立运行 若直接测试,调到DB或者是cache,是不稳定的,因为依赖网络。所以实际的单元测试中会用到mock机制

3.3 单元测试-文件处理

func ReadFirstLine() string{
  open, err := os.Open("log")
  defer open.Close()
  if err ≠ nil{
    return ""
  }
  scanner := bufio.NewScanner(open)
  for scanner.Scan(){
    return scanner.Text()
  }
return ""
}

func ProcessFirstLine() string{
  line := ReadFirstLine()
  destLine := strings.ReplaceAll(line, "11", "00")
  return destLine
}

func TestProcessFirstLine (t *testing.T){
  line := ReadFirstLine()
  assert.Equal(t, "line00", firstLine)
}

以上代码是依赖于“log”文件的 若“log”文件修改或删除了,测试就无法运行了

3.4 单元测试-mock

快速mock函数:为一个函数/方法打桩
用函数A替换函数B,B是原函数,A是打桩函数

func Patch(target, replacement interface{}) *PatchGuard{
//target是原函数,要被替换的函数
//replacement是打桩函数
//将内存中函数的地址替换成运行时函数的地址,这样在真正测试的时候运行的就是打桩函数,就实现了mock的功能
  t := reflect.ValueOf(target)
  r := reflect.ValueOf(replacement)
  patchValue(t,r)
  return &PatchGuard{t,r}
}
func Unpatch(target interface{}) bool{
//测试结束后,把桩卸载
  return unpatchValue(reflect.ValueOf(target))
}

对于上面的那个firstline的例子,现在用上mock
对ReadFirstLine打桩测试,不再依赖本地文件

func TestProcessFirstLineWithMock(t *testing.T){
  monkey.Patch(ReadFirstLine, func()string{
    return "line110"
  })
  defer monkey.Unpatch(ReadFirstLine)
  line := ProcessFirstLIne()
  assert.Equal(t, "line000", line)
}

3.5 基准测试

测试一段程序运行时的性能和CPU损耗

3.5.1 例子-服务器负载均衡

有10个服务器,随机选择一个服务器执行

import(
  "math/rand"
)
var ServerIndex [10]int

func InitServerIndex(){
  for i := 0; i<10; i++ {
    ServerIndex[i] = i+100
  }
}

func Select() int{
  return ServerIndex[rand.Intn(10)]
}

对select函数做基准测试
规则和单元测试类似,只不过它用Benchmark开头

func BenchmarkSelect(b *testing.B){
  InitServerIndex()
//定时器重置。上面那个函数不属于我们要测试的范围,所以不应该算上它的耗时,定时器重置。
  b.ResetTimer()
//做串行的压力测试
  for i:=0; i<b.N; i++{
    Select()
  }
}
//并行的测试
func BenchmarklSelectParallel(b *testing.B){
  InitServerIndex()
  b.ResetTimer()
  b.RunParallel(func(pb *testing.PB){
    for pb.Next(){
      Select()
    }
  })
}

用bench参数执行基准测试go test -bench,会显示CPU耗时。
结果显示,这个select函数并行情况下耗时会更多,性能劣化。主要原因是这个select用到了rand函数。
rand函数为了保证全局的随机性和并发安全,持有一把全局锁,这样就降低了并发性能

3.5.3 基准测试-优化

为了解决随机函数rand的性能问题,开源了一个fastrand函数
用fastrand比用rand的并行提高了百倍
fastrand牺牲了随机数列的一致性

4. 项目实战

4.1需求描述

社区话题页面

  1. 展示话题(标题,文字描述)和回帖列表
  2. 暂不考虑前端页面实现,仅仅实现一个本地web服务
  3. 话题和回帖数据用文件存储,不用数据库

4.2 需求用例

浏览消费用户:浏览页面,包含topic(话题) 和 postlist(帖子)

4.3 ER图 entity relationship diagram

ER图表征了现实世界的概念模型
有两个实体:topic 和 post
topic 和 post 的关系:一对多
image.png

4.4 分层结构

三层: 数据层repository:关联底层的数据模型model,分装外部数据的增删改查。根据我们的需求,数据会存储在本地文件上,通过本地的操作拉取话题和帖子的数据。
数据层主要面向service层,对service层透明。
repository会屏蔽下游的数据差异,service层不需要关心数据的存储方式,只要拿到repository层返回的model数据就行

逻辑层service 业务
处理核心业务逻辑输出
接收repository层的数据做打包封装,输出一个实体entity
对应我们的需求,这个entity就是话题页面,并上送给视图蹭饭

视图层controller
处理和外部的交互逻辑,以view视图的形式返回给客户端
对上游负责,包装一些数据格式
我们的需求,会json格式化结果,封装,以api的形式进行访问

不同项目有不同层次的拆分,根据项目的进行具体拆分,无需套用

4.5 组件工具

  1. Gin高性能 go web 框架
  2. go mod go mod init 初始化go.mod文件

4.6 repository

topic和post都是json数据 两个结构体

基本查询操作

实现两个基本的查询操作:

  1. QueryTopicById:通过话题id查询话题
  2. QueryPostsByParentId:通过话题id查询到和话题关联的所有帖子

index

可以在文件里面,全扫描遍历的方式查询,一个一个地对比,但不够高效
这里用到索引 map[id] = &topic

查询

地址:直接用map就是了 sync.Once: 适合高并发场景下只执行一次的时候(单例模式)

4.7 service

参数校验->准备数据(从repository层拿到的数据)->组装实体
准备数据:
获取repository层里的话题数据和帖子数据
由于这两个都是依赖的tipic id,两个之间没有相互依赖,所以可以采取并行方案,提高效率
并行用到的waitgroup 之前有讲到

4.8 controller

pagedata 的data 就用 pageinfo赋值

4.9 router

gin 搭建外部框架

初始化数据索引

生成内存map索引

初始化引擎配置

gin.Default()

构建路由

还不懂

启动服务

4.10 运行