这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
Go 工程进阶
语言进阶
从并发的编程的视角了解 Go 高性能的本质
并发 & 并行
并行:多线程程序在不同的 CPU 核心上同时执行
并发:多线程程序在一个 CPU 核心上通过不停的切换执行的线程达到当前类似并行执行的效果 Golang 能充分发挥多核的优势高效运行
协程 & 线程
协程:用户态,轻量级的并发实现方式,栈 KB 级别
线程:内核态,协程运行在线程中,栈 MB 级别
-
Hello Goroutine
package main import ( "fmt" "time" ) func hello(i int) { fmt.Printf("Hello goroutine : %d\n", i) } func main() { fmt.Println("Start") for i := 0; i < 100; i++ { go hello(i) } // 休眠 1 秒,等待协程执行完成 time.Sleep(time.Second) fmt.Println("End") }
通道(chan)
通过 make(chan <type>[,size]) 创建通道
type: 元素类型
size: 缓冲大小
没有缓冲的通道,一个读请求会等待一个写请求
有缓冲的通道,当写满了时就会等待读请求将缓冲区中的元素去出去,等缓冲区能写入了才会继续执行
使用 close 函数能关闭通道,关闭后的通道读取数据会返回 默认值和 false,同时不能再读关闭后的通道再执行 close 和 写请求
-
计算平方后输出
package main import ( "fmt" ) func main() { fmt.Println("Start") 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 { // 使用 for range 方法,能自动感知通道是否关闭了,等到缓冲区读取完成,就会自动退出 dest <- i * i } }() for i := range dest { fmt.Println(i) } fmt.Println("End") }
并发不安全
10个协程对同一个变量进行 1 万次加 1 后,输出结果不一定是 10 万
package main
import (
"fmt"
"time"
)
func main() {
id := 0
for i := 0; i < 10; i++ {
go func(i int) {
for j := 0; j < 10000; j++ {
id += 1
}
fmt.Println("协程退出:", i)
}(i)
}
time.Sleep(time.Second)
fmt.Println("id = ", id)
}
并发安全
-
Mutex
使用互斥锁能使协程部分代码变一次原子操作,这个协程执行完成,再执行其它协程或者继续执行本协程
package main import ( "fmt" "sync" "time" ) func main() { var mutex sync.Mutex id := 0 for i := 0; i < 10; i++ { go func(i int) { for j := 0; j < 10000; j++ { mutex.Lock() id += 1 mutex.Unlock() } fmt.Println("协程退出:", i) }(i) } time.Sleep(time.Second) fmt.Println("id = ", id) } -
WaitGroup
通过
time.Sleep(time.Second)等待子协程结束运行显得格格不入,当子协程提前结束了也不能感知,同时如果子协程没有结束只等待1秒又不够,需要自行估算子协程全部结束所需要的时间,通过sync.WaitGroup能简化这一操作package main import ( "fmt" "sync" ) func main() { var mutex sync.Mutex id := 0 // 使用 WaitGroup 实现主协程等待子协程 var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) // 让计数器加一 go func(i int) { defer wg.Done() // 让计数器减一 for j := 0; j < 10000; j++ { mutex.Lock() id += 1 mutex.Unlock() } fmt.Println("协程退出:", i) }(i) } wg.Wait() // 当计数器的值为 0 时,结束等待 fmt.Println("id = ", id) }
依赖管理
Golang 依赖管理的演进路线
背景
- 工程项目不可能完全基于标准库从 0~1 实现
- 管理依赖库
演进过程
- GOPATH -> Go Vendor -> Go Module
作用
- 控制不同项目依赖的版本
- 引用其它已经实现好了的库
GOPATH
-
文件夹
-
bin
- 项目编译的二进制文件
-
pkg
- 项目编译的中间产物
-
src
- 项目源码
- 通过 go get 下载的最新的包
-
-
问题
- 不能让不同的项目依赖不同的包版本
Go Vender
-
类似 PHP 的 Composer 和 NPM 的包管理方式
-
文件夹
-
vender
- 在项目目录下
- 依赖的版本优先选择 vender 文件夹下的副本
-
-
问题
- 不能很好的解决传递依赖的版本冲突的问题
Go Module
-
类似Java 语言的依赖管理工具
- Maven
- Gradle
-
文件
-
go.mod
- 指定项目依赖包和版本
- 指定传递依赖包的版本
-
go.sum
-
-
依赖配置
-
module
- 标识一个模块的名字
-
go
- 标识使用的 Go 的版本
-
require
-
项目依赖的包,可以重复
-
indirect
- 加上 // indirect 的包,表示间接依赖
- 没有则表示直接依赖
-
incompatiable
- Go 推荐 2+ 的 Major 版本使用 /v* 的后缀 module命名
- 对于之前已经创建的版本需要在版本后拼接 【+incompatiable】
-
-
-
命令
-
init
- 通过 go mod init 创建 go.mod 文件
-
tidy
- 检查项目的依赖,并自动添加删除依赖
-
测试
编写良好的单元测试,能让项目更加健壮
单元测试
-
流程
-
输入
-
测试单元
- 函数
- 模块
- 接口
- ......
-
校对
- 输出
- 期望
-
-
规则
- 所有的测试文件以 _test.go 结尾
- 函数定义为 func TestXxx(*testing.T)
- 初始逻辑放在 TestMain 方法中
-
运行
- 使用 Fleet 或者 Golang 能直接使用测试方法前的运行按钮开始测试
- 通过命令行执行 go test [flags] packages
-
代码覆盖率
- 用来检测一个单元测试是否足够完备的一种方法
- 使用 --cover 标志
Mock 测试
-
通过开源库:monkey 实现
-
提供的函数
- Patch:将一个函数的实际执行代码进行替换,即为函数打桩
- Unpatch:撤销一个函数的 Patch
基准测试
-
测试程序的运行性能和 CPU 的损耗
-
规则
- 测试文件以 _test.go 结尾
- 函数定义为 func BenchmarkXxx(*testing.B)
-
运行
- 通过命令行执行 go test -bench=.
项目实战
通过实际的项目能学习更多在工作中的操作
需求描述
- 展示话题和回帖列表
- 仅仅实现一个本地 Web 服务
分层结构
-
数据层
数据 Model ,对外部数据进行处理
-
业务层
处理核心的业务逻辑
-
视图层
处理和外部额交互逻辑
代码的编写流程
这里使用外链(自己的),因为能更好的分步写出代码的编写过程