Go语言进阶与工程实践 | 青训营笔记

81 阅读5分钟

go.jpg

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

今天青训营的内容主要分为四块:语言进阶、依赖管理、测试和项目实战。语言进阶主要讲了如何使用语言进行更高效、简洁的编程,依赖管理则讲了如何管理项目依赖关系,测试则讲了如何使用测试来保证项目的质量,项目实战则是对所学知识的实际应用。

下面是今天的学习笔记

一、语言进阶

主要讲解了Go语言下的并发编程和并发安全。

1. 并发

Go语言提供了一种简单而强大的并发编程模型,它使用了 goroutines 和 channels 来实现。

  • goroutines是轻量级的线程(协程),在用户态创建。可以通过 go 关键字来启动一个新的 goroutine。
  • channels是一种类型安全的通信机制,可以在 goroutines 之间传递消息。在go中推荐使用通道来共享内存,而不是通过共享内存来达到通信的目的。

微信截图_2023011612315.png

以下代码演示了如何使用 goroutines 和 channels 实现一个简单的并发程序

func main() {
        // 声明了两个管道
	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 {
		fmt.Println(i)
	}
}

在这段代码中,声明了两个channel:src和dest,并通过go关键字启动了两个子协程。在两个子协程中,一个协程不断的向管道src中输入数据 “ i ”,另一个协程不断的从管道src中取出数据并输入到管道dest中,最后主协程再从管道dest中取出数据。通过这样一段代码,很清晰地能看到两个协程并发的对数据进行通信、交换,最后再返回给主协程,提高程序的执行效率。

2. 并发安全

在这一节中主要讲解了若干协程并发时,可能产生的数据安全问题和Go中使用sync包下的MutexWaitGroup解决问题的方法。

首先来看一个例子

var x int64 = 0

func addWithoutLock() {
    for i := 0; i < 2000; i++ {
        x += 1
    }
}

// 执行完函数后,x的预期值为10_000
func add() {
    for i := 0; i < 5; i++ {
        go addWithoutLock()
    }
}

微信截图_20230116181505.png

我在我的环境中上执行了10次该段程序,很明显有3次的执行结果没有达到预期,这就是潜在的并发安全问题。在这段程序中,我们没有对临界区或者说临界值x做任何保护措施,因此开启的5个子协程都可以在任意时间下访问x并对x进行读写,这就导致了x可能会出现如图所示的结果。

接下来将会用到sync包中的Mutex

sync.WaitGroup是Go语言中用于等待一组goroutine结束的工具。它包含一个计数器,用于跟踪还有多少个 goroutine需要等待结束。使用者可以通过Add方法来增加计数器的值,表示有新的goroutine需要等待;通过Done 方法来减少计数器的值,表示有一个goroutine已经结束;最后通过Wait方法等待所有被等待的goroutine结束。

var locker sync.Mutex
var x int64 = 0

func addWithLock() {
    lokcer.Lock()
    x += 1
    locker.Unlock()
}

func add() {
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
}

在这段程序中,用到sync下的Mutex,在对临界区之前locker会对临界区加锁,只有第一个执行addWithLock函数的协程能够获得操作临界区的权限,此时其他协程都会被锁给阻塞而持续的等待获取临界区的权限。

func hello(n int) {
    fmt.Println("hello goroutine:", n)
} 

func main() {
    for i := 0; i < 5; i++ {
        hello(i)
    }
}

这段程序会串行的依次打印5条 “ hello goroutine: 1··· ”而如果想要加快程序的执行速度,那么就让这五次函数并发的执行就可以了。

下面会用到sync包下的WaitGroup

var wg sync.WaitGroup

func hello(n int) {
    fmt.Println("hello goroutine:", n)
} 

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

这样我们就一次性的开启了5个协程去打印这段话,并且用wg.Wait()函数让主协程等待这5个协程的结束。

二、依赖管理

Go语言的依赖管理是指管理Go语言项目所依赖的其他包(第三方包)

1. Go语言依赖管理的发展历程

  • 初期,Go语言采用的是手动管理依赖的方式,开发者需要自己手动下载依赖包并放到指定的目录中。
  • 2011年,Go语言发布了go get命令,可以通过它来自动下载依赖包并管理依赖。
  • 2013年,Go语言发布了go dep工具,它是第一个用于Go语言的依赖管理工具,使用了预设的vendor目录来管理依赖。
  • 2016年,Go语言官方发布了vendor experiment,提供了一种新的方式来管理依赖,即将依赖包放入项目的vendor目录中。
  • 2017年,Go语言正式在1.8版本中提供了内置的vendor机制,使得开发者可以直接使用go build和go test命令来编译和测试项目,而不需要使用其他工具来管理依赖。

目前Go语言推荐的依赖管理工具是go mod,该工具是 Go 1.11 引入的一个新的标准工具,可以解决 Go 语言项目依赖管理的问题。

2. Go Module

Go Module主要包括三个要素:

  • go.mod 配置文件,用于描述依赖关系
  • proxy 中心仓库,用于管理依赖库
  • go get/mod 本地依赖管理工具
1) go.mod

go.mod是Go语言中用于管理包依赖的工具,它是Go语言官方推荐的依赖管理方式。

go.mod文件是一个文本文件,其中包含了项目所依赖的其他包的信息,包括包名、版本号、源码地址 等信息。项目开发者可以通过go mod命令来管理这些依赖。

Go.mod文件是项目根目录下的go.mod文件,其中记录了项目依赖的其他包的信息。

使用go mod能够解决版本冲突,保证项目稳定运行。同时也可以使得项目更容易维护,对于更新包依赖等操作变得更加方便。

下面是go.mod中一些依赖标识的格式

语义化版本:${MAJOR}.${MINOR}.${PATCH}

如 v1.3.0、v2.3.0等

基于commit的伪版本:vX.0.0-yyyymmddhhmmss-abcdefgh1234

如 v1.0.0-20220401081311-c38fb59326b7

// indirect标识表示间接依赖

主版本2+模块会在模块路径后增加 /vN后缀

没有go.mod文件且主版本2+的依赖,会+incompatible

2) proxy

proxy是指在从远程仓库下载依赖包时,使用一个中间层来帮助下载的工具。

通过使用proxy来进行依赖分发可以保证构建稳定性、依赖可用性和减少第三方压力。

3) go get & go mod

常用的go get命令包括:

  • @update: 默认
  • @none 删除依赖
  • @v1.1.2 语义版本
  • @23dfdd5 commit伪版本
  • @master 分支最新commit

常用的go mod命令包括:

  • init: 创建一个新的go.mod文件
  • download: 下载项目依赖的包
  • edit: 编辑go.mod文件
  • tidy: 维护go.mod文件

三、测试

Go语言中的测试是指使用Go语言编写的测试用例来验证代码是否符合预期。

Go语言中的测试主要由两部分组成:测试用例和测试运行器。

  1. 测试用例: 测试用例是Go语言编写的代码,用于测试其它代码。测试用例文件必须以_test.go结尾。

  2. 测试运行器: 测试运行器是Go语言提供的工具,用于运行测试用例。通常使用go test命令来运行测试用例。

Go语言的测试用例支持多种测试方式,常用的有:

  • 单元测试: 用于测试程序中的单个函数或方法
  • 基准测试: 用于测试程序中的性能

下面是一个Mock测试基准测试的例子

import (
   "bufio"
   "os"
   "strings"
   "testing"

   "bou.ke/monkey"
   "github.com/stretchr/testify/assert"
)


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 TestProcessFirstLineWithMock(t *testing.T) {
   monkey.Patch(ReadFirstLine, func() string { return "line110" })
   defer monkey.Unpatch(ReadFirstLine)
   line := ProcessFirstLine()
   assert.Equal(t, "line010", line)
}

在这个例子中,ReadFirstLine函数用于从文件中读取文件第一行的数据,ProcessFirstLine函数把第一行数据中包含'11'的字符串替换为'00'。

在mock测试中,测试用例屏蔽了外部依赖使ReadFirstLine无需读取文件而可以返回字符串'line110',然后通过断言来判断ProcessFirstLine函数是否正确执行

下面是基准测试

import (
   "github.com/bytedance/gopkg/lang/fastrand"
)

var serverIndex [10]int

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

func Select()int {
   return serverIndex[fastrand.Intn(10)]
   //return serverIndex[rand.Intn(10)]
}
func BenchmarkSelect(b *testing.B) {
   InitSeverIndex()
   b.ResetTimer()
   for i := 0; i < b.N; i++ {
      Select()
   }
}

func BenchmarkSelectParallel(b *testing.B) {
   InitSeverIndex()

   b.ResetTimer()
   b.RunParallel(func(pb *testing.PB) {
      for pb.Next() {
         Select()
      }
   })
}

在基准测试中,用到了字节开发的fastrand,对性能进行测试,下面是在我的环境中的rand和fastrand的基准测试结果,可以很明显的看出性能的提升

rand

image.png

image.png

fastrand

image.png

image.png

四、项目实战

1. 项目背景

获取青训营的主题帖和相应的帖子回复

2. 需求描述

要能够展示主题帖的信息和回帖列表及回帖的信息

3. ER图

image.png

4. 项目结构分层

  • 数据层,对外部数据的CURD
  • 逻辑层,处理业务逻辑
  • 视图层,处理与前端的交互逻辑

5. 工具框架选择

Gin - github.com/gin-gonic/g…

6. 编码 & 结果展示

微信截图_20230116171947.png

五、今日总结

今天学习了Go语言的并发编程,依赖管理,测试和项目实战。学习了Go语言中的goroutine和channel,以及如何使用它们来实现并发编程。同时还学习了如何使用第三方包管理工具管理项目依赖,以及如何使用Go语言自带的测试工具进行单元测试。最后,还学习了如何使用gin框架构建一个简单的Web应用程序。

明天继续加油