跳到主要内容

go学习

· 阅读需 39 分钟
ahKevinXy

代码规范

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 捕获等场景中的应用非常广

特性

  1. 延迟执行
  2. 参数预计算
  3. 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)

  1. 产品经理基于项目的愿景(Vision),收集产品待办事项清单(Product Backlog)并确定优先级。
  2. 团队成员根据阶段性的成果将项目阶段拆分为一个个的 Sprint,即冲刺周期或开发周期。每一个 Sprint 开始的时候,需要开一个 Sprint 会议,把产品待办事项清单里的事项添加到当前 Sprint 中。添加的原则是,需要考虑 Sprint 的交付价值以及事项的优先级。当前 Sprint 清单里面的待办事项也被称为 Sprint Backlog
  3. Sprint Backlog 可以由团队成员拆分,并细化为每一个成员每天具体的工作任务。成员们可以根据任务进度去看板上更新任务的状态
  4. 在当前 Sprint 运行时,团队成员要参加每日站立会议(Daily Scrum Meeting),每次会议时间控制在 15 分钟内。会上成员要根据看板的内容逐个进行发言,向所有成员汇报昨天已完成、今天待完成的事项,交流不能解决的问题。会议结束后,要及时更新项目的燃尽图(Burn Down Chart),以便跟踪项目进度
  5. Sprint 结束后,团队成员一起评审(Sprint Review)本次 Sprint 的产出。这个产出物(release)可能是一个可以运行的软件,也可能是一个可展示的功能。每个人都可以自由发表看法,协助产品负责人对未来工作做出最终决定。并根据实际情况,适度调整产品待办事项列表
  6. Sprint 结束后,大家聚在一起开一次回顾会(Sprint Retrospective),回顾一下团队在流程和沟通等方面的成效。一起讨论哪里完成得好,哪里还需改进

大型互联网产品的研发流程

需求阶段

  1. 商业需求
  2. 功能需求
  3. 非功能需求

设计阶段

  • 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 可以提供通用的能力,例如熔断、重试、记录调用耗时等,可以大大节省成本,毕竟如果这些功能要在每一个服务端都实现一遍,是一种成本的浪费。

日志规范

  1. 打印调试:打印调试的意思是用日志来记录变量或者某一段逻辑,记录程序运行的流程。虽然用日志来调试通常会被人嘲笑为技术手段落后,但它确实能够解决某些难题
  2. 问题定位:有时候,系统或者业务出现问题,需要快速排查原因,这时我们就要用到日志的功能了
  3. 用户行为分析:日志的大量数据可以作为大数据分析的基础,例如用户的行为偏好等
  4. 监控:日志数据通过流处理生成的连续指标数据,可存储起来并对接监控告警平台,这有助于我们快速发现系统的异常。监控的指标可能包括:核心接口调用量是否突然下降或上升、核心的业务指标变化

版本控制规范

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 规范

  1. 格式统一,内容更加清晰和易读;
  2. 可以通过提交记录来了解本次提交的目的,更好地 CR 和重构;
  3. 更容易了解变更、定位和发现问题;
  4. 由于每个提交描述都是经过思考的,这就可以改善提交的质量。

测试阶段

  • 代码规范测试:包括对代码风格、命名等的检测,主要工具有 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 文件推荐按以下顺序进行布局。

  1. 包注释:对整个模块和功能的完整描述,写在文件头部。
  2. Package:包名称
  3. Constants:常量定义
  4. Typedefs:类型定义
  5. Functions:函数实现。

每个部分之间用一个空行分割。每个部分有多个类型定义或者有多个函数时,也用一个空行分割

空格与缩进

强制 gofmt 注释和声明应该对齐 强制 gofmt 小括号 ()、中括号[]、大括号 内侧都不加空格。 强制 gofmt 逗号、冒号(slice 中冒号除外)前都不加空格,后面加 1 个空格。 强制 gofmt 所有二元运算符前后各加一个空格,作为函数参数时除外 强制 gofmt 使用 Tab 而不是空格进行缩进。 强制 nlreturn return 前方需要加一个空行,让代码逻辑更清晰。 强制 gofmt 判断语句、for 语句需要缩进 1 个 Tab,并且右大括号}与对应的 if 关键字垂直对齐 强制 goimports 当 import 多个包时,应该对包进行分组。同一组的包之间不需要有空行,不同组之间的包需要一个空行。标准库的包应该放在第一组。这同样适用于常量、变量和类型声明: 推荐 避免 else 语句中处理错误返回,避免正常的逻辑位于缩进中。如下代码实例,else 中进行错误处理,代码逻辑阅读起来比较费劲。 推荐 函数内不同的业务逻辑处理建议用单个空行加以分割。 推荐 注释之前的空行通常有助于提高可读性——新注释的引入表明新思想的开始。

命名

  • 短容易拼写
  • 保持一致性
  • 意思准确,容易理解 没有虚假和无意义的信息

强制 revive Go 中的命名统一使用驼峰式、不要加下划线。 强制 revive 缩写的专有名词应该大写,例如: ServeHTTP、IDProcessor。 强制 区分变量名应该用有意义的名字,而不是使用阿拉伯数字:a1, a2, … aN。 强制 不要在变量名称中包含你的类型名称。 建议 变量的作用域越大,名字应该越长。

包名

包名应该简短而清晰。

强制 使用简短的小写字母,不需要下划线或混合大写字母。 建议 合理使用缩写 强制 避免无意义的包名,例如 util,common,base 等。

接口命名

建议单方法接口由方法名称加上 -er 后缀或类似修饰来命名。例如:Reader, Writer, Formatter, CloseNotifier ,当一个接口包含多个方法时,请选择一个能够准确描述其用途的名称(例如:net.Conn、http.ResponseWriter、io.ReadWriter)。

本地变量命名 建议 如果函数参数的类型已经能够看出参数的含义,那么函数参数的命名应该尽量简短: 建议如果函数参数的类型不能表达参数的含义,那么函数参数的命名应该尽量准确

函数返回值命名 建议 对于公开的函数,返回值具有文档意义,应该准确表达含义

可导出的变量名 建议 由于使用可导出的变量时会带上它所在的包名,因此,不需要对变量重复命名。例如 bytes 包中的 ByteBuffer 替换为 Buffer,这样在使用时就是 bytes.Buffer,显得更简洁。类似的还有把 strings.StringReader 修改为 strings.Reader,把 errors.NewError 修改为 errors.New。

Error 值命名

建议 错误类型应该以 Error 结尾。 建议 Error 变量名应该以 Err 开头。

函数 强制 cyclop 圈复杂度(Cyclomatic complexity)< 10。 强制 gochecknoinits 避免使用 init 函数。 强制 revive Context 应该作为函数的第一个参数。 强制 禁止 return 裸返回 强制 不要在循环里面使用 defer,除非你真的确定 defer 的工作流程。 建议 函数返回值大于 3 个时,建议通过 struct 进行包装。 建议 函数参数不建议超过 3 个,大于 3 个时建议通过 struct 进行包装。

控制结构 强制 禁止使用 goto。 强制 gosimple 当一个表达式为 bool 类型时,应该使用 expr 或 !expr 判断,禁止使用 == 或 != 与 true / false 比较。 强制 nestif if 嵌套深度不大于 5。

方法 强制 revive receiver 的命名要保持一致,如果你在一个方法中将接收器命名为 “c”,那么在其他方法中不要把它命名为 “cl”。 强制 receiver 的名字要尽量简短并有意义,禁止使用 this、self 等。

注释

Go 提供 C 风格的注释。有 /**/ 的块注释和 // 的单行注释两种注释风格。注释主要有下面几个用处。

  1. 注释不仅仅可以提供具体的逻辑细节,还可以提供代码背后的意图和决策。
  2. 帮助澄清一些晦涩的参数或返回值的含义。一般来说,我们会尽量找到一种方法让参数或返回值的名字本身就是清晰的。但是当它是标准库的一部分时,或者在你无法更改的第三方库中,一个清晰的注释会非常有用。
  3. 强调某一个重要的功能。例如,提醒开发者修改了这一处代码必须连带修改另一处代码。

强制 无用注释直接删除,无用的代码不应该注释而应该直接删除。即使日后需要,我们也可以通过 Git 快速找到。 强制 统一使用中文注释,中英文字符之间严格使用空格分隔。 强制 注释不需要额外的格式,例如星号横幅。 强制 包、函数、方法和类型的注释说明都是一个完整的句子,以被描述的对象为主语开头。Go 源码中都是这样的。 强制 Go 语言提供了文档注释工具 go doc,可以生成注释和导出函数的文档。文档注释的写法可以参考文稿中的链接。 强制 godot 注释最后应该以句号结尾。 建议 当某个部分等待完成时,可用 TODO: 开头的注释来提醒维护人员。 建议 大部分情况下使用行注释。块注释主要用在包的注释上,不过块注释在表达式中或禁用大量代码时很有用。 建议 当某个部分存在已知问题需要修复或改进时,可用 FIXME: 开头的注释来提醒维护人员。 建议 需要特别说明某个问题时,可用 NOTE: 开头的注释。

结构体 强制 不要将 Context 成员添加到 Struct 类型中。

高效 强制 Map 在初始化时需要指定长度make(map[T1]T2, hint)。 强制 Slice 在初始化时需要指定长度和容量make([]T, length, capacity)。 强制 time.After() 在某些情况下会发生泄露,替换为使用 Timer。 强制 数字与字符串转换时,使用 strconv,而不是 fmt。 强制 读写磁盘时,使用读写 buffer。 建议 谨慎使用 Slice 的截断操作和 append 操作,除非你知道下面的代码输出什么 建议 任何书写的协程,都需要明确协程什么时候退出。 建议 热点代码中,内存分配复用内存可以使用 sync.Pool 提速。 建议 将频繁的字符串拼接操作(+=),替换为 StringBuffer 或 StringBuilder。 建议 使用正则表达式重复匹配时,利用 Compile 提前编译提速。 建议 当程序严重依赖 Map 时,Map 的 Key 使用 int 而不是 string 将提速。 建议 多读少写的场景,使用读写锁而不是写锁将提速。

健壮性 强制 除非出现不可恢复的程序错误,否则不要使用 panic 来处理常规错误,使用 error 和多返回值。 强制 revive 错误信息不应该首字母大写(除专有名词和缩写词外),也不应该以标点符号结束。因为错误信息通常在其他上下文中被打印。 强制 errcheck 不要使用 _ 变量来丢弃 error。如果函数返回 error,应该强制检查。 建议 在处理错误时,如果我们逐层返回相同的错误,那么在最后日志打印时,我们并不知道代码中间的执行路径。例如找不到文件时打印的No such file or directory,这会减慢我们排查问题的速度。因此,在中间处理 err 时,需要使用 fmt.Errorf 或第三方包给错误添加额外的上下文信息。像下面这个例子,在 fmt.Errorf 中,除了实际报错的信息,还加上了授权错误信息authenticate failed : 强制 利用 recover 捕获 panic 时,需要由 defer 函数直接调用。 强制 不用重复使用 recover,只需要在每一个协程的最上层函数拦截即可。recover 只能够捕获当前协程,而不能跨协程捕获 panic,下例中的 panic 就是无法被捕获的。 强制 有些特殊的错误是 recover 不住的,例如 Map 的并发读写冲突。这种错误可以通过 race 工具来检查。

扩展性 建议 利用接口实现扩展性。接口特别适用于访问外部组件的情况,例如访问数据库、访问下游服务。另外,接口可以方便我们进行功能测试。关于接口的最佳实践,需要单独论述。 建议 使用功能选项模式对一些公共 API 的构造函数进行扩展,大量第三方库例如 gomicro、zap 等都使用了这种策略。

工具 golangci-lint 是当前大多数公司采用的静态代码分析工具,词语 Linter 指的是一种分析源代码以此标记编程错误、代码缺陷、风格错误的工具。 而 golangci-lint 是集合多种 Linter 的工具。要查看支持的 Linter 列表以及启用 / 禁用了哪些 Linter,可以使用下面的命令:

golangci-lint help linters

Pre-Commit

在代码通过 Git Commit 提交到代码仓库之前,git 提供了一种 pre-commit 的 hook 能力,用于执行一些前置脚本。在脚本中加入检查的代码,就可以在本地拦截住一些不符合规范的代码,避免频繁触发 CI 或者浪费时间。pre-commit 的配置和使用方法,可以参考TiDB。

并发检测 race Go 1.1 提供了强大的检查工具 race 来排查数据争用问题。race 可以用在多个 Go 指令中,一旦检测器在程序中找到数据争用,就会打印报告。这份报告包含发生 race 冲突的协程栈,以及此时正在运行的协程栈。可以在编译时和运行时执行 race

覆盖率 一般我们会使用代码覆盖率来判断代码书写的质量,识别无效代码。go tool cover 是 go 语言提供的识别代码覆盖率的工具,在后面的课程中还会详细介绍。

标签:
Loading Comments...