用Gin和CI/CD构建Go微服务

318 阅读27分钟

在本教程中,你将学习如何用Gin框架构建传统的Web应用和Go微服务。Gin是一个框架,可以减少通常用于构建这些应用程序的模板代码。它还可以很好地创建可重用和可扩展的代码片段。

本教程的这一部分将帮助你建立你的项目,并使用Gin建立一个简单的应用程序,显示文章列表和文章详情页。

目标

在本教程结束时,你将。

  • 学习如何使用Gin来构建一个Web应用。
  • 理解用Go编写的网络应用程序的各个部分,以及
  • 了解如何使用Semaphore持续集成来快速、安全地测试和构建应用程序。

前提条件

对于本教程,你需要在你的机器上安装GoGitcurl

你可以在这个资源库中找到本教程的全部源代码,请随时分叉

tomfern / semaphore-demo-go-gin

什么是Gin?

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

为什么是Gin?

Go的一个最好的特点是它内置的net/http 库,可以让你毫不费力地创建一个HTTP服务器。然而,它也不太灵活,需要一些模板代码来实现。

Go中没有内置支持来处理基于正则表达式或 "模式 "的路由。你可以编写代码来添加这个功能。然而,随着你的应用程序数量的增加,你很可能会到处重复这样的代码,或者创建一个库来重复使用。

这就是Gin所提供的关键所在。它包含了一组常用的功能,例如路由、中间件支持、渲染,这些功能可以减少模板代码,使编写Web应用更加简单。

设计Go的微服务

让我们快速看一下Gin中是如何处理一个请求的。一个典型的Web应用、API服务器或微服务的控制流程如下。

当一个请求进来的时候,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请求)上提供索引页。
  • /article 路由下分组文章相关的路由。在/article/view/:article_id (HTTP GET请求)上提供文章页面。请注意这个路由中的:article_id 部分。在开头的: 表示这是一个动态路由。这意味着:article_id 可以包含任何值,Gin将使这个值在路由处理程序中可用。

渲染

网络应用程序可以以各种格式渲染响应,如HTML、文本、JSON、XML或其他格式。API端点和微服务通常用数据来响应,通常是JSON格式,但也有任何其他所需的格式。

在下一节,我们将看到我们如何在不重复任何功能的情况下呈现不同类型的响应。我们将主要用一个HTML模板来响应一个请求。然而,我们也将定义两个端点,可以用JSON或XML数据进行响应。

中间件

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

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

Gin允许我们编写中间件,实现一些在处理多个路由时需要共享的通用功能。这样可以保持代码库的小型化,分离关注点,提高代码的可维护性。

我们想确保一些页面和操作,例如创建文章、注销,只对登录的用户可用。我们还想确保一些页面和操作,例如注册、登录,只对未登录的用户有效。

如果我们要在每一个路由中都设置这种逻辑,那将是相当乏味、重复和容易出错的。幸运的是,我们可以为这些任务中的每一项创建中间件,并在特定路由中重用它们。

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

启动项目

创建(或克隆)一个GitHub仓库并初始化Go模块文件。

$ go mod init github.com/YOUR_USERNAME/semaphore-demo-go-gin
$ go get -u github.com/gin-gonic/gin

这个应用程序将只使用一个外部依赖: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" . }}

还有页脚。

<!--footer.html-->

  </body>

</html>

正如你所看到的,我们正在使用开源的Bootstrap框架。这个文件的大部分是标准的HTML。然而,请注意两行。包含<title>{{ .title }}</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 文件,它将使用索引模板。我们可以通过四个步骤使用Gin来完成这个工作。

1.创建路由器

在Gin中创建路由器的默认方式如下。

router := gin.Default()

这就创建了一个路由器,可以用来定义应用程序的构建。

2.加载模板

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

router.LoadHTMLGlob("templates/*")

这将加载位于templates 文件夹中的所有模板文件。一旦加载,这些文件就不必在每次请求时再次被读取,使Gin网络应用程序非常快。

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请求的路由处理程序。它的参数是路由 (/) 和一个或多个路由处理程序,它们只是函数。

路由处理程序有一个指向上下文 (gin.Context) 的指针作为参数。这个上下文包含了处理程序可能需要的关于请求的所有信息。例如,它包括关于头文件、cookies等的信息。

该上下文也有方法来呈现HTML、文本、JSON和XML格式的响应。在这种情况下,我们使用context.HTML 方法来渲染一个HTML模板(index.html)。对这个方法的调用包括额外的数据,其中title 的值被设置为Home Page 。这是一个HTML模板可以利用的值。在这种情况下,我们在页眉的模板中的<title> 标签中使用这个值。

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 文件应该包含以下代码。

// 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/*")

   // Handle Index
   router.GET("/", showIndexPage)

  // Start serving the application
  router.Run()

}

设计文章模型

我们将保持文章结构简单,只有三个字段--IdTitleContent 。这可以用一个struct 来表示,如下所示。

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 文件中。

让我们先为getAllArticles() 函数创建单元测试(TestGetAllArticles) 。创建这个单元测试后,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" .}}

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

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

  1. 处理程序响应的HTTP状态代码为200。
  2. 返回的HTML包含一个包含文本Home Page 的标题标签。

测试的代码将被放在handlers.article_test.go 文件中的TestShowIndexPageUnauthenticated 函数中。我们将把这个函数所使用的辅助函数放在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 函数执行传入的函数,看它是否返回一个布尔真值--表示测试成功,或不成功。这个函数帮助我们避免重复测试HTTP请求的响应所需的代码。

为了检查HTTP代码和返回的HTML,我们要做以下工作。

  1. 创建一个新的路由器。
  2. 定义一个路由,使用与主程序相同的处理程序(showIndexPage)。
  3. 创建一个新的请求来访问这个路由。
  4. 创建一个处理响应的函数,以测试HTTP代码和HTML,并且
  5. 用这个新函数调用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

显示单篇文章

在上一节中,虽然我们显示了一个文章的列表,但文章的链接却没有发挥作用。在这一节中,我们将添加处理程序和模板,以便在选中一篇文章时显示它。

设置路由

我们可以设置一个新的路由,以处理对单一文章的请求,其方式与前一个路由相同。然而,我们需要考虑到这样一个事实:虽然所有文章的处理程序都是一样的,但每篇文章的URL都是不同的。Gin允许我们通过定义路由参数来处理这种情况,如下所示。

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

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

更新后的main.go 文件应包含以下代码。

func main() {

    router := gin.Default()
    router.LoadHTMLGlob("templates/*")

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

    router.Run()
}

. . .

创建视图模板

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

templates/article.html 中创建文章模板。

<!--article.html-->

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

<!--Display the title of the article-->
<h1>{{.payload.Title}}</h1>

<!--Display the content of the article-->
<p>{{.payload.Content}}</p>

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

指定对Go微服务路由器的要求

对该路由的处理程序的测试将检查以下条件。

  • 处理程序响应的HTTP状态代码为200。
  • 返回的HTML包含一个标题标签,其中包含被获取的文章的标题。

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

创建路由处理程序

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

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

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

c.Param("article_id")

其中c 是Gin Context,它是使用Gin时任何路由处理程序的参数。

2.2. 提取文章

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

article, err := getArticleByID(articleID)

在添加了getArticleByID 之后,models.article.go 文件应该看起来像这样。

// models.article.go

package main

import (
  "errors"
)

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
}

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 ,它应该是这样的。

Article

本节中添加的新文件如下。

└── templates
    └── article.html

使用JSON/XML进行响应

在这一节中,我们将对应用程序进行一些重构,这样,根据请求头的不同,我们的应用程序可以用HTML、JSON或XML格式进行响应。

创建一个可重复使用的函数

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

一个路由处理程序必须做同样的验证、数据获取和数据处理,而不考虑所需的响应格式。一旦这部分完成了,这些数据就可以被用来生成所需格式的响应。如果我们需要一个HTML响应,我们可以将这些数据传递给HTML模板并生成页面。如果我们需要一个JSON响应,我们可以将这个数据转换为JSON并发送回来。同样,对于XML也是如此。

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

在Gin中,传递给路由处理程序的Context 包含一个名为Request 的字段。这个字段包含了包含所有请求头的Header 字段。我们可以在Header 上使用Get 方法来提取Accept 头信息,方法如下。

// c is the Gin Context
c.Request.Header.Get("Accept")
  • 如果这被设置为application/json ,该函数将渲染JSON。
  • 如果设置为application/xml ,该函数将呈现XML,并且
  • 如果this被设置为其他内容或为空,该函数将渲染HTML。

render 函数如下,将其添加到handlers.article.go 文件中。

// 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 文件中添加测试来测试这些条件。我们将添加测试来。

  1. 测试当Accept 标头被设置为JSON时,应用程序返回一个文章的JSON列表。application/json
  2. 测试当Accept 标头被设置为时,应用程序返回XML格式的文章。application/xml

这些将被添加为名为TestArticleListJSONTestArticleXML 的函数。

更新路由处理程序

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

例如,handler.article.go 中的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 header设为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 header设置为application/xml

测试应用程序

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

go test -v

执行这个命令的结果应该与此类似。

=== RUN   TestShowIndexPageUnauthenticated
[GIN] 2022/05/11 - 11:33:20 | 200 |     429.084µs |                 | GET      "/"
--- PASS: TestShowIndexPageUnauthenticated (0.00s)
=== RUN   TestGetAllArticles
--- PASS: TestGetAllArticles (0.00s)
PASS
ok  	github.com/tomfern/semaphore-demo-go-gin	0.704s

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

Semaphore上的Go持续集成

持续集成(CI)可以在一个快速、干净的环境中为我们测试和构建应用程序。当我们准备发布时,持续交付(CD)可以进行发布,因为我们知道代码通过了所有的测试,所以很放心。

首先,我们要在GitHub中获取所有的代码。

  • 如果你从头开始创建应用程序。
$ git init
$ git remote add YOUR_REPOSITORY_URL
$ git add -A
$ git commit -m "initial commit"
$ git push origin main
  • 如果你喜欢用一个现成的例子工作。

tomfern / semaphore-demo-go-gin

在你的项目中添加Semaphore

将CI/CD添加到您的项目中是完全免费的 ,只需几分钟时间。

  1. 注册一个免费的Semaphore账户。
  2. 点击顶部导航栏上的 "创建新项目"。

  1. 找到你的项目并点击选择

  1. 选择Go starter工作流程,点击Customize

  1. 将工作命令中的第一行改为。
sem-version go 1.18

  1. 点击运行工作流程>开始

就这样,在每次推送时,CI管道将测试和构建应用程序。

改进管道

起步阶段的CI管道应能无缝工作,无需任何额外设置。然而,我们可以做一些改进。

  • 每次都要下载依赖项。我们可以使用一个缓存来保存它们,并加快事情的进展。
  • 测试和构建是在同一个作业中。我们应该把它分成不同的作业,这样我们以后添加更多的测试就会更容易。

使用编辑工作流按钮,打开工作流生成器。

构建器的主要元素是。

  • 管线。一个管道有一个特定的目标,例如:测试。管道是由块组成的,在代理中从左到右执行。
  • 代理。代理是为管道提供动力的虚拟机。我们有三种机器类型可供选择。该机器运行一个优化的Ubuntu 20.04图像,带有许多语言的构建工具。
  • 区块:区块将可以并行执行的作业分组。一个区块中的作业通常有类似的命令和配置。一旦一个区块中的所有工作完成,下一个区块就开始了。
  • 作业:作业定义了执行工作的命令。它们从其父块中继承其配置。

我们将对第一个块做一个改进的版本。

  1. 点击第一个块,将其名称改为:"安装依赖"。
  2. 在下面你会发现工作,把它的名字改成 "安装",并在框中输入以下命令。
sem-version go 1.18
export GO111MODULE=on
export GOPATH=~/go
export PATH=/home/semaphore/go/bin:$PATH
checkout
cache restore
go mod vendor
cache store

  • 点击右上角的 "运行工作流",然后开始。

我们修改了这个块,使它只下载Go的依赖项。

  • sem-version:Semaphore内置命令,用于管理编程语言版本。Semaphore支持大多数Go版本
  • checkout:另一个内置命令,checkout克隆版本库并改变当前目录。
  • go mod vendor:这是一条Go命令,将依赖项下载到vendor目录中,这样就可以进行缓存。
  • cache:cache命令提供了对Semaphore缓存的读写权限,这是一个项目范围内的作业存储。

作业第一次运行时,Go会下载依赖项,Semaphore会将其存储在缓存中。在接下来的所有运行中,Semaphore将恢复它们,Go不需要再次下载它们,从而大大加快了运行的速度。

使用Semaphore进行测试

我们希望我们的CI管道能够测试该项目并构建二进制文件。我们将为此再添加两个块。

  1. 点击编辑工作流程
  2. 使用**+添加块**的虚线按钮来创建一个新的块。将该块命名为 "测试"
  3. 打开 "序幕"部分,输入以下命令。这些命令会区块中的所有作业之前执行。
sem-version go 1.18
export GO111MODULE=on
export GOPATH=~/go
export PATH=/home/semaphore/go/bin:$PATH
checkout
cache restore
go mod vendor
  1. 将作业的名称设为 "测试",并输入以下命令。
go test ./...

转到微服务

  1. 添加第三个块,让我们称它为 "Build"
  2. 重复前面的序言环境变量步骤。
  3. 将作业的名称设为 "Build"
  4. 在框中输入以下命令。
go build -v -o go-gin-app
artifact push project --force go-gin-app

  1. 点击 "运行工作流",然后点击 "开始"。

现在,更新的管道已经完成。

我们在构建作业中使用的artifact命令将二进制文件上传到项目的存储器中。要访问它,请使用项目工件按钮。

Semaphore有三个独立的工件存储。作业工作流项目。欲了解更多信息,请查看工件文档。

总结

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

整个应用的代码可以在这个Github资源库中找到。

Gin很容易上手--再加上Go的内置功能,它的特性使得构建高质量、经过测试的网络应用程序和微服务变得轻而易举。如果你有任何问题或意见,欢迎在下面发表。

The postBuilding Go Microservice with Gin and CI/CDappeared first onSemaphore.