Go语言入门—工程实践| 青训营

62 阅读8分钟

Go语言入门—工程实践

语言进阶、

并发编程——快

image.png

image.png

goroutine

package main  
  
import (  
"fmt"  
"time"  
)  
  
func hello(i int) {  
println("hello goroutine:" + fmt.Sprint(i))  
}  
func main() {  
for i := 0; i < 5; i++ {  
go func(j int) {  
hello(j)  
}(i)  
}  
time.Sleep(time.Second)  
}

这段代码定义了一个 hello 函数,它接受一个整数参数 i,并使用 println 函数打印出类似 "hello goroutine:0~hello goroutine:4" 的信息。在 main 函数中,使用一个循环来创建5个 goroutine,每个 goroutine 都会调用 hello 函数并传递一个不同的参数(通过匿名函数的方式实现)。然后使用 time.Sleep 函数让主线程等待1秒钟,以便观察到 goroutine 的执行情况。

CSP(Communicating sequential processes)

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

channel

go实现CSP的形式便是channel。它是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。 go语言的channel主要有两种机制。 一种是通讯的两方必须同时在channel上,才能进行交互,有任何一方不在的时候,另一方就会被阻塞在那里等待着,直到另一方有回应才能重新进行交互。 image.png 另一种就是"Buffered Channels"。我们可以给channel指定一个容量,当容量没有满的时候,我们发送消息的一方只要容量没有满,就可以一直放,如果容量满了,那必须等接受消息的人就拿出一个数据把位子空出来,才能继续发送数据。对于接受数据的人也是如此。 image.png

package main  
  
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 {  
println(i)  
}  
}

image.png 这段代码实现了一个简单的并发程序,使用了Go语言的goroutine和channel。

首先,我们创建了一个名为src的整型通道,用于从外部传入数据。然后,我们创建了一个名为dest的整型通道,用于将处理后的数据发送出去。这两个通道都是无缓冲的,如果有多个协程同时向它们发送数据,可能会导致数据丢失或阻塞。

接下来,我们启动了两个协程,分别执行以下任务:

  • 第一个协程(称为A协程)从src通道中读取0~9的整数,并将它们发送到另一个空闲的整型通道中。由于该通道是带缓冲区的,因此即使在高并发情况下也不会阻塞主协程的执行。当A协程完成任务后,它会通过defer语句关闭src通道。
  • 第二个协程(称为B协程)从src通道中读取整数,并将它们平方后发送到dest通道中。由于dest通道是带缓冲区的,因此即使在高并发情况下也不会阻塞主协程的执行。当B协程完成任务后,它会通过defer语句关闭dest通道。

最后,在主协程中,我们使用for range循环遍历dest通道中的数据,并将其打印出来。由于B协程比A协程先启动,因此输出的结果应该是0~9的整数的平方。

并发安全lock

package main  
  
import (  
"sync"  
"time"  
)  
  
var (  
x int64  
lock sync.Mutex  
)  
  
func addWithLock() {  
for i := 0; i < 2000; i++ {  
lock.Lock()  
x += 1  
lock.Unlock()  
}  
}  
func addWithoutLock() {  
for i := 0; i < 2000; i++ {  
x += 1  
}  
}  
func main() {  
x = 0  
for i := 0; i < 5; i++ {  
go addWithoutLock()  
}  
time.Sleep(time.Second)  
println("witnoutlock", x)  
x = 0  
for i := 0; i < 5; i++ {  
go addWithLock()  
}  
time.Sleep(time.Second)  
println("witnlock", x)  
x = 0  
}

image.png 注: 使用time.Sleep使协程睡眠确保并发子协程完成 首先,我们定义了一个全局变量x,用于存储需要累加的值。然后,我们定义了一个互斥锁lock,用于保证在多线程环境下对x的操作是原子性的。

接下来,我们定义了两个函数addWithLock()和addWithoutLock(),分别使用带锁和不带锁的方式对x进行累加操作。在addWithLock()函数中,我们使用for循环2000次,每次循环先通过lock.Lock()获取锁,然后将x加1,最后通过lock.Unlock()释放锁。在addWithoutLock()函数中,我们同样使用for循环2000次,但没有使用锁来保证操作的原子性。

在main()函数中,我们首先将x初始化为0,然后启动5个goroutine分别执行addWithoutLock()函数。接着,我们等待一秒钟,打印出x的值(即addWithoutLock()函数对x的贡献)。然后,我们将x重新初始化为0,并启动另外5个goroutine分别执行addWithLock()函数。最后,我们再次等待一秒钟,打印出x的值(即addWithLock()函数对x的贡献)。

WaitGroup

package main  
  
import (  
"fmt"  
"sync"  
)  
  
func hello(i int) {  
println("hello goroutine:" + fmt.Sprint(i))  
}  
func main() {  
var wg sync.WaitGroup  
wg.Add(5)  
for i := 0; i < 5; i++ {  
go func(j int) {  
defer wg.Done()  
hello(j)  
}(i)  
}  
wg.Wait()  
}

注: 使用sync.WaitGroup等待组确保并发子协程完成 image.png 这段代码实现了一个简单的并发程序,使用了Go语言的goroutine和WaitGroup。

首先,我们定义了一个函数hello(),用于输出"hello goroutine:"加上当前传入的参数i的值。然后,在main()函数中,我们创建了一个WaitGroup类型的变量wg,并使用Add()方法将5个goroutine添加到等待组中。接着,我们使用for循环启动了5个goroutine,每个goroutine都调用了hello()函数并传入一个不同的参数值。需要注意的是,在每个goroutine函数中,我们使用了defer语句来保证在函数执行完毕后调用wg.Done()方法,通知WaitGroup已经完成了一个goroutine的计数。最后,我们使用Wait()方法阻塞主线程,直到所有goroutine都执行完毕。

依赖管理

go依赖管理演进

image.png

GOPATH

image.png

GOPATH弊端

image.png

Go Vendor

image.png

Go Vendor—弊端

image.png

Go Module

image.png

依赖配置三要素

image.png

依赖配置—go.mod

image.png

依赖配置—indirect

image.png

依赖配置—incompatible

image.png

测试

单元测试

image.png

测试规则

image.png

测试例子

image.png 用于测试HelloTom函数。HelloTom函数返回一个字符串"Jerry",而测试用例期望的输出是"Tom"。如果实际输出与期望输出不符,测试用例将报告错误。

package main  
  
import "testing"  
  
func HelloTom() string {  
return "Jerry"  
}  
func TestHelloTom(t *testing.T) {  
output := HelloTom()  
expectOutput := "Tom"  
if output != expectOutput {  
t.Error("Expected %s do not match actual %s", expectOutput, output)  
}  
}

覆盖率

image.png

image.png 第一个测试用例传入参数70,期望返回值为true;第二个测试用例传入参数50,期望返回值为false。使用assert.Equal函数来比较期望输出和实际输出是否相等,如果不相等则会触发测试失败。

func TestJudgePassLineTure(t *testing.T) {  
isPass := JudgePassLine(70)  
assert.Equal(t, true, isPass)  
}  
func TestJudgePassLineFail(t *testing.T) {  
isPass := JudgePassLine(50)  
assert.Equal(t, false, isPass)  
}

单元测试—依赖

image.png

文件处理

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) {  
firstLine := ProcessFirstLine()  
assert.Equal(t, "line00", firstLine)  
}

这段代码是一个Go语言的程序,主要包含三个函数和一个测试用例。

第一个函数ReadFirstLine()用于读取一个名为"log"的文件的第一行内容并返回。如果文件打开失败,则返回空字符串。

第二个函数ProcessFirstLine()调用了ReadFirstLine()函数获取第一行的内容,然后使用strings.ReplaceAll()函数将其中所有的"11"替换为"00",最后返回替换后的结果。

第三个函数TestProcessFirstLine()是执行单元测试的函数,它调用ProcessFirstLine()函数并将结果与期望值进行比较,如果相等则通过测试,否则会触发测试失败。

整个程序的作用是读取日志文件的第一行内容,并将其中的"11"替换为"00",然后返回替换后的结果。同时还包括了一个针对该函数的单元测试用例。

Mock

monkey: github.com/bouk/monkey 这是一个开源的mock测试库,可以对method或者实例的方法进行mock。

Monkey Patch的作用域在Runtime, 运行时通过Go的unsafe包能够将内存中函数的地址替换为运行时函数的地址,将待打桩函数或方法的实现跳转。

快速Mock函数:

  • 为一个函数打桩
  • 为一个方法打桩
func Patch(target, replacement interface{}) *PatchGuard {  
  
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))  
}  
  
func TestProcessFirstLineWithMock(t *testing.T) {  
monkey.Patch(ReadFirstLine, func() string {  
return "line110"  
})  
defer monkey.Unpatch(ReadFirstLine)  
line := ProcessFirstLine()  
assert.Equal(t, "line000", line)  
}

第一个函数Patch(target, replacement interface{}) *PatchGuard用于将目标对象中的某个属性替换为新的值,并返回一个指向PatchGuard结构体的指针,该结构体包含了被替换的目标对象和新的值。

第二个函数Unpatch(target interface{}) bool用于将目标对象中的某个属性恢复为原始值,并返回一个布尔值表示是否成功恢复。

第三个函数TestProcessFirstLineWithMock(t *testing.T)是执行单元测试的函数,它使用monkey.Patch()函数将ReadFirstLine()函数替换为一个新的匿名函数,该函数返回字符串"line110"。然后使用defer monkey.Unpatch(ReadFirstLine)语句在测试结束后恢复ReadFirstLine()函数的原始实现。接着调用ProcessFirstLine()函数获取替换后的结果,并使用assert.Equal()函数将结果与期望值进行比较,如果相等则通过测试,否则会触发测试失败。