这是我参与「第五届青训营 」笔记创作活动的第2天
本节课从工程实践角度,讲授在企业项目实际开发过程中的所遇的难题,重点讲解 Go 语言的进阶之路,以及在其依赖管理管理过程中如何演进。
1.Go语言的高性能特点
并发VS并行
多线程程序在一个核的cpu上运行/多线程程序在多个核的cpu上运行
Goroutine
协程:用户态、轻量级线程、栈MB级别 线程:内核态,线程跑多个协程、栈KB级别
协程由Go语言本身进行调动,更加灵活
Go语言创建协程:
①使用go关键字+函数名方式创建
func hello(str string){
fmt.Println(str)
}
func main(){
go hello("Hello,world!")
time.Sleep(time.Second)
}
①使用go关键字+匿名函数方式创建
func main(){
go func(){
fmt.Println("Hello,world!")
}()
time.Sleep(time.Second)
}
CSP(Communicating Sequential Process)
Go语言倾向于通过通信来共享内存
通过共享内存来进行通信会有锁操作,可能会影响程序性能。
Channel
通道是实现通信的必备工具,Go语言为管道提供了各种管道数据类型。
make(chan int) //无缓冲通道 make(chan int,2) //有缓冲通道
注意管道可以用range遍历,只有一个返回值,管道关闭后只可读不可写
func CalSquare(){
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)
}
}
并发安全Lock
var (
x int64
lock sync.Mutex
)
func addWithLock(){
for i := 0;i<2000;i++{
lock.Lock()
x+=1
lcok.Unlock()
}
}
并发安全锁可以防止在读写并发的情况。
WaitGroup
简单的保持主线程执行直到所有并发任务执行完的方式为time.Sleep()等阻塞方法
Go语言提供了 sync.WaitGroup类型 其中使用的计数器方法来统计正在运行的协程,从而保证协程全部运行结束之前主线程仍然保持运行。
Done()方法使得程序计数器-1,而Wait()方法在程序计数器大于零时阻塞主线程。
func ManyGoWait(){
var wg sync.WaitGroup
wq.Add(5)
for i:=0;i<5;i++{
go dunc(j int){
defer wg.Done()
fmt.Println(Hello world!")
}(i)
}
wg.Wait()
}
2.依赖管理
Go的依赖管理方式处于一个不断迭代的过程:
graph TD
GOPATH -->
GoVendor -->
GoModule
GOPATH
GOPATH为Go语言的环境变量,在该环境变量下,有三个文件夹bin pkg src,分别处理二进制文件、编译的中间文件、源码以及依赖。
缺点:无法实现package的多版本控制
Go Vendor
项目目录下增加vendor文件,所有依赖包的副本形式放在项目根目录的vendor下,依赖寻址方式为vendor=>GOPATH
缺点:项目的两个依赖的进一步依赖可能会出现依赖冲突,因为并没有本质上标注依赖的版本
Go Module
通过go.mod文件管理依赖包版本,用go get来获取依赖并加入go.mod文件
go.mod文件构成:
如果项目较为复杂,每一个文件夹下可能都需要一个go.mod文件进行依赖管理
依赖配置:
语义化版本:${MAJOR}.${MINOR}.${PATCH}
V1.3.0
V2.3.0
基于commit伪版本:vx.0.0-时间戳-哈希校验码
依赖类型:直接依赖/间接依赖
直接依赖库不会发生依赖冲突,而间接依赖库可能会涉及到多个版本。
Go语言会选择最低兼容版本去进行编译,在上图情况使用C 1.4版本进行编译
Proxy
在依赖分发方面,Go可以直接从代码托管平台获取依赖,但是可能无法保证构建的稳定性和依赖的可用性。
Go语言使用Proxy来缓存依赖,来保证依赖分发的稳定性
Proxy站点的获取依赖于Go Module中GOPROXY环境变量
例:GOPROXT="proxy1.cn,https://proxy2.cn"
go mod工具
go mod init:初始化,创建go.mod文件
go mod download: 下载模块到本地缓存
go mod tidy:增加需要的依赖,删除不需要的依赖
3.Go语言测试
graph TD
单元测试 --> 集成测试-->回归测试
单元测试
规则
①所有测试文件都以_test.go结尾 ②func TestXxx(*testing.T) ③初始化逻辑放到TestMain中
使用go test [flags] [packages]可以进行测试
对于测试结果也可以使用其他依赖的equal()方法
代码覆盖率
代码覆盖率是对代码测试等级的表征
在使用go test命令时,加入--cover flag可以在测试完成后输出覆盖率
一般覆盖率:50%-60%,较高覆盖率为80%
测试分支要相互独立、全面覆盖,测试单元粒度要足够小,函数单一职责。
依赖
依赖的测试有两个目标:稳定、幂等
文件处理与Mock
Mock()可以为一个函数和方法打桩,打桩函数可以让测试代码不对测试文件有强依赖
使用monkey库的Patch()方法和Unpatch()方法可以实现对函数进行打桩和桩函数的卸载。
如某场景中,待测试的A函数直接调用了B函数,而B函数调用了未实现的C函数和文件D,使用Patch()方法修改被调用函数的返回值,从而在不实际调用B函数及其依赖调用的前提下实现了对A函数的测试。
基准测试
单元测试为功能测试,而基准测试为性能测试,在基准测试中,引入测试包之后使用测试包的方法进行计时和计数,在循环执行多次下返回基本测试数据。
如服务器负载均衡场景:
import "math/rand"
var ServerIndex [10]int
func InitServerIndex(){
for i:=10;i <10; i++{
Server.Index[i] = i+100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
使用rand随机数的方式分配对应的服务器 在测试中使用BenchmarkXxxx()、BenchmarkXxxxParallel()的方法进行非并行/并行的基准测试
func BenchmarkSelect(b *testing.B){
InitServerIndex()
b.ResetTimer()
for i :=0;i<b.N;i++{
Select()
}
}
func BenchmarkSelectParallel(b *testing.B){
InitServerIndex()
b.ResetTimer()
//通过传入匿名函数作为参数的方式执行并行执行方法
b.RunParallel(func(pb *testing.PB){
for pb.Next(){
Select()
}
})
}
发现并发执行的情况下性能不如非并发的情况,这是由于rand随机数方法的全局锁的问题,导致高并发时不能提高性能。
4.Go项目实践
定义一个“社区话题页面”的需求,实现一个本地的web服务
ER图-Entity Relationship Diagram(实体关系图)
分层结构
数据层:数据Model,外部数据的增删改查
逻辑层:业务Entity,处理核心业务逻辑输出
视图层:视图View,处理和外部的交互逻辑
组件工具
Gin:开源的go web框架
Repository层
实现查询:
①数据存入内存的方式:全扫描遍历(全本扫描)、数据行存入Map进行索引
索引方式:
var (
//Topic和Post结构体预先定义,由于数据唯一,采用读写锁的方式应对并发
topicIndexMap map[int64]*Topic
postIndexMap map[int64][]*Post
rwMutex sync.RWMutex
)
//实现逐行读取文本数据并用id作为索引存入map
func initTopicIndexMap(filePath string) error {
open, err := os.Open(filePath + "topic")
if err != nil {
return err
}
scanner := bufio.NewScanner(open)
topicTmpMap := make(map[int64]*Topic)
for scanner.Scan() {
text := scanner.Text()
var topic Topic
if err := json.Unmarshal([]byte(text), &topic); err != nil {
return err
}
topicTmpMap[topic.Id] = &topic
}
topicIndexMap = topicTmpMap
return nil
}
②实现高并发下的查询
func NewTopicDaoInstance() *TopicDao {
topicOnce.Do(
func() {
topicDao = &TopicDao{}
})
return topicDao
}
func (*TopicDao) QueryTopicById(id int64) *Topic {
return topicIndexMap[id]
}
这里使用NewTopicDaoInstance()来实现topicDao的实例化,封装sync.Once类型变量进入单例模式,使得在高并发的情况下topicDao能够只被实例化一次。
注意Go语言并没有具体类的功能,但是可以通过对结构体添加方法来封装一个类: 在 QueryTopicById()前面的括号内容代表其为TopicDao的一个方法,之后该方法将会运行在TopicDao类型对象上,作为该对象的方法等待调用
Service层
graph TD
参数校验 --> 准备数据-->组装实体
服务层本身没有复杂的业务逻辑,因此并行执行对topic和post的查询即可。(实际开发中还要进行参数校验)将得到的数据封装入组装好的实体中,等待Controller的调用
func (f *QueryPageInfoFlow) prepareInfo() error {
//获取topic信息
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
topic := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
f.topic = topic
}()
//获取post列表
go func() {
defer wg.Done()
posts := repository.NewPostDaoInstance().QueryPostsByParentId(f.topicId)
f.posts = posts
}()
wg.Wait()
return nil
}
Controller层
构建业务对象,设置业务错误码
此处interface{}接口预留给了Service层中构造的实体的结构体类型。
type PageData struct {
Code int64 `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
Router
使用gin来实现web服务
①初始化数据索引:使用预先实现好的Init()函数进行初始化
②初始化引擎配置:r:=gin.Default()
③构建路由:完善r.GET()、r.POST()其中使用gin.Context类型的Param()方法和GetPostForm()方法来得到返回数据,注意这点与Java自行在Service层构造实体并直接用@response注解传参的方式不同。
④启动服务:r.Run()
func main() {
if err := Init("./data/"); err != nil {
os.Exit(-1)
}
r := gin.Default()
r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id")
data := cotroller.QueryPageInfo(topicId)
c.JSON(200, data)
})
r.POST("/community/post/do", func(c *gin.Context) {
topicId, _ := c.GetPostForm("topic_id")
content, _ := c.GetPostForm("content")
data := cotroller.PublishPost(topicId, content)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}
func Init(filePath string) error {
if err := repository.Init(filePath); err != nil {
return err
}
return nil
}
总结
本节课程通过Go语言处理高并发能力作为引入,用一个常规的实战例子介绍了Go语言在工程方面的使用和gin框架启动web服务,全面展现了Go语言在整个业务实现中的架构和各种要点,也提到了很多实际应用中利用到Go语言特性的情况,为后面深入优化和性能调优打下了良好的基础。