Go语言基础 | 豆包MarsCode AI刷题
课程目录
- Go语言上手-基础语言
- Go语言上手-工程实践
- 高质量编程与性能调优实战
- 高性能Go语言发行版优化与落地实践
Go语言入门-工程实践
目录
- 语言进阶
- 依赖管理
- 测试
- 项目实战
语言进阶
go语言运行为何会如此之快?
多线程程序在一个核上cpu运行,多相处程序在多个核的cpu上运行
go语言可以充分发挥多核优势,高效运行。换句话说,Go语言就是为并发而生的
协程:用户态,轻量级线程,栈kb级别
线程:内核态,线程跑多个协程,栈mb级别
现在我们要快速打印hello goroutine
如果我们不考虑快速,其实直接用for就可以解决
但如果我们需要快速完成任务,我们可以通过go语言的协程实现
现在涉及到另一个概念,也就是协程之间的通信
go语言提倡协程之间通过通信来共享内存,而不是通过共享内存来实现通信
通过通信共享内存涉及到某个比较重要的概念,也就是通道channel
本身gorountine1是一个go程序并发的一个执行体,通道相当于把协程进行了一个连接,使用先入先出,实现协程的一个顺序
channel是一个通信机制,允许gorountine发送某个值到另一个gorountine
如果通过共享内存实现通信,必须是通过获取连接区的一个权限来提速,某种程度上会影响程序性能
接下来看看channel的具体操作
channel是一种引用类型,需要用到make关键字
根据是否有缓冲区的[缓冲大小],channel又可以分为无缓冲通道和有缓冲通道
使用无缓冲通道,会导致发送的gorountine中和接受的gorountine同步发生。所以无缓冲通道,也被称为同步通道
解决同步问题的方式实际就是使用带有缓冲区的channel通道
通道的容量代表通道中可以放多少元素,类比仓库的货架,取走前会阻塞
两种不同的协程,a协程发送0-9数字,b协程计算输入x的平方,a协程的结果压入channel通道,在b协程中调用这个通道的数据,于是实现协程之间的通信
并发安全:go保存着通过共享内存进行通信的机制,也就是几个协程对着同一块内存进行操作。如果我需要对某个变量进行2000次+1操作,然后五个协程并发执行操作。期间如果没有加锁,可能会导致输出的结果与预期不符,并发过多导致结果出错
因为我们不确定子协程的执行时间,所以我们无法精确的给出sleep的时间,所以用sleep来阻塞,是不优雅的
go语言中可以使用waitgroup来实现并发的阻塞,本质是对一个计数器的维护,计数器的值可以增加减少,每个任务完成时使用done将计数器-1,计数器归零说明并发任务完成
简单做个小结:go通过协程gorountine实现高并发操作,go提倡使用channel通信实现共享内存,然后再sync包下使用waitgroup保证并发安全
依赖管理
依赖本质是各种开发包,要站在巨人的肩膀上,用各种工具提升自己的开发效率
工程项目不可能之基于标准库0~1编码搭建,我们要学会管理依赖库
go的依赖管理经过三个阶段,goPath goVendor goModule
不同环境的依赖的版本不同,控制依赖库的版本不同
项目代码直接依赖src下的代码,go get下载最新版本的包到src下,bin存放项目变异的二进制文件,pkg存放项目编译的中间产物,加速编译,src存放项目源码
goPath的弊端在于,假定现在有项目AB,依赖于某一package的不同版本,就无法实现Package的多版本控制
goVender在项目目录下增加vendor,所有依赖包副本形式放在¥ProjectRoot/verdor
依赖寻址方式goPath
但是vendor仍然不能解决版本依赖问题,一旦更新也可能导致编译错误
goModule,通过go.mod文件管理依赖包版本,通过go get/go mod 指令工具管理依赖包,终极目标是定义版本规则和管理项目依赖关系
依赖管理三要素:
- 配置文件,描述依赖
go.mod - 中心仓库管理依赖库
Proxy - 本地工具
go get/mod
针对前面讲的三要素,可以深入了解一下go.mod文件的构成与依赖
go.mod中可以调整依赖管理基本单元,原生库和单元依赖
关于version的定义:语义化版本(直接输入版本),基于commit伪版本(版本-时间戳-哈希码前缀)
indirect模块
如果a依赖b,b依赖c,那么a对b称直接依赖,a对c称间接依赖
主版本2+模块会在模块路径增加/vN后缀
对于没有go.mod文件并且主版本2+的依赖,会+incompatiple
不同的版本之间可能存在不相互兼容,为了兼容这部分仓库,版本号后面加上incompatible后缀,标志可能存在不兼容的代码逻辑
如果X项目依赖ab两个项目,ab分别依赖c的v1.2与v1.3,最终编译时所使用的C项目的版本为v1.3,因为go进行版本选择的算法,会选择最低的兼容版本
依赖分发-回源
我们可以直接在github或者别的代码托管平台里面直接下载对应的依赖版本来进行版本管理
但是这其中存在某些问题:
- 无法保证构建稳定性
- 无法保证依赖可用性
- 增加第三方压力
所以我们可以引入一个服务站点Proxy,缓存源站中的内容,同时内容也不会改变
换句话说把依赖库缓存在Proxy里面,调用直接在Proxy里面拉取依赖
可以使用工具go get来拉取指定的版本
测试
单元测试,mock测试和基准测试
测试关系系统的质量,关系系统的稳定性
实际项目开发过程中,可能存在各种事故,会造成各种损失
测试是避免事故的最后一道屏障
测试大致分为三种类型:回归测试,集成测试和单元测试
回归测试就是直接使用app,比方说就是直接拿出手机尝试使用一下,回归主流场景
集成测试就是对系统功能维度进行测试,对系统暴露的某个接口进行集中性的测试
单元测试是针对测试开发阶段,开发者针对单独的单元模块进行测试,
从上到下,针对测试覆盖率逐步上升,成本渐高
单元测试,对单元输入,得到输出,并与期望值进行一个校对
这样就验证了我们代码的能力与正确性
单元测试的单元可以是某个函数,模块或者接口等等
单元测试的一些规则:
- 所有测试文件以_test.go结尾
publish_post_test.go - 测试的函数命名,要以Test开始,然后挨着一个大写的函数
TestXxx(*testing.T) - 初始化逻辑放到TestMain执行
有问题,就去定位解决,这里可以用assert的关键字去解决
如何衡量代码是否经过了足够的测试?如何去评估项目的测试水准?如何评估项目是否达到了高水准测试等级?
这里就有一个概念,那就是代码覆盖率
整体上覆盖的测试用例的覆盖率,越完备,越能代表测试代码的质量有保证
可以使用特殊的代码go test judgment_test.go judgment.go --cover
Tips:
- 一般覆盖率:50%~60%,较高覆盖率80%+
- 测试分支相互独立,全面覆盖
- 测试单元粒度足够小,函数单一职责(函数要足够小,保证专一)
单元测试有两个目标,一个是幂等,另一个是稳定。幂等指的是我们重复去进行一个测试,他的结果与之前是一样的。稳定指的是单元或者函数之间是相互隔离的
mock
假定有一个文件处理的函数,作用是读入文件的内容,然后这里执行一个单元测试。由于函数是对本地的文本进行操作,换个环境可能测试单元就不能运行了
mock有很多,以一个开源的monkey为例
mock可以用实例的方法,为一个函数打桩,用一个函数a去替换一个函数b
核心原理是在运行时,利用go的unsafe包,然后将内存中函数的地址,替换换成运行时函数地址。所以我们最终在测试的时候,调用的是打桩函数
通过一个mock,实现不对file文件的强依赖,可以在任何时候进行测试
基准测试:优化代码,对当前代码分析。内置的测试框架提供了基准测试的能力
服务器负载均衡:随机选择执行服务器
基本上和单元测试的基准是一致的
在多协程并行的压力测试下,部分可能产生性能的损耗
比方说,可以用fastrand取代rand,牺牲一些随机数性,提升百倍的效率
项目实战
项目开发思路流程
首先需要描述一下需求:
- 展示话题,标题,文字描述和回帖列表
- 暂不考虑前端界面实现,仅仅实现一个本地web服务
- 话题和回帖数据用文件存储
再来研究一下用例
-
浏览消费用户
- 浏览话题
- 浏览帖子
-
话题
- id
- 标题
- 文本
- 创建时间
-
帖子
- id
- 话题id
- 文本
- 创建时间
分层结构:提高代码的可读性
-
数据层:数据Model,外部数据的增删改查
我们的数据存储在本地文件上,通过拉取本地文件,屏蔽下游数据差异,把数据存在数据库里
-
逻辑层:业务Entity,处理核心业务逻辑输出
通过接受数据层的数据,对数据进行封装,得到一个个实例
-
视图层:视图view,处理和外部的交互逻辑
做成各种各样的api和接口,与外部沟通
组件工具:gin高性能goweb框架
数据层
元数据作为索引,子啊数据航也就是内存Map中查找
可以提前定制一个目录,然后在这个索引中快速查找(有点像key)
打开这个文件,通过迭代器的方式,数据行遍历
通过定义一个实体,包含一个Topic和一个PostList
流程上,第一步是参数校验(非法字符或者0之类的),第二步是准备数据(查找),第三步是组装实体(字典之类的)
接下来借用goi的特性,我们可以并行处理话题信息和回帖信息,提升效率
controller
逻辑上构建这个对象,这一块做了一个类型的转换
然后封装好实例
业务逻辑整体就已经完成了
最后进入路由层Router
初始化数据索引
初始化引擎配置
构建路由
启动服务