后端小白学习笔记(二):Go语言工程实践 | 青训营笔记

125 阅读5分钟

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

1、语言进阶

1.1 Goroutine(协程)

线程上可以并发地跑多个协程

package concurrence

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) {
            defer wg.Done()
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}

1.2 CSP(Communicating Sequential Processes)

Go中提倡通过 通信共享内存 而不是通过 共享内存实现通信

使用共享内存实现通信,有可能会出现程序竞态的问题,在一定程度上会影响程序的性能

1.3 Channel

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

  • 无缓冲通道(同步通道) make(chan int)
  • 有缓冲通道(典型的C/S模型) make(chan int, 2)

A协程将0-9放入src通道里;B协程从src通道中取出0-9,并将其平方放入dest通道中,主协程打印dest中的数

实现了一个 生产者/消费者 模型

package concurrence

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 {
		println(i)
	}
}

1.4 并发安全 Lock

5个协程实现1加到2000的操作

import (
    "sync"
)

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

1.5 WaitGroup

waitgroup可以实现并发任务的同步

使用waitGroup替换time.Sleep函数

2、依赖管理

2.1 Go 依赖管理演进

GOPATH -> GO Vendor -> Go Module

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

2.1.1 GOPATH

  • 环境变量 GOPATH
  • 项目源码直接依赖 src 下的代码
  • go get 下载最新版本的包到 src 目录下

GOPATH-弊端:对于依赖同一个package的情况下,无法实现package的多版本控制

2.1.2 Go Vendor

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

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

Go Vendor-弊端:

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

2.1.3 Go Module

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

它实现了定义版本规则和管理项目依赖关系

2.2 依赖管理三要素

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

2.3.1 依赖配置-go.mod

如果我们建立的包想要被其他人引用的话,那么需要在每一个包的文件夹下面都建立一个go.mod文件

// 依赖管理基本单元
module github.com/Moonlight-Zhao/go-project-example

// 原生库
go 1.16

// 单元依赖
require (
	github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/gin-gonic/gin v1.3.0 // indirect
	github.com/go-playground/validator/v10 v10.10.0 // indirect
	github.com/goccy/go-json v0.9.6 // indirect
)

2.3.2 依赖配置-version(重点)

GOPATH 和 GO Vendor 都是以源码副本的形式实现依赖,没有版本规则的概念。

GO module 为了实现更好的版本管理,定义了一套自己的规则

  1. 语义化版本

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

不同MAJOR之间是版本隔离的,即不兼容的。MINOR通常是新增函数等,需要保持前后兼容。PATCH一般是做代码bug修复(主要来源于git相关概念)

例子:

V1.3.0

V2.3.0

  1. 基于 commit 伪版本

vX.0.0-yyyymmddhhmmss-abcdefgh1234

版本前缀(与语义化版本前缀一样)-时间戳(commit的时间戳)-提交commit的12位hash前缀

例子:

v0.0.0-20220401081311-abcdefgh1234

v1.0.0-20201130134442-10cb98257c6c

2.3.3 依赖配置-indirect

Go module 中对于没有直接导入的模块都会标识为非直接依赖,用indirect标识出来

2.3.4 依赖配置-incompatible

go module 中认为 v2 以上的版本需要在前面加上 /vN 的后缀

对于没有go.mod文件并且主版本在 v2 以上的依赖,会+incompatible

2.3.4 依赖配置-依赖图

Go会选择一个最低兼容的版本去编译

2.3.5 依赖分发-回源

常见的依赖是 Github、SVN 等,但是如果直接依赖代码托管平台可能会存在以下问题:

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

2.3.5 依赖分发-Proxy

为了解决上述问题,使用Proxy(没有什么是proxy解决不了的,如果有那就两层proxy

2.3.6 依赖分发-变量 GOPROXY

go.mod 通过 GOPTOXY 环境变量来实现 goproxy

goproxy是一系列的URL列表,使用逗号分隔。如果proxy中没有的话那就会回源到第三方代码平台上去

GOPROXY="https://proxy1.cn,https://proxt2.cn,direct"
服务站点 URL 列表,“direct”表示源站

2.3.7 工具-go get

若直接 go get 那就会默认拉取最新版本的

2.3.8 工具-go mod

3、测试(从测试的理念出发,提高质量意识)

3.1 单元测试

3.1.1 单元测试-规则

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

3.1.2 单元测试-例子

3.1.3 单元测试-运行

3.1.4 单元测试-assert

3.1.5 单元测试-覆盖率

使用代码覆盖率来评估代码的等级

运行 go test 在结尾加上 --cover 就可以计算出代码的覆盖率

上图中 66.7% 的结果原因是:输入70进去,会走前两行,此时已经返回,则第三行的return false(忽略右大括号)不会被运行,也就只验证了2行正确,第三行无法验证

3.1.5 单元测试-Tips

  • 一般覆盖率:50%-60%,则主流程问题不大,对于资金类的交易要求 80%
  • 测试分支相互独立、全面覆盖
  • 测试单元粒度足够小,函数单一职责

3.2 单元测试-依赖

3.3 单元测试-文件处理

存在问题: 对于文件处理等测试,当文件被修改或者删除后,测试文件也变得无法使用。

3.4 单元测试-Mock

为了解决3.3中出现的问题,可以使用mock进行打桩(Patch 和 Unpatch)

打桩:使用一个函数来替换原本的函数,这样可以让测试文件对代码没有强依赖,在任何环境下执行

3.5 基准测试

Go中提供了测试框架来实现基准测试(例如:对代码进行性能分析)

3.5.1 基准测试-例子

随机选择一个服务器实现负载均衡

3.5.2 基准测试-运行

3.5.3 基准测试-优化

对于随机数的优化可以考虑使用 fastrand

4、项目实战

4.1 需求描述

社区话题页面

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

4.2 需求用例

4.3 ER 图-Entity Relationship Diagram

4.4 分层结构

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

对于本节课:数据层主要是从本地拉取文件

4.5 组件工具

  • Gin 高性能 go web 框架
  • Go Mod
    • go mod init
    • go get gopkg.in/gin-gonic/gin.v1@v1.3.0

4.6 Repository

存放依赖数据

Repository-index

Repository-查询

得到:1. 话题ID->话题 2. 话题ID->回帖列表

var (
    topicDao *TopicDao
    topicOnce sync.Once		// 该行实现了单例模式,可以减少内存浪费
)

4.7 Service

考虑到话题和回帖两部分信息是不会相互影响的,因此使用创建子协程的方式来分别实现两个部分

4.8 Controller

  • 构建View对象
  • 业务错误码

4.9 Router

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

4.10 运行

和客户端的交互,其实就是给客户端一些接口,客户端根据获取的json进行渲染

PS

本文主要作用是作为上课笔记,如有错误,欢迎大家评论,我会及时改正