项目背景

随着我的C++外卖项目要向着微服务架构转型,一个好的鉴权系统将会称为整个系统的看门技术,为此我将引入IAM系统作为微服务架构中的一环。同时该Go项目作为我的最新项目,我将按照更规范的项目设计和使用更优雅的代码开发来完成这个项目,这将作为我软件工程能力进入新阶段的重要一步。

非常特别的一点是,之前的项目要么是工具轮子,要么就是比较直接的服务应用,而这次的iam项目则是一个平台级项目,既有轮子的接口开放性,又有服务应用的架构复杂庞大和对效率的要求。甚至这次至少需要用到两个代码仓存储项目代码

IAM是什么?

IAM(Identity and Access Management,身份识别与访问管理)系统是用 Go 语言编写的一个 Web 服务,用于给第三方用户提供访问控制服务。

IAM 系统可以帮用户解决的问题是:在特定的条件下,谁能够/不能够对哪些资源做哪些操作(Who is able to do what on something given some context),也即完成资源授权功能。

  1. 用户需要提供昵称、密码、邮箱、电话等信息注册并登录到 IAM 系统,这里是以用户名和密码作为唯一的身份标识来访问 IAM 系统,并且完成认证。
  2. 因为访问 IAM 的资源授权接口是通过密钥(secretID/secretKey)的方式进行认证的,所以用户需要在 IAM 中创建属于自己的密钥资源。
  3. 因为 IAM 通过授权策略完成授权,所以用户需要在 IAM 中创建授权策略。
  4. 请求 IAM 提供的授权接口,IAM 会根据用户的请求内容和授权策略来决定一个授权请求是否被允许。

这里不详细介绍IAM,剩下的内容我们交给后序的设计文档

项目规范

一个好的软件项目,无论是否开源,为了保证其健壮性、可扩展性和可读性等设计要求,以及方便团队协作开发,一套公认的规范必不可少。根据是否与编码相关,可以分为非编码类规范编码类规范

我们来通过一张图总结它们各自的具体实现方式

至于编码类规范我们有各种技术文档来规范

项目目标

相比上一个略显粗糙、缝缝补补的外卖项目,我们这次要实现一个更为优雅的Go项目,从一开始我们就要以工程化的思维完善这整个项目,而不是就着需求功能开发出一个应用草草了事

写出易Mock的代码

上一个项目写的代码实际上基本都是不可测试的,它的接口都严重依赖数据库连接、第三方服务,在单元测试环境下会失效,意味着函数时不可测试的

解决方法也很简单:将依赖的数据库、第三方服务等抽象成接口,在被测代码中调用接口的方法,在测试时传入mock类型,从而将数据库、第三方服务等依赖从具体的被测函数中解耦出去。如下图所示:

为了写出易Mock的代码,我们有如下要求

  • 要尽可能减少function中的依赖,让function只依赖必要的模块。编写一个功能单一、职责分明的函数,会有利于减少依赖。
  • 依赖模块应该是易Mock的。

下面给出一个不可测代码的例子和一个可测试代码的例子

1
2
3
4
5
6
7
8
9
10
11
12
package post

import "google.golang.org/grpc"

type Post struct {
Name string
Address string
}

func ListPosts(client *grpc.ClientConn) ([]*Post, error) {
return client.ListPosts()
}

这个函数中,它依赖client.ListPosts()方法,该方法依赖于一个gRPC连接。而我们在做单元测试时,可能因为没有配置gRPC服务的地址、网络隔离等原因,导致没法建立gRPC连接,从而导致ListPosts函数执行失败。

但是我们改改接口,就可以把它改成可测试的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

type Post struct {
Name string
Address string
}

type Service interface {
ListPosts() ([]*Post, error)
}

func ListPosts(svc Service) ([]*Post, error) {
return svc.ListPosts()
}

上面代码中,ListPosts函数入参为Service接口类型,只要我们传入一个实现了Service接口类型的实例,ListPosts函数即可成功运行。

而对于这个自定义的Service对象,我们就有测试的控件了,比如在测试中实现一个不依赖任何第三方服务(这里是gtest连接)的对象和对应接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 假对象
func NewFakeService() Service {
return &fakeService{}
}
// 假接口
func (s *fakeService) ListPosts() ([]*Post, error) {
posts := make([]*Post, 0)
posts = append(posts, &Post{
Name: "colin",
Address: "Shenzhen",
})
posts = append(posts, &Post{
Name: "alex",
Address: "Beijing",
})
return posts, nil
}

至于常见的mock工具,我们有下面的选择

  1. golang/mock,是官方提供的Mock框架。它实现了基于interface的Mock功能,能够与Golang内置的testing包做很好的集成,是最常用的Mock工具。golang/mock提供了mockgen工具用来生成interface对应的Mock源文件。
  2. sqlmock,可以用来模拟数据库连接。数据库是项目中比较常见的依赖,在遇到数据库依赖时都可以用它。
  3. httpmock,可以用来Mock HTTP请求
  4. bouk/monkey,猴子补丁,能够通过替换函数指针的方式来修改任意函数的实现。如果golang/mock、sqlmock和httpmock这几种方法都不能满足我们的需求,我们可以尝试通过猴子补丁的方式来Mock依赖。可以这么说,猴子补丁提供了单元测试 Mock 依赖的最终解决方案。

开始编写文档

越是大型的项目,越要有规范的文档去保证开发质量,我们将开发文档放在docs/devel目录下