Go的并发 | 青训营笔记

57 阅读3分钟

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

go并发

go的并发是基于协程。协程是基于用户线程的。我们将线程认为的切分为多个部分。通过线程中我们自己定义的调度方式来调用我们划分的模块,这就为协程。相较于线程的切换。我们的协程的切换代价更小。保存协程信息所占用的空间更小。所以,go原生就支持高并发。

以下是一个简单的并发的例子:

 var wg sync.WaitGroup
 ​
 func coroutine(i int) {
     fmt.Println(i)
     wg.Done()
 }
 ​
 func main() {
 ​
     wg.Add(100)
     for i := 0; i < 100; i++ {
         go coroutine(rand.Intn(100))
     }
     wg.Wait()
     
 }

在并发中,我们需要考虑到的一个问题就是不同流程中的通信。我们在操作系统中学习了mutex互斥锁与临界区的概念。而在go的标准库sync中,就包含了这一套互斥锁。

 var (
     x    int
     lock sync.Mutex
     wg   sync.WaitGroup
 )
 ​
 func coroutine() {
     lock.Lock()
     x += 1
     lock.Unlock()
     wg.Done()
 }
 ​
 func main() {
 ​
     wg.Add(10000)
     for i := 0; i < 10000; i++ {
         go coroutine()
     }
     wg.Wait()
     fmt.Println(x)
 }

进入临界区是加锁,离开临界区是解锁。我们期望的结果是10000。如果不进行加锁操作的话,由于多个流程同时读取数据的问题,会造成脏读。从而使得结果不正确。

进程通信还有一种管道的模式。go使用这种模式来进行由通信到共享内存。而不是由共享内存到通信。chan对象,在go中使用chan关键。

 func main() {
 ​
     src := make(chan int, 2)
     dest := make(chan int, 2)
 ​
     go func() {
         defer close(src)
         src <- rand.Intn(100)
     }()
 ​
     go func() {
         defer close(dest)
         for i := range src {
             dest <- i * 2
         }
     }()
     for i := range dest {
         fmt.Println(i)
     }
 }

这里的"<-"就相当于cpp的流运算。我们将数据送入到channel中。

项目开发

总体上分为三个部分。第一个部分是项目的拆解。也就是需求分析。这方面又会划分的更加细致。

需求分析

在需求分析中,我们需要详细的描述出对方的需求。这样才能直到我们的代码需要什么样的架构与逻辑。

我们需要在需求分析中,抽出相关的实体的模型。从而将实体抽象化,得到一个逻辑上的,底层可以存储的对象模型。

根据需求用例进行用户分析得到尽可能的完善的业务逻辑。我们可以使用ER图来帮助我们划分实体之间的关系。如果使用的是关系型数据库的话。ER的化简还可以来帮助我们进行数据模型的设计。

代码设计

从实体模型出发,分层结构:

  • 数据层。数据模型,外部数据的增删改查。

例如我们的用户数据的存储,与读取,就是需要在数据层解决的问题。

  • 逻辑层, 业务实体,处理核心业务逻辑输出

在逻辑层也就是我们具体的服务代码实现。例如我们的服务提供一个查找不同时间段登录用户数据。那么就需要在此层进行编写。而关于数据的查询相关操作则是调用数据层提供的接口即可。

  • 视图层, 试图, 处理和外部的交互逻辑

视图层则是提供给客户端的接口API层。我们的整体所有能够提供的服务都由此入口进入。

这三层从下到上一次从client到service。

测试运行

为了保证我们的代码正确可靠。我们需要进行测试。测试分为回归测试,集成测试与单元测试。

回归测试则是模拟正常用户的操作逻辑与行为对程序进行测试。

集成测试是针对代码暴露出来的API接口进行测试。

而单元测试则是以函数为基本单元进行期望的测试。

程序的正常运行离不开测试。这点很重要。