go学习
go 的一些命令行工具
- go bug 打开浏览器,报告错误信息
- go build 编译源代码
- go clean 移除目标文件和缓存文件
- go env 打印 go 环境信息
- go fix 旧版本代码修正为新的版本
- go fmt 格式化源文件
- go generate 扫描特殊注释,用于自动生成go 文件
- go get 添加指定版本的依赖
- go list 列出指定代码包的信息
- go mod 依赖管理工具
- go run 编译运行源代码
- go test 测试代码
- go tool 运行特殊的go 工具
- go version 查看go 版本
- go vet 静态扫描代码,报告代码中可能的问题
表达式与运算符
- 算术运算符;
- 关系运算符;
- 逻辑运算符;
- 位运算符;
- 赋值运算符;
- 地址运算符。
优先级(由高到低) 操作符
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||
go 基础知识
defer
defer 是 Go 语言中的关键字,也是 Go 语言的重要特性之一,defer 在资源释放、panic 捕获等场景中的应用非常广
特性
- 延迟执行
- 参数预计算
- LIFO 执行顺序
并发编程
进程协程线程进程是操作系统资源分配的最小单元,线程是操作系统资源调度的基本单位,协程是用户态,是在线程基础上构建的轻量级别的调度单位并发并行并行指的是 同时做很多事情,并发是指同时管理很多事情主协程子协程main函数是特殊的主协程,它退出之后整个程序都会退出,而其它的协程都是子协程,子协程退出不影响主程序
Go 语言运行时为我们托管了协程的启动与调度工作,我们关心的重点只要放在如何优雅安全地关闭协程,以及如何进行协程间的通信就可以了。
- 通道声明与初始化
package main
import "fmt"
func main() {
ch := make(chan int, 10)
fmt.Println(ch)
}
- 通道写入信息
ch := make(chan int, 10)
ch <-5
- 通道读取数据
data := <-c
- 通道关闭
close(ch)
- 通道作为参数
package main
import "fmt"
func work(id int, c chan int) {
for n := range c {
fmt.Println(n,id)
}
}
-
通道作为返回值(一般用于创建通道的阶段)
-
单方向的通道,用于只读和只写场景
-
select 监听多个通道实现多路复用。当 case 中多个通道状态准备就绪时,select 随机选择一个分支进行执行
-
传统的同步原语:原子锁。Go 提供了 atomic 包用于处理原子操作。
-
传统的同步原语:互斥锁。
-
传统的同步原语:读写锁。适合多读少写场景。
-
除此之外,Go 语言在传统的同步原语基础上还提供了许多有用的同步工具,包括 sync.Once、sync.Cond、sync.WaitGroup
工具
- go doc
go doc 工具可以生成和阅读代码的文档说明。文档是使软件可访问和可维护的重要组成部分。当然,它需要写得好且准确,也需要易于编写和维护。理想情况下,文档注释应该与代码本身耦合,以便文档与代码一起发展
-
golangci-lint
-
go race
Go 1.1 后提供了强大的检查工具 race,它可以排查数据争用问题。race 可以用在多个 Go 指令中
当检测器在程序中发现数据争用时,将打 印报告。这份报告包含发生 race 冲突的协程栈,以及此时正在运行的协程栈。
-
gops
gops 是谷歌推出的调试工具,它的作用是诊断系统当前运行的 Go 进程。gops 可以显示出当前系统中所有的 Go 进程,并可以查看特定进程的堆栈信息、内存信息等。
GMP
G 代表的是 Go 语言中的协程(Goroutine),M 代表的是实际的线程,而 P 代表的是 Go 逻辑处理器(Process)。Go 语言为了方便协程调度与缓存,抽象出了逻辑处理器的概念。在任一时刻,一个 P 可能在本地包含多个 G,同时,一个 P 在任一时刻只能绑定一个 M。
全局共享的全局运行队列、本地运行队列可以获取全局运行队列中的协程,全局运行队列也可以接收本地运行队列中的协程。
大型Go 项目的开发 流程
瀑布模式 vs 敏捷模式
瀑布模式
- 需求在规划和设计阶段就已经确定了,而且在项目开发周期内,需求没有或极少有变化。例如航空航天或者金融核心系统等
- 团队对这一技术领域很熟悉,风险低、规模小。
- 合同式的合作方式。项目的开发严格依据说明,客户需求明确且不参与软件实现过程。
敏捷模式 敏捷开发的核心思想是拥抱变化,强调对于变化的适应性,更强调开发者 与业务专家、客户之间的互动,强调持续改进和持续交互产品,持续提高客户满意度。
敏捷开发是增量构建的,每次迭代都满足总需求的一部分,而不是试图一次性交付所有功能。这使新功能的价值可以得到快速验证。 有多种实现了敏捷开发思想的框架供我们使用,比较知名的 Scrum、看板(Kanban)、极限编程(XP)、精益软件开发(Lean Software Development)
- 产品经理基于项目的愿景(Vision),收集产品待办事项清单(Product Backlog)并确定优先级。
- 团队成员根据阶段性的成果将项目阶段拆分为一个个的 Sprint,即冲刺周期或开发周期。每一个 Sprint 开始的时候,需要开一个 Sprint 会议,把产品待办事项清单里的事项添加到当前 Sprint 中。添加的原则是,需要考虑 Sprint 的交付价值以及事项的优先级。当前 Sprint 清单里面的待办事项也被称为 Sprint Backlog
- Sprint Backlog 可以由团队成员拆分,并细化为每一个成员每天具体的工作任务。成员们可以根据任务进度去看板上更新任务的状态
- 在当前 Sprint 运行时,团队成员要参加每日站立会议(Daily Scrum Meeting),每次会议时间控制在 15 分钟内。会上成员要根据看板的内容逐个进行发言,向所有成员汇报昨天已完成、今天待完成的事项,交流不能解决的问题。会议结束后,要及时更新项目的燃尽图(Burn Down Chart),以便跟踪项目进度
- Sprint 结束后,团队成员一起评审(Sprint Review)本次 Sprint 的产出。这个产出物(release)可能是一个可以运行的软件,也可能是一个可展示的功能。每个人都可以自由发表看法,协助产品负责人对未来工作做出最终决定。并根据实际情况,适度调整产品待办事项列表
- Sprint 结束后,大 家聚在一起开一次回顾会(Sprint Retrospective),回顾一下团队在流程和沟通等方面的成效。一起讨论哪里完成得好,哪里还需改进
大型互联网产品的研发流程
需求阶段
- 商业需求
- 功能需求
- 非功能需求
设计阶段
- UI 设计师
- 交互设计师 了解用户的思维方式和行为习惯 设计交互流程
- 系统架构师对系统架构进行设计,对技术进行选型例如 如何拆分微服务,如何保证分布式系统的一致性,选择哪一种语音,框架,中间件和数据库
- 研发工程师需要设计技术方案,梳理功能流程,明确接口定义和上下游的调度流程,如何选择合适的算法和数据结构以保证程序的效率和目标
- 测试工程师 为了验证某个需求是否实现,是否存在缺陷,在测试之前会设计一套详细的测试方案和测试集
明确设计方案之后,就要对需求进行排期,包括确定好开放时间,联调时间,QA测试时间,以及上线时间
开发流程规范
研发实现阶段
需要遵循多种开发规范,包括编码规范、接口规范、日志规范、测试规范、Commit 规范、版本控制规范、发布规范等等。
编码规范
- 整洁
- 高效
- 健壮
- 可扩展性
接口规范 服务之间的通信,最常用的是 HTTP、Thrift 和 gRPC 协议。以使用最多的 HTTP 协议为例,大多数 Web 服务使用了 RESTful 风格的 API。RESTful 规范了资源 访问的 URL,规定了使用标准的 HTTP 方法,例如 GET、POST、PUT、DELETE 等,并且明确了这些方法对应的语义。除此之外,接口规范还需要定义状态码如何赋值、如何保证接口向后兼容等一系列问题
单独管理 API 接口,甚至会有一套专门描述软件组件接口的计算机语言,被称为 IDL(接口描述语言,Interface description language)
IDL 通过独立于编程语言的方式来描述接口,每一种编程语言都会根据 IDL 生成一套自己语言的 SDK。即便是相同的语言,也可能生成不同协议(例如 HTTP 协议 、gRPC 协议)的 SDK。使用 IDL 有下面几个好处:
- 作为接口说明文档,IDL 统一规范了接口定义和使用方法,不同使用方不用反复沟通接口的使用方法;
- 不同语言编写的程序可以很方便地相互通信,屏蔽了开发语言上的差异;
- 生成的 SDK 可以提供通用的能力,例如熔断、重试、记录调用耗时等,可以大大节省成本,毕竟如果这些功能要在每一个服务端都实现一遍,是一种成本的浪费。
日志规范
- 打印调试:打印调试的意思是用日志来记录变量或者某一段逻辑,记录程序运行的流程。虽然用日志来调试通常会被人嘲笑为技术手段落后,但它确实能够解决某些难题
- 问题定位:有时候,系统或者业务出现问题,需要快速排查原因,这时我们就要用到日志的功能了
- 用户行为分析:日志的大量数据可以作为大数据分析的基础,例如用户的行为偏好等
- 监控:日志数据通过流处理生成的连续指标数据,可存储起来并对接监控告警平台,这有助于我们快速发现系统的异常。监控的指标可能包括:核心接口调用量是否突然下降或上升、核心的业务指标变化
版本控制规范
Git 与工作流
Git 让你能够基于项目的稳定代码库开辟新的分支,和团队成员并行工作。还可以确保新的特性或实验性代码实现。创建“分支”是一种非常常见的做法,它可以确保主开发线的完整性,避免任何意外的更改破坏主分支。
Git 的特性催生了基于 Git 的多种工作流模式,包括:
- 集中式工作流
- Git Flow 工作流
- GitHub Flow 工作流
- GitLab Flow 工作
Gitflow 工作流(Gitflow Workflow)
Master分支:作为唯一一个正式对外发布的分支,是所有分支里最稳定的Develop分支:是根据 Master 分支创建出来的。Develop 分支作为一种集成分支 (Integration Branch),专门用来集成已经开发完的各种特性。Feature分支:根据 Develop 分支创建出来。Gitflow 工作流里的每个新特性都有自己的 Feature 分支。当特性开发结束以后,这些分支上的工作会被合并到 Develop 分支Release分支:当积累了足够多的已完成特性,或者预定的系统发布周期临近的时候,我们就会从 Develop 分支创建出一个 Release 分支,专门做和当前版本发布有关的工作。Release 分支一旦创建,就不允许再有新的特性被加入到这个分支了,只有修复 Bug 或者编辑文档之类的工作才能够进入该分支。Release 分支上的内容最终会被合并到 Master 分支Hotfix分支:直接根据 Master 分支创建,目的是给运行在生产环境中的系统快速提供补丁。当 Hotfix 分支上的工作完成以后,可以合并到 Master 分支、Develop 分支以及当前的 Release 分支。如果有版本的更新,也可以为 Master 分支打上相应的 Tag。
在 GitHub Flow 工作流
- 项目维护者将代码推送到主仓库。
- 开发者克隆(Fork)此仓库,做出修改。
- 开发者将修改后的临时代码分支推送到自己的公开仓库。
- 开发者创建一个合并请求(Pull Request),包含进行本次更改有关的信息(例如目标仓库、目标分支、关联要修复的 issue 问题),以便维护者进行代码评审。
- RP 通过后,临时代码分支将被合并到指定的分支,合并前可能有一些需要解决的代码冲突。合并完成后,GitHub 在合并分支的 commit 记录中可以连接到之前 PR 的页面,帮助我们了解更改的历史、背景和评论。
- 合并拉取请求后,维护者可以删除临时代码分支。这表明该分支上的工作已经完成,同时也可以防止其他人意外使用旧分支。
Commit 规范
- 格式统一,内容更加清晰和易读;
- 可以通过提交记录来了解本次提交的目的,更好地 CR 和重构;
- 更容易了解变更、定位和发现问题;
- 由于每个提交描述都是经过思考的,这就可以改善提交的质量。
测试阶段
- 代码规范测试:包括对代码风格、命名等的检测,主要工具有 gofmt、goimport、golangci-lint 等。
- 代码质量测试:包括代码的覆盖率。主要工具有 go tool cover 等。
- 代码逻辑测试:包括并发错误测试、新增功能测试。主要工具有 race、单元测试等。
- 性能测试 :包括 Benchmark 测试、性能对比。主要工具有 Benchmark、ab、pprof、trace 等。
- 服务测试:测试服务接口的功能与准确性,以及服务的可用性测试。
- 系统测试:包括端到端测试,确保上下游接口与参数传递的准确性、确保产品功能符合预期。
上线部署阶段
- 上线避开高峰期上线,尽量不要在节假日之前上线变更较大的版本,不在业务量大的时期做任何变更操作。
- 提前通报利益相关者,例如项目组成员、组内成员、有影响的上下游。
- 严格规范上线执行步骤、确定检查清单与回滚方案。
- 灰度发布,并且对于要变更的功能,采取逐步放量的方式。例如只放量 A 城市 M 品类 30% 的流量,这样 A 城市 M 品类 30% 的用户才会体验到新功能,最大限度地减少变更时候的风险。借助灰度发布也能进行 A/B 实验,这样可以观察不同策略下用户的行为,从而做出科学的决策
- 分级发布,遵循先少量,再部分,最后全量的原则;严格控制每一次部署的时间间隔。通常大公司还有接近线上环境的预发环境,首先在预发环境中部署服务并验证服务的检查清单。对于核心服务来说,上千个容器是很常见的事情,分级发布有助于减少问题出现时的影响面,因为大部分问题都是服务的变更引起的。
- 另外,在上线时进行检查,观察当前服务指标是否异常,如发现异常应尽快回滚,止损后再查明问题原因。部署过程中,需要观察程序的核心指标,包括系统指标(上下游调用 错误率、接口的平均响应时间和 P99 响应时间、CPU、内存、磁盘的利用率等)和业务指标(服务错误率、核心接口请求量等)。以打车业务为例,监控的指标包括:是否出现天价账单、特定费用项(如时长费、动态调节费)是否出现大的波动,平台是否抽成过高等问题。
- 最后,在上线过程中还要及时关注报警群,关注上下游的反馈,收集错误日志,并抓取 case 查看。
服务部署完成后,一些重要变更还需要 QA 工程师进行回归测试,验证服务在线上是否符合预期。
运维阶段
SRE 工程师日常会涉及到开发工作,他们用系统来维护系统,通过自动化、工具化等手段提升服务管理效率,确保集群的可观测性、稳定性、可用性。下图是腾讯的 SRE 稳定性建设的全景图。
SRE 工程师通常是围绕着缩减下面的几个时间来提高整个系统的稳定性水平:
- MTTI (Mean Time To ldentify)平均故障发现时间
- MTTA(Mean Time To Acknowledge)平均故障确认时间
- MTTL (Mean Time To Location)平均故障定位时间
- MTTT (Mean Time To Troubleshooting)平均故障解决时间
- MTTV (Mean Time To Verify)平均故障验证时间
Google 提出了下面这套 VALET 法
- Volume(容量):服务承诺的最大容量。比如常见的 QPS、TPS、会话数、吞吐量以及活动连接数等等。
- Availablity(可用性):服务是否正常 / 稳定。比如请求调用 HTTP 200 状态的成功率,任务执行成功率等。
- Latency(时延):服务响应速度。有时我们需要判断时延是否符合正态分布,或者指定不同的区间,比如常见的 P90、P95、P99 等。
- Error(错误率):服务错误率,比如 5XX、4XX,以及自定义的状态码。
- Ticket(人工干预):服务是否需要人工干预,面对一些复杂的故障场景,就需要人工介入来恢复服务。
go 语言规范
格式化
代码长度
强制 lll一行内不超过 120 个字符,同时应当避免刻意断行。如果你发现某一行太长了,要么改名,要么调整语义,往往就可以解决问题了
强制 funlen 单个函数的行数不超过 40 行,过长则表示函数功能不专一、定义不明确、程序结构不合理,不易于理解。当函数过长时,可以提取函数以保持正文小且易读。
强制 单个文件不超过 2000 行,过长说明定义不明确,程序结构划分不合理,不利于维护。
代码布局
建议 Go 文件推荐按以下顺序进行布局。
- 包注释:对整个模块和功能的完整描述,写在文件头部。
- Package:包名称
- Constants:常量定义
- Typedefs:类型定义
- Functions:函数实现。
每个部分之间用一个空行分割。每个部分有多个类型定义或者有多个函数时,也用一个空行分割
空格与缩进
