从零基于 Go 语言构建企业级的 RESTful API 服务

715 阅读13分钟

[从零基于 Go 语言构建企业级的 RESTful API 服务 (flbook.com.cn)]

完整pdf在线阅读(flbook.com.cn/c/q3dGZXswn…) 项目源码 github.com/qq601046124…

本⼩册所实现的 API 功能 本⼩册所实现的功能 本⼩册通过实现⼀个账号系统,来演⽰如何构建⼀个真实的 API 服务器,构建⽅法和技术是笔者根据多年的⼤型 API 服务器开发经验不断优化沉淀⽽成。通过实战展⽰了 API 构建过程中各个流程(准备 -> 设计 -> 开发 -> 测试 -> 部署)的实现⽅法,⼩册涵盖的内容如下(包括但不限于): 详细为: 准备阶段 如何安装和配置 Go 开发环境 如何安装和配置 Vim IDE 设计阶段 API 构建技术选型 API 基本原理 API 规范设计 开发阶段 如何读取配置⽂件 如何管理和记录⽇志 如何做数据库的 CURD 操作 如何⾃定义错误 Code 如何读取和返回 HTTP 请求 如何进⾏业务逻辑开发 如何对请求插⼊⾃⼰的处理逻辑 如何进⾏ API ⾝份验证 如何进⾏ HTTPS 加密 如何⽤ Makefile 管理 API 源码 如何给 API 命令添加版本功能 如何管理 API 命令 如何⽣成 Swagger 在线⽂档 测试阶段 如何进⾏单元测试 如何进⾏性能测试(函数性能) 如何做性能分析 API 性能测试和调优 部署阶段 如何⽤ Nginx 部署 API 服务 如何做 API ⾼可⽤ 通过以上各功能的介绍,读者可以完整、系统地学习 API 构建⽅法和技巧,笔者也会在⽂章中融⼊⾃⼰的开发经验以供读者参考。 账号系统业务功能 本⼩册为了演⽰,构建了⼀个账号系统(后⾯统称为apiserver),功能如下: API 服务器状态检查 登录⽤户 新增⽤户 删除⽤户 更新⽤户 获取指定⽤户的详细信息 获取⽤户列表 本⼩册执⾏环境 本⼩册所有的软件安装,运⾏均是在 CentOS 7.1 系统上执⾏的。 理论上本⼩册所构建的 API 可以在所有的 Unix/Linux 系统上编译和运⾏,⼩册中的软件安装⽤的是 yum ⼯具,⼩册中所列举的 yum 软件理论上可以在 CentOS 6 和 CentOS 7 上直接执⾏ yum 命令安装。 ⼩结 本⼩节介绍了⼩册所要实现的 API 功能,以及 API 系统的业务功能,让读者在实战前对⼩册所要构建的系统有个整体了解,以便于接下来的学习。⼩册每⼀节都会提供源码,供读者学习参考。 RESTful API 介绍 什么是 API API(Application Programming Interface,应⽤程序编程接⼜)是⼀些预先定义的函数或者接⼜,⽬的是提供应⽤程序与开发⼈员基于某软件或硬件得以访问⼀组例程的能⼒,⽽又⽆须访问源码,或理解 内部⼯作机制的细节。 要实现⼀个 API 服务器,⾸先要考虑两个⽅⾯:API 风格和媒体类型。Go 语⾔中常⽤的 API 风格是 RPC 和 REST,常⽤的媒体类型是 JSON、XML 和 Protobuf。在 Go API 开发中常⽤的组合是 gRPC + Protobuf 和 REST + JSON。 REST 简介 REST 代表表现层状态转移(REpresentational State Transfer),由 Roy Fielding 在他的 论⽂ 中提出。REST 是⼀种软件架构风格,不是技术框架,REST 有⼀系列规范,满⾜这些规范的 API 均可称为 RESTful API。REST 规范中有如下⼏个核⼼:

  1. REST 中⼀切实体都被抽象成资源,每个资源有⼀个唯⼀的标识 —— URI,所有的⾏为都应该是在资源上的 CRUD 操作
  2. 使⽤标准的⽅法来更改资源的状态,常见的操作有:资源的增删改查操作
  3. ⽆状态:这⾥的⽆状态是指每个 RESTful API 请求都包含了所有⾜够完成本次操作的信息,服务器端⽆须保持 Session ⽆状态对于服务端的弹性扩容是很重要的。 REST 风格虽然适⽤于很多传输协议,但在实际开发中,REST 由于天⽣和 HTTP 协议相辅相成,因此 HTTP 协议已经成了实现 RESTful API 事实上的标准。在 HTTP 协议中通过 POST、DELETE、PUT、 GET ⽅法来对应 REST 资源的增、删、改、查操作,具体的对应关系如下: HTTP ⽅法 ⾏为 URI ⽰例说明 GET 获取资源列表 /users 获取⽤户列表 GET 获取⼀个具体的资源 /users/admin 获取 admin ⽤户的详细信息 POST 创建⼀个新的资源 /users 创建⼀个新⽤户 PUT 以整体的⽅式更新⼀个资源 /users/1 更新 id 为 1 的⽤户 DELETE 删除服务器上的⼀个资源 /users/1 删除 id 为 1 的⽤户 RPC 简介 根据维基百科的定义:远程过程调⽤(Remote Procedure Call,RPC)是⼀个计算机通信协议。该协议允许运⾏于⼀台计算机的程序调⽤另⼀台计算机的⼦程序,⽽程序员⽆须额外地为这个交互作⽤编 程。 通俗来讲,就是服务端实现了⼀个函数,客户端使⽤ RPC 框架提供的接⼜,调⽤这个函数的实现,并获取返回值。RPC 屏蔽了底层的⽹络通信细节,使得开发⼈员⽆须关注⽹络编程的细节,⽽将更多的 时间和精⼒放在业务逻辑本⾝的实现上,从⽽提⾼开发效率。 RPC 的调⽤过程如下(图⽚来⾃ How RPC Works):
  4. Client 通过本地调⽤,调⽤ Client Stub
  5. Client Stub 将参数打包(也叫 Marshalling)成⼀个消息,然后发送这个消息
  6. Client 所在的 OS 将消息发送给 Server
  7. Server 端接收到消息后,将消息传递给 Server Stub
  8. Server Stub 将消息解包(也叫 Unmarshalling)得到参数
  9. Server Stub 调⽤服务端的⼦程序(函数),处理完后,将最终结果按照相反的步骤返回给 Client Stub 负责调⽤参数和返回值的流化(serialization)、参数的打包解包,以及负责⽹络层的通信。Client 端⼀般叫 Stub,Server 端⼀般叫 Skeleton。 REST vs RPC 在做 API 服务器开发时,很多⼈都会遇到这个问题 —— 选择 REST 还是 RPC。RPC 相⽐ REST 的优点主要有 3 点:
  10. RPC+Protobuf 采⽤的是 TCP 做传输协议,REST 直接使⽤ HTTP 做应⽤层协议,这种区别导致 REST 在调⽤性能上会⽐ RPC+Protobuf 低
  11. RPC 不像 REST 那样,每⼀个操作都要抽象成对资源的增删改查,在实际开发中,有很多操作很难抽象成资源,⽐如登录操作。所以在实际开发中并不能严格按照 REST 规范来写 API,RPC 就不存 在这个问题
  12. RPC 屏蔽⽹络细节、易⽤,和本地调⽤类似 这⾥的易⽤指的是调⽤⽅式上的易⽤性。在做 RPC 开发时,开发过程很烦琐,需要先写⼀个 DSL 描述⽂件,然后⽤代码⽣成器⽣成各种语⾔代码,当描述⽂件有更改时,必须重新定义和编 译,维护性差。 但是 REST 相较 RPC 也有很多优势:
  13. 轻量级,简单易⽤,维护性和扩展性都⽐较好
  14. REST 相对更规范,更标准,更通⽤,⽆论哪种语⾔都⽀持 HTTP 协议,可以对接外部很多系统,只要满⾜ HTTP 调⽤即可,更适合对外,RPC 会有语⾔限制,不同语⾔的 RPC 调⽤起来很⿇烦
  15. JSON 格式可读性更强,开发调试都很⽅便
  16. 在开发过程中,如果严格按照 REST 规范来写 API,API 看起来更清晰,更容易被⼤家理解 在实际开发中,严格按照 REST 规范来写很难,只能尽可能 RESTful 化。 其实业界普遍采⽤的做法是,内部系统之间调⽤⽤ RPC,对外⽤ REST,因为内部系统之间可能调⽤很频繁,需要 RPC 的⾼性能⽀撑。对外⽤ REST 更易理解,更通⽤些。当然以现有的服务器性能,如 果两个系统间调⽤不是特别频繁,对性能要求不是⾮常⾼,以笔者的开发经验来看,REST 的性能完全可以满⾜。本⼩册不是讨论微服务,所以不存在微服务之间的⾼频调⽤场景,此外 REST 在实际开发 中,能够满⾜绝⼤部分的需求场景,所以 RPC 的性能优势可以忽略,相反基于 REST 的其他优势,笔者更倾向于⽤ REST 来构建 API 服务器,本⼩册正是⽤ REST 风格来构建 API 的。 媒体类型选择 媒体类型是独⽴于平台的类型,设计⽤于分布式系统间的通信,媒体类型⽤于传递信息,⼀个正式的规范定义了这些信息应该如何表⽰。HTTP 的 REST 能够提供多种不同的响应形式,常见的是 XML 和 JSON。JSON ⽆论从形式上还是使⽤⽅法上都更简单。相⽐ XML,JSON 的内容更加紧凑,数据展现形式直观易懂,开发测试都⾮常⽅便,所以在媒体类型选择上,选择了 JSON 格式,这也是很多⼤公 司所采⽤的格式。 ⼩结 本⼩节介绍了软件架构中 API 的实现⽅式,并简单介绍了相应的技术,通过对⽐,得出本⼩册所采⽤的实现⽅式:API 风格采⽤ REST,媒体类型选择 JSON。通过本⼩节的学习,读者可以了解⼩册所构 建 API 服务器核⼼技术的选型和原因。 API 流程和代码结构 为了使读者在开始实战之前对 API 开发有个整体的了解,这⾥选择了两个流程来介绍: HTTP API 服务器启动流程 HTTP 请求处理流程 本⼩节也提前给出了程序代码结构图,让读者从宏观上了解将要构建的 API 服务器的功能。 HTTP API 服务器启动流程 如上图,在启动⼀个 API 命令后,API 命令会⾸先加载配置⽂件,根据配置做后⾯的处理⼯作。通常会将⽇志相关的配置记录在配置⽂件中,在解析完配置⽂件后,就可以加载⽇志包初始化函数,来初 始化⽇志实例,供后⾯的程序调⽤。接下来会初始化数据库实例,建⽴数据库连接,供后⾯对数据库的 CRUD 操作使⽤。在建⽴完数据库连接后,需要设置 HTTP,通常包括 3 ⽅⾯的设置:
  17. 设置 Header
  18. 注册路由
  19. 注册中间件 之后会调⽤ net/http 包的 ListenAndServe() ⽅法启动 HTTP 服务器。 在启动 HTTP 端⼜之前,程序会 go ⼀个协程,来ping HTTP 服务器的 /sd/health 接⼜,如果程序成功启动,ping 协程在 timeout 之前会成功返回,如果程序启动失败,则 ping 协程最终会 timeout,并终 ⽌整个程序。 解析配置⽂件、初始化 Log 、初始化数据库的顺序根据⾃⼰的喜好和需求来排即可。 HTTP 请求处理流程 ⼀次完整的 HTTP 请求处理流程如上图所⽰。(图⽚出⾃《HTTP 权威指南》,推荐想全⾯理解 HTTP 的读者阅读此书。)
  20. 建⽴连接 客户端发送 HTTP 请求后,服务器会根据域名进⾏域名解析,就是将⽹站名称转变成 IP 地址:localhost -> 127.0.0.1,Linux hosts⽂件、DNS 域名解析等可以实现这种功能。之后通过发起 TCP 的三次握⼿ 建⽴连接。TCP 三次连接请参考 TCP 三次握⼿详解及释放连接过程,建⽴连接之后就可以发送 HTTP 请求了。
  21. 接收请求 HTTP 服务器软件进程,这⾥指的是 API 服务器,在接收到请求之后,⾸先根据 HTTP 请求⾏的信息来解析到 HTTP ⽅法和路径,在上图所⽰的报⽂中,⽅法是 GET,路径是 /index.html,之后根据 API 服务器注册的路由信息(⼤概可以理解为:HTTP ⽅法 + 路径和具体处理函数的映射)找到具体的处理函数。
  22. 处理请求 在接收到请求之后,API 通常会解析 HTTP 请求报⽂获取请求头和消息体,然后根据这些信息进⾏相应的业务处理,HTTP 框架⼀般都有⾃带的解析函数,只需要输⼊ HTTP 请求报⽂,就可以解析到需要 的请求头和消息体。通常情况下,业务逻辑处理可以分为两种:包含对数据库的操作和不包含对数据的操作。⼤型系统中通常两种都会有:
  23. 包含对数据库的操作:需要访问数据库(增删改查),然后获取指定的数据,对数据处理后构建指定的响应结构体,返回响应包。数据库通常⽤的是 MySQL,因为免费,功能和性能也都能满⾜企 业级应⽤的要求。
  24. 不包含对数据库的操作:进⾏业务逻辑处理后,构建指定的响应结构体,返回响应包。
  25. 记录事务处理过程 在业务逻辑处理过程中,需要记录⼀些关键信息,⽅便后期 Debug ⽤。在 Go 中有各种各样的⽇志包可以⽤来记录这些信息。 HTTP 请求和响应格式介绍 ⼀个 HTTP 请求报⽂由请求⾏(request line)、请求头部(header)、空⾏和请求数据四部分组成,下图是请求报⽂的⼀般格式。 第⼀⾏必须是⼀个请求⾏(request line),⽤来说明请求类型、要访问的资源以及所使⽤的 HTTP 版本 紧接着是⼀个头部(header)⼩节,⽤来说明服务器要使⽤的附加信息 之后是⼀个空⾏ 再后⾯可以添加任意的其他数据(称之为主体:body) HTTP 响应格式跟请求格式类似,也是由 4 个部分组成:状态⾏、消息报头、空⾏和响应数据。 ⽬录结构 ├── admin.sh # 进程的start|stop|status|restart控制⽂件 ├── conf # 配置⽂件统⼀存放⽬录 │ ├── config.yaml # 配置⽂件 │ ├── server.crt # TLS配置⽂件 │ └── server.key ├── config # 专⻔⽤来处理配置和配置⽂件的Go package │ └── config.go ├── db.sql # 在部署新环境时,可以登录MySQL客户端,执⾏source db.sql创建数据库和表 ├── docs # swagger⽂档,执⾏ swag init ⽣成的 │ ├── docs.go │ └── swagger │ ├── swagger.json │ └── swagger.yaml ├── handler # 类似MVC架构中的C,⽤来读取输⼊,并将处理流程转发给实际的处理函数,最后返回结果 │ ├── handler.go │ ├── sd # 健康检查handler │ │ └── check.go │ └── user # 核⼼:⽤户业务逻辑handler │ ├── create.go # 新增⽤户 │ ├── delete.go # 删除⽤户 │ ├── get.go # 获取指定的⽤户信息 │ ├── list.go # 查询⽤户列表 │ ├── login.go # ⽤户登录 │ ├── update.go # 更新⽤户 │ └── user.go # 存放⽤户handler公⽤的函数、结构体等 ├── main.go # Go程序唯⼀⼊⼝ ├── Makefile # Makefile⽂件,⼀般⼤型软件系统都是采⽤make来作为编译⼯具 ├── model # 数据库相关的操作统⼀放在这⾥,包括数据库初始化和对表的增删改查 │ ├── init.go # 初始化和连接数据库 │ ├── model.go # 存放⼀些公⽤的go struct │ └── user.go # ⽤户相关的数据库CURD操作 ├── pkg # 引⽤的包 │ ├── auth # 认证包 │ │ └── auth.go │ ├── constvar # 常量统⼀存放位置 │ │ └── constvar.go │ ├── errno # 错误码存放位置 │ │ ├── code.go │ │ └── errno.go │ ├── token