golang我爱编程

使用Gin构建Go Web应用程序和微服务

2018-03-21  本文已影响3416人  devabel

使用Gin构建Go Web应用程序和微服务

原文链接:https://semaphoreci.com/community/tutorials/building-go-web-applications-and-microservices-using-gin
翻译:devabel

熟悉Gin并了解它如何帮助您减少重复代码并构建请求,处理分发,处理管道。

介绍

在本教程中,您将学习如何使用Gin框架在Go中构建传统Web应用程序和微服务。Gin是一种可以减少通常用于构建这些应用程序的样板代码的框架。它也非常适合创建可重用和可扩展的代码片断。

本教程的这一部分将帮助您设置您的项目并使用Gi​​n构建一个简单的应用程序,该应用程序将显示文章列表和文章详细信息页面。

目标

在本教程结束时,您将:

学习如何使用Gin建立一个Web应用程序,和

了解用Go编写的Web应用程序的各个部分。

先决条件

对于本教程,您将需要Go并curl安装在您的机器上。

注意:如果您没有curl,则可以使用通常用于测试API端点的任何其他工具。

什么是Gin?

Gin是一种高性能的微型框架,可用于构建Web应用程序和微服务。它使得从模块化,可重用的部分构建请求处理管道变得非常简单。它通过允许您编写可插入一个或多个请求处理程序或请求处理程序组的中间件来实现此目的。

为什么要使用Gin?

Go的最佳功能之一是其内置的net/http库,可以让您轻松地创建HTTP服务器。但是,它也不太灵活,需要一些样板代码才能实现。

Go中没有内置的支持来处理基于正则表达式或“模式”的路由。您可以编写代码来添加此功能。但是,随着应用程序数量的增长,您很可能会在任何地方重复使用这些代码,或者创建一个用于重用的库。

这是Gin提供的关键。它包含一组常用的功能,例如路由,中间件支持,渲染,可以减少样板代码并简化Web应用程序的编写。

设计应用程序

让我们快速看一下在Gin中处理请求的方式。典型的Web应用程序,API服务器或微服务的控制流程如下所示:

Request -> Route Parser -> [Optional Middleware] -> Route Handler -> [Optional Middleware] -> Response

当请求进入时,Gin先分析路由。如果找到匹配的路由定义,则Gin将按路由定义的顺序调用路由处理程序和零个或多个中间件。当我们在后面的章节中查看代码时,我们会看到这是如何完成的。

应用功能

我们将建立的应用程序是一个简单的文章管理。此应用程序包括以下功能:

让用户使用用户名和密码进行注册(仅限未登录的用户),

让用户使用用户名和密码登录(仅限未登录的用户),

让用户注销(仅限登录用户),

让用户创建新文章(仅限登录用户),

显示主页上所有文章的列表(针对所有用户)和

在其自己的页面上显示一篇文章(适用于所有用户)。

除此功能外,文章列表和单个文章应该可以以HTML,JSON和XML格式访问。

这将使我们能够说明Gin如何用于设计传统的Web应用程序,API服务器和微服务。

为了达到这个目的,我们将使用由Gin提供的以下功能:

路由 - 处理各种URL,

自定义渲染 - 处理响应格式和

中间件 - 实现认证。

我们还会编写测试来验证所有功能是否按预期工作。

路由

路由是所有现代框架提供的核心功能之一。任何网页或API端点都可以通过URL访问。框架使用路由来处理对这些URL的请求。如果URL是http://www.example.com/some/random/route,路由将是/some/random/route。

Gin提供了一个易于配置和使用的快速路由器。除了处理指定的URL之外,Gin路由器还可以处理模式和分组URL。

在我们的应用程序中,我们将:

在路由服务索引页面/(HTTP GET请求),

路由下的用户相关路由分组/u,

在/u/login(HTTP GET请求)服务登录页面,

在/u/login(HTTP POST请求)处理登录凭证,

在/u/logout(HTTP GET请求)注销,

在/u/register(HTTP GET请求)服务注册页面,

在/u/register(HTTP POST请求)处理注册信息,

在路线下分组文章相关/article路线,

在/article/create(HTTP GET请求)服务文章创建页面,

处理提交的文章/article/create(HTTP POST请求)和

在/article/view/:article_id(HTTP GET请求)服务文章页面。记下:article_id该路线中的部分。在:一开始表示这是一个动态的路线。这意味着:article_id可以包含任何值,并且Gin将使这个值在路由处理器中可用。

渲染

Web应用程序可以呈现各种格式的响应,如HTML,文本,JSON,XML或其他格式。API端点和微服务通常以数据进行响应,通常以JSON格式进行响应,但也以任何其他所需的格式进行响应。

在下一节中,我们将看到如何在不重复任何功能的情况下呈现不同类型的响应。我们将主要通过HTML模板回应请求。但是,我们还将定义可以使用JSON或XML数据响应的两个端点。

中间件

在Go Web应用程序的上下文中,中间件是可以在处理HTTP请求的任何阶段执行的一段代码。它通常用于封装您想要应用于多个路由的通用功能。我们可以在处理HTTP请求之前和/或之后使用中间件。中间件的一些常见用途包括授权,验证等。

如果在处理请求之前使用了中间件,那么它对请求所做的任何更改都将在主要路由处理程序中可用。如果我们想对某些请求实施一些验证,这很方便。另一方面,如果在路由处理程序之后使用中间件,它将具有来自路由处理程序的响应。这可以用来修改路由处理程序的响应。

Gin允许我们编写实现一些常见功能的中间件,这些功能需要在处理多条路由时进行共享。这使代码库变小,分离问题并提高代码可维护性。

我们希望确保一些页面和操作,例如。创建一篇文章并注销,只能用于已登录的用户。我们还希望确保某些页面和操作,例如。注册,登录,仅适用于未登录的用户。

如果我们将这种逻辑放在每个路由上,那将是相当繁琐,重复和容易出错的。幸运的是,我们可以为这些任务中的每一个创建中间件,并在特定路由中重用它们。

我们还将创建将应用于所有路由的中间件。该中间件(setUserStatus)将检查请求是否来自经过验证的用户。然后它将设置一个标志,可以在模板中使用该标志来修改基于该标志的某些菜单链接的可见性。

安装依赖关系

此应用程序将只使用一个外部依赖项 - Gin框架。我们可以使用以下命令安装最新版本:

go get -u github.com/gin-gonic/gin

创建可重用模板

我们的应用程序将使用其模板显示网页。但是,会有几个部分,例如标题,菜单,侧边栏和页脚,这些在所有页面中都是常见的。Go允许我们创建可以在其他模板中导入的可重用模板片段。

页眉和页脚将是将在所有模板中重复使用的常见部分。我们还将在其自己的模板文件中创建菜单,该模板文件将由标题模板使用。最后,我们将为导入页眉和页脚的索引页创建模板。所有模板文件将被放置在templates项目目录中的目录中。

我们首先为菜单创建模板,templates/menu.html如下所示

<!--menu.html-->

<nav class="navbar navbar-default">
  <div class="container">
    <div class="navbar-header">
      <a class="navbar-brand" href="/">
        Home
      </a>
    </div>
  </div>
</nav>

最初,菜单仅包含主页的链接。随着我们向应用程序添加更多功能,我们将添加到此。标题的模板将被放置在templates/header.html文件中,如下所示:

<!--header.html-->

<!doctype html>
<html>

  <head>
    <!--Use the `title` variable to set the title of the page-->
    <title>{{ .title }}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="UTF-8">

    <!--Use bootstrap to make the application look nice-->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
    <script async src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
  </head>

  <body class="container">
    <!--Embed the menu.html template at this location-->
    {{ template "menu.html" . }}

正如你所看到的,我们正在使用开源的Bootstrap框架。这个文件大部分是标准的HTML。但是,请注意两行。包含的行{{ .title }}用于使用.title必须在应用程序内设置的变量动态设置页面的标题。其次,包含的行 {{ template "menu.html" . }}用于从menu.html文件中导入菜单模板 。这就是Go如何让你在另一个模板中导入一个模板。

页脚模板包含静态HTML。索引页面的模板使用页眉和页脚,并显示一个简单的Hello Gin消息:

<!--index.html-->

<!--Embed the header.html template at this location-->
{{ template "header.html" .}}

  <h1>Hello Gin!</h1>

<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}

与索引模板一样,其他页面的模板也会以类似的方式重新使用页眉和页脚的模板。

完成并验证安装程序

一旦创建了模板,就可以创建应用程序的入口文件了。我们将main.go 用最简单的Web应用程序为此创建文件,该应用程序将使用索引模板。我们可以用四个步骤使用Gin:

1.创建路由器

在Gin中创建路由器的默认方式如下所示:

router := gin.Default()

这将创建一个可用于定义应用程序构建的路由器。

2.加载模板

一旦你创建了路由器,你可以像这样加载所有的模板:

router.LoadHTMLGlob("templates/*")

这会加载位于该templates文件夹中的所有模板文件。一旦加载,这些都不必在每次请求时都要重新读取,这使得Gin Web应用程序非常快速。

3.定义路由处理程序

Gin的核心是如何将应用程序分成不同的路由并为每条路由定义处理程序。我们将为索引页和内联路由处理程序创建路由。

router.GET("/", func(c *gin.Context) {

  // Call the HTML method of the Context to render a template
  c.HTML(
      // Set the HTTP status to 200 (OK)
      http.StatusOK,
      // Use the index.html template
      "index.html",
      // Pass the data that the page uses (in this case, 'title')
      gin.H{
          "title": "Home Page",
      },
  )

})

该router.GET方法用于为GET请求定义路由处理程序。它接受作为参数的路由(/)和一个或多个路由处理程序,它们只是函数。

路由处理程序有一个指向context(gin.Context)作为其参数的指针。此上下文包含有关处理程序可能需要处理的请求的所有信息。例如,它包含有关标题,Cookie等的信息。

Context还具有以HTML,文本,JSON和XML格式呈现响应的方法。在这种情况下,我们使用该context.HTML方法来呈现HTML模板(index.html)。对此方法的调用包括其中值title设置为的附加数据Home Page。这是HTML模板可以使用的值。在这种情况下,我们在标头模板的标签中使用这个值。

4.启动应用程序

要启动应用程序,您可以使用Run路由器的方法:

router.Run()

这会默认启动应用程序localhost并在8080端口上提供服务。

完整的main.go文件如下所示:

// main.go

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

var router *gin.Engine

func main() {

  // Set the router as the default one provided by Gin
  router = gin.Default()

  // Process the templates at the start so that they don't have to be loaded
  // from the disk again. This makes serving HTML pages very fast.
  router.LoadHTMLGlob("templates/*")

  // Define the route for the index page and display the index.html template
  // To start with, we'll use an inline route handler. Later on, we'll create
  // standalone functions that will be used as route handlers.
  router.GET("/", func(c *gin.Context) {

    // Call the HTML method of the Context to render a template
    c.HTML(
      // Set the HTTP status to 200 (OK)
      http.StatusOK,
      // Use the index.html template
      "index.html",
      // Pass the data that the page uses (in this case, 'title')
      gin.H{
        "title": "Home Page",
      },
    )

  })

  // Start serving the application
  router.Run()

}

要从命令行执行应用程序,请转到您的应用程序目录并执行以下命令:

go build -o app

这将构建您的应用程序并创建一个可执行的命令app,您可以运行如下:

./app

如果一切顺利,您应该可以访问您的应用程序, http://localhost:8080它应该如下所示:

在这个阶段你的应用程序的目录结构应该如下所示:

├── main.go
└── templates
    ├── footer.html
    ├── header.html
    ├── index.html
    └── menu.html

显示文章列表

在本节中,我们将添加功能来显示索引页面上所有文章的列表。

设置路由

在前一节中,我们在main.go文件本身中创建了路由和路由定义 。随着应用程序的增长,将路径定义移动到其自己的文件中是有意义的。我们将initializeRoutes()在该routes.go文件中创建一个函数,并从该main()函数调用该函数来设置所有路由。我们将定义它们作为单独的函数,而不是定义内联的路由处理程序。

进行这些更改后,该routes.go文件将包含以下内容:

// routes.go

package main

func initializeRoutes() {

  // Handle the index route
  router.GET("/", showIndexPage)
}

由于我们将在索引页面上显示文章列表,因此在重构代码后,我们不需要定义任何其他路由。

该main.go文件应该包含以下代码:

// main.go

package main

import "github.com/gin-gonic/gin"

var router *gin.Engine

func main() {

  // Set the router as the default one provided by Gin
  router = gin.Default()

  // Process the templates at the start so that they don't have to be loaded
  // from the disk again. This makes serving HTML pages very fast.
  router.LoadHTMLGlob("templates/*")

  // Initialize the routes
  initializeRoutes()

  // Start serving the application
  router.Run()

}

设计文章模型

我们将保持简单的文章结构只有三个字段属性 - Id,Title和Content。这可以用结构体表示 如下:

type article struct {
  ID      int    `json:"id"`
  Title   string `json:"title"`
  Content string `json:"content"`
}

大多数应用程序将使用数据库来保存数据。为了简单起见,我们将保留内存中的文章列表,并用两个硬编码文章初始化列表,如下所示:

var articleList = []article{
  article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
  article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}

我们将把上面的代码放在一个名为的新文件中models.article.go。在这个阶段,我们需要一个函数返回所有文章的列表。我们将命名这个函数getAllArticles()并将它放在同一个文件中。我们也会为它写一个测试。该测试将被命名TestGetAllArticles并将被放置在models.article_test.go文件中。

我们首先创建函数的单元测试(TestGetAllArticles) getAllArticles()。创建此单元测试后,该models.article_test.go文件应包含以下代码:

// models.article_test.go

package main

import "testing"

// Test the function that fetches all articles
func TestGetAllArticles(t *testing.T) {
  alist := getAllArticles()

  // Check that the length of the list of articles returned is the
  // same as the length of the global variable holding the list
  if len(alist) != len(articleList) {
    t.Fail()
  }

  // Check that each member is identical
  for i, v := range alist {
    if v.Content != articleList[i].Content ||
      v.ID != articleList[i].ID ||
      v.Title != articleList[i].Title {

      t.Fail()
      break
    }
  }
}

该单元测试使用该getAllArticles()函数来获取所有文章的列表。此测试首先确保此函数获取的文章列表和全局变量articleList中存在的文章列表完全相同。然后它遍历文章列表以验证每篇文章是否相同。如果这两项检查中的任何一项失败,则测试失败。

一旦我们编写了测试,我们就可以继续编写实际的代码。该 models.article.go文件应该包含以下代码:

// models.article.go

package main

type article struct {
  ID      int    `json:"id"`
  Title   string `json:"title"`
  Content string `json:"content"`
}

// For this demo, we're storing the article list in memory
// In a real application, this list will most likely be fetched
// from a database or from static files
var articleList = []article{
  article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
  article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}

// Return a list of all the articles
func getAllArticles() []article {
  return articleList
}

创建视图模板

由于文章列表将显示在索引页面上,因此我们不需要创建新模板。但是,我们确实需要更改index.html 模板以将当前内容替换为文章列表。

为了做出这个改变,我们假定文章列表将被传递给名为变量的模板payload。有了这个假设,下面的代码片段应该显示所有文章的列表:

  {{range .payload }}
    <!--Create the link for the article based on its ID-->
    <a href="/article/view/{{.ID}}">
      <!--Display the title of the article -->
      <h2>{{.Title}}</h2>
    </a>
    <!--Display the content of the article-->
    <p>{{.Content}}</p>
  {{end}}

该片段将循环payload显示变量中的所有项目,并显示每篇文章的标题和内容。上面的代码片段也会链接到每篇文章。但是,由于我们尚未定义用于显示单篇文章的路由处理程序,因此这些链接无法按预期工作。

更新后的index.html文件应包含以下代码:

<!--index.html-->

<!--Embed the header.html template at this location-->
{{ template "header.html" .}}

  <!--Loop over the `payload` variable, which is the list of articles-->
  {{range .payload }}
    <!--Create the link for the article based on its ID-->
    <a href="/article/view/{{.ID}}">
      <!--Display the title of the article -->
      <h2>{{.Title}}</h2>
    </a>
    <!--Display the content of the article-->
    <p>{{.Content}}</p>
  {{end}}

<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}

用单元测试指定路由处理程序的要求

在为索引路由创建处理程序之前,我们将创建一个测试来定义此路由处理程序的预期行为。该测试将检查以下情况:

处理程序以200的HTTP状态码进行响应,

返回的HTML包含一个包含文本的标题标签Home Page。

测试代码将被放置在文件中的TestShowIndexPageUnauthenticated 函数中handlers.article_test.go。我们将把这个函数使用的帮助函数放在common_test.go文件中。

内容handlers.article_test.go如下:

// handlers.article_test.go

package main

import (
  "io/ioutil"
  "net/http"
  "net/http/httptest"
  "strings"
  "testing"
)

// Test that a GET request to the home page returns the home page with
// the HTTP code 200 for an unauthenticated user
func TestShowIndexPageUnauthenticated(t *testing.T) {
  r := getRouter(true)

  r.GET("/", showIndexPage)

  // Create a request to send to the above route
  req, _ := http.NewRequest("GET", "/", nil)

  testHTTPResponse(t, r, req, func(w *httptest.ResponseRecorder) bool {
    // Test that the http status code is 200
    statusOK := w.Code == http.StatusOK

    // Test that the page title is "Home Page"
    // You can carry out a lot more detailed tests using libraries that can
    // parse and process HTML pages
    p, err := ioutil.ReadAll(w.Body)
    pageOK := err == nil && strings.Index(string(p), "<title>Home Page</title>") > 0

    return statusOK && pageOK
  })
}

内容common_test.go如下:

package main

import (
  "net/http"
  "net/http/httptest"
  "os"
  "testing"

  "github.com/gin-gonic/gin"
)

var tmpArticleList []article

// This function is used for setup before executing the test functions
func TestMain(m *testing.M) {
  //Set Gin to Test Mode
  gin.SetMode(gin.TestMode)

  // Run the other tests
  os.Exit(m.Run())
}

// Helper function to create a router during testing
func getRouter(withTemplates bool) *gin.Engine {
  r := gin.Default()
  if withTemplates {
    r.LoadHTMLGlob("templates/*")
  }
  return r
}

// Helper function to process a request and test its response
func testHTTPResponse(t *testing.T, r *gin.Engine, req *http.Request, f func(w *httptest.ResponseRecorder) bool) {

  // Create a response recorder
  w := httptest.NewRecorder()

  // Create the service and process the above request.
  r.ServeHTTP(w, req)

  if !f(w) {
    t.Fail()
  }
}

// This function is used to store the main lists into the temporary one
// for testing
func saveLists() {
  tmpArticleList = articleList
}

// This function is used to restore the main lists from the temporary one
func restoreLists() {
  articleList = tmpArticleList
}

为了实现这个测试,我们写了一些帮助函数。当我们编写额外的测试来测试类似的功能时,这些也将帮助我们减少样板代码。

该TestMain函数设置Gin使用测试模式并调用其余的测试功能。该getRouter函数以类似于主应用程序的方式创建并返回路由器。该saveLists()函数将原始文章列表保存在一个临时变量中。该restoreLists()函数使用该临时变量在执行单元测试后将文章列表恢复到初始状态。

最后,该testHTTPResponse函数执行传入的函数以查看它是否返回布尔值true值 - 表示测试成功或不成功。此功能可帮助我们避免重复测试HTTP请求响应所需的代码。

要检查HTTP代码和返回的HTML,我们将执行以下操作:

创建一个新的路由器,

定义一个使用主应用程序使用的相同处理程序的路由(showIndexPage),

创建一个访问此路线的新请求,

创建一个处理响应以测试HTTP代码和HTML的函数

testHTTPResponse()用这个新功能调用完成测试。

创建路由处理程序

我们将在handlers.article.go文件中为文章相关功能创建所有路由处理程序 。索引页面的处理程序showIndexPage 执行以下任务:

1.获取文章列表

这可以使用getAllArticles之前定义的函数来完成:

articles := getAllArticles()

2.呈现index.html通过文章列表的模板

这可以使用下面的代码完成:

c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the index.html template
    "index.html",
    // Pass the data that the page uses
    gin.H{
        "title":   "Home Page",
        "payload": articles,
    },
)

与前一节中唯一的不同之处在于,我们传递了将通过名为变量的模板访问的文章列表payload。

该handlers.article.go文件应该包含以下代码:

// handlers.article.go

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func showIndexPage(c *gin.Context) {
  articles := getAllArticles()

  // Call the HTML method of the Context to render a template
  c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the index.html template
    "index.html",
    // Pass the data that the page uses
    gin.H{
      "title":   "Home Page",
      "payload": articles,
    },
  )

}

如果您现在构建并运行应用程序并http://localhost:8080在浏览器中访问,它应该如下所示:

这些是本节添加的新文件:

├── common_test.go
├── handlers.article.go
├── handlers.article_test.go
├── models.article.go
├── models.article_test.go
└── routes.go

显示单个文章

在最后一节中,虽然我们显示了一篇文章列表,但文章的链接不起作用。在本节中,我们将添加处理程序和模板以在选中文章时显示文章。

设置路由

我们可以建立一条新路由,以与前一路由相同的方式处理单篇文章的请求。但是,我们需要说明的是,虽然所有文章的处理程序都是相同的,但每篇文章的URL都会有所不同。Gin允许我们通过如下定义路由参数来处理这些情况:

router.GET("/article/view/:article_id", getArticle)

该路由将匹配与上述路径相匹配的所有请求,并将路由的最后一部分的值存储在article_id 我们可以在路由处理程序中访问的路由参数中。对于这条路由,我们将在名为的函数中定义处理程序getArticle。

更新后的routes.go文件应包含以下代码:

// routes.go

package main

func initializeRoutes() {

  // Handle the index route
  router.GET("/", showIndexPage)

  // Handle GET requests at /article/view/some_article_id
  router.GET("/article/view/:article_id", getArticle)

}

创建视图模板

我们需要创建一个新模板templates/article.html来显示单篇文章的内容。这可以以与index.html模板类似的方式创建 。但是,而不是payload包含文章列表的变量,在这种情况下,它将包含一篇文章。

您可以在Github存储库中看到article.html的内容。

用单元测试指定路由处理程序的要求

对这条路线的处理程序的测试将检查以下条件:

处理程序以200的HTTP状态码进行响应,

返回的HTML包含一个标题标签,其中包含所提取文章的标题。

测试代码将被放置在文件中的TestArticleUnauthenticated 函数中handlers.article_test.go。我们将把这个函数使用的帮助函数放在common_test.go文件中。

创建路由处理程序

文章页面的处理程序getArticle执行以下任务:

1.提取要显示的文章的ID

要获取并显示正确的文章,我们首先需要从上下文中提取其ID。这可以提取如下:

c.Param("article_id")

其中c是Gin 上下文的一个参数。

2.获取文章

这可以使用文件中getArticleByID()定义的函数 完成models.article.go:

article, err := getArticleByID(articleID)

的getArticleByID(在功能上models.article.go)如下:

func getArticleByID(id int) (*article, error) {
  for _, a := range articleList {
    if a.ID == id {
      return &a, nil
    }
  }
  return nil, errors.New("Article not found")
}

该函数遍历文章列表并返回其ID与传入的ID相匹配的文章。如果找不到匹配的文章,则返回一个指示相同的错误。

3.呈现article.html将文章传递给它的模板

这可以使用下面的代码完成:

c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the article.html template
    "article.html",
    // Pass the data that the page uses
    gin.H{
        "title":   article.Title,
        "payload": article,
    },
)

更新后的handlers.article.go文件应包含以下代码:

// handlers.article.go

package main

import (
  "net/http"
  "strconv"

  "github.com/gin-gonic/gin"
)

func showIndexPage(c *gin.Context) {
  articles := getAllArticles()

  // Call the HTML method of the Context to render a template
  c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the index.html template
    "index.html",
    // Pass the data that the page uses
    gin.H{
      "title":   "Home Page",
      "payload": articles,
    },
  )

}

func getArticle(c *gin.Context) {
  // Check if the article ID is valid
  if articleID, err := strconv.Atoi(c.Param("article_id")); err == nil {
    // Check if the article exists
    if article, err := getArticleByID(articleID); err == nil {
      // Call the HTML method of the Context to render a template
      c.HTML(
        // Set the HTTP status to 200 (OK)
        http.StatusOK,
        // Use the index.html template
        "article.html",
        // Pass the data that the page uses
        gin.H{
          "title":   article.Title,
          "payload": article,
        },
      )

    } else {
      // If the article is not found, abort with an error
      c.AbortWithError(http.StatusNotFound, err)
    }

  } else {
    // If an invalid article ID is specified in the URL, abort with an error
    c.AbortWithStatus(http.StatusNotFound)
  }
}

如果您现在构建并运行应用程序并http://localhost:8080/article/view/1在浏览器中访问 ,它应该如下所示:

本节添加的新文件如下所示:

└── templates
    └── article.html

使用JSON / XML进行响应

在本节中,我们将重构应用程序,以便根据请求标头,我们的应用程序可以使用HTML,JSON或XML格式进行响应。

创建可重用函数

到目前为止,我们一直使用HTMLGin上下文的方法直接从路由处理程序中进行渲染。当我们总想渲染HTML时,这很好。但是,如果我们想根据请求更改响应的格式,我们应该将此部分重构为一个负责渲染的函数。通过这样做,我们可以让路由处理程序专注于验证和数据获取。

路由处理程序必须执行相同类型的验证,数据读取和数据处理,而不考虑所需的响应格式。完成此部分后,可以使用此数据以所需格式生成响应。如果我们需要HTML响应,我们可以将这些数据传递给HTML模板并生成页面。如果需要JSON响应,我们可以将这些数据转换为JSON并发送回去。对于XML也是如此。

我们将创建一个render函数,main.go供所有路由处理程序使用。这个函数将负责基于请求Accept头以正确的格式进行渲染。

在杜松子酒中,Context传递给路由处理程序包含一个名为的字段 Request。该字段包含Header包含所有请求标题的字段。我们可以使用该Get方法Header来提取Accept标题,如下所示:

// c is the Gin Context
c.Request.Header.Get("Accept")

如果设置为application/json,该函数将呈现JSON,

如果设置为application/xml,该函数将呈现XML和

如果这设置为其他任何内容或为空,则该函数将呈现HTML。

完整的render功能如下:

// Render one of HTML, JSON or CSV based on the 'Accept' header of the request
// If the header doesn't specify this, HTML is rendered, provided that
// the template name is present
func render(c *gin.Context, data gin.H, templateName string) {

  switch c.Request.Header.Get("Accept") {
  case "application/json":
    // Respond with JSON
    c.JSON(http.StatusOK, data["payload"])
  case "application/xml":
    // Respond with XML
    c.XML(http.StatusOK, data["payload"])
  default:
    // Respond with HTML
    c.HTML(http.StatusOK, templateName, data)
  }

}

使用单元测试修改路由处理程序的要求

由于我们现在预计会设置JSON和XML响应,因此我们应该在handlers.article_test.go文件中添加测试以测试这些条件。我们将添加测试到:

测试应用程序在Accept 标题设置为时返回文章的JSON列表application/json

测试应用程序在Accept 标题设置为时返回XML格式的文章application/xml

这些将被添加为名为TestArticleListJSON和 函数TestArticleXML。

更新路由处理程序

路由处理程序并不需要太多改变,因为以任何格式呈现的逻辑几乎都是相同的。所有需要做的就是使用 render函数而不是使用c.HTML方法进行渲染。

例如,showIndexPage路由处理程序将从中更改

func showIndexPage(c *gin.Context) {
  articles := getAllArticles()

  // Call the HTML method of the Context to render a template
  c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the index.html template
    "index.html",
    // Pass the data that the page uses
    gin.H{
      "title":   "Home Page",
      "payload": articles,
    },
  )

}

func showIndexPage(c *gin.Context) {
  articles := getAllArticles()

  // Call the render function with the name of the template to render
  render(c, gin.H{
    "title":   "Home Page",
    "payload": articles}, "index.html")

}

以JSON格式检索文章列表

要查看我们的最新动态更新,请构建并运行您的应用程序。然后执行以下命令:

curl -X GET -H "Accept: application/json" http://localhost:8080/

这应该返回一个响应,如下所示:

[{"id":1,"title":"Article 1","content":"Article 1 body"},{"id":2,"title":"Article 2","content":"Article 2 body"}]

正如你所看到的,我们的请求得到了JSON格式的响应,因为我们设置了 Accept标头application/json。

以XML格式检索文章

现在让我们的应用程序以XML格式回复特定文章的细节。要做到这一点,首先要开始你的应用程序,如上所述。现在执行以下命令:

curl -X GET -H “Accept:application / xml” http:// localhost:8080 / article / view / 1

这应该返回一个响应,如下所示:

<article><ID>1</ID><Title>Article 1</Title><Content>Article 1 body</Content></article>

正如您所看到的,我们的请求以XML格式得到响应,因为我们将Accept标头设置 为application/xml。

测试应用程序

由于我们一直在使用测试来为我们的路由处理程序和模型创建规范,因此我们应该不断运行它们以确保功能按预期工作。现在让我们运行我们编写的测试并查看结果。在您的项目目录中,执行以下命令:

go test -v

执行这个命令应该会输出如下结果:

=== RUN   TestShowIndexPageUnauthenticated
[GIN] 2016/06/14 - 19:07:26 | 200 |     183.315µs |  |   GET     /
--- PASS: TestShowIndexPageUnauthenticated (0.00s)
=== RUN   TestArticleUnauthenticated
[GIN] 2016/06/14 - 19:07:26 | 200 |     143.789µs |  |   GET     /article/view/1
--- PASS: TestArticleUnauthenticated (0.00s)
=== RUN   TestArticleListJSON
[GIN] 2016/06/14 - 19:07:26 | 200 |      51.087µs |  |   GET     /
--- PASS: TestArticleListJSON (0.00s)
=== RUN   TestArticleXML
[GIN] 2016/06/14 - 19:07:26 | 200 |      38.656µs |  |   GET     /article/view/1
--- PASS: TestArticleXML (0.00s)
=== RUN   TestGetAllArticles
--- PASS: TestGetAllArticles (0.00s)
=== RUN   TestGetArticleByID
--- PASS: TestGetArticleByID (0.00s)
PASS
ok    github.com/demo-apps/go-gin-app 0.084s

从这个输出中可以看出,这个命令运行我们编写的所有测试,在这种情况下,表明我们的应用程序正在按照我们的意图工作。如果仔细观察输出,您会注意到Go在测试路由处理程序的过程中发出了HTTP请求。

在Semaphore上进行GO持续集成

您可以通过在Semaphore上设置持续集成来自动执行测试,它将在每台托管机器上运行您的测试。git push

首先,如果您还没有Semaphore帐户,请注册一个免费信号量帐户。剩下要做的就是添加你的仓库,并且下面的构建步骤将被添加来运行你的测试:

go get -t -d -v ./... && go build -v ./...
go test -v ./...

Semaphore附带预装的几个版本的Go,这会加快您的设置时间。

结论

在本教程中,我们使用Gin创建了一个新的Web应用程序,并逐渐添加了更多功能。我们使用测试来构建健壮的路由处理程序,并了解如何以最小的努力重复使用相同的代码以多种格式呈现响应。

所有应用程序的代码:https://github.com/demo-apps/go-gin-app

Gin很容易入门 - 与Go的内置功能相结合,其功能使构建高质量,经过充分测试的Web应用程序和微服务变得轻而易举。如果您有任何问题或意见,请随时在下面发布。

上一篇下一篇

猜你喜欢

热点阅读