GO语言上手-工程实践的整理和理解 | 青训营笔记

120 阅读7分钟

GO语言上手-工程实践的整理和理解 | 青训营笔记

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

1.语言进阶

并发编程

1.1 并发VS并行

并发:并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。

并行:并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

也就是说,虽然并发和并行带给我们的感觉好像,这几个进程是同时进行的,但是对于并发来说,它只是一种伪同时,对并行而言,它是绝对的同时。所以,并发是指在一段时间宏观上多个程序同时运行。并行指的是同一个时刻,多个任务确实真的在同时运行。

并发的多个任务之间是互相抢占资源的。 并行的多个任务之间是不互相抢占资源的、

只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

1.2 协程和线程

协程:用户态,轻量级线程,栈MB级别

线程:内核态,线程跑多个协程,栈KB级别

线程是操作系统的资源,当java程序创建一个线程,虚拟机会向操作系统请求创建一个线程,虚拟机本身没有能力创建线程。而线程又是昂贵的系统资源,创建、切换、停止等线程属性都是重量级的系统操作,非常消耗资源,所以在java程序中每创建一个线程都需要经过深思熟虑的思考,否则很容易把系统资源消耗殆尽。

而协程,看起来和线程差不多,但创建一个协程却不用调用操作系统的功能,编程语言自身就能完成这项操作,所以协程也被称作用户态线程。我们知道无论是java还是go程序,都拥有一个主线程,这个线程不用显示编码创建,程序启动时默认就会创建。协程是可以跑在这种线程上的,你可以创建多个协程,这些协程跑在主线程上,它们和线程的关系是一对多。如果你要创建一个线程,那么你必须进行操作系统调用,创建的线程和主线程是同一种东西。显然,协程比线程要轻量的多。

但是协程不能替代线程,因为协程和线程两个不同层面上的东西。协程可以说是一个独立于线程的功能,它是在线程的基础上,针对某些应用场景进一步发展出来的功能。我们知道,线程在多核的环境下是能做到真正意义上的并行执行的,注意,是并行,不是并发,而协程是为并发而生的。

打个简单的比方,射雕英雄传看过吧,周伯通教郭靖一手画圆,一手画方,两只手同时操作,左右互搏,这个就是并行。普通人肯定做不到,不信你试试。你不能并行,却可以并发,你先左手画一笔,然后右手画一笔,同一时候只有一只手在操作,来回交替,直到完成两个图案是,这就是并发,协程主要的功能。

func HelloGoRoutine()  {
    for i := 0; i<5;i++{
        go func(j int){               //使用go 启动协程
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}
1.3 CSP(communicating Sequential Processes)

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

Go的CSP并发模型,是通过goroutine和channel来实现的。

goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。 channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。

1.4 Channel

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

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

通信机制channel也很方便,传数据用channel <- data,取数据用<-channel。

在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。

而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。

生成一个goroutine的方式非常的简单:Go一下,就生成了。

go f();

A 子协程发送0~9数字

B 子协程计算输入数字的平方

主协程输出最后的平方数

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)
    }
}
1.5 并发安全lock

确保线程安全最常见的做法是利用锁机制(Lock、sychronized)来对共享数据做互斥同步,这样在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,那么操作必然是原子性的,线程安全的。

1.6 WaitGroup

Go语言中除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务

在 sync.WaitGroup(等待组)类型中,每个 sync.WaitGroup 值在内部维护着一个计数,此计数的初始默认值为零。

等待组有下面几个方法可用,如下表所示。

方法名功能
(wg * WaitGroup) Add(delta int)等待组的计数器 +1
(wg * WaitGroup) Done()等待组的计数器 -1
(wg * WaitGroup) Wait()当等待组计数器不等于 0 时阻塞直到变 0。
package main
import (
    "fmt"
    "net/http"
    "sync"
)
func main() {
    // 声明一个等待组
    var wg sync.WaitGroup
    // 准备一系列的网站地址
    var urls = []string{
        "http://www.github.com/",
        "https://www.qiniu.com/",
        "https://www.golangtc.com/",
    }
    // 遍历这些地址
    for _, url := range urls {
        // 每一个任务开始时, 将等待组增加1
        wg.Add(1)
        // 开启一个并发
        go func(url string) {
            // 使用defer, 表示函数完成时将等待组值减1
            defer wg.Done()
            // 使用http访问提供的地址
            _, err := http.Get(url)
            // 访问完成后, 打印地址和可能发生的错误
            fmt.Println(url, err)
            // 通过参数传递url地址
        }(url)
    }
    // 等待所有的任务完成
    wg.Wait()
    fmt.Println("over")
}

1.7 小结

  • Goroutine
  • Channel
  • Sync

2.依赖管理

(学会站在巨人的肩膀上学习)

GoPATH -> Go Vendor -> Go Module

  • 不同环境依赖的版本不同
  • 控制依赖库的版本
2.1 GoPATH->Go Vendor -> Go Moudle
2.1.1 环境变量 $GOPATH
bin  //项目编译的二进制文件
pkg  // 项目编译的中间产物,加速编译
src  //项目源码
  • 项目代码直接依赖src下的代码
  • go get 下载最新版本的包到src目录下
2.1.2 GOPATH 的弊端

无法实现package的多版本控制

2.1.3 Go Vendor
  • 项目目录下增加vendor 文件,所有依赖包副本形式放在$ProjectRoot/vendor
  • 依赖寻址方式:vendor -> GoPATH

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题

2.1.4Go Vendor的弊端
  • 无法控制依赖的版本
  • 更新项目又可能出现依赖冲突,导致编译出错
2.4.5 GO Moudle
  • 通过go.mod 文件管理依赖包版本
  • 通过 go get/ go mod 指令工具管理依赖包

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

2.2 依赖管理三要素
  1. 配置文件,描述依赖 go.mod
  2. 中心仓库管理依赖库 Proxy
  3. 本地工具 go get/mod
2.3 依赖分发 - 回源 - Proxy - 变量GoProxy

小结

  • GO 依赖管理演进
  • GO module 依赖管理方案

3.测试

回归测试

集成测试

单元测试

从上到下,覆盖率逐层变大,成本却逐层降低

3.1单元测试

保证质量,提升效率

好的单元测试应当包含四种特性:正确,清晰,完整,健壮

3.1.1 单元测试规则
  • 所有测试文件 以 _test.go结尾
  • func TsetXxx(*testing.T)
  • 初始化逻辑放到TestMain中
func HelloTom() string {
    return "Jerry"
}
/*测试函数的基本格式*/
func TestHelloTom(t *testing.T){
    output := HelloTom()
    expectOutput := "Tom"
    if output != expectOutput {
        t.Errorf("expected %s do not match actual %s ",expectOutput,output)
    }
}
/*基准测试的基本格式*/
func BenchmarkName(b *testing.B){
    //...
}
3.1.2 单元测试 - 覆盖率

在做单元测试时,代码覆盖率通常被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况。

一个设计良好的单元测试的覆盖率可以达到80%甚至90%

代码覆盖率 = 代码的覆盖程度,一种度量方式

PS:一个合格的单元测试不应该仅仅着眼于代码行数的覆盖,更应该体现出对单元程序逻辑执行的覆盖。

3.1.3 单元测试 - 依赖
3.2 MocK测试
3.3 基准测试
类型格式作用
测试函数函数名前缀为Test测试程序的一些逻辑
基准函数函数名前缀为Benchmark测试函数的性能
示例函数函数名前缀为Example为文档提供示例文档

go test命令会遍历所有的_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

4.项目实战

4.1 需求分析

准确地回答“系统必须做什么”这个问题,也就是对目标系统提出完整、准确、清晰、具体的要求

1、确定对系统的综合要求:功能需求、性能需求、可靠性和可用性需求、出错处理需求、接口需求、约束(设计约束或实现约束描述在设计或实现应用系统时应遵守的限制约束条件)、逆向需求(说明软件系统不应该做什么)、将来可能提出的需求

2、分析系统的数据需求

3、导出系统的逻辑模型

4、修正系统开发计划

4.2 需求用例

用例(Use Case)是一种描述系统需求的方法。运用用例这种方法来描述系统需求称之为用例建模。

一个用例就是一个功能

4.3 ER图

E-R图也称实体-联系图 (Entity Relationship Diagram),提供了表示实体类型、属性和联系的方法,用来描述 现实世界 的 概念模型 。. 它是描述现实世界关系概念 模型 的有效方法。. 是表示概念关系模型的一种方式

根据设计好的ER图我们可以设计好关系型数据库

4.4 分层结构

数据库:数据Model,外部数据的增删改查

逻辑层:业务Entity,处理核心业务逻辑输出

视图层:视图view,处理和外部的交互逻辑

总结:通过对本节内容的复盘,我加深了对并行和并发的理解。又get到了协程的概念。尤其知道了Go语言相对于java等其他语言的优点之一:高并发性。