这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
昨天使用Go 语言基础实现了猜谜游戏、在线词典、SOCKS5代理,并且加以巩固记忆。
今天我将对于Go 语言进阶、依赖管理以及工程实践测试做出总结
一.语言进阶——并发编程
1. 并发 VS 并行
- 并发:多线程程序在一个核的CPU上运行
- 并行:多线程程序在多个核的CPU上运行
2. Goroutine
协程:用户态,轻量级线程,栈 KB 级别
线程:内核态,线程可以跑多个协程,栈 MB 级别
快速打印:
package main
import (
"fmt"
"time"
)
func main() {
HelloGoroutine()
}
func hello(i int) {
println("hello goroutine : " + fmt.Sprint(i))
}
func HelloGoroutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
3.CSP(Communicating Sequential Processes)
提倡通过通信共享内存而不是通过共享内存而实现通信
4. Channel 通道
make(chan 元素类型,[缓冲大小])
- 无缓冲通道 make(chan int)
- 有缓存通道 make(chan int, 2)
例子:
A 子协程发送0~9数字
B 子协程计算输入数字的平方
主协程输出最后的平方数
package main
func main() {
CalSquare()
}
func CalSquare() {
src := make(chan int)
dest := make(chan int, 2)
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)
}
}
5. 并发安全 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 Add() {
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock:", x)
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", x)
}
func main() {
Add()
}
6.WaitGroup
计数器
开启协程+1,执行结束协程-1;主协程阻塞直到计数器为0
快速打印优化:
func ManyGoWait() {
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()
}
二.依赖管理
1.背景
- 工程项目不可能基于标准库0~1编码构建
- 管理依赖库
2. 依赖管理演进
- 不同环境(项目)依赖的版本不同
- 控制依赖的版本
2.1.1 GOPATH
✔ 环境变量 $GOPATH
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物
- src:项目源码
✔ 项目代码直接依赖 src 下的代码
✔ go get 下载最新版本的包到 src 目录下
2.1.2 GOPATH弊端
场景:A 与 B 依赖与某一package的不同版本
问题:无法实现package的多版本控制
2.1.3 GO Vendor
- 项目目录下增加 vendor 文件,所有依赖包副本形式放在 $ProjectRoot/vendor
- 依赖寻址方式: vendor => GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
2.1.4 GO Vendor 弊端
问题:
- 无法控制依赖的版本
- 更新项目有可能出现依赖冲突, 导致编译错误
3. GO Moudle
- 通过
go.mod文件管理依赖包版本 - 通过go get/go mod 指令工具管理依赖包
3.1 依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具
go get/mod
3.2 依赖配置——go.mod
依赖标识:[Moudle Path] [Version/Pseudo-version]
3.3 依赖配置——version
语义化版本
${MAJOR}.${MINOR}.${PATCH}
>V1.3.0
>V2.3.0
基于 commit 伪版本
> vx.0.0-yyyymmddhhmmss-abcdefgh1234
3.4 依赖配置——indirct
{ A -> B 直接依赖
A -> B -> C
{ A -> C 简介依赖
对于go.mod,没有直接导入的依赖模块就会被标识为 indirect
3.5 依赖配置——incompatible
- 主版本2+模块会在模块路径增加/vN 后缀
- 对于没有go.mod 文件并且主版本2+的依赖,会+incompatible
3.6 依赖分发——回源
-
无法保证构建稳定性
- 增加/修改/删除软件版本
-
无法保证依赖可用性
- 删除软件
-
增加第三方压力
- 代码托管平台负载问题
3.7 依赖分发——Proxy
3.8 依赖分发——变量 GOPROXY
GOPROXY="proxy1.cn,https://proxy2.cn,…"
服务站点URL列表,"direct" 表示源站
Proxy 1 ---> Proxy 2 ---> Direct
4.工具
4.1 工具——go get
go get example.prg/pkg {@...}
@update 默认
@none 删除依赖
@v1.1.2 tag版本,语义版本
@23dfdd5 特定的commit
@master 分支的最新commit
4.2 工具——go mod
go mod {...}
init 初始化,创建go.mod文件
download 下载模块到本地缓存
tidy 增加需要的依赖,删除不需要的依赖
三.测试
1. 单元测试
1.1 单元测试——规则
- 所有测试文件以_test.go结尾
- func TestXxx(*testing.T)
func TestPublishPost(t *testing.T) {}
- 初始化逻辑放在TestMain中
func TestMain(m *testing.M) {
//测试前:数据装载、配置初始化等前置工作
code := m.Run()
//测试后:释放资源等收尾工作
os.Exit(code)
}
1.2 单元测试——例子
package main
import "testing"
func HelloTom() string {
return "jerry"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}
1.3 单元测试——assert
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func HelloTom() string {
return "Tom"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
1.4 单元测试——覆盖率
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
func TestJudgePassLineFail(t *testing.T) {
isPass := JudgePassLine(30)
assert.Equal(t, false, isPass)
}
1.5 单元测试——Tips
- 一般覆盖率:50%~60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
1.6 单元测试——依赖
外部依赖 >= 稳定&幂等
1.7 单元处理——文件处理
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)
}
2. Mock 测试
开源测试包:Monkey | monkey:github.com/bouk/monkey
快速Mock函数
- 为一个函数打桩
- 为一个方法打桩
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
3. 基准测试
- 优化代码,需要对当前代码进行分析
- 内测的测试框架提供了基准测试的能力
3.1 基准测试——例子
3.2 基准测试——运行
3.4 基准测试——优化
四.项目实战
1. 需求设计
1.1 需求描述
社区话题页面
✔ 展示话题(标题、文字描述)和回帖列表
✔ 暂不考虑前端页面实现, 仅实现一个本地web服务
✔ 话题和回帖数据用文件存储
1.2 需求用例
浏览消费用户
1.3 E-R图
1.4 分层结构
✔ 数据层 数据 Model,外部数据的增删改查
✔ 逻辑层 业务 Entity,处理核心业务逻辑输出
✔ 视图层 视图 view ,处理和外部的交互逻辑
1.5 组件工具
✔ Gin 高性能 go web 框架
https://github/gin-gonic/gin#installation
✔ Go Mod
go mod init
go get gopkg.in/gin-gonic/gin.v1@v1.3.0
2. 代码开发
2.1 Repository
2.2 Repository——index
2.3 Repository——查询
var (
topicDao *TopicDao
topicOnce sync.Once //类似于单例模式
)
func NewTopicDaoInstance() *TopicDao {
topicOnce.Do(
func() {
topicDao = &TopicDao{}
})
return topicDao
}
//参数为 话题ID
func (*TopicDao) QueryTopicById(id int64) *Topic {
return topicIndexMap[id]
}
2.4 Service
实体
type PageInfo struct {
Topic *repository.Topic
PostList []*repository.Post
}
流程
代码流程编排
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
}
2.5 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,
}
}
2.6 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)
})
err := r.Run()
if err != nil {
return
}
}
func Init(filePath string) error {
if err := repository.Init(filePath); err != nil {
return err
}
return nil
}