Go语言入门—工程实践
语言进阶、
并发编程——快
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)
提倡通过通信共享内存而不是通过共享内存而实现通信
channel
go实现CSP的形式便是channel。它是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。
go语言的channel主要有两种机制。
一种是通讯的两方必须同时在channel上,才能进行交互,有任何一方不在的时候,另一方就会被阻塞在那里等待着,直到另一方有回应才能重新进行交互。
另一种就是"Buffered Channels"。我们可以给channel指定一个容量,当容量没有满的时候,我们发送消息的一方只要容量没有满,就可以一直放,如果容量满了,那必须等接受消息的人就拿出一个数据把位子空出来,才能继续发送数据。对于接受数据的人也是如此。
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)
}
}
这段代码实现了一个简单的并发程序,使用了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
}
注: 使用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等待组确保并发子协程完成
这段代码实现了一个简单的并发程序,使用了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依赖管理演进
GOPATH
GOPATH弊端
Go Vendor
Go Vendor—弊端
Go Module
依赖配置三要素
依赖配置—go.mod
依赖配置—indirect
依赖配置—incompatible
测试
单元测试
测试规则
测试例子
用于测试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)
}
}
覆盖率
第一个测试用例传入参数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)
}
单元测试—依赖
文件处理
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()函数将结果与期望值进行比较,如果相等则通过测试,否则会触发测试失败。