Go语言进阶 | 青训营笔记

97 阅读8分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记

1 Go语言进阶

1.1 Goroutine 协程

通过高效的调度模型,实现协程的高并发的操作

1.2 Channel 通道

通过通信实现共享内存

1.3 Sync包的两个关键字

  • Lock
  • WaitGroup

为了实现并发安全操作和协程间的一些同步

2 依赖管理

2.1 背景 Go依赖管理演进

站在巨人的肩膀上

  • 工程不可能基于标准库0~1代码编码搭建
  • 管理依赖库

依赖管理演进: GoPATH --> Go Vendor --> GoModule

  • 不同环境(项目)依赖的版本不同
  • 控制依赖库的版本

2.1.1 GOPATH

  • 环境变量$GOPATH
bin --- 项目编译的二进制文件
pkg --- 项目编译的中间产物,加速编译
src --- 项目源码
  • 项目代码直接依赖src下的代码
  • go get 下载最新版本的包到src目录下

弊端:无法实现package的多版本控制。

2.1.2 Go Vendor

  • 项目目录下增加 vendor 文件夹,所有依赖包副本放在$ProjectRoot/vendor
  • 依赖寻址方式:vendor => GOPATH

优点:通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。

弊端:

  • 无法控制依赖的版本。
  • 更新项目又可能出现依赖冲突,导致编译出错。

2.1.3 Go Module

  • 通过go.mod文件管理依赖包版本
  • 通过go get/go mod指令工具管理依赖包

终极目标:定义版本规则和管理项目依赖关系

2.2 依赖管理三要素

  1. 配置文件,描述依赖 go.mod
  2. 中心仓库管理依赖库 Proxy
  3. 本地工具 go get/go mod

2.3.1 依赖配置 - go.mod

module github.com/Moonlight-Zhao/go-project-example //依赖管理基本单元go 1.16 //原生库
​
require ( //单元依赖
    github.com/gin-contrib/sse v0.1.0 
    github.com/gin-gonic/gin v1.3.0 // indirect
)

依赖标识:[Module Path][Version/Pseudo-version]

2.3.2 依赖配置-version

  • 语义化版本

${MAJOR}.${MINOR}.${PATCH}

V1.3.0

V2.3.0

MAJOR:大版本,不兼容,代码隔离

MINOR:新增函数、各种功能,对于一个MAJOR前后兼容

PATCH:代码bug修复

  • 基于commit伪版本

vx.0.0-yyyymmddhhmmss-abcdefgh1234

v1.0.0-20200725025532-5a5fe074e612

2.3.3 依赖配置-indirect

A->B->C

  • A->B 直接依赖
  • A->C 间接依赖 // indirect

2.3.4 依赖配置-incompatible

  • 主版本2+模块会在模块路径增加/vN后缀。
  • 对于没有go.mod文件且主版本2+的依赖,会+incompatible

2.3.4 依赖配置-依赖图

1.png

编译时使用的C项目的版本为v1.4(选择最低的兼容版本)

2.3.5 依赖分发-回源

2.png

  • 无法保证构建稳定性(增加/修改/删除软件版本)
  • 无法保证依赖可用性(删除软件)
  • 增加第三方压力(代码托管平台负载问题)

2.3.5 依赖分发-Proxy

3.png

2.3.6 依赖分发-变量 GOPROXY

GOPROXY="https://proxy1.cn, https://proxy2.cn, direct"

服务站点URL列表,"direct"表示原站

Proxy 1 ==> Proxy 2 ==> direct

2.3.7 工具-go get

go get example.org/pkg + 
@update     默认
@none       删除依赖
@v1.1.2     tag版本,语义版本
@23dfdd5    特定的commit
@master     分支最新的commit
​
go mod + 
init        初始化,创建go.mod文件
download    下载模块到本地缓存
tidy        增加需要的依赖,删除不需要的

3 测试

质量就是“生命”

三种类型

  • 回归测试:手动通过终端回归一些固定的主流场景。
  • 集成测试:系统功能维度自动测试,自动化的回归测试。
  • 单元测试:面对测试开发阶段,开发者对单独的函数模块测试验证。

从上到下,覆盖率逐层变大,成本却逐层降低。

3.1 单元测试

4.png

3.1.1 单元测试-规则

  • 所有测试文件以_test.go结尾
  • func TestXxx(*testing.T)
  • 初始化逻辑放到TestMain(*testing.M)

3.1.2 单元测试-例子

main.go:

package main
​
import "fmt"func HelloTom() string {
    return "Jerry"
}
​
func main() {
    fmt.Println(HelloTom())
}

main_test.go:

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

3.1.3 单元测试-运行

go test main_test.go main.go

PS D:\myfiles\WorkSpace\GoWorkPlace> go test main.go main_test.go
--- FAIL: TestHelloTom (0.00s)
    main_test.go:9: Expected Tom do not match actual Jerry
FAIL
FAIL    command-line-arguments  0.041s
FAIL
​

3.1.4 单元测试-assert

package main
​
import (
    "github.com/stretchr/testify/assert"
    "testing"
)
​
func TestHelloTom(t *testing.T) {
    output := HelloTom()
    expectOutput := "Tom"
    assert.Equal(t, expectOutput, output)
}

3.1.5 单元测试-覆盖率

func JudgePassLine(score int16) bool {
    if score >= 60 {
        return true
    }
    return false
}
func TestJudgePassLine(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t, true, isPass)
}

--cover:测试覆盖率

66.7% :JudgePassLine()中语句只执行到了return true,执行了三分之二。

PS D:\myfiles\WorkSpace\GoWorkPlace> go test .\judgement_test.go .\judgement.go --cover
ok      command-line-arguments  0.255s  coverage: 66.7% of statements

3.1.5 单元测试-Tips

  • 一般覆盖率:50%~60%,较高覆盖率80%+。
  • 测试分支相互独立、全面覆盖。
  • 测试单元粒度足够小,函数单一职责。

3.2 单元测试-依赖

5.png

外部依赖 ==> 稳定&幂等

幂等:重复执行多次结果一样。

稳定:单元测试相互隔离,任何时间任何函数独立运行。

3.3 单元测试-文件处理

package main

import (
	"bufio"
	"os"
	"strings"
)

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
}
package main

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestProcessFirstLine(t *testing.T) {
	firstLine := ProcessFirstLine()
	assert.Equal(t, "line00", firstLine)
}

3.4 单元测试-Mock

monkey: https://github.com/bouk/monkey

快速Mock函数

  • 为一个函数打桩
  • 为一个方法打桩

对ReadFirstLine打桩测试,不再依赖本地文件

package main

import (
	"bou.ke/monkey"
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestProcessFirstLineWithMock(t *testing.T) {
	monkey.Patch(ReadFirstLine, func() string {
		return "line110"
	})
	defer monkey.Unpatch(ReadFirstLine)//对桩函数的卸载
	line := ProcessFirstLine()
	assert.Equal(t, "line000", line)
}

3.5 基准测试

  • 优化代码,需要对当前代码分析
  • 内置的测试框架提供了基准测试的能力

3.5.1 基准测试-例子

package main

import "math/rand"

var ServerIndex [10]int

func InitServerIndex() {
	for i := 10; i < 10; i ++ {
		ServerIndex[i] = i + 100
	}
}

func Select() int {
	return ServerIndex[rand.Intn(10)]
}

随机选择执行服务器

3.5.2 基准测试-运行

package main

import "testing"

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()
		}
	})
}

并行进行测试性能劣化:Select使用了sdk的rand函数,为了保证全局的随机性和并发安全,使用了全局锁,降低了性能。

BenchmarkSelect-8   			75599122	        14.71 ns/op
BenchmarkSelectParallel-8   	24799228	        46.56 ns/op

3.5.3 基准测试-优化

fastrand:github.com/bytedance/g…

func FastSelect() int {
	return ServerIndex[fastrand.Intn(10)]
}
BenchmarkSelectParallel-8   	1000000000	         0.9790 ns/op

4 项目实践

4.1 需求描述

社区话题页面

  • 展示话题(标题,文字描述)和回帖列表
  • 暂不考虑前端页面实现,仅仅实现一个本地web服务
  • 话题和回帖数据用文件存储

4.2 需求用例

浏览消费用户

6.png

4.3 ER图-Entity Relationship Diagram

  • 话题
  • 帖子

7.png

4.4 分层结构

8.png

  • 数据层:数据Model,外部数据的增删改查
  • 逻辑层:业务Entity,处理核心业务逻辑输出
  • 视图层:试图View,处理和外部的交互逻辑

4.5 组件工具

4.6 Repository

两个结构体

  • Topic
{
	"id":1,
    "title":"青训营来啦", 
    "content":"小姐姐,快到碗里来~",
    "create_time":1650437625
}
  • Post
{
	"id":1, 
	"parent_id":1,
	"content":"小姐姐快来1",
	"create_time":1650437616
}

两个基本查询操作

QueryTopicById //通过话题id查询话题
QueryPostByParentId //通过话题id查询话题关联的所有帖子

4.6 Repository-index

元数据 索引

数据行 ==>内存Map

var (
	topicIndexMap map[int64]*Topic
	postIndexMap map[int64][]*Post
)

初始化话题数据索引

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 initPostIndexMap(filePath string) error{
	open, err := os.Open(filePath + "post")
	if err != nil {
		return err
	}
	scanner := bufio.NewScanner(open)
	postTmpMap := make(map[int64][]*Post)
	for scanner.Scan() {
		text := scanner.Text()
		var post Post
		if err := json.Unmarshal([]byte(text), &post); err != nil {
			return err
		}
		posts, ok := postTmpMap[post.ParentId]
		if !ok {//如果topic里没有parent
			postTmpMap[post.ParentId] = []*Post{&post}//新建一个映射
			continue
		}
		posts = append(posts, &post)//Map里面添加该post
		postTmpMap[post.ParentId] = posts//再映射回去
	}
	postIndexMap = postTmpMap
	return nil
}

4.6 Repository-查询

索引:话题ID

数据:话题

type Topic struct {
	Id         int64  `json:"id"`
	Title      string `json:"title"`
	Content    string `json:"content"`
	CreateTime int64  `json:"create_time"`
}
type TopicDao struct {
}
var (
	topicDao  *TopicDao
	topicOnce sync.Once //适合在高并发场景下只执行一次,减少存储浪费
)
func NewTopicDaoInstance() *TopicDao {
	topicOnce.Do(
		func() {
			topicDao = &TopicDao{}
		})
	return topicDao
}
func (*TopicDao) QueryTopicById(id int64) *Topic {
	return topicIndexMap[id]
}

索引:帖子所在话题的id

数据:帖子

type Post struct {
	Id         int64  `json:"id"`
	ParentId   int64  `json:"parent_id"`
	Content    string `json:"content"`
	CreateTime int64  `json:"create_time"`
}
type PostDao struct {
}
var (
	postDao *PostDao
	postOnce sync.Once
)
func NewPostDaoInstance() *PostDao {
	postOnce.Do(
		func() {
			postDao = &PostDao{}
		})
	return postDao
}
func (*PostDao) QueryPostsByParentId(parentId int64) []*Post {
	return postIndexMap[parentId]
}

4.7 Service

代码流程编排

func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
	if err := f.checkParam(); err != nil {
		return nil, err
	}
	if err := f.prepareInfo(); err != nil {
		return nil, err
	}
	if err := f.packPageInfo(); err != nil {
		return nil, err
	}
	return f.pageInfo, nil
}

可用性

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
}

并行处理话题信息和回帖信息

4.8 Controller

  • 构建View对象
  • 业务错误码
type PageData struct {
	Code int64       `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}
func QueryPageInfo(topicIdStr string) *PageData {
	topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	pageInfo, err := service.QueryPageInfo(topicId)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	return &PageData{
		Code: 0,
		Msg:  "success",
		Data: pageInfo,
	}
}

4.9 Router

  • 初始化数据索引
  • 初始化引擎配置
  • 构建路由
  • 启动服务
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)//json化,将data数据返回
	})
    //启动服务
	err := r.Run()
	if err != nil {
		return
	}
}

func Init(filePath string) error {
	if err := repository.Init(filePath); err != nil {
		return err
	}
	return nil
}

4.10 运行

Windows本地ip为127.0.0.1 ,默认端口8080

go run server.go
curl --location --request GET 'http://127.0.0.1:8080/community/page/get/2'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   543  100   543    0     0  60922      0 --:--:-- --:--:-- --:--:-- 67875
{"code":0,"msg":"success","data":{"Topic":{"id":1,"title":"青训营来啦!","content":"小姐姐,快到碗里来~","create_time":1650437625},"PostList":[{"id":1,"parent_id":1,"content":"小姐姐快来1","create_time":1650437616},{"id":2,"parent_id":1,"content":"小姐姐快来2","create_time":1650437617},{"id":3,"parent_id":1,"content":"小姐姐快来3","create_time":1650437618},{"id":4,"parent_id":1,"content":"小姐姐快来4","create_time":1650437619},{"id":5,"parent_id":1,"content":"小姐姐快来5","create_time":1650437620}]}}

5 课后实践

  • 支持发布帖子。
  • 本地Id生成需要保证不重复、唯一性。
  • Append文件,更新索引,注意Map的并发安全问题。