1.2 Go语言上手-工程实践

代码链接:https://github.com/Moonlight-Zhao/go-project-example/tree/V0

Go语言上手-工程实践

1.语言进阶

从并发编程的视角带大家了解Go高性能的本质

1.1 并发 VS 并行

并发:多线程程序在一个核的cpu上运行
在这里插入图片描述
并行:多线程程序在多个核的cpu上运行
在这里插入图片描述
Go可以充分发挥多核优势,高效运行

1.2 协程【Goroutine】

协程:用户态,轻量级线程,栈MB级别
线程:内核态,线程跑多个协程,栈KB级别
在这里插入图片描述

package main

import (
	"fmt"
	"time"
)

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

func main() {
	HelloGoRoutine()
}

运行结果

在这里插入图片描述

1.3 【CSP(Communication Sequential Processes)】

提倡通过通信共享内存而不是通过共享内存而实现通信
在这里插入图片描述

1.4 Channel

make(chan 元素类型,[缓冲大小])

  • 无缓冲通道[同步通道] make(chan int)
  • 有缓冲通道 make(chan int,2)
    在这里插入图片描述

A 子协程发送0~9数字
B 子协程计算输入数字的平方
M 主协程输出最后的平方数

package main

func CalSquare()  {
	src := make(chan int)
	dest := make(chan int,3)

	//A
	go func() {
		//延迟的资源关闭
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()

	//B
	go func() {
		defer close(dest)
		for i := range src{
			dest <- i * i
		}
	}()

	//M
	for i := range dest{
		//复杂操作
		println(i)
	}
}

func main() {
	CalSquare()
}

运行结果

在这里插入图片描述

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

运行结果

在这里插入图片描述

1.6 WaitGroup

在这里插入图片描述

计数器:开启协程+1;执行结束-1;主协程阻塞直到计数器为0

package main

import (
	"fmt"
	"sync"
)

func hello_new(i int) {
	println("hello goroutine :" + fmt.Sprint(i))
}

func ManyGoWait() {
	var wg sync.WaitGroup
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(j int) {
			defer wg.Done()
			hello_new(j)
		}(i)
	}
	wg.Wait()
}

func main() {
	ManyGoWait()
}

运行结果

在这里插入图片描述

2.依赖管理

了解Go语言依赖管理的演进路线

2.1 背景

在这里插入图片描述

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

2.2 Go依赖管理演进

GOPATH->GO Vendor -> Go Module

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

2.2.1 GOPATH

  • 环境变量 $GOPATH
    在这里插入图片描述

  • 项目代码直接依赖src下的代码

  • go get下载最新版本的包到src目录下

2.2.1 GOPATH-弊端

在这里插入图片描述

场景:A和B依赖于某一package的不同版本
问题:无法实现package的多版本控制

2.2.2 Go Vender

  • 项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
  • 依赖寻址方式:vendor => GOPATH
  • 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
    在这里插入图片描述

2.2.2 Go Vender-弊端

在这里插入图片描述

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

2.2.3 Go Module

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

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

2.3 依赖管理三要素

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

2.3.1 依赖配置-go.mod

在这里插入图片描述

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

2.3.2 依赖配置-version

  • 语义化版本
    $ {MAJOR}. $ {MINOR}.$[PATCH}
    V1.3.0
    V2.3.0
  • 基于commit伪版本
    vX.0.0-yyyymmddhhmmss-abcdefgh1234
    v0.0.0-20220401081311-c38fb59326b7
    v1.0.0-20201130134442-10cb98267c6c

2.3.3 依赖配置-indirect

在这里插入图片描述

A->B->C
A->B直接依赖
A->C间接依赖

2.3.4 依赖配置-incompatible

在这里插入图片描述

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

2.3.5 依赖配置-依赖图

在这里插入图片描述
如果X项目依赖了A、B两个项目,且A、B分别依赖了C项目的v1.3、v1.4两个版本,最终编译时所使用的C项目的版本为如下哪个选项?(单逃)
A. v1.3
B. v1.4
C.A用到c时用v1.3编译,B用到c时用v1.4编译

:B
解析:选择最低的兼容版本

2.3.6 依赖分发-回源

在这里插入图片描述

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

2.3.5 依赖分发-Proxy

Go Proxy是一个服务站点,它会缓源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供“immutability”和"available"的依赖分发。使用Go Proxy之后,构建时会直接从Go Proxy站点拉取依赖
在这里插入图片描述

2.3.6 依赖分发-变量 GOPROXY

GOPROXY="https://proxy1.cn, https://proxy2.cn ,direct”
服务站点URL列表,“direct”表示源站
对于实例配置,整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,后下钻proxy2寻找,如果proxy2中不存在,则会回源到源站直接下载依赖,缓存到proxy站点中
在这里插入图片描述

2.3.7 工具- go get

go get example.org/pkg

  • @update 默认
  • @one 删除依赖
  • @v1.1.2 tag版本,语义版本
  • @23dfdd5 特定的commit
  • @master 分支的最新的commit

2.3.8 工具- go mod

go mod

  • init 初始化,创建go.mod文件
  • download 下载模块到本地缓存
  • tidy 增加需要的依赖,删除不需要的依赖

3.测试

从单元测试实践出发,提升大家的质量意识
在这里插入图片描述
测试一般分为:
回归测试:同学手动通过终端回归一些固定的主流程场景
集成测试:对系统功能维度做测试验证
单元测试:测试开发阶段,开发者对单独的函数,模块做功能验证

层级从上至下,测试成本逐渐减低,而测试覆盖率确逐步上升,所以单元测试的覆盖率一定程度上决定这代码的质量

在这里插入图片描述
从上到下,覆盖率逐层变大,成本却逐层降低

3.1 单元测试

在这里插入图片描述
在这里插入图片描述

3.1.1 规则

  • 所有测试文件以_test.go结尾
    在这里插入图片描述

  • func TestXxx(*testing.T)
    在这里插入图片描述

  • 初始化逻辑放到TestMain中
    在这里插入图片描述

3.1.2 例子

testing.T 是普通测试包

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

运行结果:
在这里插入图片描述

在这里插入图片描述

备注:
记住将文件名改为以_test.go结尾
在这里插入图片描述

导入开源的包改变比较的函数:
先下载git
然后cmd 中安装:go get github.com/bmizerany/assert

package newTom

import (
	"github.com/bmizerany/assert"
	"testing"
)


func HelloTom() string {
	return "Jerry"
}

func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"

	assert.Equal(t,expectOutput,output)
}

运行结果:

在这里插入图片描述

3.1.3 覆盖率

  • 衡量代码是否经过了足够的测试
  • 评价项目的测试水准
  • 评估项目是否达到了高水准测试等级
  • -cover 开启测试覆盖率

coverage.go

package coverage

func JudgePassLine(score int16) bool {
	if score >= 60{
		return true
	}
	return false
}

coverage_test.go

package coverage

import (
	"github.com/bmizerany/assert"
	"testing"
)

func TestJudgePassLineTrue(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t,true,isPass)
}

运行结果:

在这里插入图片描述
改变coverage_test.go代码,增加一个不及格的测试case

package coverage

import (
	"github.com/bmizerany/assert"
	"testing"
)

func TestJudgePassLineTrue(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t,true,isPass)
}

func TestJudgePassLineFalse(t *testing.T) {
	isPass := JudgePassLine(50)
	assert.Equal(t,true,isPass)
}

运行结果:

在这里插入图片描述

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

3.2 依赖

工程中复杂的项目一般会有外部依赖,而在单元测试中需要保证稳定性和幂等性

  • 稳定:指相互隔离,能在任何时间,任何环境,运行测试
  • 幂等:指每一次测试运行都应该产生与之前一样的结果
  • 要实现这一目的就要用到mock机制

在这里插入图片描述

3.3 文件处理

例子:将文件中的第一行字符串中的11替换成00,执行单元测试

file.go:

package file

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
}

file_test.go:

package file

import (
	"github.com/bmizerany/assert"
	"testing"
)


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

log:

line11
line22
line33
line44
line55

运行结果:

在这里插入图片描述

单元测试需要依赖本地的文件,如果文件被修改或者删除测试就会fail,为了保证测试case的稳定性,对读取文件函数进行mock,屏蔽对于文件的依赖

3.4 Mock

monkey是一个开源的mock测试库,可以对method,或者实例的方法进行mock,反射,指针赋值
monkey : https://github.com/bouk/monkey
快速Mock函数:

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

patch源代码:

// Patch replaces a function with another
func Patch(target, replacement interface{}) *PatchGuard {
	t := reflect.ValueOf(target)
	r := reflect.ValueOf(replacement)
	patchValue(t, r)

	return &PatchGuard{t, r}
}

Unpatch源代码:

// Unpatch removes any monkey patches on target
// returns whether target was patched in the first place
func Unpatch(target interface{}) bool {
	return unpatchValue(reflect.ValueOf(target))
}

将上面测试代码更改:

通过patch对Readfineline进行打桩mock,默认返回line110,这里通过defer卸载mock,这样整个测试函数就摆脱了本地文件的束缚和依赖

package file

import (
	"github.com/bmizerany/assert"
	"bou.ke/monkey"
	"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 例子

举一个服务器负载均衡的例子,首先有10个服务器列表,每次随机执行select函数随机选择一个执行

practice.go:

package main

import "math/rand"

var ServerIndex [10]int

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

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

3.5.2 测试运行

practice_test.go:

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

运行结果:
-bench regexp 执行相应的 benchmarks(基准),例如 -bench=.;

在这里插入图片描述
-bench=.表示运行 benchmark_test.go 文件里的所有基准测试

  • BenchmarkSelect-16:基准测试名称
  • 70778086:表示测试的次数,也就是 testing.B 结构中提供给程序使用的 N
  • “16.6 ns/op”:表示每一个操作耗费多少时间(纳秒)

3.5.3 优化

字节为了解决随机性能问题,开源了一个高性能随机数方法fastrand,开源地址为:http://github.com/bytedance/gopkg

先导入:go get github.com/bytedance/gopkg
将初试代码改为:

package main

import (
	"github.com/bytedance/gopkg/lang/fastrand"
)


var ServerIndex [10]int

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

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

运行结果:

在这里插入图片描述
性能提高了百倍,主要思想是牺牲了一定的数列一致性

4.项目实战

通过项目需求,需求拆解,逻辑设计,代码实现带领大家感受下真实的项目开发

4.1 需求设计

社区话题页面

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

4.2 需求用例

主要功能点:用户浏览消费,涉及页面的展示,包括话题内容和回帖的列表
在这里插入图片描述

4.3 ER图

  • 话题
  • 帖子

在这里插入图片描述

4.4 分层结构

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

在这里插入图片描述

  • 数据层关联底层数据模型,也就是这里的model,封装外部数据的增删改查,由于我们的数据存储在本地文件,通过文件操作拉取话题,帖子数据
  • 数据从面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的
  • Service逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层
  • Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结构,api形式访问就好

4.5 组件工具

  • Gin高性能 go web框架
    https://github.com/gin-gonic/gin#installation
  • Go Mod
    go mod init
    go get gopkg.in/ginogonic/gin.v1@v1.3.0

4.6 定义struct

根据之前的ER图,先定义struct
在这里插入图片描述
定义数据data:
在这里插入图片描述
post:

{"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}
{"id":6,"parent_id":2,"content":"小哥哥快来1","create_time":1650437621}
{"id":7,"parent_id":2,"content":"小哥哥快来2","create_time":1650437622}
{"id":8,"parent_id":2,"content":"小哥哥快来3","create_time":1650437623}
{"id":9,"parent_id":2,"content":"小哥哥快来4","create_time":1650437624}
{"id":10,"parent_id":2,"content":"小哥哥快来5","create_time":1650437625}

topic:

package repository

import (
	"sync"
)

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]
}

4.7 Repository

在这里插入图片描述

4.7.1 Repository-index

  • 可以使用全扫描遍历,但是效率不高,这里引出索引的概念
  • 这里用map实现内存索引,在服务对外暴露前,利用文件元数据初始化全局内存索引,这样就可以实现0/1的时间复杂度查找操作
    在这里插入图片描述
    db_init.go:
var(
    topicIndexMap map[int64]*Topic
    postIndexMap map[int64][]*Post
)

4.7.2 初始化话题数据索引

  1. 打开文件
  2. 基于file初始化scanner
  3. 通过迭代器方式遍历数据行
  4. 转化为结构体存储至内存map

db_init.go:

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
}

4.7.3 查询操作

  • 直接根据查询key获得map中的value
  • 使用sync.once 主要适用高并发的场景下只执行一次的场景
  • 这里基于once的实现模型就是我们平常的单例模型,减少存储的浪费

topic.go:

package repository

import (
	"sync"
)

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]
}

4.8 Service

在这里插入图片描述
在这里插入图片描述
通过err控制流程退出,正常会返回页面信息,err为nil

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
}

关于prepareInfo方法,话题和回帖信息的获取都依赖topicid,这样就可以并行执行,提高执行效率
在这里插入图片描述

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.9 Controller

在这里插入图片描述

定义一个view对象,通过code msg打包业务状态信息,用data承载业务实体信息

package cotroller

import (
	"strconv"

	"github.com/Moonlight-Zhao/go-project-example/service"
)

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.10 Router

在这里插入图片描述

最后是web服务的引擎配置

  • 初始化数据索引
  • 初始化引擎配置
  • 构建路由
  • 启动服务

path映射到具体的controller,通过path变量传递话题id

package main

import (
	"github.com/Moonlight-Zhao/go-project-example/cotroller"
	"github.com/Moonlight-Zhao/go-project-example/repository"
	"gopkg.in/gin-gonic/gin.v1"
	"os"
)

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
}

4.11 运行

参考博客:https://blog.csdn.net/easysec/article/details/112760038

运行server.go文件:
在这里插入图片描述
打开页面输入:http://localhost:8080/community/page/get/1

在这里插入图片描述
打开页面输入:http://localhost:8080/community/page/get/2

在这里插入图片描述
此时运行台
在这里插入图片描述
如果想在cmd中输出需要输入
curl --location --request GET http://localhost:8080/community/page/get/1
curl --location --request GET http://localhost:8080/community/page/get/2

4.12 测试

在这里插入图片描述
覆盖率:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值