Go中的REST服务器:第1部分–标准库

2021-01-17 04:48:12

这是有关在Go中编写REST服务器的系列文章中的第一篇。我在本系列文章中的计划是使用几种不同的方法来实现一个简单的REST服务器,这应该使比较和对比这些方法及其相对优点变得容易。

刚开始使用语言的开发人员经常会问"我应该使用哪种框架来进行X&#34 ;?作为他们的第一个问题。尽管这对于使用多种语言的Web应用程序和服务器完全有意义,但是在Go语言中,对这个问题的回答是细微的。支持和反对使用框架都有强烈的意见。我在这些职位中的目标是从多个角度客观地研究该问题。

首先,我假定读者知道什么是REST服务器。如果您需要复习,这是一个很好的资源,但还有很多其他资源。本系列的其余部分假设您知道我所说的" path"," HTTP标头",& #34;响应代码"等。

在我们的例子中,服务器是任务管理应用程序的简单后端(例如Google Keep,Todoist等);它向客户端[1]提供以下REST API:

POST / task /:创建一个任务,返回IDGET / task /< taskid> :通过IDGET / task /返回单个任务:返回所有任务。删除/ task /< taskid> :通过IDGET / tag /< tagname>删除任务:返回带有此标签的任务列表。GET/ due /< yy> /< mm> /< dd> :返回到该日期到期的任务列表

我们的服务器支持GET,POST和DELETE请求,其中一些请求具有多个潜在路径。尖括号< ...>之间的部分。表示客户端作为请求的一部分提供的参数;例如,GET / task / 42是获取ID为42的任务的请求,等等。任务由ID唯一标识。

数据编码为JSON。在POST / task /中,客户端将发送要创建任务的JSON表示形式。同样,到处都显示服务器“返回”东西,返回的数据在HTTP响应的主体中被编码为JSON。

这篇文章的其余部分将分部分介绍Go语言中的服务器代码。可以在这里找到服务器的完整代码,它是一个独立的Go模块,没有依赖性。克隆或复制项目目录后,无需安装任何内容即可运行服务器:

注意,SERVERPORT可以是任何端口。这是本地服务器正在侦听的TCP端口。服务器运行后,您可以使用curl命令或其他适合您的方式在单独的终端中与服务器进行交互。有关示例,请参见此脚本。包含此脚本的目录还具有服务器的自动测试功能。

让我们从讨论服务器的模型(或数据层)开始-任务存储包(项目目录中的内部/任务存储)。这是代表任务数据库的简单抽象;这是它的API:

func New()* TaskStore // CreateTask在存储中创建一个新任务。 func(ts * TaskStore)CreateTask(文本字符串,标签[]字符串,到期时间。Time)int // GetTask通过ID从存储中检索任务。如果不存在这样的ID,则返回//错误。 func(ts * TaskStore)GetTask(id int)(Task,error)// DeleteTask删除具有给定id的任务。如果不存在这样的ID,则返回错误//。 func(ts * TaskStore)DeleteTask(id int)错误// DeleteAllTask​​s删除存储中的所有任务。 func(ts * TaskStore)DeleteAllTask​​s()错误// GetAllTask​​s以任意顺序返回存储中的所有任务。 func(ts * TaskStore)GetAllTask​​s()[] Task // GetTasksByTag以任意//顺序返回所有具有给定标签的任务。 func(ts * TaskStore)GetTasksByTag(标签字符串)[]任务// GetTasksByDueDate以//任意顺序返回具有给定到期日期的所有任务。 func(ts * TaskStore)GetTasksByDueDate(年int,月时间。月,日int)[]任务

类型Task struct {Id int`json:" id"`文本字符串`json:" text"`标签[]字符串`json:" tags"`到期时间。时间`json:" due"`}

taskstore包使用一个简单的map [int] Task来实现此API,但是您可以轻松地想象它是使用数据库来实现的。在区域应用程序中,TaskStore可能是多个后端可以实现的接口,但是对于我们的简单示例,当前的API足够了。如果您希望进行扩展练习,请继续使用MongoDB之类的东西来实现TaskStore。

func main(){mux:= http。 NewServeMux()服务器:= NewTaskServer()mux。 HandleFunc(" / task /",服务器。taskHandler)多路复用器。 HandleFunc(" / tag /",服务器.tagHandler)多路复用器。 HandleFunc(" / due /&#34 ;,服务器。DueHandler)日志。致命(http。ListenAndServe(" localhost:" + os。Getenv(" SERVERPORT"),mux))}

让我们花点时间讨论一下NewTaskServer,然后我们再来讨论路由器和路径处理程序。

NewTaskServer是我们的服务器类型taskServer的构造函数。服务器将TaskStore和一个互斥体包装在一起,以防止其并发访问。我们简单的任务存储实现不是线程安全的,并且Go HTTPserver是开箱即用的并发。

键入taskServer struct {sync。互斥存储*任务存储。 TaskStore} func NewTaskServer()* taskServer {store:= taskstore。 New()返回& taskServer {存储:存储}}

标准的多路复用器非常简单。这既是力量又是弱点。力量,因为它超级容易理解-绝不涉及任何魔术。弱点是因为它有时会使路径匹配变得繁琐,并分成几个地方,正如我们将很快看到的。

由于标准的多路复用器仅支持路径前缀的精确匹配,因此我们非常被迫只匹配顶层的根,并将更详细的匹配推迟到处理程序中。

func(ts * taskServer)taskHandler(w http。ResponseWriter,req * http。Request){如果req。网址。路径==" /任务/" {//请求是简单的" / task /&#34 ;,没有结尾的ID。如果要求。方法== http MethodPost {ts。 createTaskHandler(w,req)}否则为req。方法== http MethodGet {ts。 getAllTask​​sHandler(w,req)}否则为req。方法== http方法删除{ts。 deleteAllTask​​sHandler(w,req)}其他{http。错误(w,fmt。Sprintf(在/ task /上期望方法GET,DELETE或POST,得到%v",要求方法),http。StatusMethodNotAllowed)返回}

我们从/ task /的路径的精确匹配开始(意味着没有< taskid>跟随)。在这里,我们必须弄清楚使用哪种HTTP方法,并调用适当的服务器方法。大多数处理程序都是TaskStore API的相当简单的包装器。让我们详细研究一个:

func(ts * taskServer)getAllTask​​sHandler(w http。ResponseWriter,req * http。Request){日志。 Printf(处理会在%s \ n"处获取所有任务,需要URL路径)ts。 Lock()allTask​​s:= ts。商店。 GetAllTask​​s()ts。解锁()js,err:= json。如果err!= nil {http。 Error(w,err。Error(),http。StatusInternalServerError)return} w。标头()。设置(" Content-Type"," application / json")w。写(js)}

两者都很简单,但是如果您检查服务器中的其他处理程序,您会注意到第二个处理程序有点重复-封送JSON,编写正确的HTTP响应标头,等等。稍后我们将返回至此。 。

现在回到taskHandler;到目前为止,我们已经看到了它如何处理/ task /路径的直接匹配。 / task /< taskid>呢?那就是函数的下一部分出现的地方:

} else {//请求具有ID,如" / task /< id>"中所示。路径:=字符串。修剪(request.URL.Path," /")pathParts:=字符串。如果len(pathParts)<则分割(path," /") 2 {http。错误(w,"期望/ task /< id>在任务处理程序"中,http。StatusBadRequest)返回} id,err:= strconv。 Atoi(pathParts [1])如果err!= nil {http。错误(w,err。错误(),http。StatusBadRequest)返回}。方法== http方法删除{ts。 deleteTaskHandler(w,req,int(id))}否则为req。方法== http MethodGet {ts。 getTaskHandler(w,req,int(id))}其他{http。错误(w,fmt。Sprintf(在/ task /< id>预期方法GET或DELETE,得到%v",要求方法),http。StatusMethodNotAllowed)返回}}

当路径与/ task /不完全匹配时,我们希望在斜杠后有一个字母数字ID。上面的代码解析此数字ID并调用适当的处理程序(基于HTTP方法)。

其余代码大致相同,并且应该很容易理解。唯一有点特殊的处理程序是createTaskHandler,因为它必须解析客户端在请求正文中发送的JSON数据。在我未涵盖的请求中,JSON解析存在一些细微差别-请查看这篇文章,以获取更彻底的方法。

现在,我们已经可以使用服务器的基本版本,现在该考虑潜在的问题和改进了。

我们可以改善的一个明显地方是HTTP响应中的重复JSON呈现,如前所述。为此,我创建了一个名为stdlib-factorjson的服务器的单独版本。我将其分开以帮助您轻松将其与原始服务器进行区分,并查看发生了什么变化。它包含的主要新颖之处在于该功能:

// renderJSON渲染' v'作为JSON并将其作为响应写入w。 func renderJSON(w http。ResponseWriter,v interface {}){js,err:= json。如果err!= nil {http。 Error(w,err。Error(),http。StatusInternalServerError)return} w。标头()。设置(" Content-Type"," application / json")w。写(js)}

使用它,我们可以重写所有处理程序,使其更加简洁。例如,getAllTask​​sHandler现在变为:

func(ts * taskServer)getAllTask​​sHandler(w http。ResponseWriter,req * http。Request){日志。 Printf(处理会在%s \ n"处获取所有任务,需要URL路径)ts。 Lock()allTask​​s:= ts。商店。 GetAllTask​​s()ts。解锁()renderJSON(w,allTask​​s)}

更根本的改进是使路径匹配更清洁,更集中。尽管当前的路径匹配方法易于调试,但一目了然却不那么容易,因为它分散在多个功能上。例如,假设我们试图找出对/ task /< taskid>的DELETE请求位于何处。要去。

首先,我们在main中找到多路复用器,并看到/ task /根目录进入taskHandler

然后,在taskHandler中必须找到else子句来处理与/ task /的不完全匹配。在那里,我们必须阅读解析< taskid>的代码。分成整数

最后,我们看一下if语句,它列出了此路由支持的不同方法,并发现DELETE由deleteTaskHandler处理。 第三方HTTP路由器程序包旨在解决将所有这些逻辑放在一个易于消耗的地方。 这是本系列第2部分的重点。 请注意为服务器指定REST API的即席性质。我们将在本系列的后续部分中讨论更多结构化/标准的方式。