Go 语言进阶 | 青训营笔记

66 阅读5分钟

Go 语言进阶与依赖管理

这一节课讲了一些进阶的知识,例如并发,有 Goroutine 和Channel,h合理运用这些特性能让我们的程序运行的更快和安全。

1. 并发编程:

1.1 Goroutine:

  • 协程:用户态,轻量级线程,栈KB级别。

  • 线程:内核态,线程跑多个协程,栈MB级别。

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

func  HelloGoroutine(){
    for i:=0;i<5;i++{
        go func (j int){
        hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}

输出结果是乱序,可以看出 goroutine 是并行的。

1.2 CSP (Communicating Sequential Processes)

Goroutine csp.png

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

Channel 相当于把 Goroutine 做了一个连接,就像是传输队列。遵循先入先出。能保证收发数据的顺序。 Channel 就是可以让一个 Goroutine 发特定的数据到另一个 Goroutine的一个通信机制。

当然 Go 也保留了通过共享内存实现通信,如右图所示,通过共享内存进行数据交换。需要通过 互斥量 Mutex 对内存进行加速,需要获取临界区的权限。但在这样的情况下,不同的Goroutine 容易发生 数据竞态的的问题。在一定条件下会影响性能。

扩展阅读(需要补充学习): 记得切换到 EN Wiki

互斥锁 - 维基百科,自由的百科全书

Peterson算法 - 维基百科,自由的百科全书

Let's Talk Locks!

1.3 Channel

转存失败,建议直接上传图片文件

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

  • 无缓冲通道    make(chan int) 发送和接收的 Goroutine同步化,又称为同步通道。解决同步问题的方式就是 使用带缓冲的通道

  • 有缓冲通道 make(chan int,2) 通道的容量代表能存放多少元素,满了就阻塞。典型的生产-消费模型。

1.4 并发安全 Lock

1.5 WaitGroup

waitgroup.png

暴露了三个方法。

  • Add(delta int) :计数器+delta

  • Done() :计数器-1

  • Wait():阻塞直到计数器为0

就是一个 计数器, 开启协程 +1; 执行结束-1;主协程阻塞直到计数器为0。

Example:

func ManyGo() {
    var wg sync.WaitGroup
    wg.Add(5) // 计数器+5 
    for i := 0; i < 5; i++ {
        wg.Add(1) 
        go func(j int) {
            defer wg.Done() // 通过 done 对计数器-1
            hello(j)
        }(i)
    }
    wg.Wait() // 主协程堵塞
}

让我们回到最初多个协程带打印hello gorouting的例子,现在我们用watigroup实现协程的同步阻塞。如刚才说述,首先通过add方法,对计数器+5,然后开启协程,每个协程执行完后,通过done对计数器减少1,最后 wait 主协程阻塞,计数器为0 退出主协程。右边是最终的输出结果。

小结:

  • Goroutine

  • Channel

  • Sync (mutex waitGroup)


2. Go Package Manage (depend manage)

2.1 Go 依赖管理演进

GOPATH -> Go Vendor -> Go Module

  • 不太环境(项目)依赖的版本不同

  • 控制依赖库的版本

2.1.1 GOPATH

.
├── bin    //项目编译的二进制文件 
├── pkg    //项目编译的中间产物,加速编译
└── src    //项目源码
  • 项目代码直接依赖 src 下的代码

  • go get 下载 latest package 到 src 目录下

弊端:

Go Path drawback.png

场景:A 和 B 依赖于某个 package 的不同版本

问题:无法实现 package 的多版本控制

2.1.2 Go Vendor

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

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

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package 依赖的冲突问题。 例,A项目的 vendor 下是 V1 版本,B项目的 vendor 下是 V2 版本。保证两个项目都能构建成功。

弊端:

Vendor drawback.png

如图项目 A 依赖 package B和C,而 Package B、C 又同时依赖于 Package D, 所以D 有 V1 和 V2 版本。通过 Vendor 没有办法很好的控制 V1 和 V2 的版本选择。一旦更新项目,可能出现依赖冲突,导致编译出错。

问题:

  • 无法控制依赖的版本。

  • 更新项目又可能出现依赖冲突,导致编译出错。

2.1.3 Go Module

  • 通过 go.mod 文件管理依赖包版本

  • 通过 go get / go mod指令工具管理依赖包

目标: 定义版本规则和管理项目依赖关系

2.2 依赖管理三要素

    1. 配置文件,描述依赖    go.mod
    1. 中心仓库管理依赖库    Proxy
    1. 本地工具                        go get/mod

2.3.1 依赖配置 go.mod

module exmaple/project/app  // 依赖管理基本单元
//module github.com/<user_name>/<repo_name>/app 


go 1.16              // 原生库

require(             // 单元依赖
    example/lib1 v1.0.2
    example/lib2 v1.0.2  //indirect
    example/lib3 v1.0.2-20190725025543-5a5f
    example/lib5/v3 v3.0.2
    example/lib6 v3.0.2+incompatible
)

依赖标识:[Module Path][Version/Pseudo-version]

从模块路径可以看出在哪找到该模块, github前缀表示可以从github 仓库找到。如果项目的子包想被单独引用,则需要通过单独的 init go.mod 文件进行管理。

下面是依赖的原生 SDK 版本。最下面是单元依赖,每个依赖单元用模块路径+版本号来唯一标示。

2.3.2 依赖配置 - version

语义化版本:

${MAJOR}.${MINOR}$.{PATCH}

  • V1.3.0

  • V2.3.0

基于 commit 的伪版本:

vx.0.0-yyyymmddhhmmss-abcdefgh1234

  • V0.0.0-20220401081311-c34098211231a

  • V1.0.0-20200401081311-10c340211231x

2.3.3 依赖配置 - indirect

A->B->C

  • A->B 直接依赖

  • A->C 间接依赖

没有直接导入的间接依赖模块, 就会标识为 indirect

2.3.4 依赖配置 - incompatible

  • 主版本 V2+ 的模块会在模块路径添加 /vN 后缀。

  • 对于没有 go.mod 文件且主版本 v2+ 的依赖,会 +incompatible

为了兼容性,做版本标识。

依赖配置会优先选择最低的兼容版本。

2.3.5 依赖分发 - 回源

Dependent distribution- back source.png

表示依赖去哪里下载,如何下载的问题。

github是比较常见的代码托管系统平台,而Go Modules 系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本,这样的话,对于go.mod中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。

但直接使用版本管理仓库下载依赖,存在多个问题。

  • 无法保证构建确定性:软件作者可以直接代码平台增加/修改/删除 软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。

  • 无法保证依赖可用性:依赖软件作者可以直接代码平台删除软件,导致依赖不可用;

  • 大幅增加第三方代码托管平台 压力

2.3.5 依赖分发 - Proxy

Dependent distribution-proxy.png

Go Proxy 是一个服务站点,会缓冲源站中的软件内容,缓存版本不会改变,在源站删除软件后依旧可用。从而实现了更“immutability”和“available”(稳定和可靠)的依赖分发。使用了 Go Proxy,构建时直接从 Go Proxy 站点拉取。

类比项目中,下游无法满足我们上游的需求,使用 Go Proxy 或 适配器解决,如果解决不了就两层 Proxy。

PS: 类似 Linux Mirrors.

2.3.5 依赖分发 - 变量 GOPROXY

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

Go Module 通过 环境变量来配置 GoProxy。

服务器站点URL列表,“direct” 表示源站(github, go.dev)。

查找依赖顺序示意图: Proxy1 => Proxy2 => Direct

类似设计缓存。

2.3.7 工具 - go get

Go-get.png

  • @update:直接 go get,拉取 major 默认版本的最新提交 加不加一样

  • @none:删除依赖

  • @v1.1.2: 拉取特定的语义版本 tag

  • @23dfdd5: 拉取特定的 commit

  • @< breanch-name >: 某个分支最新的commit

2.3.8 工具 - go mod

go-mod.png

  • init:初始化项目,创建 go.mod 文件。 每一个项目开始前必须的步骤。

  • Download: 下载模块到本地缓存。也就是 Pull.

  • tidy: 经常用到,根据项目增删需要的和不需要的依赖。

每次项目提交前,执行下 tidy。减少构建项目的时间。

02. 依赖管理三要素

depend-manage-point.png

小结:

  • Go 依赖管理演进

  • Go Module 依赖管理方案


4. 项目实践

4.1 需求分析

require-Description.png

4.2 需求用例

requirement-use-case.png

4.3 ER图

E-R.png

4.4 分层结构

layer-structures.png

整体分为三层,repository数据层,service逻辑层,controoler视图层。 数据层关联底层数据模型,也就是这里的model,封装外部数据的增删改查,我们的数据存储在本地文件,通过文件操作拉取话题,帖子数据;数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。 Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层。 Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问就好。

4.5 组件工具

  • Gin

  • Go Mod

    • go mod init

    • go get

4.6 Repository

Repository-funs.png

4.6 Repository-index

repository-index.png

4.6 Repository-查询

repository-query.png

4.7 Service

Service-entity.png

代码流程编排

func (f *QueryPageInfoFlow) Do()(*PageInfo,error){
    if err:= f.checkParam();err!=nil{
    return nil,err
}
    if err:=f.prepareInfo();err!=nil{
    return nil,err
}
    if err:=f.packPageInfo();err!=nil{
    return nil,err
}
    return f.pageInfo,nil
}

4.8 Controller