第一讲 开源规范 开源协议概述

MIT是相对自由开发的协议

第二讲 文档规范

最重要的三类文档:README文档、项目文档和API文档

README规范

主要用来介绍项目的功能、安装、部署和使用

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 # 项目名称 # 功能特性 # 软件架构(可选) # 快速开始 # 依赖检查 # 构建 # 运行 # 使用指南 # 如何贡献 # 社区(可选) # 关于作者 # 谁在用(可选) # 许可证 项目文档规范

通常放在/docs目录下,包含开发文档和用户文档

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 docs ├── devel # 开发文档,可以提前规划好,英文版文档和中文版文档 │ ├── en-US/ # 英文版文档,可以根据需要组织文件结构 │ └── zh-CN # 中文版文档,可以根据需要组织文件结构 │ └── development.md # 开发手册,可以说明如何编译、构建、运行项目 ├── guide # 用户文档 │ ├── en-US/ # 英文版文档,可以根据需要组织文件结构 │ └── zh-CN # 中文版文档,可以根据需要组织文件结构 │ ├── api/ # API文档 │ ├── best-practice # 最佳实践,存放一些比较重要的实践文章 │ │ └── authorization.md │ ├── faq # 常见问题 │ │ ├── iam-apiserver │ │ └── installation │ ├── installation # 安装文档 │ │ └── installation.md │ ├── introduction/ # 产品介绍文档 │ ├── operation-guide # 操作指南,里面可以根据RESTful资源再划分为更细的子目录,用来存放系统核心/全部功能的操作手册 │ │ ├── policy.md │ │ ├── secret.md │ │ └── user.md │ ├── quickstart # 快速入门 │ │ └── quickstart.md │ ├── README.md # 用户文档入口文件 │ └── sdk # SDK文档 │ └── golang.md └── images # 图片存放目录 └── 部署架构v1.png API接口规范

API文档生成方式包括通过注释生成、编写Markdown格式文档, 通常拆分为多个文件

README: API介绍整体文档 CHANGELOG: PAI变更历史 generic: 通用的请求参数、返回参数、认证方法、响应状态码等说明 struct: 接口使用的数据结构 error_code: 业务错误码说明 按资源划分API文档: API详细说明 第三讲 版本规范

版本格式为:主版本号.次版本号.修订号(X.Y.Z)

版本号递增规则

主版本(major): 不兼容API修改 次版本(minor): 新增功能(一般偶数为稳定版,奇数为开发版本) 修订版本:问题修正

版本号建议

开始开发时,以0.1.0作为第一个开发版本号,后续发行时递增次版本号 发布第一个稳定版时定为1.0.0 后续迭代 fix commit将修订版本+1 feat commit将次版本号+1 BREAKING CHANGE commit将主版本号+1 第四讲 Commit规范

采用Angular风格的Commit Message, 其包含三个部分:Header、Body和Footer,具体格式如下:

1 2 3 4 5 [optional scope]: // blank line [optional body] // blink line [optional footer] Header

type说明commit类型,主要分为Development和Production两大类

Development类别修改的是项目管理类的变更,如CI流程、构建方式等,不会影响到最终用户,具体类型 style:代码格式类的变更,如格式化代码、删除空行等 test: 增加或更新测试用例 ci:持续集成和部署相关的改动 docs:更新文档 chore:其他类型,如构建流程、依赖管理或辅助工具的变更等 Production类别会影响到最终用户,提交前需要做好充分的测试 feat: 新增功能 fix: bug修复 perf: 提高代码性能的变更 refactor: 不属于上面三类的其他类型,如简化代码、重命名变量等

scope用来说明影响范围,应根据项目情况设计大类如api、pkg、docs等

descrption是对commit的简单描述,必须以动词开头,使用现在时态,结尾不加句号

Body

body是对commit的详细描述,同样以动词开头,使用现在时态,内容包含改动的原因和改动点

Footer

通产用来说明不兼容的改动和关闭的issue,如下示例

1 2 BREAKING CHANGE: XXXXX Cloes: #123, #234 Revert Commit

当还原commit的时,在还原的Header前面加revert: , Body里面说明还原的commit hash,如:

1 2 3 revert: feat(api): add 'Host' option This reverts commit fjsdf34353534vdf 其他

合并提交: 对于过多的commit使用git rebase进行合并

修改message: 使用git rebase(注:修改message会将导致当前及置换的hash变更)

自动化工具:

commitizen-go:格式化填充commit message gitlint: 检查commit message git-changelog: 自动生成change log gmemver: 语义化版本自动生成 第五讲 目录结构设计 目录介绍 1. /web

web目录主要存放web静态资源

2. /cmd

一个项目可能有多个组件,每个组件的main函数所在文件夹放在该目录

3. /internal

存放私有应用的代码,不能被其他项目导入

项目内应用之间共享代码存放于/internal/pkg

开发建议:最开始将共享代码都放/internal/pkg,做好对外发布的准备时再转到/pkg目录

IAM项目internal目录结构如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ├── apiserver │ ├── api │ │ └── v1 │ │ └── user │ ├── options │ ├── config │ ├── service │ │ └── user.go │ ├── store │ │ ├── mysql │ │ │ └── user.go │ │ ├── fake │ └── testing ├── authzserver │ ├── api │ │ └── v1 │ ├── options │ ├── store │ └── testing ├── iamctl │ ├── cmd │ │ ├── cmd.go │ │ ├── info └── pkg ├── code ├── middleware ├── options └── validation

主要分为三大类

/internal/pkg: 内部共享包存放目录 /internal/iamctl: 对于大型项目,可能会存在客户端工具 /internal/apiserver: 应用目录

针对具体的应用目录,也会根据功能来划分:

/internal/apiserver/api/v1: HTTP API接口具体实现 /internal/apiserver/options: command flag /internal/apiserver/service: 业务逻辑代码 /internal/apiserver/store/mysql: 数据库交互

/internal/pkg通常也会划分:

/internal/pkg/code: 业务Code码 /internal/pkg/validation: 通用验证函数 /internal/pkg/code: HTTP处理链 4. /pkg

pkg目录存放外部应用可以使用的代码库,应谨慎考虑

5. /vendor

项目依赖,通过go mod vendor创建

6. /third_party

外部帮助工具,比如fork了一个第三方go包,并做了小改动,可以放置该目录

7. /test

存放其他外部测试应用和测试数据

8. /configs

存放配置文件模板或默认配置

9. /deployments

存放系统和容器编排部署模板和配置

10. /init

存放初始化系统和进程管理配置文件,如systemd、supervisord等

11. /Makefile

项目管理文件

12. /scripts

存放脚本文件,通常可能分为三个目录

/scripts/make-rules: 存放maker文件,实现Makerfile文件中的各个功能 /scripts/lib: 存放shell脚本 /scripts/intall: 如果项目支持自动化部署,可以将部署脚本放在该目录 13. /build

存放安装包和持续集成相关的文件,通常可能包含三个目录

/build/package: 存放容器(Docker)、系统(deb,rpm)的包配置和脚本 /build/ci: 存放CI(travis,circle)的配置文件 /build/docker: 存放子项目各个组件的Dockerfile文件 14. /tools

存放这个项目的支持工具,这些工具可导入来自/pkg和/internal目录的代码

15. /githooks

git钩子

16. /assets

项目使用的其他资源(图片、CSS、Javascript等)

17. /website

放置项目网站相关的数据

18. /README.md

一般包含项目介绍、功能介绍、快速按照、使用指引、详细文档连接和开发指引等 文件较长时可以用tocenize加目录

19. /docs

存放设计文档、开放文档和用户文档等,可能包含下面几个目录

/docs/devel/{en-US, zh-CN}: 存放开发文档 /docs/guide/{en-US, zh-CN}: 存放用户手册 /docs/images: 存放图片文件

20. /CONTRIBUTING.md

用来说明如何贡献代码,规范协同流程

21. /api

存放项目提供的各种不同类型的API接口定义文件,可能有openapi、swagger等目录

1 2 3 4 5 6 ├── openapi/ │ └── README.md └── swagger/ ├── docs/ ├── README.md └── swagger.yaml 22. /LICENSE

版权文件

如果需要给源码文件加license头时,可以使用addlicense

项目依赖第三方包使用的license检查使用glice

23. /CHANGELOG

项目更新日志,可结合Angular规范和git-chglog自动生成内容

24. /examples

存放代码示例

其他建议 不使用/model目录,按功能拆分到使用的模块中 目录和包尽量使用单数 小项目可以先包含cmd、pkg、internal三个目录 第六讲 工作流设计 功能分支工作流

开发新功能时,基于master分支新建一个功能分支,在功能分支上进行开发,开发完之后合并到master

该模式适合小规模、人员固定的项目

Git Flow工作流

Git Flow定义了5种分支: master、develop、release、 feature、hotfix,详细介绍如下

假设当前在一个future分支开发,突然发现了线上bug,需要hotfix,则流程如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ git stash # 1. 开发工作只完成了一半,还不想提交,可以临时保存修改至堆栈区 $ git checkout -b hotfix/print-error master # 2. 从 master 建立 hotfix 分支 $ vi main.go # 3. 修复 bug,callmainfunction -> call main function $ git commit -a -m 'fix print message error bug' # 4. 提交修复 $ git checkout develop # 5. 切换到 develop 分支 $ git merge --no-ff hotfix/print-error # 6. 把 hotfix 分支合并到 develop 分支 $ git checkout master # 7. 切换到 master 分支 $ git merge --no-ff hotfix/print-error # 8. 把 hotfix 分支合并到 master $ git tag -a v0.9.1 -m "fix log bug" # 9. master 分支打 tag $ go build -v . # 10. 编译代码,并将编译好的二进制更新到生产环境 $ git branch -d hotfix/print-error # 11. 修复好后,删除 hotfix/xxx 分支 $ git checkout feature/print-hello-world # 12. 切换到开发分支下 $ git merge --no-ff develop # 13. 因为 develop 有更新,这里最好同步更新下 $ git stash pop # 14. 恢复到修复前的工作状态

该模式适合人员固定、规模较大的项目

Forking工作流

开源项目常用模式

第七讲 研发流程设计 研发流程

通常研发流程包括6个阶段

需求阶段 设计阶段 开发阶段 测试阶段 发布阶段 运营阶段

每个阶段结束时,需要一个最终产物,可以是文档、代码或者部署组件,这个产物是下一个阶段的输入

研发模式

研发模式有三种: 瀑布模式、迭代模式和敏捷模式

瀑布模式

瀑布墨迹按照预先规划好的阶段来推进研发进度,流程清晰,但研发周期长,交付后变更困难

迭代模式

研发任务被切分为一系列轮次,先把主要功能搭建起来,在通过客户反馈不断完善

敏捷模式

敏捷模式把大需求分成多个、可分阶段完成的小迭代,每个迭代交付都是一个可用的软件,开发过程中,软件一直处于可用状态

迭代模式关注研发流程。而敏捷模式不仅会关注研发流程,还会关注之外的一些东西,例如:团队协作,需求拆分

CI/CD

CI/CD通过自动化的手段来快速执行代码检查、测试、构建和部署任务,从而提高研发效率

CI:Continuous Integration,持续集成 CD:Continuous Delivery,持续交付 CD:Continuous Deployment,持续部署

持续集成的核心在代码,持续交付的核心在可交付的产物,持续部署的核心在自动部署

持续集成

在代码push到git仓库后,CI工具会进行扫描、测试和构建,并将结果反馈给开发者

CI流程可以将问题在开发阶段就暴露出来,这会让开发人员交付代码时更有信心

持续交付

在持续集成的基础上,就构建产物自动部署到目标环境(测试、预发布)

持续部署

在持续交付的基础上,将经过充分测试的代码自动部署到生产环境,整个流程不在需要审核,完全自动化

DevOps

DevOps是一组过程、方法和系统的统称,用于促进开发、运维、质量部门之间的协作整合

目前常用的Ops手段: AIOps、ChatOps、GitOps

ChatOps

通过发送指令给聊天机器人,执行某个任务 ChatOps对操作者友好,信息透明可追溯

GitOps

基于Git和K8S实现云原生的持续交付

AIOps

利用AI技术来智能化运维IT系统

第八讲 设计方法 代码结构

按功能拆分目录而非按MVC等模块拆封

代码规范

可参考Uber的规范:Go Style Guide

可使用惊呆检查工具:golangcli-lint

官方CodeReview实践:Go Code Review Comments

代码质量

测试相关工具:

官方Mock框架: golang/mock 数据库Mock: golang/mock HTTP Mock: golang/mock 万能Mock: golang/mock

覆盖率检查

1 2 $ go test -race -cover -coverprofile=./coverage.out -timeout=10m -short -v ./... $ go tool cover -func ./coverage.out 编程哲学 面向接口编程 面向对象编程 软件设计方法 设计模式 SOLD原则 高效项目管理

使用Makefile管理项目

自动生成代码

对接CI、CD

编写高质量的文档

第九讲 设计模式 创建型模式

创建型模式(Creational Patterns)提供了在创建对象的同事隐藏创建逻辑的方式,而不是使用new运算符直接实例化对象。

比较常见的是单例模式和工厂模式

单例模式

单例模式(Singleton Pattern)指全局只有一个实例,有利于减少内存开销,防止冲突等优点,常用于数据库实例、全局配置等

饿汉模式:初始化时创建 1 2 3 4 5 6 7 8 9 10 package singleton type singleton struct { } var ins *singleton = &singleton{} func GetInsOr() *singleton { return ins } 懒汉模式:实际使用时创建,可能有并发问题,需要加锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package singleton import ( "sync" ) type singleton struct { } var ins *singleton var once sync.Once func GetInsOr() *singleton { once.Do(func() { ins = &singleton{} }) return ins } 工厂模式 简单工厂模式:接受参数,返回一个对象实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type Person struct { Name string Age int } func (p Person) Greet() { fmt.Printf("Hi, my name is %s\n", p.Name) } func NewPerson(name string, age int) *Person { return &Person{ Name: name, Age: age, } } 抽象工厂模式: 返回接口而非结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type Doer interface { Do(req *http.Request) (*http.Response, error) } func NewHTTPClient() Doer { return &http.Client{} } type mockHTTPClient struct{} func (*mockHTTPClient) Do(req *http.Request) (*http.Response, error) { res := httptest.NewRecorder() return res.Result(), nil } func NewMockHTTPClient() Doer { return &mockHTTPClient{} } 工厂方法模式: 通过实现工厂接口来创建多种工厂 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type Person struct { name string age int } func NewPersonFactory(age int) func(name string) Person { return func(name string) Person { return Person{ name: name, age: age, } } } newBaby := NewPersonFactory(1) baby := newBaby("john") newTeenager := NewPersonFactory(16) teen := newTeenager("jill") 结构型模式

结构型模式的特点是关注类和对象的组合

策略模式

定义策略接口,并实现不同的策略,策略执行者可以设置不同的策略

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type IStrategy interface { do(a, b int) int } type add struct { } func (*add) do(a, b int) int { return a + b } type reduce struct { } func (*reduce) do(a, b int) int { return a - b } type Operator struct { strategy IStrategy } func (o *Operator) setStrategy(strategy IStrategy) { o.strategy = strategy } func (o *Operator) calculate(a, b int) int { return o.strategy.do(a, b) } 模板模式

将一个类中能公共使用的方法放置在抽象类中实现,将不能公共使用的方法作为抽象方法强制子类去实现

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package template import "fmt" type Cooker interface { fire() cooke() outfire() } // 类似于一个抽象类 type CookMenu struct { } func (CookMenu) fire() { fmt.Println("开火") } // 做菜,交给具体的子类实现 func (CookMenu) cooke() { } func (CookMenu) outfire() { fmt.Println("关火") } // 封装具体步骤 func doCook(cook Cooker) { cook.fire() cook.cooke() cook.outfire() } type XiHongShi struct { CookMenu } func (*XiHongShi) cooke() { fmt.Println("做西红柿") } type ChaoJiDan struct { CookMenu } func (ChaoJiDan) cooke() { fmt.Println("做炒鸡蛋") } 行为型模式

行为模式的特点是关注对象之间的通信

代理模式

代理模式Proxy pattern 可以为另外一个对象提供一个替身或者占位符,以控制对这个对象的访问

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package proxy import "fmt" type Seller interface { sell(name string) } // 火车站 type Station struct { stock int //库存 } func (station *Station) sell(name string) { if station.stock > 0 { station.stock-- fmt.Printf("代理点中:%s买了一张票,剩余:%d \n", name, station.stock) } else { fmt.Println("票已售空") } } // 火车代理点 type StationProxy struct { station *Station // 持有一个火车站对象 } func (proxy *StationProxy) sell(name string) { if proxy.station.stock > 0 { proxy.station.stock-- fmt.Printf("代理点中:%s买了一张票,剩余:%d \n", name, proxy.station.stock) } else { fmt.Println("票已售空") } } 选项模式

通过选项模式可以创建一个带有默认值得struct变量,并选择性的修改其中一些参数的值

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package options import ( "time" ) type Connection struct { addr string cache bool timeout time.Duration } const ( defaultTimeout = 10 defaultCaching = false ) type options struct { timeout time.Duration caching bool } // Option overrides behavior of Connect. type Option interface { apply(*options) } type optionFunc func(*options) func (f optionFunc) apply(o *options) { f(o) } func WithTimeout(t time.Duration) Option { return optionFunc(func(o *options) { o.timeout = t }) } func WithCaching(cache bool) Option { return optionFunc(func(o *options) { o.caching = cache }) } // Connect creates a connection. func NewConnect(addr string, opts ...Option) (*Connection, error) { options := options{ timeout: defaultTimeout, caching: defaultCaching, } for _, o := range opts { o.apply(&options) } return &Connection{ addr: addr, cache: options.caching, timeout: options.timeout, }, nil } 第十讲 API风格 Restful API设计原则 URI设计

通常情况下:

资源使用名词复数表示 URI结尾不包含/ 推荐使用- 使用小写 避免层级过深,超过2层时将其他资源转为?参数,比如 1 2 /schools/tsinghua/classes/rooma/students/zhang # 不推荐 /students?school=qinghua&class=rooma # 推荐

实际场景中某些操作不能很好地映射为资源,可以参考如下做法:

将一个操作变成一个资源的属性,比如禁用用户可以设计URI: /users/zhangsan?active=false 将操作当做是一个资源的嵌套资源,如github star: 1 2 PUT /gits/:id/star DELETE /gits/:id/star 有时也可以打破规范,如登录操作URI设计为:/login 操作和方法之间的映射

对资源的操作应蛮子安全性和幂等性:

安全性:不会改变资源状态,可以理解为只读 幂等性:执行1次和执行N次,对资源状态改变的效果是等价的

POST一般用在新建和批量删除这两种场景,批量删除更建议使用:DELETE /users?id=1,2,3

统一分页/过滤/排序/搜索功能 分页:在列出一个Collection所有Member时应该提供分页功能,如:/users?offset=0&limit=20 过滤:当不用返回一个资源的全部属性时可以指定,如:/users?fields=email,username,address 排序:根据指定字段排序,如:/users?sort=age,desc 搜索:当一个资源的Member太多时,可能需要提供搜索功能,搜索建议按模糊搜索来匹配 域名

主要有两种方式:

https://abc.com/api 适用该域名只有一套API系统的情况 https://project.api.abc.com 适用域名下有多个系统API RPC API

RPC(Remote Procedure Call)即远程过程调用,通俗地来讲就是服务端实现了一个函数, 客户端使用RPC框架提供的接口,想调用本地函数一样调用这个函数,并获取返回值。

protobuf3可以使用optional关键字来支持显示判断是否传入该字段

golang会将该字段转为指针类型,可以判断是否为nil python可以通过HasField方法来判断

可以通过grpc-gateway来同时支持restful-api

第十一讲 Makefile 基本形式 1 2 : [tab] target: 目标,目标非文件时称之为伪目标PHONBY prerequisities: 前置依赖目标 command: 具体的shell命令,以tab起手,每一行都是单独的session, 可用;共用session,使用\作为换行符 基本语法 声明伪目标

避免有和target同名的文件

1 2 3 .PHONY: clean clean: rm -rf *.c 关闭回声

命令前面加@不会先打印命令

1 2 test: @echo 'hello world' 忽略错误

命令前面加-可忽略错误,继续往下执行

1 2 3 test: -eco 'hello world' @echo 'hello world2' 通配符

支持%、*、~

变量和赋值 使用=自动以变量 1 msg = 'Hello world' 四种赋值运算符 1 2 3 4 5 6 7 8 9 10 11 VARIABLE = value # 在执行时扩展,允许递归扩展。 VARIABLE := value # 在定义时扩展。 VARIABLE ?= value # 只有在该变量为空时才设置值。 VARIABLE += value # 将值追加到变量的尾端。 使用$()引用变量 1 2 3 msg = 'hello world' test: echo $(msg) 调用系统变量是需在再加一个$ 1 2 test: echo: $$HOME 自动变量 $@: 指当前目标 1 2 a.txt: echo 'hello' > $@ $