WEB 开展进程 & RESTful API 规范 & JSON 规范呼应数据

能够惋惜,但不要后悔。

咱们留在这儿,从来不是身不由己。

——— 而是挑选在这儿阅历生活

目录

本文介绍了 GolangWeb 编程中的运用。从 Web 开展进程开端,别离探讨了九个方面的内容,虽然它们相互独立,可是彼此之间仍有着因果关系。

本文首要聚集 GolangRESTful API 中的最佳实践,涵盖了相关的开发规范和技能关键。关于前端栈的常识,本文仅作扼要介绍。相信阅读完本文的你,将有所收获,能够更深入地了解 RESTful APIWeb 开发中的完结和运用。

值得思考:为什么是 RESTful,怎样用 RESTful 以及怎样用好 RESTful

  1. WEB 开展阶段进程
  2. 前后端别离运用架构
  3. HTTP 根底概念
  4. RESTful 接口规范
  5. API 呼应实体格局
  6. 前后端协作规范
  7. 通用的 API 呼应实体
  8. 自界说服务端过错码
  9. 一致 JSON 的呼应格局

WEB 开展阶段进程

Part 1: 了解 Web 开展进程能够为学习和了解 RESTful API 供给更深入的布景和上下文,然后更好地运用和实践这种依据 HTTP 的架构风格。

前端栈的首要时刻线

时期 年份 描绘
原生JavaScript时期 前期 – 2006年 jQuery 呈现之前,运用 JavaScript 进行 DOM 操作非常繁琐,需求编写许多的冗长代码。为了简化 JavaScript 开发,各种库和结构相继呈现。它们的呈现为 jQuery 的诞生奠定了根底。
jQuery和Bootstrap时期 2006年 – 2013年 jQuery2006 年初次发布,它大大简化了 DOM 操作和事情处理,成为 Web 开发的规范库之一。Bootstrap2011 年初次发布,它供给了一套易用的 UI 组件和呼应式布局,为前端开发供给了很大的便当。
AngularJS时期 2010年 – 2016年 AngularJS2010 年初次发布,它引进了 MVVM 的概念,供给了一套完好的结构来支撑前端开发。AngularJS 在开发企业级运用方面有很大的优势,但也面临着功用和杂乱度的问题。
React和Vue时期 2013年 – 至今 React2013 年初次发布,它供给了一套声明式的 UI 编程办法,能够协助开发人员愈加高效地构建杂乱的 UI 界面。Vue2014 年初次发布,它供给了一套易用的 API 和生态体系,成为了一个快速、轻量级的前端结构。ReactVue 都成为了现在最盛行的前端结构之一。
Node.js时期 2009年 – 至今 Node.js 是依据 Chrome V8 引擎开发的 JavaScript 运转环境,它能够在服务器端运转 JavaScript 代码,供给了非阻塞 IO 和事情驱动的编程模型,能够高效地处理许多并发恳求。Node.js 也逐步成为了 Web 开发的干流技能之一。
TypeScript时期 2012年 – 至今 TypeScript 是由 Microsoft 开发的一种超集 JavaScript 的言语,它供给了愈加强大的类型检查和面向目标编程的特性,能够进步代码的可保护性和可扩展性。TypeScript 也逐步成为了前端开发的干流言语之一。
gantt
title Web 前端开展时刻轴
dateFormat  YYYY-MM
section 里程碑 (Web1.0 - Web2.0 - Web3.0 ...)
HTML1.0超文本标记言语: t0, 1993-06, 30d
W3C创建: t1, 1994-10, 30d
网景推出第一款浏览器: t2, 1994-12, 30d
CSS发布: t3, 1996-12, 30d
JavaScript诞生: t4, 1995-12, 30d
XHTML1.0修订: t5, 2000-01, 30d
Ajax呈现: t7, 2005-02, 30d
CSS3规范: t8, 2005-12, 30d
jQuery发布: t9, 2006-08, 30d
V8引擎问世: t10, 2008-09, 30d
Node发布: t11, 2009-05, 30d
ES5规范发布: t12, 2009-12, 30d
NPM东西: t12, 2010-01, 30d
TypeScript发布: t13, 2012-10, 30d
Angular诞生: t14, 2010-10, 30d
Webpack东西: t15, 2012-03, 30d
ESLint项目: t16, 2013-07, 30d
React诞生: t17, 2013-05, 30d
Vue诞生: t18, 2014-02, 30d
Babel项目: t19, 2014-09, 30d
HTML5.0规范: t6, 2014-10, 30d
Electron项目: t20, 2015-01, 30d
VSCode软件: t21, 2015-04, 30d
ES6发布(ES2015): t22, 2015-06, 30d
Yarn东西: t23, 2016-10, 30d
WebAssembly规范: t24, 2017-03, 30d
Vite东西: t25, 2020-12, 30d
ES2022(ES-Next): t26, 2021-06, 30d

WEB 1.0 静态 WEB 年代

  1. 静态 Web 年代:前期的 Web 运用,首要是静态网页的展现和信息传递,没有动态交互和数据交互。这个时期的 Web 运用基本上都是由后端来烘托生成 HTML 静态页面,前端的责任相对较少。
  2. 动态 Web 年代:跟着后端和网站的开展,开端选用后端技能(如 CGIASPJSP 等)来生成动态内容,完结了网页的动态交互和数据交互。虽然 ASP 等技能能够生成动态的 HTML 页面,但这并没有改动 Web 1.0 年代的中心特色。
前端被戏称为 "切图仔" 的时期首要是在 2000 年代初期到中期,那时分前端的首要作业是将规划师供给的 PSD 文件转化为 HTML 和 CSS 代码,并进行浏览器的兼容性测验和调试。这个时期前端技能相对简略,开发人员的作业首要是对已有技能进行运用和调试。
跟着 Web 运用的杂乱化和前端技能的不断开展,Angular 和 React 等前端结构的呈现和普及,使得前端工程师开端拥有更多的技能和东西来进行 Web 运用的开发和保护,也让前端技能逐步从单纯的切图转化为一个独立的范畴。

Go Web 编程在 RESTful API 中的应用和最佳实践

WEB 2.0 交际网络年代

  1. 前后端不别离时期:
  • 前端首要担任烘托页面和简略交互
  • 后端首要担任事务逻辑和数据处理
  • 依赖于服务器端烘托,运用少数 JSCSS 提升用户体会
  • Ajax 技能呈现(Web 2.0 的一个重要特征),但前后端代码仍混合,保护困难
  1. 前后端别离时期:
  • 前后端技能独立开发、布置、保护
  • 前端首要担任页面烘托和用户交互
  • 后端首要担任供给 RESTful API 接口和数据逻辑
  • 前后端经过 HTTP 恳求和呼应通讯
  • 前端结构盛行,AngularReactVue 协助快速构建杂乱 Web 运用
Web 2.0 的开展离不开 Web 技能的普及,Ajax 技能的呈现,移动设备的普及,前端结构的呈现,Node.js 的呈现,以及 Web 规范 W3C 组织的推行。这些因素一起促进了 Web 2.0 的开展,为 Web 运用程序的开发供给了更多的技能手段和支撑。

Go Web 编程在 RESTful API 中的应用和最佳实践

前后端别离运用架构

Part 2: 前后端别离是 WEB 2.0运用架构的一种趋势,其首要原因是为了更好地完结前后端的责任别离和事务解耦,然后进步 Web 运用的可保护性、可扩展性和可测验性。

前后端别离的意义

前后端别离带来的优势包括:

  1. 拆分关注点:前后端别离能够让前端开发人员专注于 UI 和用户体会,而后端开发人员专注于事务逻辑和数据处理。这种拆分能够使得开发人员愈加专注于自己的范畴,进步了开发功率和质量。
  2. 松耦合:前后端别离能够下降前端和后端之间的耦合度,使得体系愈加灵敏和可扩展。在前后端别离的架构中,前端和后端之间经过 API 进行交互,API 的格局是明晰的,两边的责任明晰明晰,能够防止不必要的交流和重复的作业。
  3. 前端技能开展:前端技能的不断开展使得前端能够承当更多的作业,比方模板烘托、数据处理、路由管理等等,这些作业原本是后端的责任,可是现在能够经过前端结构和东西来完结。
  4. 进步功用:前后端别离能够进步 Web 运用的功用,因为前端能够缓存数据、预加载资源,减少了对后端的恳求次数,一起前端也能够经过一些技能来进步用户体会,比方 PWA(Progressive Web App)

前后端别离面临的首要应战包括:

  1. 前后端开发人员需求具有不同的技能和常识体系,关于中小型公司来说,或许需求投入更多的人力物力去完结前后端别离的开发。
  2. 前后端别离带来了前后端通讯的问题,需求选用特定的技能协议和数据格局,例如 RESTful APIJSON 数据格局。
  3. 前后端别离会导致页面烘托功用的下降,需求选用一些优化措施,例如异步加载和缓存机制。

Go Web 编程在 RESTful API 中的应用和最佳实践

在这样的布景下,跟着前后端别离的趋势,以 AngularReactVue 为代表的前端结构得到了广泛运用,为 Web 2.0 飞速开展开启了新的年代。这些前端结构和东西的呈现,使得前端开发人员能够承当更多的作业,然后进步了开发功率和质量,一起也带来了新的应战和问题。

接口界说责任

前后端交互接口:

通俗意义上的这类接口一般是由后端工程师界说的,因为接口是由后端服务器供给的数据和功用。后端工程师需求规划和完结 API(运用程序编程接口),这些 API 界说了客户端能够调用的恳求和呼应。

前端工程师能够运用这些 API 来开发前端运用程序,经过调用后端供给的接口来获取数据并与后端交互。

在一些情况下,前端工程师也或许会参与 API 规划的过程中,以确保 API 满意前端运用程序的需求。

OpenAPI 接口:

Open API 接口能够由上游或下流界说,详细取决于 API 的规划和完结。

假如 Open API 接口是由上游体系(如数据源或服务供给商)供给的,那么上游体系将界说和完结 API,并将其发布给下流体系(如运用程序或客户端)。在这种情况下,下流体系需求遵从上游体系界说的 API 规范,以便与上游体系进行交互并获取所需的数据或服务。

另一方面,假如 Open API 接口是由下流体系界说的,那么下流体系将界说和完结 API,并将其发布给上游体系。在这种情况下,上游体系需求遵从下流体系界说的 API 规范,以便为下流体系供给所需的数据或服务。

不管是上游仍是下流界说 Open API 接口,都需求确保 API 的规范明晰、一致和易于运用,以便各方能够顺利进行交互和集成。

提示: 下文如无特别阐明,“接口” 一词一概代指前后端交互的 RESTful API 接口。

前后端协作开发

  1. 前后端一般会经过异步接口 (AJAX/JSONP) 来进行编程开发
  2. 前后端都各自有自己的开发流程,构建东西和测验集合
  3. 关注点别离,前后端变得相对独立并松耦合
后端 前端
编程言语 JavaC#PythonPHPNodeJs HTMLCSSJavaScript
WEB结构 Spring BootASP.NETDjangoLaravelExpress.js AngularJSReactVue.js
首要责任 数据存储、逻辑处理、接口供给等使命,聚集于各类后端运用组件、上下流数据打通,数据安全、持久化、功用提升等 展现用户界面和处理用户交互,轻量的数据逻辑处理,聚集于界面交互,烘托逻辑等
服务目标 呼应大前端(浏览器、APP、小程序、桌面端) 呼运用户
软件架构办法 服务端 MVC 架构 (Model-View-Controller) 客户端 MV* 架构,MVVM(Model-View-ViewModel)MVP(Model-View-Presenter)
运转环境 后端代码运转在服务器上,经过网络接口向客户端(如浏览器)供给数据和服务 前端代码运转在客户端浏览器中,能够向服务器主张恳求获取数据或服务,并将其展现给用户

当用户在浏览器中拜访 Web 运用程序时,浏览器将向服务器主张恳求,服务器经过后端代码处理恳求并回来数据或服务,浏览器接收并解析呼应数据,并运用前端代码烘托用户界面。整个过程中,后端代码和前端代码别离在服务器端和浏览器端运转,一起构成了一个完好的 Web 运用程序。

Go Web 编程在 RESTful API 中的应用和最佳实践

HTTP 根底概念

Part 3: 在正式介绍 RESTful 之前,快速补充下 HTTP 协议的相关概念,它是一种依据 TCP/IP 通讯协议的运用层协议,用于传递超文本到本地浏览器,并包括协议、头信息、状况码和恳求办法等内容。

HTTP Protocol

  • HTTP 协议特色
特色 描绘
无衔接 每次恳求都需求与服务器树立一个衔接,恳求结束后,衔接就会关闭。这种衔接的特性使得 HTTP 不能支撑客户端和服务器之间的长衔接,因而每次恳求都会产生较大的开销,这也是 HTTP 的一个缺点。
无状况 每个恳求都是相互独立的,服务器不会在不同恳求之间保存任何状况信息。这意味着服务器无法知道客户端之前的操作,因而不能主动适应客户端的需求。
依据恳求和呼应 HTTP 协议是依据恳求和呼应的协议。客户端向服务器发送恳求,服务器则会向客户端发送呼应。这种办法简略明晰,使得 HTTP 易于完结和运用。
可扩展性 HTTP 的恳求和呼应音讯是由 HTTP 头和正文组成的,头部信息包括了恳求和呼应的元数据,正文部分包括了实践的数据。这种结构使得 HTTP 协议能够很容易地被扩展,添加新的头部字段和正文格局。
支撑多媒体 HTTP支撑多种类型的数据,如 HTMLCSSJavaScript、图片、视频等。
明文传输 HTTP 是明文传输的,即数据在传输过程中不会被加密,因而存在安全隐患。
  • HTTP 作业办法
协议版别 作业办法 描绘
http1.0 单工 因为是短衔接,客户端主张恳求后,服务端处理完恳求并收到客户端的呼应后,即断开衔接。
http1.1 半双工 选用的是恳求/呼应模型,在这种办法下,客户端和服务器之间只能单向通讯,即在同一时刻内只能有一方向另一方发送音讯。HTTP/1.1 默许开启长衔接 keep-alive,一个衔接能够发送多个恳求和呼应,然后减少了树立衔接和断开衔接的开销,进步了功用。关于 HTTP/1.1,恳求头中应该包括 Host 字段。
http2.0 全双工 答应客户端和服务器在同一时刻内进行双向通讯。在 HTTP/2 中,通讯两边都能够发送音讯,并且能够一起发送多个音讯,这些音讯被分为帧并经过一个同享的衔接发送,这种办法比 HTTP/1.x 中的屡次恳求和呼应更高效。此外,HTTP/2 还支撑多路复用,即能够经过一个衔接并发处理多个恳求和呼应,然后减少了延迟和资源占用,进步了功用。关于 HTTP/2.0,恳求头中则应该包括 :authority 字段。

提示:HTTP/1.1 规范中规矩,假如客户端发送的恳求报文中没有包括 Host 头字段,那么服务器应该回来一个 400 Bad Request 的呼应码。这是因为在 HTTP/1.1 中,引进了虚拟主机的概念,同一个 IP 地址下能够托管多个域名的网站。因而,服务器需求依据 Host 头字段的值来确认客户端恳求的是哪个网站的资源,然后供给正确的呼应。

HTTP Headers

  • HTTP 恳求头

客户端经过恳求头会传递给服务器遵从 HTTP 协议的信息,并依照该协议内容进行 URL 解码。

称号 阐明 示例
Accept 表明浏览器可接受的 MIME 类型 Accept: text/html
Accept-Charset 表明浏览器可接受的字符集 Accept-Charset: utf-8, iso-8859-1;q=0.5
Accept-Encoding 表明浏览器能够进行解码的数据编码办法 Accept-Encoding: gzip, compress, br
Accept-Language 表明浏览器所期望的言语种类,当服务器能够供给一种以上的言语版别时要用到 Accept-Language: en-US,en;q=0.5
Authorization 表明授权信息,一般呈现在对服务器发送的 WWW-Authenticate 头的应对中 Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
Connection 表明浏览器恳求完后是断开链接仍是保持链接 Connection: keep-alive
Content-Length 表明恳求音讯正文的长度 Content-Length: 408
Cookie 表明 HTTP 恳求报头包括存储先前经过与所述服务器发送的 HTTP cookies Set-Cookie Cookie:PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;
From 恳求发送者的 email 地址,由一些特别的 Web 客户程序运用,浏览器不会用到它 From: webmaster@example.org
Host 初始要拜访的服务器 URL 地址,一般是域名,或主机和端口号 Host: developer.cdn.mozilla.net
Pragma 表明服务器有必要回来一个改写后的文档,即便它是署理服务器并且现已有了页面的本地拷贝 Pragma: no-cache
Referer 表明客户机是哪个页面来的(防盗链) Referer: https://developer.mozilla.org/en-US/docs/Web/JavaScript
User-Agent 表明用户署理恳求头包括一个特征串,其答应网络协议对等体,以确认恳求软件的用户署理的运用程序类型,操作体系,软件供应商或软件版别 Mozilla/5.0(X11; Linux x86_64) AppleWebKit/537.36(KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36
  • HTTP 呼应头

服务器经过呼应头会传递给客户端遵从 HTTP 协议的信息,并依照该协议内容进行动作。

称号 阐明 示例
Allow 表明服务器支撑哪些恳求办法(如GETPOST等) Allow:GET,POST,HEAD
Content-Encoding 表明文档的编码 Encode 办法,只要在解码之后才能够得到 Content-Type 头指定的内容类型 Content-Encoding: gzip
Content-Length 表明内容长度,只要当浏览器运用持久 HTTP 衔接时才需求这个数据 Content-Length: 549
Content-Type 表明后边的文档属于什么 MIME 类型 Content-Type: text/html; charset=utf-8
Date 表明当时的 GMT 时刻 Date: Wed,21 Oct 201507:28:00GMT
Expires 表明应该在什么时分认为文档现已过期,然后不再缓存它 Expires: Wed,21 Oct 201507:28:00GMT
Last-Modified 文档的最终改动时刻 Last-Modified: Wed,21 Oct 201507:28:00GMT
Location 表明客户应当到哪提取文档,Location 一般不是直接设置的,而是后端言语的Redirect 办法 Location:/index.html
Refresh 表明浏览器应该在多少秒之后改写文档 Refresh: 10; url=/index.html
  • HTTP 实体头

实体头用作实体内容的元信息,描绘了实体内容的特点,包括实体信息类型,长度,紧缩办法,最终一次修正时刻,数据有效性等。(可参阅呼应头)

Allow
Content-Encoding
Content-Language
Content-Length
Content-Location
Content-MD5
Content-Range
Content-Type
Expires
Last-Modified
  • HTTP 扩展头

HTTP 音讯中,也能够运用一些在 HTTP1.1 正式规范里没有界说的头字段,这些头字段统称为自界说的 HTTP 头或许扩展头,它们一般被当作是一种实体头处理。

现在盛行的浏览器实践上都支撑以下常用的扩展头字段

Content-Disposition
Content-Type
Cookie
Set-Cookie
Refresh

Request Method

  • HTTP1.0 界说了三种恳求办法:GETPOSTHEAD 办法。
  • HTTP1.1 新增了五种恳求办法:OPTIONSPUTDELETETRACECONNECT 办法。
办法 描绘
GET 恳求指定的页面信息,并回来实体主体。
HEAD 类似于 GET 恳求,只不过回来的呼应中没有详细的内容,用于获取报头。
POST 向指定资源提交数据进行处理恳求(例如提交表单或许上传文件)。数据被包括在恳求体中。POST 恳求或许会导致新的资源的树立和/或已有资源的修正。
PUT 从客户端向服务器传送的数据取代指定的文档的内容。
DELETE 恳求服务器删去指定的页面。
CONNECT HTTP/1.1协议中预留给能够将衔接改为管道办法的署理服务器。
OPTIONS 答应客户端检查服务器的功用。
TRACE 回显服务器收到的恳求,首要用于测验或诊断。

Status Code

  • 1xx 状况码:信息恳求收到,持续处理
  • 2xx 状况码:代表成功,行为被成功地接收、了解及采纳
  • 3xx 状况码:重定向,完结此恳求有必要进一步处理
  • 4xx 状况码:客户端过错,恳求包括语法过错或恳求无法完结
  • 5xx 状况码:服务端的内部过错
状况码 描绘 阐明
100 Continue 客户有必要持续发出恳求
101 Switching Protocols 客户要求服务器依据恳求转化 HTTP 协议版别
200 OK [GET] 服务器端成功回来用户恳求的数据
201 Created [POST/PUT/PATCH] 用户新建或修正数据成功
202 Accepted 该恳求现已进入后台排队(一般是异步使命)
204 No Content [DELETE] 用户删去数据成功
300 Multiple Choices 恳求的资源可在多处得到
302 Found 暂时重定向,服务器向客户端回来该状况码时,一般还会在呼应头中添加一个 Location 字段,告诉客户端需求重定向到哪个 URL 上去。浏览器会主动跳转到这个新的 URL
303 See Other 主张客户拜访其他 URL 页面
304 Not Modified 不需求重新传输恳求的资源,这是对缓存资源的隐式重定向
307 Temporary Redirect 该资源已暂时移动到由 Location 给定的 URL
308 Permanent Redirect 该资源已明晰永久移动到 Location 标题给定的 URL
400 Bad Request 用户发出的恳求有过错,服务器不了解客户端的恳求,未做任何处理
401 Unauthorized 表明用户没有权限(令牌、用户名、密码过错)
403 Forbidden 表明用户得到授权了,可是拜访被禁止了,不具有拜访资源的权限
404 Not Found 所恳求的资源不存在,或不可用
405 Method Not Allowed 用户现已经过了身份验证,但所用的 HTTP 办法不在它的权限之内
406 Not Acceptable 用户的恳求的格局不可得(如用户恳求的是 JSON 格局,可是只要 XML 格局)
410 Gone 用户恳求的资源被搬运或被删去,且不会再得到的
415 Unsupported Media Type 客户端要求的回来格局不支撑,如 API 只能回来 JSON 格局,可是客户端要求回来 XML 格局
422 Unprocessable Entity 客户端上传的附件无法处理,导致恳求失利
429 Too Many Requests 客户端的恳求次数超过限额
500 Internal Server Error 服务器遇到阻止它实行恳求的意外情况
502 Bad Gateway 服务器充当网关或署理时收到来自上游服务器的无效呼应
503 Service Unavailable 服务器当时没有准备优点理恳求
504 Gateway Timeout 服务器充当网关或署理时无法及时得到呼应
505 HTTP Version Not Supported 服务器不支撑恳求中运用的 HTTP 版别
511 Network Authentication Required 客户端需求进行身份验证才能获得网络拜访权限

提示: 事实上,这些 HTTP 状况码并不需求刻意去记住。在实践项目中,一般只会运用最根底的一些原生状况码,例如 200302404500 等。其他的状况码或许会经过自界说过错码来呈现,以更好地表达事务意义。举例来说,假如你正在开发一个教育体系,那么就会有许多事务过错与教育相关;假如是电商体系,则事务过错或许与付出、订单相关等。即便是原生状况码能够精确地描绘呼应信息,也不主张直接运用。例如,关于短少 token 的情况,一般会选用回来原生状况码 200 正常状况,在自界说事务过错码中判别,而不是直接运用 401 状况码。这样能够更好地让前端代码编写并显现,并防止不必要的影响。

RESTful 接口规范

Part 4: 当咱们了解了 HTTP 基本概念、Web 开展前史以及前后端别离架构之后,咱们会很快意识到前后端别离所带来的最大问题是数据传输。为了处理这个问题,咱们需求了解前端怎么获取数据以及后端怎么回来数据。这触及到一些概念,如 URIHTTP 动词、状况码和资源等。在接下来的内容中,咱们将对这些概念进行介绍。

RESTful 概念介绍

URIUniform Resource Identifier 的缩写,它是标识资源的仅有办法。在 RESTful API 中,咱们运用 URI 来定位资源,留意 URI 应该是名词,而不是动词。

恳求办法      版别       资源称号       资源ID        子资源称号        子资源ID
[GET]    /{version}/{resources}/{resource_id}/{sub_resources}/{sub_resource_id}

HTTP 动词指定对资源进行的操作。RESTful API 中常见的 HTTP 动词包括 GETPOSTPUTDELETE

Resource 是由 URI 标识的实体。在 RESTful API 中,咱们运用资源来表明运用程序中的任何实体,例如用户、文章、谈论等。

总归,RESTful API 是一种简略、明晰、易于了解的办法,使得不同的体系之间能够进行数据的交互。

RESTful 成熟度模型

  • Level 0:运用恳求呼应办法的根底架构。
  • Level 1:引进资源的概念,让每个资源能够独自创建一个 URI,并运用 Resources 分而治之的办法来处理杂乱事务逻辑。
  • Level 2:严格遵守 HTTP 协议界说的动词,运用 HTTP 呼应状况码来规范化 Web API 的规范。
  • Level 3:运用超媒体 (HATEOAS),使协议具有自我描绘的才能。

一般成熟度模型能够到达 Level 2 就现已足够规范了!

Go Web 编程在 RESTful API 中的应用和最佳实践

规范的 API 示例

下面是一个简易博客网站的 RESTful API,咱们能够运用以下 URI 来标识资源:

GET     /api/blogs                                 - 获取全部文章
GET     /api/blogs/:id                             - 获取指定 ID 的文章
POST    /api/blogs                                 - 创建一篇新文章
PUT     /api/blogs/:id                             - 更新指定 ID 的文章
DELETE  /api/blogs/:id                             - 删去指定 ID 的文章
POST    /api/blogs/:id/actions/like                - 文章顶一下
POST    /api/blogs/:id/actions/dislike             - 文章踩一下
GET     /api/blogs/:id/comments                    - 回来指定文章的谈论列表
GET     /api/blogs/:id/comments/:id                - 获取指定文章的单个谈论
POST    /api/blogs/:id/comments/:id                - 为指定文章创建一条谈论
DELETE  /api/blogs/:id/comments/:id                - 删去某条谈论及子谈论
POST    /api/blogs/:id/comments/:id/actions/reply  - 对某条谈论进行回复
POST    /api/blogs/:id/comments/:id/actions/top    - 神评置顶

提示: 咱们应该在遵从 RESTful API 的规范时,尽或许地运用规范的资源途径和 HTTP 办法来完结接口功用是比较好的实践。可是,有些情况下或许的确难以运用规范的 RESTful API 命名办法,例如一些杂乱的事务逻辑。这时分,能够考虑运用一些特别的 actions 来完结接口功用,但需求留意尽量防止乱用 actions,确保接口的可保护性和可扩展性。

RESTful 恳求格局规范

以下都是符合 RESTful API 规范的运用细节:

关注点 示例 阐明
恳求参数中多单词运用蛇形 /api/blogs?page_size=20&page_num=5 能够让 URI 愈加可读且易于了解
恳求途径中多单词运用中横线 /api/blogs/my-personal-center 当运用中横线来分隔单词时,能够协助搜索引擎更好地了解你的网站结构,使其对 SEO 愈加友爱。因为搜索引擎能够将中横线解释为单词的分隔符,这样能够让搜索引擎更好地了解你的网站结构和内容,进步网站在搜索引擎成果页中的排名。
恳求体 或 呼应体 { "username": "admin", "password": "12345", "remember_me": 1 } 全部 JSON 数据的 key 一概运用蛇形命名,使得数据愈加易于了解,也便利进行数据处理。
恳求头 key { "Token": "xxxxxx", "X-Cookie": "xxxxxxxxx" } 运用首字母大写,多单词每个首写字母大写并用中横线衔接,这样能够使得恳求头愈加规范化,易于了解和保护。

提示: 除了恳求头和恳求途径主张运用中横线,最为常用的恳求参数、恳求体(呼应体)都应该选用蛇形命名法(snake_case)来表明,例如 user_idfirst_name 等。一个友爱的 API 传输的数据不主张运用静态言语中的驼峰命名习惯。

API 呼应实体格局

Part 5: 上面咱们介绍了 HTTP 协议及 RESTful API 的规划风格。接下来,咱们将重点解说在 API 开发中最常用的 application/json 实体类型的恳求和呼应。在本章节中,咱们暂不触及其它 MIME 媒体文件类型,和其它接口通讯技能,如 WebSocketsSSEGraphQLgRPC 等。

HTTP 状况码 [200]

HTTP 协议规矩的状况码 (100 - 600) 无法完全描绘事务过错信息。因而,为了满意事务需求,咱们在 HTTP 呼应中界说了自界说回来码。这些自界说回来码的编号一般大于 100010000。咱们主张不要将这些自界说回来码与 HTTP 状况码混淆。

在实践运用中,咱们需求依据详细事务场景来挑选适宜的 HTTP 状况码和自界说回来码,以便能够精确地反映出恳求的处理状况。请留意,关于事务过错而言,直接回来 400500 是不适宜的,咱们应该挑选愈加精准的自界说回来码来描绘事务过错信息。

下面记录了事务成功和失利的情况以及呼应异常的结构。考虑到不同事务的差异性,只列出了最常见的几个字段,依据实践情况能够进行调整,例如添加 occurrence_time 记录出错时刻,便利快速定位问题等。

事务正常

  • 呼应实体为空️
HTTP/1.1 200 OK
Content-Type: application/json
{
    "code": 0,
    "msg": "success",
    "data": null
}
  • 呼应实体格局
HTTP/1.1 200 OK
Content-Type: application/json
{
    "code": 0,
    "msg": "success",
    "data": {
        "name": "Pokeya",
        "age": 30,
        "male": true,
        "job": "developer",
        "tech_stack": "backend"
    }
}
  • 呼应列表格局
HTTP/1.1 200 OK
Content-Type: application/json
{
    "code": 0,
    "msg": "success",
    "data": {
        list: [
            {
                "id": 1,
                "name": "Pokeya",
                "age": 30,
                "male": true,
                "job": "developer",
                "tech_stack": "backend"
            },
            {
                "id": 2,
                "name": "King",
                "age": 31,
                "male": true,
                "job": "dba",
                "tech_stack": "database"
            },
            ...
        ]
    }
}
  • 呼应分页格局
HTTP/1.1 200 OK
Content-Type: application/json
{
    "code": 0,
    "msg": "success",
    "data": {
        page_info: {
            "page_num": 5,      // 第5页
            "page_size": 20,    // 每页20条
            "total_count": 281, // 共281条
            "total_pages": 15   // 共15页
        },
        items: [
            {
                "id": 1,
                "name": "Pokeya",
                "age": 30,
                "male": true,
                "job": "developer",
                "tech_stack": "backend"
            },
            {
                "id": 2,
                "name": "King",
                "age": 31,
                "male": true,
                "job": "dba",
                "tech_stack": "database"
            },
            ...
        ]
    }
}
  • 特别内容规范
    • 前端的静态初始值,如下拉框、复选框、单选框(一致收口到后端,前端只做烘托,尽量防止事务逻辑处理)。
    • Boolean 类型在 JSON 数据传输的界说办法,运用 1/0 代替 true/false
    • Date 类型在 JSON 数据传输的界说办法,一概运用字符串类型,详细日期格局因事务而定。

事务失利

  • 规范描绘
HTTP/1.1 200 OK
Content-Type: application/json
{
    "code": 10004,
    "msg": "invalid request parameter"
}
  • 详细描绘
HTTP/1.1 200 OK
Content-Type: application/json
{
    "code": 10004,                           // 某类过错的过错码
    "msg": "invalid request parameter",      // 过错的摘要信息,便于快速定位
    "detail": "ip_address field not found"   // 记录过错的详细过错信息及原因
}

HTTP 状况码 [302]

  • 后端的 endpoint 视图函数给前端 302 路由重定向的指令
HTTP/1.1 302 Found
Location: "https://www.example.com/new-location"

HTTP 状况码 [404]

  • 无效的路由恳求
HTTP/1.1 404 Not Found
Content-Type: application/json
{
    "code": 10001,
    "msg": "invalid URL path"
}

HTTP 状况码 [500]

  • 服务端产生严峻的 panic/fatal 级别的过错
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
    "code": 50002,
    "msg": "failed",
    "detail": "unknown internal error"
}

前后端协作规范

Part 6: 主张在 API 规划中挑选一种命名办法并保持一致性,一起在前端中也要一致选用相同的命名办法进行解析,能够凭借中间件或东西来转化命名办法,以确保数据传递的正确性。不管运用哪种办法,都需求在前后端之间树立明晰的命名规范。

呼应命名规范抵触

  • Pythondict / Gostruct / APIJSON / JavaScriptObject
阶段 言语/规范代表 key 键命名主张 阐明
后端(动态言语) Python dict 蛇形 python dictkeyAPI 命名规范一致,完美适配,无需任何调整
后端(静态言语) Go struct 大驼峰 如需导出 JSON 数据,将 struct 的字段名运用大驼峰(揭露),别的设置 json tag 为蛇形,完美适配解析
HTTP/1.1 传输 RESTful API json 蛇形 RESTful 规范并没有强制规矩 JSON 数据中的 key 命名办法,可是主张运用小写字母加下划线的蛇形命名办法,这样能够添加可读性和易于了解,也有利于与其他开发者协作开发。
前端 Web 浏览器 JavaScript Object 小驼峰 一般在 JavaScript 中,目标的键名主张运用小驼峰命名法,也便是第一个单词的首字母小写,后边的单词首字母大写并去掉空格和符号。这是 JavaScript 社区的一个约好俗成的规范,能够进步代码的可读性和可保护性。

约好呼应命名规范

咱们不难看出,后端到 HTTP 能够完美适配 RESTful API 的规范,但假如这样的话,前端需求解析蛇形的特点,咱们知道不管 JS 仍是 TS 社区都推重目标特点运用小驼峰命名法,那怎么解析呢?两种计划:

  1. 因为 RESTful API 仅仅作为主张,为了让前端更好的解析,咱们后端能够恰当牺牲些代码规范,将全部 JSON key 换为 小驼峰,运用 HTTP 发送的 RESTful API JSON 数据,这样前端 JS/TS 就能够完美的解析了。这是前后端协作,最便利快捷地处理办法。

  2. 假如后端不愿意退让,后端便是用规范的蛇形来回来 API 数据。那么前端还剩下两个办法。

    2-1. 舍弃前端的小驼峰命名规范,经过在特点名上运用反引号 (backtick) 来指定特点名,然后处理特点命名规矩不一致的问题,只不过用反引号办法对前端的 Object 操作的确不太友爱。别的,假如你的前端工程启用了 ESLint 并且装备了 camelcase 规矩,它会警告你运用了蛇形命名的键。你能够经过在 ESLint 装备中禁用 camelcase 规矩来处理这个问题,或许在相关代码上方添加注释来疏忽特定的 ESLint 规矩。这样 ESLint 将会疏忽这行代码的 camelcase 规矩检查。总归,这种办法了解即可,不推荐!!!

    // eslint-disable-next-line camelcase
    console.log(data[`page_info`]);
    

    2-2. 当后端不愿退让在 API 中传递小驼峰的 Key 特点,前端也不退让在接收解析 API 值的时分运用蛇形,那么,只能凭借中间件来完结命名的转化,以确保数据传递的正确性。在前端 axios.interceptors.response 拦截呼应并进行 “蛇形 -> 小驼峰” 的转化。这样就完美处理了,属功用够直接 “目标.特点” 操作出来了。

    console.log(data.pageInfo);
    

    可是需求奉告的是,因为运用递归去处理多层级 JSON,“格局转化函数” 会添加前端浏览器额外开销负担。并且写在 axios.interceptors 中,意味着不管 API 接口是否适配 JS 的小驼峰,都会验证这个转化函数,这无疑是对资源的一种糟蹋,功用必定也是会受损的。咱们要权衡运用该办法的利弊。

约好恳求命名规范

上面聊完了有关呼应的命名差异性,恳求其实也是一样的,假如前后端都不愿退让,前端代码中执意传入驼峰的 Key,相同主张在前端 axios.interceptors.request 恳求拦截器中添加格局转化 “小驼峰 -> 蛇形”,对恳求参数和恳求体进行格局转化。这样也完美的处理既不会影响前端代码的规范,又不影响 RESTful API 和 后端的规范。

当然假如前端不做任何处理,那么 RESTful API 中的 JSON key 就只能是驼峰的了,此时,皮球就扔给了后端,相同后端也能够写中间件来处理,其实没有必要,假如运用 Go 言语,直接改动 json tag 为驼峰就能够解析数据了,写法仍是很灵敏的。

一个完好的示例

后端准备数据,呼应给前端:

type PageInfo struct {
    PageNum    int `json:"page_num"`
    PageSize   int `json:"page_size"`
    TotalCount int `json:"total_count"`
    TotalPages int `json:"total_pages"`
}
func main() {
    r := gin.Default()
    r.GET("/list", func(c *gin.Context) {
        p := PageInfo{
            PageNum: 5,
            PageSize: 20,
            TotalCount: 281,
            TotalPages:15,
        }
        c.JSON(200, &p)
    })
    _ = r.Run()
}

比方,后端传递规范的 RESTful API 格局数据,呼应体如下:

HTTP/1.1 200 OK
Content-Type: application/json
{
    "page_info": {
        "page_num": 5,
        "page_size": 20,
        "total_count": 281,
        "total_pages": 15
    }
}

前端进行解析处理,在拦截器中添加转化函数:

import Case from 'case';
// 蛇形转驼峰函数,递归处理json数据
export const convertSnakeToCamel = (data: Record<string, any>): any => {
  if (typeof data !== 'object' || data === null) {
    return data;
  }
  if (Array.isArray(data)) {
    return data.map((item) => convertSnakeToCamel(item));
  }
  const camelCaseData: Record<string, any> = {};
  Object.keys(data).forEach((key) => {
    const camelCaseKey = Case.camel(key);
    camelCaseData[camelCaseKey] = convertSnakeToCamel(data[key]);
  });
  return camelCaseData;
};

为了演示便利,前端事务代码也都直接写在拦截器中了:

import axios from 'axios';
import { convertSnakeToCamel } from '@/utils/case';
export interface Pagination {
  pageNum: number;
  pageSize: number;
  totalCount: number;
  totalPages: number;
}
axios.interceptors.response.use(
  (response: AxiosResponse<Pagination>) => {
    // 转化前(副作用是或许引发eslint报错,代码不美观)
    console.log(response[`page_num`]);
    console.log(response[`page_size`]);
    console.log(response[`total_count`]);
    console.log(response[`total_pages`]);
    // 格局转化
    const res = convertSnakeToCamel(response);
    // 转化后(驼峰完美适配前端命名规范)
    console.log(res.pageNum);
    console.log(res.pageSize);
    console.log(res.totalCount);
    console.log(res.totalPages);
    return res;
  },
  (error) => {
    return Promise.reject(error);
  }
);

通用的 API 呼应实体

Part 7: 当咱们在编写运用程序时,一般需求界说一些通用的数据结构和类型,这些通用的数据结构和类型或许被多个模块和文件运用。为了进步可读性和可保护性,咱们一般会将这些通用的数据结构和类型独自界说在一个包或目录中,以便于管理和复用。

后端 – Go 界说泛型结构体

  • 关于后端而言,咱们能够将通用的呼应实体界说在一个 entity 包中,这样不仅能够减少代码冗余,还能够使代码愈加明晰和易于保护
package entity
type Response[T any] struct {
   Code int    `json:"code"`
   Msg  string `json:"msg"`
   Data T      `json:"data"`
}
type PageInfo struct {
   PageNum    int `json:"page_num"`
   PageSize   int `json:"page_size"`
   TotalCount int `json:"total_count"`
   TotalPages int `json:"total_pages"`
}
type PaginationEntity[T any] struct {
   PageInfo PageInfo `json:"page_info"`
   Items    []T      `json:"items"`
}
type ListEntity[T any] struct {
   List []T `json:"list"`
}
  • 编写一个简略的接口,供前端恳求调用
package main
import (
    "net/http"
    . "your-project/common/entity"
    "github.com/gin-gonic/gin"
)
type Users struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Age       int    `json:"age"`
    Male      bool   `json:"male"`
    Job       string `json:"job"`
    TechStack string `json:"tech_stack"`
}
func main() {
    router := gin.Default()
    router.GET("/api/users", func(c *gin.Context) {
        resp := Response[PaginationEntity[Users]]{
            Code: 0,
            Msg:  "success",
            Data: PaginationEntity[Users]{
                PageInfo: PageInfo{
                    PageNum:    5,
                    PageSize:   20,
                    TotalCount: 281,
                    TotalPages: 15,
                },
                Items: []Users{
                    {
                        ID:        1,
                        Name:      "Pokeya",
                        Age:       30,
                        Male:      true,
                        Job:       "developer",
                        TechStack: "backend",
                    },
                    {
                        ID:        2,
                        Name:      "King",
                        Age:       31,
                        Male:      true,
                        Job:       "dba",
                        TechStack: "database",
                    },
                },
            },
        }
        c.JSON(http.StatusOK, resp)
    })
    router.Run(":8080")
}

HTTP – JSON 数据

  • API 内容输出
HTTP/1.1 200 OK
Content-Type: application/json
{
  "code": 0,
  "msg": "success",
  "data": {
    "page_info": {
      "page_num": 5,
      "page_size": 20,
      "total_count": 281,
      "total_pages": 15
    },
    "items": [
      {
        "id": 1,
        "name": "Pokeya",
        "age": 30,
        "male": true,
        "job": "developer",
        "tech_stack": "backend"
      },
      {
        "id": 2,
        "name": "King",
        "age": 31,
        "male": true,
        "job": "dba",
        "tech_stack": "database"
      }
    ]
  }
}

前端 – TS 界说泛型接口

  • 关于前端而言,咱们能够将通用的呼应 interface 类型界说在一个 types 目录中,这样能够使代码愈加可读性高,易于保护和扩展。
export interface PageInfo {
  pageNum: number;
  pageSize: number;
  totalCount: number;
  totalPages: number;
}
export interface PaginationEntity<T> {
  pageInfo: PageInfo;
  items: T[];
}
export interface Response<T> {
  msg: string;
  code: number;
  data: T;
}
  • 前端恳求后端的接口,并解析呼应内容
import axios from 'axios';
import type { AxiosResponse } from 'axios';
import type { PaginationEntity, Response } from '@/types/http';
export interface User {
  id: number;
  name: string;
  age: number;
  male: boolean;
  job: string;
  techStack: string;
}
export const getUsers = async (): Promise<Response<PaginationEntity<User>>> => {
  const res = await axios.get<Response<PaginationEntity<User>>>('/api/users');
  // 留意这儿界说的都是ts规范的小驼峰(解析时应留意接口键名格局问题)
  return res.data;
};

自界说服务端过错码

Part 8: 在实践开发中,咱们需求依据详细情况来挑选运用原生的 HTTP 状况码或自界说过错码来描绘呼应信息。运用 HTTP 规范的状况码能够更好地遵从 RESTful API 的规范,可是在某些情况下,经过自界说过错码能够更好地让前端代码处理和显现过错信息。一起,关于客户端来说,过多的过错细节和自界说过错码只会让 API 处理愈加杂乱,难以了解。因而,咱们需求依据实践情况来决议怎么取舍,但全体来说,咱们应该遵从下降 API 处理杂乱度的准则。

怎么编写规范的过错码文档

  • 规范的通用过错码应该包括以下几个要素:
  1. 过错码:一个仅有标识该过错的数字或字符串代码。
  2. 阐明摘要:扼要阐明过错的意义,便利开发人员快速定位问题。
  3. 排查主张:关于该过错的或许原因和常见处理计划的描绘和主张,有助于开发人员快速处理问题。
  • 一般,过错码应该按模块区分,不同模块的过错码前缀应该不同,例如数据库模块的过错码能够以 "DB" 最初,网络模块的过错码能够以 "NET" 最初等。一起,应该规矩过错码的取值规模和分配机制,防止重复和抵触。

  • 可参阅: 飞书敞开平台、 抖音敞开平台、 微信敞开平台

供给 errox 包规划思路

在自界说过错处理时,有两种常见的思路。

第一种思路是不管过错的详细情况怎么,都运用 HTTP 状况码 200,而将更详细的过错信息运用自界说过错码来表明。这种办法的优点在于不会因为过错的详细情况而引发 HTTP 状况码的改变,可是需求界说更多的自界说过错码以表明不同的过错情况。可是,这种办法或许会导致公司内不同体系之间的一致性问题,因为不同平台或许会有不同的规划码。

另一种思路是为每种过错情况都界说一个过错码,并将其映射到一个相应的 HTTP 状况码上。这种办法的理念更接近于运用原生 HTTP 状况码,但需求额外的作业来界说和保护自界说过错码。

其中挑选哪种办法需求依据详细情况来考虑。全体来说,咱们应该遵从下降 API 处理杂乱度为准则进行规划。

规划一个极简的 errorx 包

errorx 是一个轻量级的过错处理库,它供给了一种简略的办法来界说过错码常量,并能够依据恳求的言语环境回来相应的过错信息。这个包易于引进和运用,能够简化过错处理的流程,进步代码的可保护性和可读性。可是,关于企业和大型项目来说,这样的界说或许过于根底和简略,需求愈加杂乱和全面的处理计划来满意其需求。

目录树

common
└── errorx           // errorx 过错包
  ├── code.go      // 界说过错码常量
  ├── locale.go    // 用于判别恳求的言语环境,支撑en-US/zh-CN
  └── message.go   // HashMap相应的中/英文过错的摘要描绘信息

code.go 文件

package errorx
const (
    CodeSuccess = 0
    CodeError   = 50000 // 500
)
/*
提示码(非过错)
*/
const (
    CodeConsultFailedEncoding = 1001
    CodeConsultFailedLanguage = 1002
)
const (
    CodePermanentRedirect = 3001 // 301
    CodeTemporaryRedirect = 3002 // 302
)
/*
过错码
*/
const (
    CodeRequestInvalidUrlPath = 10001 // 404
    CodeRequestInvalidMethod  = 10002
    CodeRequestInvalidParam   = 10003
    CodeRequestInvalidQuery   = 10004
    CodeRequestInvalidBody    = 10005
    CodeRequestInvalidForm    = 10006
    CodeRequestInvalidHeader  = 10007
)
const (
    CodeAuthTokenIllegal = 20001
    CodeAuthTokenExpired = 20002
)
const (
    CodeModelEntryDuplication = 30001
    CodeModelEntryNotExist    = 30002
)
const (
    CodeServiceFatal = 40001
)
const (
    CodeBusinessPanicError   = 50001
    CodeUnknownInternalError = 50002
)

locale.go 文件

package errorx
import (
    "strings"
    "github.com/gin-gonic/gin"
)
type lang uint8
const (
    en lang = iota + 1
    zh
)
func (l lang) String() string {
    switch l {
    case en:
        return "en"
    case zh:
        return "zh"
    default:
        return "en"
    }
}
// getPreferredLanguage returns the preferred language of the client, 
// based on the "Accept-Language" header.
func getPreferredLanguage(c *gin.Context) lang {
    locale := c.GetHeader("Accept-Language")
    if strings.Contains(locale, string(en)) || locale == "" || locale == "*" {
        return en
    }
    if strings.Contains(locale, string(zh)) {
        return zh
    }
    return en
}

message.go 文件

package errorx
import (
    "github.com/gin-gonic/gin"
)
var (
    enUSText = map[int]string{
        CodeSuccess: "success",
        CodeError:   "failed",
        CodeConsultFailedEncoding: "invalid encoding",
        CodeConsultFailedLanguage: "invalid language",
        CodePermanentRedirect: "permanent redirect",
        CodeTemporaryRedirect: "temporary redirect",
        CodeRequestInvalidUrlPath: "invalid URL path",
        CodeRequestInvalidMethod:  "invalid HTTP method",
        CodeRequestInvalidParam:   "invalid dynamic route parameter",
        CodeRequestInvalidQuery:   "invalid request parameter",
        CodeRequestInvalidBody:    "invalid request body",
        CodeRequestInvalidForm:    "invalid form data",
        CodeRequestInvalidHeader:  "invalid request header",
        CodeAuthTokenIllegal: "invalid authentication token",
        CodeAuthTokenExpired: "expired authentication token",
        CodeModelEntryDuplication: "model entry already exists",
        CodeModelEntryNotExist:    "model entry does not exist",
        CodeServiceFatal: "fatal error in service",
        CodeBusinessPanicError:   "panic error in business logic",
        CodeUnknownInternalError: "unknown internal error",
    }
    zhCNText = map[int]string{
        CodeSuccess: "恳求成功",
        CodeError:   "恳求失利",
        CodeConsultFailedEncoding: "无效的编码",
        CodeConsultFailedLanguage: "无效的言语",
        CodePermanentRedirect: "永久重定向",
        CodeTemporaryRedirect: "暂时重定向",
        CodeRequestInvalidUrlPath: "无效的 URL 途径",
        CodeRequestInvalidMethod:  "无效的 HTTP 办法",
        CodeRequestInvalidParam:   "无效的动态路由参数",
        CodeRequestInvalidQuery:   "无效的恳求参数",
        CodeRequestInvalidBody:    "无效的恳求体",
        CodeRequestInvalidForm:    "无效的表单数据",
        CodeRequestInvalidHeader:  "无效的恳求头",
        CodeAuthTokenIllegal: "无效的身份验证令牌",
        CodeAuthTokenExpired: "身份验证令牌已过期",
        CodeModelEntryDuplication: "模型条目已存在",
        CodeModelEntryNotExist:    "模型条目不存在",
        CodeServiceFatal: "服务产生丧命过错",
        CodeBusinessPanicError:   "事务逻辑产生溃散过错",
        CodeUnknownInternalError: "产生不知道的内部过错",
    }
)
// 仅回来英文 API 音讯
func GetMsg(code int) string {
    return enUSText[code]
}
// 依据当时接口恳求的言语设定,回来相应语系
func ApiMsg(c *gin.Context, code int) string {
    locale := getPreferredLanguage(c)
    switch locale {
    case en:
        if msg, ok := enUSText[code]; ok {
            return msg
        }
        fallthrough
    case zh:
        if msg, ok := zhCNText[code]; ok {
            return msg
        }
        fallthrough
    default:
        return GetMsg(CodeConsultFailedLanguage)
    }
}

一致 JSON 的呼应格局

Part 9: 让咱们以 Gin WEB Framework 为例,一步步地展现怎么从最简略的代码进阶到愈加优雅、可读性更高、更易于保护的写法。实践上,写法都较为根底,并没有用到非常高阶的用法。

坚韧黑铁

代码:

package main
import (
    "net/http"
    "github.com/gin-gonic/gin"
)
func main() {
    router := gin.Default()
    router.GET("/SayHello", func(c *gin.Context) {
        c.JSON(200, map[string]interface{}{
            "result": "Hello World", 
        })
    })
    _ = router.Run(":8080")
}

点评:

  • 无规范 JSON 呼应格局。可参阅Part 1
  • 别的,URI 途径不主张运用驼峰办法。
  • 这一段位的选手一般为 Go 初学者,前期学习阶段能够运用,但出产一般不主张运用全能的 map[string]interface{} 进行数据回来,接口难以保护,否则会被暴揍。

代码规范准则:

  • 代码中回绝全部神仙数,回绝全部直接硬编码的字符串!
  • 基本类型运用 const,杂乱类型运用 var 预界说。
  • 尽量运用一些已界说的 shortcut

改善:

package main
import (
    "net/http"
    "github.com/gin-gonic/gin"
)
const (
    CodeKey = "code"
    MsgKey  = "msg"
    DataKey = "data"
)
const (
    OkCode = 0
    OkMsg  = "success"
    ErrCode = 50000
    ErrMsg  = "failed"
)
func NewResponseWithH(code int, msg string, data any) (r gin.H) {
    r = gin.H{
        CodeKey: code,
        MsgKey:  msg,
        DataKey: data,
    }
    return
}
func HandlerView(c *gin.Context) {
    data := map[string]any{
        "result": "Hello World",
    }
    resp := NewResponseWithH(OkCode, OkMsg, data)
    c.JSON(http.StatusOK, resp)
}
func main() {
    router := gin.Default()
    router.GET("/hello", HandlerView)
    _ = router.Run(":8080")
}

英勇青铜

代码:

package main
import (
    "net/http"
    "github.com/gin-gonic/gin"
)
type Response struct {
    Code int         `json:"code"`
    Msg  string      `json:"msg"`
    Data interface{} `json:"data,omitempty"`
}
type User struct {
    Name      string `json:"name"`
    Age       int    `json:"age"`
    Male      bool   `json:"male"`
    Job       string `json:"developer"`
    TechStack string `json:"tech_stack"`
}
func main() {
    resp := &Response{
        Code: 0,
        Msg:  "success",
        Data: User{
            Name:      "Pokeya",
            Age:       30,
            Male:      true,
            Job:       "developer",
            TechStack: "backend",
        },
    }
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, resp)
    })
    _ = router.Run(":8080")
}

点评:

  • JSON 呼应格局已初步成型,运用了 struct json tag 进行规范 API 数据呼应。
  • 这一段位的选手鱼龙混杂,有刚参加 Go 的小白,有躺平的 CRUD 程序员,也有爱摆烂的大佬。
  • 玩具或许小项目能够这样写,没什么毛病,正式出产项目可不要这样写,仍然会被打。

不平白银

代码:

response.go 文件

package response
type baseResponse struct {
    Code   int    `json:"code"`
    Msg    string `json:"msg"`
    Data   any    `json:"data,omitempty"`
    Detail string `json:"detail,omitempty"`
}
type Response struct {
    Status int
    Body   baseResponse
}
func NewResponse() *Response {
    return &Response{}
}
func (r *Response) SetStatus(status int) *Response {
    r.Status = status
    return r
}
func (r *Response) SetCode(code int) *Response {
    r.Body.Code = code
    return r
}
func (r *Response) SetMsg(msg string) *Response {
    r.Body.Msg = msg
    return r
}
func (r *Response) SetData(data any) *Response {
    r.Body.Data = data
    return r
}
func (r *Response) SetDetail(detail string) *Response {
    r.Body.Detail = detail
    return r
}
func (r *Response) GetStatus() int {
    return r.Status
}
func (r *Response) GetBody() *baseResponse {
    return &r.Body
}

main.go 文件

package main
import (
    "net/http"
    . "your-project/common/errorx"
    "your-project/common/response"
    "github.com/gin-gonic/gin"
)
func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        resp := response.NewResponse()
        resp.
            SetStatus(http.StatusOK).
            SetCode(CodeSuccess).
            SetMsg(GetMsg(CodeSuccess)).
            SetData(map[string]any{
                "Greeting": "hello",
            })
        c.JSON(resp.Status, resp.Body)
    })
    _ = router.Run(":8080")
}

点评:

  • 运用了 Builder 建造者办法。
  • 隐蔽原 struct 的特点,经过办法设置改动特点值。
  • 运用了办法链的办法,便利操作者调用。
  • 参加了 errorx 包,来界说项目规范的过错码。
  • 运用结构函数来创建一个指向 Response 结构的指针。
  • 虽然写法上有了些许的进步,反倒代码质变更多了,甚至不如上面直接赋值原始 struct,怎么挑选视情况而定,该办法并非是一种很好的解法。

概念:

术语 阐明
办法设置 Go 言语中,咱们能够经过绑定办法来修正结构体的特点,这种办法一般被称为 “办法设置” 或 “函数设置”。与直接修正结构体特点不同,这种办法愈加安全,因为能够在绑定办法中对输入进行验证,然后防止了过错的修正。一起,它还能够更好地封装结构体特点,使得外部无法直接修正结构体特点,然后进步了代码的可保护性和可读性。
办法链 办法链 (Method Chaining) 是一种在面向目标编程中常见的技能,它经过将办法的回来值设置为目标自身来完结连续调用多个办法的效果。也便是说,在一个目标上连续调用多个办法,每个办法都回来目标自身,以便在调用链中持续运用下一个办法,然后完结一系列操作的连贯性。办法链能够使代码愈加简练、易读和易于保护。

荣耀黄金

运用 Go 1.18 特性 T 泛型

response.go 文件

package response
// 这儿直接写 Response[T any] 即可,全能空接口,包括全部
type Response [T any | *any | []any | map[string]any | []map[string]any] struct {
   Code   int    `json:"code"`
   Msg    string `json:"msg"`
   Data   T      `json:"data,omitempty"`
   Detail string `json:"detail,omitempty"`
}
func ParseResponse[T any]() *Response[T] {
   return &Response[T]{}
}
func BaseResponse[T any](code int, msg string, data T) *Response[T] {
   return &Response[T]{
      Code: code,
      Msg:  msg,
      Data: data,
   }
}

main.go 文件

package main
import (
    "net/http"
    . "your-project/common/errorx"
    . "your-project/common/response"
    . "your-project/model"
    "github.com/gin-gonic/gin"
)
func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        u := User{
           Name:      "Pokeya",
           Age:       30,
           Male:      true,
           Job:       "developer",
           TechStack: "backend",
        }
        resp := BaseResponse[*User](CodeSuccess, GetMsg(CodeSuccess), &u)
        c.JSON(http.StatusOK, resp)
    })
    _ = router.Run(":8080")
}

点评:

  • 其实和之前的普通结构体没什么区别,只不过运用泛型特性,拉了点好感分。
  • Response 泛型结构体能够用于作为 gin 结构的呼应回来,也能够用于解析其他接口的呼应数据。
  • 当然 anyinterface{} 空接口类型也能够当作 “泛型”,这不过 T 泛型能够让代码愈加简练,合理运用。

优势:

相较于上面的普通结构体,泛型结构体具有以下优势:

  1. 更严格的类型束缚:Data 字段的类型是泛型的,可是能够经过类型束缚指定泛型类型的规模,例如 any 表明能够是恣意类型,*any 表明能够是恣意指针类型,[]any 表明能够是恣意切片类型,map[string]any 表明能够是恣意键为字符串类型的字典类型,[]map[string]any 表明能够是恣意字典类型的切片类型。这样能够愈加精确地指定 Data 字段的类型,减少类型过错的产生。
  2. 更好的类型推断:因为 Data 字段的类型是泛型的,因而能够依据传入的详细类型主动推断 Data 字段的类型,防止了显式类型转化的麻烦。
  3. 愈加灵敏的数据处理:因为 Data 字段的类型是泛型的,能够依据不同的需求传入不同的数据类型,愈加灵敏地处理数据。例如,能够将 Data 字段的类型指定为 []map[string]any,然后将一个包括多个字典类型的数组赋值给 Data 字段,能够很便利地处理多个字典类型的数据。

华贵铂金

结合实践事务与 RESTful API 呼应规范,梳理如下。

response.go 文件

package response
import (
    "net/http"
    . "your-project/common/errorx"
    "github.com/gin-gonic/gin"
)
type Responder interface {
    // 状况码200,事务成功
    Ok()
    // 状况码200,事务成功,有 data 实体
    OkWithData(data any)
    // 状况码200,事务失利,规范 errorx 输出
    Fail(code int)
    // 状况码200,事务失利,自界说 errorx 输出
    FailWithMsg(code int, msg string)
    // 状况码200,事务失利,规范 errorx 输出 + 自界说详细信息
    FailWithDetail(code int, detail string)
    // 状况码302,路由重定向
    Redirect(url string)
    // 状况码404,路由未找到
    NoRoute()
    // 状况码500,服务端丧命过错,规范 errorx 输出 + 自界说详细信息
    Fatal(code int, detail string)
}
type Ctx struct {
    GetCtx *gin.Context
}
func Mount(c *gin.Context) *Ctx {
    return &Ctx{GetCtx: c}
}
type Response struct {
    Code   int    `json:"code"`
    Msg    string `json:"msg"`
    Data   any    `json:"data,omitempty"`
    Detail string `json:"detail,omitempty"`
}
func result(c *gin.Context, status, code int, msg, detail string, data any) {
    switch status {
    case http.StatusOK:
        c.JSON(status, &Response{
            Code:   code,
            Msg:    msg,
            Data:   data,
            Detail: detail,
        })
        break
    case http.StatusNotFound:
        c.AbortWithStatusJSON(status, &Response{
            Code: code,
            Msg:  msg,
        })
        break
    case http.StatusInternalServerError:
        c.AbortWithStatusJSON(status, &Response{
            Code:   code,
            Msg:    msg,
            Detail: detail,
        })
        break
    case http.StatusFound:
        c.Redirect(status, data.(string))
        break
    default:
        c.AbortWithStatusJSON(status, &Response{
            Code:   code,
            Msg:    msg,
            Detail: detail,
        })
        break
    }
}
// [200] - 0 - data (is null)
func (c *Ctx) Ok() {
    result(c.GetCtx, http.StatusOK, CodeSuccess, GetMsg(CodeSuccess), "", (*any)(nil))
}
// [200] - 0 - data (has entity)
func (c *Ctx) OkWithData(data any) {
    result(c.GetCtx, http.StatusOK, CodeSuccess, GetMsg(CodeSuccess), "", data)
}
// [200] - 非0 - code&msg (built-in)
func (c *Ctx) Fail(code int) {
    result(c.GetCtx, http.StatusOK, code, GetMsg(code), "", nil)
}
// [200] - 非0 - code&msg (customize)
func (c *Ctx) FailWithMsg(code int, msg string) {
    result(c.GetCtx, http.StatusOK, code, msg, "", nil)
}
// [200] - 非0 - code&msg (built-in) detail (customize)
func (c *Ctx) FailWithDetail(code int, detail string) {
    result(c.GetCtx, http.StatusOK, code, GetMsg(code), detail, nil)
}
// [302] - 非0 - data (redirect)
func (c *Ctx) Redirect(url string) {
    result(c.GetCtx, http.StatusFound, CodeSuccess, GetMsg(CodeTemporaryRedirect), "", url)
}
// [404] - 非0 - code&msg (built-in)
func (c *Ctx) NoRoute() {
    result(c.GetCtx, http.StatusNotFound, CodeRequestInvalidUrlPath, GetMsg(CodeRequestInvalidUrlPath), "", nil)
}
// [500] - 非0 - code&msg (built-in) detail (customize)
func (c *Ctx) Fatal(code int, detail string) {
    result(c.GetCtx, http.StatusInternalServerError, code, GetMsg(code), detail, nil)
}

main.go 文件

package main
import (
    . "your-project/common/errorx"
    . "your-project/common/response"    
    "github.com/gin-gonic/gin"
)
func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        mnt := Mount(c)
        // 事务逻辑
        if BusinessOK {
            Responder(mnt).OkWithData(map[string]any{
                "name":       "Pokeya",
                "age":        30,
                "male":       true,
                "job":        "developer",
                "tech_stack": "backend",
            })
        } else {
            Responder(mnt).FailWithDetail(CodeRequestInvalidQuery, "未找到 user_id 恳求参数")
        }
    })
    _ = router.Run(":8080")
}

点评:

  • 这段代码的全体结构明晰、函数接口界阐明晰,关于运用者来说比较友爱。
  • 当然还有一些能够改善的当地 …

璀璨钻石

实践上,上述写法都比较根底,没有运用较为高档的编程技巧。

秉承着开源与常识共享的精力,嗯,后边更高阶办法便是付费内容了…

哈哈哈,今日的共享就止于此了,下回见

超凡大师

欢迎留言,各路神仙指教,评论更优的写法 …