第五届字节跳动青训营Class2笔记 | 青训营笔记

198 阅读7分钟

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

本节课从工程实践角度,讲授在企业项目实际开发过程中的所遇的难题,重点讲解 Go 语言的进阶之路,以及在其依赖管理管理过程中如何演进。

1.Go语言的高性能特点

并发VS并行

多线程程序在一个核的cpu上运行/多线程程序在多个核的cpu上运行

Goroutine

image.png

协程:用户态、轻量级线程、栈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语言倾向于通过通信来共享内存

image.png

通过共享内存来进行通信会有锁操作,可能会影响程序性能。

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文件构成:

image.png

如果项目较为复杂,每一个文件夹下可能都需要一个go.mod文件进行依赖管理

依赖配置:

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

基于commit伪版本:vx.0.0-时间戳-哈希校验码

依赖类型:直接依赖/间接依赖

直接依赖库不会发生依赖冲突,而间接依赖库可能会涉及到多个版本。

image.png

Go语言会选择最低兼容版本去进行编译,在上图情况使用C 1.4版本进行编译

Proxy

在依赖分发方面,Go可以直接从代码托管平台获取依赖,但是可能无法保证构建的稳定性和依赖的可用性。

image.png

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
单元测试 --> 集成测试-->回归测试

单元测试

image.png

规则

①所有测试文件都以_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(实体关系图)

image.png

分层结构

image.png

数据层:数据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语言特性的情况,为后面深入优化和性能调优打下了良好的基础。

引用

源码:Moonlight-Zhao/go-project-example at v0.1 (github.com)