Go-Web-开发秘籍(全)

Go Web 开发秘籍(全)

原文:zh.annas-archive.org/md5/6712F93A50A8E516D2DB7024F42646AC

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Go 是一种设计用于扩展并支持语言级并发性的开源编程语言,这使得开发人员可以轻松编写大型并发的 Web 应用程序。

从创建 Web 应用程序到在 AWS 上部署,这将是一个学习 Go Web 开发的一站式指南。无论您是新手程序员还是专业开发人员,本书都将帮助您快速掌握 Go Web 开发。

本书将专注于在 Go 中编写模块化代码,并包含深入的信息性配方,逐步构建基础。您将学习如何创建服务器、处理 HTML 表单、会话和错误处理、SQL 和 NoSQL 数据库、Beego、创建和保护 RESTful Web 服务、创建、单元测试和调试 WebSockets,以及创建 Go Docker 容器并在 AWS 上部署它们等概念和配方。

通过本书,您将能够将您在 Go 中学到的新技能应用于在任何领域创建和探索 Web 应用程序。

本书适合人群

本书适用于希望使用 Go 编写大型并发 Web 应用程序的开发人员。对 Go 有一定了解的读者会发现本书最有益。

本书内容

第一章《在 Go 中创建您的第一个服务器》解释了如何编写和与 HTTP 和 TCP 服务器交互,使用 GZIP 压缩优化服务器响应,并在 Go Web 应用程序中实现路由和日志记录。

第二章《处理模板、静态文件和 HTML 表单》介绍了如何创建 HTML 模板;从文件系统中提供静态资源;创建、读取和验证 HTML 表单;以及为 Go Web 应用程序实现简单的用户身份验证。

第三章《在 Go 中处理会话、错误和缓存》探讨了实现 HTTP 会话、HTTP cookie、错误处理和缓存,以及使用 Redis 管理 HTTP 会话,这对于在多个数据中心部署的 Web 应用程序是必需的。

第四章《在 Go 中编写和消费 RESTful Web 服务》解释了如何编写 RESTful Web 服务、对其进行版本控制,并创建 AngularJS 与 TypeScript 2、ReactJS 和 VueJS 客户端来消费它们。

第五章《使用 SQL 和 NoSQL 数据库》介绍了在 Go Web 应用程序中使用 MySQL 和 MongoDB 数据库实现 CRUD 操作。

第六章《使用微服务工具包 Go 编写微服务》专注于使用协议缓冲区编写和处理微服务,使用微服务发现客户端(如 Consul),使用 Go Micro 编写微服务,并通过命令行和 Web 仪表板与它们进行交互,以及实现 API 网关模式以通过 HTTP 协议访问微服务。

第七章《在 Go 中使用 WebSocket》介绍了如何编写 WebSocket 服务器及其客户端,以及如何使用 GoLand IDE 编写单元测试并进行调试。

第八章《使用 Go Web 应用程序框架-Beego》介绍了设置 Beego 项目架构,编写控制器、视图和过滤器,实现与 Redis 支持的缓存,以及使用 Nginx 监控和部署 Beego 应用程序。

第九章《使用 Go 和 Docker》介绍了如何编写 Docker 镜像、创建 Docker 容器、用户定义的 Docker 网络、使用 Docker Registry,并运行与另一个 Docker 容器链接的 Go Web 应用程序 Docker 容器。

第十章,保护 Go Web 应用程序,演示了使用 OpenSSL 创建服务器证书和私钥,将 HTTP 服务器转移到 HTTPS,使用 JSON Web Token(JWT)保护 RESTful API,并防止 Go Web 应用程序中的跨站点请求伪造。

第十一章,将 Go Web 应用程序和 Docker 容器部署到 AWS,讨论了设置 EC2 实例,交互以及在其上运行 Go Web 应用程序和 Go Docker 容器。

充分利用本书

读者应具备 Go 的基本知识,并在计算机上安装 Go 以执行说明和代码。

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,以便将文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packtpub.com

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Go-Web-Development-Cookbook。我们还有来自丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。快去看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/GoWebDevelopmentCookbook_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:“GZIP 压缩意味着从服务器以.gzip格式向客户端发送响应,而不是发送纯文本响应。”

代码块设置如下:

for 
{
  conn, err := listener.Accept()
  if err != nil 
  {
    log.Fatal("Error accepting: ", err.Error())
  }
  log.Println(conn)
}

任何命令行输入或输出都以以下方式编写:

$ go get github.com/gorilla/handlers
$ go get github.com/gorilla/mux

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中出现。例如:“AngularJS 客户端页面具有 HTML 表单,其中显示如下的 Id、FirstName 和 LastName 字段。”

警告或重要说明看起来像这样。

提示和技巧看起来像这样。

章节

在本书中,您会经常看到几个标题(准备工作如何做工作原理更多内容另请参阅)。

为了清晰地说明如何完成配方,使用以下各节:

准备工作

本节告诉您该配方中可以期望的内容,并描述了为该配方设置任何软件或任何先决设置所需的步骤。

如何做…

本节包含遵循该配方所需的步骤。

工作原理…

本节通常包括对前一节发生的事情的详细解释。

更多内容…

本节包含有关该配方的其他信息,以使您对该配方更加了解。

另请参阅

本节提供了有关该配方的其他有用信息的链接。

第一章:在 Go 中创建你的第一个服务器

在本章中,我们将涵盖以下内容:

  • 创建一个简单的 HTTP 服务器

  • 在一个简单的 HTTP 服务器上实现基本身份验证

  • 使用 GZIP 压缩优化 HTTP 服务器响应

  • 创建一个简单的 TCP 服务器

  • 从 TCP 连接读取数据

  • 向 TCP 连接写入数据

  • 实现 HTTP 请求路由

  • 使用 Gorilla Mux 实现 HTTP 请求路由

  • 记录 HTTP 请求

介绍

Go 是为了解决多核处理器的新架构带来的问题而创建的,它创建了高性能网络,可以处理数百万个请求和计算密集型任务。Go 的理念是通过实现快速原型设计、减少编译和构建时间以及实现更好的依赖管理来提高生产力。

与大多数其他编程语言不同,Go 提供了net/http包,用于创建 HTTP 客户端和服务器。本章将介绍在 Go 中创建 HTTP 和 TCP 服务器。

我们将从一些简单的示例开始,创建一个 HTTP 和 TCP 服务器,并逐渐转向更复杂的示例,其中我们实现基本身份验证、优化服务器响应、定义多个路由和记录 HTTP 请求。我们还将涵盖 Go 处理程序、Goroutines 和 Gorilla 等概念和关键字-Go 的 Web 工具包。

创建一个简单的 HTTP 服务器

作为程序员,如果你需要创建一个简单的 HTTP 服务器,那么你可以很容易地使用 Go 的net/http包来编写,我们将在这个示例中介绍。

如何做…

在这个示例中,我们将创建一个简单的 HTTP 服务器,当我们在浏览器中浏览http://localhost:8080或在命令行中执行curl http://localhost:8080时,它将呈现 Hello World!执行以下步骤:

  1. 创建http-server.go并复制以下内容:
package main
import 
(
  "fmt"
  "log"
  "net/http"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
func helloWorld(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Hello World!")
}
func main() 
{
  http.HandleFunc("/", helloWorld)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-server.go

它是如何工作的…

一旦我们运行程序,一个 HTTP 服务器将在本地监听端口8080。在浏览器中打开http://localhost:8080将显示来自服务器的 Hello World!,如下面的屏幕截图所示:

你好,世界!

让我们理解程序中每一行的含义:

  • package main: 这定义了程序的包名称。

  • import ( "fmt" "log" "net/http" ): 这是一个预处理命令,告诉 Go 编译器包括fmtlognet/http包中的所有文件。

  • const ( CONN_HOST = "localhost" CONN_PORT = "8080" ): 我们使用const关键字在 Go 程序中声明常量。这里我们声明了两个常量-一个是CONN_HOST,值为 localhost,另一个是CONN_PORT,值为8080

  • func helloWorld(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello World!") }: 这是一个 Go 函数,它以ResponseWriterRequest作为输入,并在 HTTP 响应流上写入Hello World!

接下来,我们声明了main()方法,程序执行从这里开始,因为这个方法做了很多事情。让我们逐行理解它:

  • http.HandleFunc("/", helloWorld): 在这里,我们使用net/http包的HandleFunc注册了helloWorld函数与/URL 模式,这意味着每当我们访问具有模式/的 HTTP URL 时,helloWorld会被执行,并将(http.ResponseWriter, *http.Request)作为参数传递给它。

  • err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil): 在这里,我们调用http.ListenAndServe来处理每个传入连接的 HTTP 请求,每个连接在一个单独的 Goroutine 中处理。ListenAndServe接受两个参数-服务器地址和处理程序。在这里,我们将服务器地址传递为localhost:8080,处理程序为nil,这意味着我们要求服务器使用DefaultServeMux作为处理程序。

  • if err != nil { log.Fatal("error starting http server : ", err) return}:在这里,我们检查是否有问题启动服务器。如果有问题,那么记录错误并以状态码1退出。

在简单的 HTTP 服务器上实现基本身份验证

一旦创建了 HTTP 服务器,您可能希望限制特定用户访问资源,例如应用程序的管理员。如果是这样,那么您可以在 HTTP 服务器上实现基本身份验证,我们将在这个配方中介绍。

准备工作

由于我们已经在上一个配方中创建了一个 HTTP 服务器,我们只需扩展它以包含基本身份验证。

如何做…

在这个配方中,我们将通过添加BasicAuth函数并修改HandleFunc来调用它来更新我们在上一个配方中创建的 HTTP 服务器。执行以下步骤:

  1. 创建http-server-basic-authentication.go并复制以下内容:
package main
import 
(
  "crypto/subtle"
  "fmt"
  "log"
  "net/http"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  ADMIN_USER = "admin"
  ADMIN_PASSWORD = "admin"
)
func helloWorld(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Hello World!")
}
func BasicAuth(handler http.HandlerFunc, realm string) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) 
  {
    user, pass, ok := r.BasicAuth()
    if !ok || subtle.ConstantTimeCompare([]byte(user),
    []byte(ADMIN_USER)) != 1||subtle.ConstantTimeCompare([]byte(pass), 
    []byte(ADMIN_PASSWORD)) != 1 
    {
      w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
      w.WriteHeader(401)
      w.Write([]byte("You are Unauthorized to access the
      application.\n"))
      return
    }
    handler(w, r)
  }
}
func main() 
{
  http.HandleFunc("/", BasicAuth(helloWorld, "Please enter your
  username and password"))
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-server-basic-authentication.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080上启动。

一旦服务器启动,在浏览器中访问http://localhost:8080将提示您输入用户名和密码。提供adminadmin将在屏幕上呈现 Hello World!对于其他用户名和密码的组合,它将呈现您未经授权访问应用程序。

要从命令行访问服务器,我们必须在curl命令中提供--user标志,如下所示:

$ curl --user admin:admin http://localhost:8080/
Hello World!

我们还可以使用base64编码的username:password令牌访问服务器,我们可以从任何网站(例如https://www.base64encode.org/)获取,并将其作为curl命令中的授权标头传递,如下所示:

$ curl -i -H 'Authorization:Basic YWRtaW46YWRtaW4=' http://localhost:8080/

HTTP/1.1 200 OK
Date: Sat, 12 Aug 2017 12:02:51 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8
Hello World!

让我们了解我们引入的更改作为这个配方的一部分:

  • import函数添加了一个额外的包,crypto/subtle,我们将使用它来比较用户输入凭据中的用户名和密码。

  • 使用const函数,我们定义了两个额外的常量,ADMIN_USERADMIN_PASSWORD,我们将在验证用户时使用它们。

  • 接下来,我们声明了一个BasicAuth()方法,它接受两个输入参数——一个处理程序,在用户成功验证后执行,和一个领域,返回HandlerFunc,如下所示:

func BasicAuth(handler http.HandlerFunc, realm string) http.HandlerFunc 
{
  return func(w http.ResponseWriter, r *http.Request)
  {
    user, pass, ok := r.BasicAuth()
    if !ok || subtle.ConstantTimeCompare([]byte(user),
    []byte(ADMIN_USER)) != 1||subtle.ConstantTimeCompare
    ([]byte(pass),
    []byte(ADMIN_PASSWORD)) != 1
    {
      w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
      w.WriteHeader(401)
      w.Write([]byte("Unauthorized.\n"))
      return
    }
    handler(w, r)
  }
}

在前面的处理程序中,我们首先使用r.BasicAuth()获取请求的授权标头中提供的用户名和密码,然后将其与程序中声明的常量进行比较。如果凭据匹配,则返回处理程序,否则设置WWW-Authenticate以及状态码401,并在 HTTP 响应流上写入You are Unauthorized to access the application

最后,我们在main()方法中引入了一个更改,以从HandleFunc中调用BasicAuth,如下所示:

http.HandleFunc("/", BasicAuth(helloWorld, "Please enter your username and password"))

我们只需传递一个BasicAuth处理程序,而不是nilDefaultServeMux来处理所有带有 URL 模式为/的传入请求。

使用 GZIP 压缩优化 HTTP 服务器响应

GZIP 压缩意味着从服务器以.gzip格式向客户端发送响应,而不是发送纯文本响应,如果客户端/浏览器支持的话,发送压缩响应总是一个好习惯。

通过发送压缩响应,我们节省了网络带宽和下载时间,最终使页面加载更快。 GZIP 压缩的原理是浏览器发送一个请求标头,告诉服务器它接受压缩内容(.gzip.deflate),如果服务器有能力以压缩形式发送响应,则发送压缩形式的响应。如果服务器支持压缩,则它将设置Content-Encoding: gzip作为响应标头,否则它将向客户端发送一个纯文本响应,这清楚地表示要求压缩响应只是浏览器的请求,而不是要求。我们将使用 Gorilla 的 handlers 包在这个配方中实现它。

如何做…

在本教程中,我们将创建一个带有单个处理程序的 HTTP 服务器,该处理程序将在 HTTP 响应流上写入 Hello World!并使用 Gorilla CompressHandler.gzip格式将所有响应发送回客户端。执行以下步骤:

  1. 使用大猩猩处理程序,首先我们需要使用go get命令安装包,或者手动将其复制到$GOPATH/src$GOPATH,如下所示:
$ go get github.com/gorilla/handlers
  1. 创建http-server-mux.go并复制以下内容:
package main
import 
(
  "io"
  "net/http"
  "github.com/gorilla/handlers"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
func helloWorld(w http.ResponseWriter, r *http.Request) 
{
  io.WriteString(w, "Hello World!")
}
func main() 
{
  mux := http.NewServeMux()
  mux.HandleFunc("/", helloWorld)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT,
  handlers.CompressHandler(mux))
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-server-mux.go

工作原理…

运行程序后,HTTP 服务器将在本地监听端口8080

在浏览器中打开http://localhost:8080将显示来自服务器的 Hello World!并显示 Content-Encoding 响应头值 gzip,如下面的屏幕截图所示:

你好,世界!

让我们了解程序中每一行的含义:

  • package main:这定义了程序的包名称。

  • import ( "io" "net/http" "github.com/gorilla/handlers" ): 这是一个预处理命令,告诉 Go 编译器包括来自ionet/httpgithub.com/gorilla/handlers包的所有文件。

  • const ( CONN_HOST = "localhost" CONN_PORT = "8080" ): 我们使用 const 关键字在 Go 程序中声明常量。在这里,我们声明了两个常量,一个是值为 localhost 的CONN_HOST,另一个是值为 8080 的CONN_PORT

  • func helloWorld(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "Hello World!")}: 这是一个接受ResponseWriterRequest作为输入参数并在 HTTP 响应流上写入Hello World!的 Go 函数。

接下来,我们声明了main()方法,程序的执行从这里开始。由于这个方法做了很多事情,让我们逐行理解它:

  • mux := http.NewServeMux(): 这将分配并返回一个新的 HTTP 请求多路复用器(ServeMux),它将匹配每个传入请求的 URL 与已注册模式列表,并调用最接近 URL 的模式的处理程序。使用它的好处之一是程序完全控制与服务器一起使用的处理程序,尽管任何使用DefaultServeMux注册的处理程序都将被忽略。

  • http.HandleFunc("/", helloWorld): 在这里,我们使用net/http包的HandleFunchelloWorld函数注册到/URL 模式,这意味着每当我们访问具有/模式的 HTTP URL 时,helloWorld将被执行,并将(http.ResponseWriter, *http.Request)作为参数传递给它。

  • err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, handlers.CompressHandler(mux)): 在这里,我们调用http.ListenAndServe来为我们处理每个传入连接的 HTTP 请求。ListenAndServe接受两个参数——服务器地址和处理程序。在这里,我们将服务器地址传递为localhost:8080,处理程序为CompressHandler,它用.gzip处理程序包装我们的服务器以将所有响应压缩为.gzip格式。

  • if err != nil { log.Fatal("error starting http server: ", err) return}: 在这里,我们检查是否有任何启动服务器的问题。如果有问题,记录错误并以状态码 1 退出。

创建一个简单的 TCP 服务器

每当你需要构建高性能导向系统时,编写 TCP 服务器总是优于 HTTP 服务器的最佳选择,因为 TCP 套接字比 HTTP 更轻。Go 支持并提供了一种方便的方法来编写使用net包的 TCP 服务器,我们将在本教程中介绍。

如何做…

在本教程中,我们将创建一个简单的 TCP 服务器,它将在localhost:8080上接受连接。执行以下步骤:

  1. 创建tcp-server.go并复制以下内容:
package main
import 
(
  "log"
  "net"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  CONN_TYPE = "tcp"
)
func main() 
{
  listener, err := net.Listen(CONN_TYPE, CONN_HOST+":"+CONN_PORT)
  if err != nil 
  {
    log.Fatal("Error starting tcp server : ", err)
  }
  defer listener.Close()
  log.Println("Listening on " + CONN_HOST + ":" + CONN_PORT)
  for 
  {
    conn, err := listener.Accept()
    if err != nil 
    {
      log.Fatal("Error accepting: ", err.Error())
    }
    log.Println(conn)
  }
}
  1. 使用以下命令运行程序:
$ go run tcp-server.go

工作原理…

运行程序后,TCP 服务器将在本地监听端口8080

让我们理解程序中每一行的含义:

  • package main: 这定义了程序的包名称。

  • import ( "log" "net"): 这是一个预处理命令,告诉 Go 编译器包括lognet包中的所有文件。

  • const ( CONN_HOST = "localhost" CONN_PORT = "8080" CONN_TYPE = "tcp" ): 我们使用 const 关键字在 Go 程序中声明常量。在这里,我们声明了三个常量——一个是CONN_HOST,值为localhost,另一个是CONN_PORT,值为8080,最后一个是CONN_TYPE,值为tcp

接下来,我们从main()方法中声明了main()方法,程序执行从这里开始。由于这个方法做了很多事情,让我们逐行理解它:

  • listener, err := net.Listen(CONN_TYPE, CONN_HOST+":"+CONN_PORT): 这将在本地端口8080上创建一个 TCP 服务器。

  • if err != nil { log.Fatal("Error starting tcp server: ", err) }: 在这里,我们检查是否有问题启动 TCP 服务器。如果有问题,就记录错误并以状态码 1 退出。

  • defer listener.Close(): 这个延迟语句在应用程序关闭时关闭 TCP 套接字监听器。

接下来,我们在一个常量循环中接受 TCP 服务器的传入请求,如果在接受请求时出现任何错误,我们将记录并退出;否则,我们只是在服务器控制台上打印连接对象,如下所示:

for 
{
  conn, err := listener.Accept()
  if err != nil 
  {
    log.Fatal("Error accepting: ", err.Error())
  }
  log.Println(conn)
}

从 TCP 连接读取数据

在任何应用程序中最常见的情况之一是客户端与服务器进行交互。TCP 是这种交互中最广泛使用的协议之一。Go 提供了一种方便的方式通过实现缓冲的Input/Output来读取传入连接数据,我们将在这个示例中介绍。

准备就绪…

由于我们已经在之前的示例中创建了一个 TCP 服务器,我们将更新它以从传入连接中读取数据。

如何做…

在这个示例中,我们将更新main()方法,调用handleRequest方法并传递连接对象以读取和打印服务器控制台上的数据。执行以下步骤:

  1. 创建tcp-server-read-data.go并复制以下内容:
package main
import 
(
  "bufio"
  "fmt"
  "log"
  "net"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  CONN_TYPE = "tcp"
)
func main() 
{
  listener, err := net.Listen(CONN_TYPE, CONN_HOST+":"+CONN_PORT)
  if err != nil 
  {
    log.Fatal("Error starting tcp server : ", err)
  }
  defer listener.Close()
  log.Println("Listening on " + CONN_HOST + ":" + CONN_PORT)
  for 
  {
    conn, err := listener.Accept()
    if err != nil 
    {
      log.Fatal("Error accepting: ", err.Error())
    }
    go handleRequest(conn)
  }
}
func handleRequest(conn net.Conn) 
{
  message, err := bufio.NewReader(conn).ReadString('\n')
  if err != nil 
  {
    fmt.Println("Error reading:", err.Error())
  }
  fmt.Print("Message Received from the client: ", string(message))
  conn.Close()
}
  1. 使用以下命令运行程序:
$ go run tcp-server-read-data.go

工作原理…

一旦我们运行程序,TCP 服务器将在本地端口8080上开始监听。从命令行执行echo命令将向 TCP 服务器发送消息:

$ echo -n "Hello to TCP server\n" | nc localhost 8080

这显然会将其记录到服务器控制台,如下面的屏幕截图所示:

让我们理解这个示例中引入的变化:

  1. 首先,我们使用go关键字从main()方法中调用handleRequest,这意味着我们在 Goroutine 中调用函数,如下所示:
func main() 
{
  ...
  go handleRequest(conn)
  ...
}
  1. 接下来,我们定义了handleRequest函数,它将传入的连接读入缓冲区,直到第一个\n出现,并在控制台上打印消息。如果在读取消息时出现任何错误,则打印错误消息以及错误对象,最后关闭连接,如下所示:
func handleRequest(conn net.Conn) 
{
  message, err := bufio.NewReader(conn).ReadString('\n')
  if err != nil 
  {
    fmt.Println("Error reading:", err.Error())
  }
  fmt.Print("Message Received: ", string(message))
  conn.Close()
}

向 TCP 连接写入数据

在任何 Web 应用程序中,另一个常见且重要的情况是向客户端发送数据或响应客户端。Go 提供了一种方便的方式,以字节的形式在连接上写入消息,我们将在这个示例中介绍。

准备就绪…

由于我们已经在之前的示例中创建了一个 TCP 服务器,用于读取传入连接的数据,所以我们只需更新它以将消息写回客户端。

如何做…

在这个示例中,我们将更新程序中的handleRequest方法,以便向客户端写入数据。执行以下步骤:

  1. 创建tcp-server-write-data.go并复制以下内容:
package main
import 
(
  "bufio"
  "fmt"
  "log"
  "net"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  CONN_TYPE = "tcp"
)
func main() 
{
  listener, err := net.Listen(CONN_TYPE, CONN_HOST+":"+CONN_PORT)
  if err != nil 
  {
    log.Fatal("Error starting tcp server : ", err)
  }
  defer listener.Close()
  log.Println("Listening on " + CONN_HOST + ":" + CONN_PORT)
  for 
  {
    conn, err := listener.Accept()
    if err != nil 
    {
      log.Fatal("Error accepting: ", err.Error())
    }
    go handleRequest(conn)
  }
}
func handleRequest(conn net.Conn) 
{
  message, err := bufio.NewReader(conn).ReadString('\n')
  if err != nil 
  {
    fmt.Println("Error reading: ", err.Error())
  }
  fmt.Print("Message Received:", string(message))
  conn.Write([]byte(message + "\n"))
  conn.Close()
}
  1. 使用以下命令运行程序:
$ go run tcp-server-write-data.go

工作原理…

一旦我们运行程序,TCP 服务器将在本地端口8080上开始监听。从命令行执行echo命令,如下所示:

$ echo -n "Hello to TCP server\n" | nc localhost 8080

这将为我们提供来自服务器的以下响应:

Hello to TCP server

让我们看看我们在这个示例中引入的更改,以便向客户端写入数据。handleRequest中的一切都与上一个示例中完全相同,只是我们引入了一行新的代码,将数据作为字节数组写入连接,如下所示:

func handleRequest(conn net.Conn) 
{
  ...
  conn.Write([]byte(message + "\n"))
  ...
}

实现 HTTP 请求路由

大多数情况下,您必须在 Web 应用程序中定义多个 URL 路由,这涉及将 URL 路径映射到处理程序或资源。在这个示例中,我们将学习如何在 Go 中实现它。

如何做…

在这个示例中,我们将定义三个路由,如//login/logout,以及它们的处理程序。执行以下步骤:

  1. 创建http-server-basic-routing.go并复制以下内容:
package main
import 
(
  "fmt"
  "log"
  "net/http"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
func helloWorld(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Hello World!")
}
func login(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Login Page!")
}
func logout(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Logout Page!")
}
func main() 
{
  http.HandleFunc("/", helloWorld)
  http.HandleFunc("/login", login)
  http.HandleFunc("/logout", logout)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-server-basic-routing.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080,并且从浏览器或命令行访问http://localhost:8080/http://localhost:8080/loginhttp://localhost:8080/logout将呈现相应处理程序定义中的消息。例如,从命令行执行http://localhost:8080/,如下所示:

$ curl -X GET -i http://localhost:8080/

这将为我们提供来自服务器的以下响应:

我们也可以从命令行执行http://localhost:8080/login,如下所示:

$ curl -X GET -i http://localhost:8080/login

这将为我们提供来自服务器的以下响应:

让我们了解我们编写的程序:

  1. 我们首先定义了三个处理程序或 Web 资源,如下所示:
func helloWorld(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Hello World!")
}
func login(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Login Page!")
}
func logout(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Logout Page!")
}

在这里,helloWorld处理程序在 HTTP 响应流上写入Hello World!。类似地,登录和注销处理程序在 HTTP 响应流上写入Login Page!Logout Page!

  1. 接下来,我们使用http.HandleFunc()DefaultServeMux上注册了三个 URL 路径——//login/logout。如果传入的请求 URL 模式与注册的路径之一匹配,那么相应的处理程序将被调用,并将(http.ResponseWriter*http.Request)作为参数传递给它,如下所示:
func main() 
{
  http.HandleFunc("/", helloWorld)
  http.HandleFunc("/login", login)
  http.HandleFunc("/logout", logout)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}

使用 Gorilla Mux 实现 HTTP 请求路由

Go 的net/http包为 HTTP 请求的 URL 路由提供了许多功能。它做得不太好的一件事是动态 URL 路由。幸运的是,我们可以通过gorilla/mux包实现这一点,我们将在这个示例中介绍。

如何做…

在这个示例中,我们将使用gorilla/mux来定义一些路由,就像我们在之前的示例中所做的那样,以及它们的处理程序或资源。正如我们在之前的示例中已经看到的,要使用外部包,首先我们必须使用go get命令安装包,或者我们必须手动将其复制到$GOPATH/src$GOPATH。我们在这个示例中也会这样做。执行以下步骤:

  1. 使用go get命令安装github.com/gorilla/mux,如下所示:
$ go get github.com/gorilla/mux
  1. 创建http-server-gorilla-mux-routing.go并复制以下内容:
package main
import 
(
  "net/http"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
var GetRequestHandler = http.HandlerFunc
(
  func(w http.ResponseWriter, r *http.Request) 
  {
    w.Write([]byte("Hello World!"))
  }
)
var PostRequestHandler = http.HandlerFunc
(
  func(w http.ResponseWriter, r *http.Request) 
  {
    w.Write([]byte("It's a Post Request!"))
  }
)
var PathVariableHandler = http.HandlerFunc
(
  func(w http.ResponseWriter, r *http.Request) 
  {
    vars := mux.Vars(r)
    name := vars["name"]
    w.Write([]byte("Hi " + name))
  }
)
func main() 
{
  router := mux.NewRouter()
  router.Handle("/", GetRequestHandler).Methods("GET")
  router.Handle("/post", PostRequestHandler).Methods("POST")
  router.Handle("/hello/{name}", 
  PathVariableHandler).Methods("GET", "PUT")
  http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
}
  1. 使用以下命令运行程序:
$ go run http-server-gorilla-mux-routing.go

它是如何工作…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080,并且从浏览器或命令行访问http://localhost:8080/http://localhost:8080/posthttp://localhost:8080/hello/foo将产生相应处理程序定义中的消息。例如,从命令行执行http://localhost:8080/,如下所示:

$ curl -X GET -i http://localhost:8080/

这将为我们提供来自服务器的以下响应:

我们也可以从命令行执行http://localhost:8080/hello/foo,如下所示:

$ curl -X GET -i http://localhost:8080/hello/foo

这将为我们提供来自服务器的以下响应:

让我们了解我们在这个示例中所做的代码更改:

  1. 首先,我们定义了GetRequestHandlerPostRequestHandler,它们只是在 HTTP 响应流上写入一条消息,如下所示:
var GetRequestHandler = http.HandlerFunc
(
  func(w http.ResponseWriter, r *http.Request) 
  {
    w.Write([]byte("Hello World!"))
  }
)
var PostRequestHandler = http.HandlerFunc
(
  func(w http.ResponseWriter, r *http.Request) 
  {
    w.Write([]byte("It's a Post Request!"))
  }
)
  1. 接下来,我们定义了PathVariableHandler,它提取请求路径变量,获取值,并将其写入 HTTP 响应流,如下所示:
var PathVariableHandler = http.HandlerFunc
(
  func(w http.ResponseWriter, r *http.Request) 
  {
    vars := mux.Vars(r)
    name := vars["name"]
    w.Write([]byte("Hi " + name))
  }
)
  1. 然后,我们将所有这些处理程序注册到gorilla/mux路由器中,并对其进行实例化,调用 mux 路由器的NewRouter()处理程序,如下所示:
func main() 
{
  router := mux.NewRouter()
  router.Handle("/", GetRequestHandler).Methods("GET")
  router.Handle("/post", PostCallHandler).Methods("POST")
  router.Handle("/hello/{name}", PathVariableHandler).
  Methods("GET", "PUT")
  http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
}

记录 HTTP 请求

在故障排除 Web 应用程序时,记录 HTTP 请求总是很有用,因此记录具有适当消息和记录级别的请求/响应是一个好主意。Go 提供了log包,可以帮助我们在应用程序中实现日志记录。然而,在这个示例中,我们将使用 Gorilla 日志处理程序来实现它,因为该库提供了更多功能,比如记录 Apache Combined 日志格式和 Apache Common 日志格式,这些功能目前还不受 Go log包支持。

准备就绪...

由于我们已经在之前的示例中创建了一个 HTTP 服务器并使用 Gorilla Mux 定义了路由,我们将更新它以整合 Gorilla 日志处理程序。

如何做...

让我们使用 Gorilla 处理程序实现日志记录。执行以下步骤:

  1. 使用go get命令安装github.com/gorilla/handlergithub.com/gorilla/mux包,如下所示:
$ go get github.com/gorilla/handlers
$ go get github.com/gorilla/mux
  1. 创建http-server-request-logging.go并复制以下内容:
package main
import 
(
  "net/http"
  "os"
  "github.com/gorilla/handlers"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
var GetRequestHandler = http.HandlerFunc
(
  func(w http.ResponseWriter, r *http.Request) 
  {
    w.Write([]byte("Hello World!"))
  }
)
var PostRequestHandler = http.HandlerFunc
(
  func(w http.ResponseWriter, r *http.Request) 
  {
    w.Write([]byte("It's a Post Request!"))
  }
)
var PathVariableHandler = http.HandlerFunc
(
  func(w http.ResponseWriter, r *http.Request) 
  {
    vars := mux.Vars(r)
    name := vars["name"]
    w.Write([]byte("Hi " + name))
  }
)
func main() 
{
  router := mux.NewRouter()
  router.Handle("/", handlers.LoggingHandler(os.Stdout,
  http.HandlerFunc(GetRequestHandler))).Methods("GET")
  logFile, err := os.OpenFile("server.log",
  os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
  router.Handle("/post", handlers.LoggingHandler(logFile,
  PostRequestHandler)).Methods("POST")
  router.Handle("/hello/{name}",
  handlers.CombinedLoggingHandler(logFile,
  PathVariableHandler)).Methods("GET")
  http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
}
  1. 运行程序,使用以下命令:
$ go run http-server-request-logging.go

它是如何工作的...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

从命令行执行GET请求,如下所示:

$ curl -X GET -i http://localhost:8080/

这将在 Apache Common 日志格式中记录请求的详细信息,如下面的屏幕截图所示:

我们也可以从命令行执行http://localhost:8080/hello/foo,如下所示:

$ curl -X GET -i http://localhost:8080/hello/foo

这将在server.log中以 Apache Combined 日志格式记录请求的详细信息,如下面的屏幕截图所示:

让我们了解一下在这个示例中我们做了什么:

  1. 首先,我们导入了两个额外的包,一个是os,我们用它来打开一个文件。另一个是github.com/gorilla/handlers,我们用它来导入用于记录 HTTP 请求的日志处理程序,如下所示:
import ( "net/http" "os" "github.com/gorilla/handlers" "github.com/gorilla/mux" )
  1. 接下来,我们修改了main()方法。使用router.Handle("/", handlers.LoggingHandler(os.Stdout,

http.HandlerFunc(GetRequestHandler))).Methods("GET"),我们用 Gorilla 日志处理程序包装了GetRequestHandler,并将标准输出流作为写入器传递给它,这意味着我们只是要求在控制台上以 Apache Common 日志格式记录每个 URL 路径为/的请求。

  1. 接下来,我们以只写模式创建一个名为server.log的新文件,或者如果它已经存在,则打开它。如果有任何错误,那么记录下来并以状态码 1 退出,如下所示:
logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil 
{
  log.Fatal("error starting http server : ", err)
  return
}
  1. 使用router.Handle("/post", handlers.LoggingHandler(logFile, PostRequestHandler)).Methods("POST"),我们用 Gorilla 日志处理程序包装了GetRequestHandler,并将文件作为写入器传递给它,这意味着我们只是要求在名为/hello/{name}的文件中以 Apache Common 日志格式记录每个 URL 路径为/post的请求。

  2. 使用router.Handle("/hello/{name}", handlers.CombinedLoggingHandler(logFile, PathVariableHandler)).Methods("GET"),我们用 Gorilla 日志处理程序包装了GetRequestHandler,并将文件作为写入器传递给它,这意味着我们只是要求在名为server.log的文件中以 Apache Combined 日志格式记录每个 URL 路径为/hello/{name}的请求。

第二章:使用模板、静态文件和 HTML 表单

在本章中,我们将涵盖以下内容:

  • 创建您的第一个模板

  • 通过 HTTP 提供静态文件

  • 使用 Gorilla Mux 通过 HTTP 提供静态文件

  • 创建您的第一个 HTML 表单

  • 阅读您的第一个 HTML 表单

  • 验证您的第一个 HTML 表单

  • 上传您的第一个文件

介绍

我们经常希望创建 HTML 表单,以便以指定的格式从客户端获取信息,将文件或文件夹上传到服务器,并生成通用的 HTML 模板,而不是重复相同的静态文本。有了本章涵盖的概念知识,我们将能够在 Go 中高效地实现所有这些功能。

在本章中,我们将从创建基本模板开始,然后继续从文件系统中提供静态文件,如.js.cssimages,最终创建、读取和验证 HTML 表单,并将文件上传到服务器。

创建您的第一个模板

模板允许我们定义动态内容的占位符,可以由模板引擎在运行时替换为值。然后可以将它们转换为 HTML 文件并发送到客户端。在 Go 中创建模板非常容易,使用 Go 的html/template包,我们将在本示例中介绍。

如何做…

在这个示例中,我们将创建一个first-template.html,其中包含一些占位符,其值将在运行时由模板引擎注入。执行以下步骤:

  1. 通过执行以下 Unix 命令在templates目录中创建first-template.html
$ mkdir templates && cd templates && touch first-template.html
  1. 将以下内容复制到first-template.html中:
<html>
  <head>
    <meta charset="utf-8">
    <title>First Template</title>
    <link rel="stylesheet" href="/static/stylesheets/main.css">
  </head>
  <body>
    <h1>Hello {{.Name}}!</h1>
    Your Id is {{.Id}}
  </body>
</html>

上述模板有两个占位符,{{.Name}}{{.Id}},它们的值将由模板引擎在运行时替换或注入。

  1. 创建first-template.go,在其中我们将为占位符填充值,生成 HTML 输出,并将其写入客户端,如下所示:
import 
(
  "fmt"
  "html/template"
  "log"
  "net/http"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Person struct 
{
  Id   string
  Name string
}
func renderTemplate(w http.ResponseWriter, r *http.Request) 
{
  person := Person{Id: "1", Name: "Foo"}
  parsedTemplate, _ := template.ParseFiles("templates/
  first-template.html")
  err := parsedTemplate.Execute(w, person)
  if err != nil 
  {
    log.Printf("Error occurred while executing the template
    or writing its output : ", err)
    return
  }
}
func main() 
{
  http.HandleFunc("/", renderTemplate)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}

一切就绪后,目录结构应如下所示:

  1. 使用以下命令运行程序:
$ go run first-template.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080上启动。

浏览http://localhost:8080将显示模板引擎提供的 Hello Foo!,如下截图所示:

从命令行执行curl -X GET http://localhost:8080如下:

$ curl -X GET http://localhost:8080

这将导致服务器返回以下响应:

让我们了解我们编写的 Go 程序:

  • type Person struct { Id string Name string }: 在这里,我们定义了一个person结构类型,具有IdName字段。

类型定义中的字段名称应以大写字母开头;否则,将导致错误并且不会在模板中被替换。

接下来,我们定义了一个renderTemplate()处理程序,它执行了许多操作。

  • person := Person{Id: "1", Name: "Foo"}: 在这里,我们初始化了一个person结构类型,其中Id1NameFoo

  • parsedTemplate, _ := template.ParseFiles("templates/first-template.html"): 在这里,我们调用html/template包的ParseFiles,它创建一个新模板并解析我们传入的文件名,即templates目录中的first-template.html。生成的模板将具有输入文件的名称和内容。

  • err := parsedTemplate.Execute(w, person): 在这里,我们在解析的模板上调用Execute处理程序,它将person数据注入模板,生成 HTML 输出,并将其写入 HTTP 响应流。

  • if err != nil {log.Printf("Error occurred while executing the template or writing its output : ", err) return }: 在这里,我们检查执行模板或将其输出写入响应流时是否出现任何问题。如果有问题,我们将记录错误并以状态码 1 退出。

通过 HTTP 提供静态文件

在设计 Web 应用程序时,最好的做法是从文件系统或任何内容传递网络CDN)(如 Akamai 或 Amazon CloudFront)提供静态资源,例如.js.cssimages,而不是从 Web 服务器提供。这是因为所有这些类型的文件都是静态的,不需要处理;那么为什么我们要给服务器增加额外的负载呢?此外,它有助于提高应用程序的性能,因为所有对静态文件的请求都将从外部来源提供,并因此减少了对服务器的负载。

Go 的net/http包足以通过FileServer从文件系统中提供静态资源,我们将在本教程中介绍。

准备就绪…

由于我们已经在上一个教程中创建了一个模板,我们将扩展它以从static/css目录中提供静态.css文件。

如何做…

在本教程中,我们将创建一个文件服务器,它将从文件系统中提供静态资源。执行以下步骤:

  1. static/css目录中创建main.css,如下所示:
$ mkdir static && cd static && mkdir css && cd css && touch main.css
  1. 将以下内容复制到main.css中:
body {color: #00008B}
  1. 创建serve-static-files.go,在那里我们将创建FileServer,它将为所有带有/static的 URL 模式从文件系统中的static/css目录提供资源,如下所示:
package main
import 
(
  "fmt"
  "html/template"
  "log"
  "net/http"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Person struct 
{
  Name string
  Age string
}
func renderTemplate(w http.ResponseWriter, r *http.Request) 
{
  person := Person{Id: "1", Name: "Foo"}
  parsedTemplate, _ := template.ParseFiles("templates/
  first-template.html")
  err := parsedTemplate.Execute(w, person)
  if err != nil 
  {
    log.Printf("Error occurred while executing the template 
    or writing its output : ", err)
    return
  }
}
func main() 
{
  fileServer := http.FileServer(http.Dir("static"))
  http.Handle("/static/", http.StripPrefix("/static/", fileServer))
  http.HandleFunc("/", renderTemplate)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 更新first-template.html(在我们的上一个教程中创建)以包含来自文件系统中的static/css目录的main.css
<html>
  <head>
    <meta charset="utf-8">
    <title>First Template</title>
    <link rel="stylesheet" href="/static/css/main.css">
  </head>
  <body>
    <h1>Hello {{.Name}}!</h1>
    Your Id is {{.Id}}
  </body>
</html>

一切就绪后,目录结构应如下所示:

  1. 使用以下命令运行程序:
$ go run serve-static-files.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080。浏览http://localhost:8080将显示与上一个教程中相同的输出,但是这次文本颜色已从默认的黑色更改为蓝色,如下图所示:

如果我们查看 Chrome DevTools 的网络选项卡,我们可以看到main.css是从文件系统中的static/css目录加载的。

让我们了解我们在本教程的main()方法中引入的更改:

  • fileServer := http.FileServer(http.Dir("static")):在这里,我们使用net/http包的FileServer处理程序创建了一个文件服务器,它从文件系统中的static目录提供 HTTP 请求。

  • http.Handle("/static/", http.StripPrefix("/static/", fileServer)):在这里,我们使用net/http包的HandleFunchttp.StripPrefix("/static/", fileServer)处理程序注册到/staticURL 模式,这意味着每当我们访问带有/static模式的 HTTP URL 时,http.StripPrefix("/static/", fileServer)将被执行,并将(http.ResponseWriter, *http.Request)作为参数传递给它。

  • http.StripPrefix("/static/", fileServer):这将返回一个处理程序,通过从请求 URL 的路径中删除/static来提供 HTTP 请求,并调用文件服务器。StripPrefix通过用 HTTP 404 回复处理不以前缀开头的路径的请求。

使用 Gorilla Mux 通过 HTTP 提供静态文件

在上一个教程中,我们通过 Go 的 HTTP 文件服务器提供了static资源。在本教程中,我们将看看如何通过 Gorilla Mux 路由器提供它,这也是创建 HTTP 路由器的最常见方式之一。

准备就绪…

由于我们已经在上一个教程中创建了一个模板,该模板从文件系统中的static/css目录中提供main.css,因此我们将更新它以使用 Gorilla Mux 路由器。

如何做…

  1. 使用go get命令安装github.com/gorilla/mux包,如下所示:
$ go get github.com/gorilla/mux
  1. 创建serve-static-files-gorilla-mux.go,在那里我们将创建一个 Gorilla Mux 路由器,而不是 HTTPFileServer,如下所示:
package main
import 
(
  "html/template"
  "log"
  "net/http"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Person struct 
{
  Id string
  Name string
}
func renderTemplate(w http.ResponseWriter, r *http.Request) 
{
  person := Person{Id: "1", Name: "Foo"}
  parsedTemplate, _ := template.ParseFiles("templates/
  first-template.html")
  err := parsedTemplate.Execute(w, person)
  if err != nil 
  {
    log.Printf("Error occurred while executing the template 
    or writing its output : ", err)
    return
  }
}
func main() 
{
  router := mux.NewRouter()
  router.HandleFunc("/", renderTemplate).Methods("GET")
  router.PathPrefix("/").Handler(http.StripPrefix("/static",
  http.FileServer(http.Dir("static/"))))
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run serve-static-files-gorilla-mux.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080上启动。

浏览http://localhost:8080将显示与我们上一个示例中看到的相同的输出,如下屏幕截图所示:

让我们了解我们在本示例的main()方法中引入的更改:

  • router :=mux.NewRouter():在这里,我们调用mux路由器的NewRouter()处理程序实例化了gorilla/mux`路由器。

  • router.HandleFunc("/",renderTemplate).Methods("GET"):在这里,我们使用renderTemplate处理程序注册了/ URL 模式。这意味着renderTemplate将对每个 URL 模式为/`的请求执行。

  • router.PathPrefix("/").Handler(http.StripPrefix("/static", http.FileServer(http.Dir("static/"))):在这里,我们将/`注册为一个新的路由,并设置处理程序在调用时执行。

  • http.StripPrefix("/static", http.FileServer(http.Dir("static/"))):这返回一个处理程序,通过从请求 URL 的路径中删除/static并调用文件服务器来提供 HTTP 请求。StripPrefix`通过回复 HTTP 404 来处理不以前缀开头的路径的请求。

创建您的第一个 HTML 表单

每当我们想要从客户端收集数据并将其发送到服务器进行处理时,实现 HTML 表单是最佳选择。我们将在本示例中介绍这个。

如何做...

在本示例中,我们将创建一个简单的 HTML 表单,其中包含两个输入字段和一个提交表单的按钮。执行以下步骤:

  1. templates目录中创建login-form.html,如下所示:
$ mkdir templates && cd templates && touch login-form.html
  1. 将以下内容复制到login-form.html中:
<html>
  <head>
    <title>First Form</title>
  </head>
  <body>
    <h1>Login</h1>
    <form method="post" action="/login">
      <label for="username">Username</label>
      <input type="text" id="username" name="username">
      <label for="password">Password</label>
      <input type="password" id="password" name="password">
      <button type="submit">Login</button>
    </form>
  </body>
</html>

上述模板有两个文本框——用户名密码——以及一个登录按钮。

单击登录按钮后,客户端将对在 HTML 表单中定义的操作进行POST调用,我们的情况下是/login

  1. 创建html-form.go,在那里我们将解析表单模板并将其写入 HTTP 响应流,如下所示:
package main
import 
(
  "html/template"
  "log"
  "net/http"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
func login(w http.ResponseWriter, r *http.Request) 
{
  parsedTemplate, _ := template.ParseFiles("templates/
  login-form.html")
  parsedTemplate.Execute(w, nil)
}
func main() 
{
  http.HandleFunc("/", login)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}

一切就绪后,目录结构应如下所示:

  1. 使用以下命令运行程序:
$ go run html-form.go

它是如何工作的...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080上启动。浏览http://localhost:8080将显示一个 HTML 表单,如下屏幕截图所示:

让我们了解我们编写的程序:

  • func login(w http.ResponseWriter, r *http.Request) { parsedTemplate, _ := template.ParseFiles("templates/login-form.html") parsedTemplate.Execute(w, nil) }:这是一个接受ResponseWriterRequest作为输入参数的 Go 函数,解析login-form.html`并返回一个新模板。

  • http.HandleFunc("/", login):在这里,我们使用net/http包的HandleFunc将登录函数注册到/ URL 模式,这意味着每次访问/模式的 HTTP URL 时,登录函数都会被执行,传递ResponseWriterRequest`作为参数。

  • err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil):在这里,我们调用http.ListenAndServe来提供处理每个传入连接的 HTTP 请求的服务。ListenAndServe接受两个参数——服务器地址和处理程序——其中服务器地址为localhost:8080,处理程序为nil`。

  • if err != nil { log.Fatal("error starting http server : ", err) return}:在这里,我们检查是否启动服务器时出现问题。如果有问题,记录错误并以状态码1`退出。

阅读您的第一个 HTML 表单

一旦提交 HTML 表单,我们必须在服务器端读取客户端数据以采取适当的操作。我们将在本示例中介绍这个。

准备好...

由于我们已经在上一个示例中创建了一个 HTML 表单,我们只需扩展该示例以读取其字段值。

如何做...

  1. 使用以下命令安装github.com/gorilla/schema包:
$ go get github.com/gorilla/schema
  1. 创建html-form-read.go,在这里我们将使用github.com/gorilla/schema包解码 HTML 表单字段,并在 HTTP 响应流中写入 Hello,后跟用户名。
package main
import 
(
  "fmt"
  "html/template"
  "log"
  "net/http"
  "github.com/gorilla/schema"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type User struct 
{
  Username string
  Password string
}
func readForm(r *http.Request) *User 
{
  r.ParseForm()
  user := new(User)
  decoder := schema.NewDecoder()
  decodeErr := decoder.Decode(user, r.PostForm)
  if decodeErr != nil 
  {
    log.Printf("error mapping parsed form data to struct : ",
    decodeErr)
  }
  return user
}
func login(w http.ResponseWriter, r *http.Request) 
{
  if r.Method == "GET" 
  {
    parsedTemplate, _ := template.ParseFiles("templates/
    login-form.html")
    parsedTemplate.Execute(w, nil)
  } 
  else 
  {
    user := readForm(r)
    fmt.Fprintf(w, "Hello "+user.Username+"!")
  }
}
func main() 
{
  http.HandleFunc("/", login)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run html-form-read.go

工作原理...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080。浏览http://localhost:8080将显示一个 HTML 表单,如下面的屏幕截图所示:

一旦我们输入用户名和密码并单击登录按钮,我们将在服务器的响应中看到 Hello,后跟用户名,如下面的屏幕截图所示:

让我们了解一下我们在这个配方中引入的更改:

  1. 使用import ("fmt" "html/template" "log" "net/http" "github.com/gorilla/schema"),我们导入了两个额外的包——fmtgithub.com/gorilla/schema——它们有助于将structsForm值相互转换。

  2. 接下来,我们定义了User struct类型,它具有UsernamePassword字段,如下所示:

type User struct 
{
  Username string
  Password string
}
  1. 接下来,我们定义了readForm处理程序,它以HTTP 请求作为输入参数,并返回User,如下所示:
func readForm(r *http.Request) *User {
 r.ParseForm()
 user := new(User)
 decoder := schema.NewDecoder()
 decodeErr := decoder.Decode(user, r.PostForm)
 if decodeErr != nil {
 log.Printf("error mapping parsed form data to struct : ", decodeErr)
 }
 return user
 }

让我们详细了解一下这个 Go 函数:

  • r.ParseForm(): 在这里,我们将请求体解析为一个表单,并将结果放入r.PostFormr.Form中。

  • user := new(User): 在这里,我们创建了一个新的User struct类型。

  • decoder := schema.NewDecoder(): 在这里,我们正在创建一个解码器,我们将使用它来用Form值填充一个用户struct

  • decodeErr := decoder.Decode(user, r.PostForm): 在这里,我们将从POST体参数中解码解析的表单数据到一个用户struct中。

r.PostForm只有在调用ParseForm之后才可用。

  • if decodeErr != nil { log.Printf("error mapping parsed form data to struct : ", decodeErr) }: 在这里,我们检查是否有任何将表单数据映射到结构体的问题。如果有,就记录下来。

然后,我们定义了一个login处理程序,它检查调用处理程序的 HTTP 请求是否是GET请求,然后从模板目录中解析login-form.html并将其写入 HTTP 响应流;否则,它调用readForm处理程序,如下所示:

func login(w http.ResponseWriter, r *http.Request) 
{
  if r.Method == "GET" 
  {
    parsedTemplate, _ := template.ParseFiles("templates/
    login-form.html")
    parsedTemplate.Execute(w, nil)
  } 
  else 
  {
    user := readForm(r)
    fmt.Fprintf(w, "Hello "+user.Username+"!")
  }
}

验证您的第一个 HTML 表单

大多数情况下,我们在处理客户端输入之前必须对其进行验证,这可以通过 Go 中的许多外部包来实现,例如gopkg.in/go-playground/validator.v9gopkg.in/validator.v2github.com/asaskevich/govalidator

在这个配方中,我们将使用最著名和常用的验证器github.com/asaskevich/govalidator来验证我们的 HTML 表单。

准备工作...

由于我们已经在上一个配方中创建并读取了一个 HTML 表单,我们只需扩展它以验证其字段值。

如何做...

  1. 使用以下命令安装github.com/asaskevich/govalidatorgithub.com/gorilla/schema包:
$ go get github.com/asaskevich/govalidator
$ go get github.com/gorilla/schema
  1. 创建html-form-validation.go,在这里我们将读取一个 HTML 表单,使用github.com/gorilla/schema对其进行解码,并使用github.com/asaskevich/govalidator对其每个字段进行验证,验证标签定义在User struct中。
package main
import 
(
  "fmt"
  "html/template"
  "log"
  "net/http"
  "github.com/asaskevich/govalidator"
  "github.com/gorilla/schema"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  USERNAME_ERROR_MESSAGE = "Please enter a valid Username"
  PASSWORD_ERROR_MESSAGE = "Please enter a valid Password"
  GENERIC_ERROR_MESSAGE = "Validation Error"
)
type User struct 
{
  Username string `valid:"alpha,required"`
  Password string `valid:"alpha,required"`
}
func readForm(r *http.Request) *User 
{
  r.ParseForm()
  user := new(User)
  decoder := schema.NewDecoder()
  decodeErr := decoder.Decode(user, r.PostForm)
  if decodeErr != nil 
  {
    log.Printf("error mapping parsed form data to struct : ",
    decodeErr)
  }
  return user
}
func validateUser(w http.ResponseWriter, r *http.Request, user *User) (bool, string) 
{
  valid, validationError := govalidator.ValidateStruct(user)
  if !valid 
  {
    usernameError := govalidator.ErrorByField(validationError,
    "Username")
    passwordError := govalidator.ErrorByField(validationError,
    "Password")
    if usernameError != "" 
    {
      log.Printf("username validation error : ", usernameError)
      return valid, USERNAME_ERROR_MESSAGE
    }
    if passwordError != "" 
    {
      log.Printf("password validation error : ", passwordError)
      return valid, PASSWORD_ERROR_MESSAGE
    }
  }
  return valid, GENERIC_ERROR_MESSAGE
}
func login(w http.ResponseWriter, r *http.Request) 
{
  if r.Method == "GET" 
  {
    parsedTemplate, _ := template.ParseFiles("templates/
    login-form.html")
    parsedTemplate.Execute(w, nil)
  } 
  else 
  {
    user := readForm(r)
    valid, validationErrorMessage := validateUser(w, r, user)
    if !valid 
    {
      fmt.Fprintf(w, validationErrorMessage)
      return
    }
    fmt.Fprintf(w, "Hello "+user.Username+"!")
  }
}
func main() 
{
  http.HandleFunc("/", login)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run html-form-validation.go

工作原理...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080。浏览http://localhost:8080将显示一个 HTML 表单,如下面的屏幕截图所示:

然后提交具有有效值的表单:

它将在浏览器屏幕上显示 Hello,后跟用户名,如下面的屏幕截图所示:

在任何字段中提交值为非字母的表单将显示错误消息。例如,提交用户名值为1234的表单:

它将在浏览器上显示错误消息,如下面的屏幕截图所示:

此外,我们可以从命令行提交 HTML 表单,如下所示:

$ curl --data "username=Foo&password=password" http://localhost:8080/

这将给我们在浏览器中得到的相同输出:

让我们了解一下我们在这个示例中引入的更改:

  1. 使用import ("fmt", "html/template", "log", "net/http" "github.com/asaskevich/govalidator" "github.com/gorilla/schema" ),我们导入了一个额外的包——github.com/asaskevich/govalidator,它可以帮助我们验证结构。

  2. 接下来,我们更新了User struct类型,包括一个字符串字面标签,keyvalidvaluealpha, required,如下所示:

type User struct 
{
  Username string `valid:"alpha,required"`
  Password string 
  valid:"alpha,required"
}
  1. 接下来,我们定义了一个validateUser处理程序,它接受ResponseWriterRequestUser作为输入,并返回boolstring,分别是结构的有效状态和验证错误消息。在这个处理程序中,我们调用govalidatorValidateStruct处理程序来验证结构标签。如果在验证字段时出现错误,我们将调用govalidatorErrorByField处理程序来获取错误,并将结果与验证错误消息一起返回。

  2. 接下来,我们更新了login处理程序,调用validateUser并将(w http.ResponseWriter, r *http.Request, user *User)作为输入参数传递给它,并检查是否有任何验证错误。如果有错误,我们将在 HTTP 响应流中写入错误消息并返回它。

上传您的第一个文件

在任何 Web 应用程序中,最常见的情景之一就是上传文件或文件夹到服务器。例如,如果我们正在开发一个求职门户网站,那么我们可能需要提供一个选项,申请人可以上传他们的个人资料/简历,或者,比如说,我们需要开发一个电子商务网站,其中客户可以使用文件批量上传他们的订单。

在 Go 中实现上传文件的功能非常容易,使用其内置的包,我们将在本示例中进行介绍。

如何做…

在这个示例中,我们将创建一个带有file类型字段的 HTML 表单,允许用户选择一个或多个文件通过表单提交上传到服务器。执行以下步骤:

  1. templates目录中创建upload-file.html,如下所示:
$ mkdir templates && cd templates && touch upload-file.html
  1. 将以下内容复制到upload-file.html中:
<html>
  <head>
    <meta charset="utf-8">
    <title>File Upload</title>
  </head>
  <body>
    <form action="/upload" method="post" enctype="multipart/
    form-data">
      <label for="file">File:</label>
      <input type="file" name="file" id="file">
      <input type="submit" name="submit" value="Submit">
    </form>
  </body>
</html>

在前面的模板中,我们定义了一个file类型的字段,以及一个Submit按钮。

点击“提交”按钮后,客户端将对请求的主体进行编码,并对表单操作进行POST调用,这在我们的情况下是/upload

  1. 创建upload-file.go,在其中我们将定义处理程序来渲染文件上传模板,从请求中获取文件,处理它,并将响应写入 HTTP 响应流,如下所示:
package main
import 
(
  "fmt"
  "html/template"
  "io"
  "log"
  "net/http"
  "os"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
func fileHandler(w http.ResponseWriter, r *http.Request) 
{
  file, header, err := r.FormFile("file")
  if err != nil 
  {
    log.Printf("error getting a file for the provided form key : ",
    err)
    return
  }
  defer file.Close()
  out, pathError := os.Create("/tmp/uploadedFile")
  if pathError != nil 
  {
    log.Printf("error creating a file for writing : ", pathError)
    return
  }
  defer out.Close()
  _, copyFileError := io.Copy(out, file)
  if copyFileError != nil 
  {
    log.Printf("error occurred while file copy : ", copyFileError)
  }
  fmt.Fprintf(w, "File uploaded successfully : "+header.Filename)
}
func index(w http.ResponseWriter, r *http.Request) 
{
  parsedTemplate, _ := template.ParseFiles("templates/
  upload-file.html")
  parsedTemplate.Execute(w, nil)
}
func main() 
{
  http.HandleFunc("/", index)
  http.HandleFunc("/upload", fileHandler)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}

一切就绪后,目录结构应该如下所示:

  1. 使用以下命令运行程序:
$ go run upload-file.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080。浏览http://localhost:8080将会显示文件上传表单,如下面的屏幕截图所示:

在选择文件后按下“提交”按钮将会在服务器上创建一个名为uploadedFile的文件,位于/tmp目录中。您可以通过执行以下命令来查看:

此外,成功上传将在浏览器上显示消息,如下面的屏幕截图所示:

让我们了解一下我们编写的 Go 程序:

我们定义了fileHandler()处理程序,它从请求中获取文件,读取其内容,最终将其写入服务器上的文件。由于这个处理程序做了很多事情,让我们逐步详细介绍一下:

  • file, header, err := r.FormFile("file"): 在这里,我们调用 HTTP 请求的FormFile处理程序,以获取提供的表单键对应的文件。

  • if err != nil { log.Printf("error getting a file for the provided form key : ", err) return }: 在这里,我们检查是否在从请求中获取文件时出现了任何问题。如果有问题,记录错误并以状态码1退出。

  • defer file.Close(): defer语句会在函数返回时关闭file

  • out, pathError := os.Create("/tmp/uploadedFile"): 在这里,我们创建了一个名为uploadedFile的文件,放在/tmp目录下,权限为666,这意味着客户端可以读写但不能执行该文件。

  • if pathError != nil { log.Printf("error creating a file for writing : ", pathError) return }: 在这里,我们检查在服务器上创建文件时是否出现了任何问题。如果有问题,记录错误并以状态码1退出。

  • _, copyFileError := io.Copy(out, file): 在这里,我们将从接收到的文件中的内容复制到/tmp目录下创建的文件中。

  • fmt.Fprintf(w, "File uploaded successfully : "+header.Filename): 在这里,我们向 HTTP 响应流写入一条消息和文件名。

第三章:在 Go 中处理会话、错误和缓存

在本章中,我们将涵盖以下示例:

  • 创建你的第一个 HTTP 会话

  • 使用 Redis 管理你的 HTTP 会话

  • 创建你的第一个 HTTP cookie

  • 在 Go 中实现缓存

  • 在 Go 中实现 HTTP 错误处理

  • 在 Web 应用程序中实现登录和注销

介绍

有时,我们希望在应用程序级别持久保存用户数据等信息,而不是将其持久保存在数据库中,这可以很容易地通过会话和 cookies 来实现。两者之间的区别在于,会话存储在服务器端,而 cookies 存储在客户端。我们还可能需要缓存静态数据,以避免不必要地调用数据库或 Web 服务,并在开发 Web 应用程序时实现错误处理。通过掌握本章涵盖的概念,我们将能够以相当简单的方式实现所有这些功能。

在本章中,我们将从创建一个 HTTP 会话开始,然后学习如何使用 Redis 进行管理,创建 cookies,缓存 HTTP 响应,实现错误处理,最终以在 Go 中实现登录和注销机制结束。

创建你的第一个 HTTP 会话

HTTP 是一个无状态协议,这意味着每次客户端检索网页时,客户端都会打开一个独立的连接到服务器,服务器会对其进行响应,而不保留任何关于先前客户端请求的记录。因此,如果我们想要实现一个机制,让服务器知道客户端发送给它的请求,那么我们可以使用会话来实现。

当我们使用会话时,客户端只需要发送一个 ID,数据就会从服务器加载出来。我们可以在 Web 应用程序中实现这三种方式:

  • Cookies

  • 隐藏表单字段

  • URL 重写

在这个示例中,我们将使用 HTTP cookies 来实现一个会话。

如何做…

  1. 使用go get命令安装github.com/gorilla/sessions包,如下所示:
$ go get github.com/gorilla/sessions
  1. 创建http-session.go,在其中我们将创建一个 Gorilla cookie 存储来保存和检索会话信息,定义三个处理程序—/login/home/logout—在这里我们将创建一个有效的会话 cookie,向 HTTP 响应流写入响应,以及分别使会话 cookie 失效,如下所示:
package main
import 
(
  "fmt"
  "log"
  "net/http"
  "github.com/gorilla/sessions"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
var store *sessions.CookieStore
func init() 
{
  store = sessions.NewCookieStore([]byte("secret-key"))
}
func home(w http.ResponseWriter, r *http.Request) 
{
  session, _ := store.Get(r, "session-name")
  var authenticated interface{} = session.Values["authenticated"]
  if authenticated != nil 
  {
    isAuthenticated := session.Values["authenticated"].(bool)
    if !isAuthenticated 
    {
      http.Error(w, "You are unauthorized to view the page",
      http.StatusForbidden)
      return
    }
    fmt.Fprintln(w, "Home Page")
  } 
  else 
  {
    http.Error(w, "You are unauthorized to view the page",
    http.StatusForbidden)
    return
  }
}
func login(w http.ResponseWriter, r *http.Request) 
{
  session, _ := store.Get(r, "session-name")
  session.Values["authenticated"] = true
  session.Save(r, w)
  fmt.Fprintln(w, "You have successfully logged in.")
}
func logout(w http.ResponseWriter, r *http.Request) 
{
  session, _ := store.Get(r, "session-name")
  session.Values["authenticated"] = false
  session.Save(r, w)
  fmt.Fprintln(w, "You have successfully logged out.")
}
func main() 
{
  http.HandleFunc("/home", home)
  http.HandleFunc("/login", login)
  http.HandleFunc("/logout", logout)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-session.go

工作原理…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,我们将执行一些命令来看会话是如何工作的。

首先,我们将通过执行以下命令访问/home

$ curl -X GET http://localhost:8080/home

这将导致服务器显示未经授权的访问消息,如下面的屏幕截图所示:

这是因为我们首先必须登录到一个应用程序,这将创建一个服务器将在提供对任何网页的访问之前验证的会话 ID。所以,让我们登录到应用程序:

$ curl -X GET -i http://localhost:8080/login

执行前面的命令将给我们一个Cookie,它必须被设置为一个请求头来访问任何网页:

接下来,我们将使用提供的Cookie来访问/home,如下所示:

$ curl --cookie "session-name=MTUyMzEwMTI3NXxEdi1CQkFFQ180SUFBUkFCRUFBQUpmLUNBQUVHYzNSeWFXNW5EQThBRFdGMWRHaGxiblJwWTJGMFpXUUVZbTl2YkFJQ0FBRT18ou7Zxn3qSbqHHiajubn23Eiv8a348AhPl8RN3uTRM4M=;" http://localhost:8080/home

这将导致服务器作为响应的主页:

让我们了解我们编写的 Go 程序:

  • 使用var store *sessions.CookieStore,我们声明了一个私有的 cookie 存储,用来使用安全的 cookies 来存储会话。

  • 使用func init() { store = sessions.NewCookieStore([]byte("secret-key")) },我们定义了一个在main()之前运行的init()函数,用来创建一个新的 cookie 存储并将其分配给store

init()函数总是被调用,无论是否有主函数,所以如果你导入一个包含init函数的包,它将被执行。

  • 接下来,我们定义了一个home处理程序,在那里我们从 cookie 存储中获取一个会话,将其添加到注册表中并使用store.Get获取authenticated键的值。如果为 true,则我们将Home Page写入 HTTP 响应流;否则,我们将写入一个403HTTP 代码以及消息 You are unauthorized to view the page.。

  • 接下来,我们定义了一个login处理程序,在那里我们再次获取一个会话,将authenticated键设置为true,保存它,最后将 You have successfully logged in.写入 HTTP 响应流。

  • 接下来,我们定义了一个logout处理程序,在那里我们获取一个会话,将一个authenticated键设置为false,保存它,最后将 You have successfully logged out.写入 HTTP 响应流。

  • 最后,我们定义了main(),在那里我们将所有处理程序homeloginlogout映射到/home/login/logout,并在localhost:8080上启动 HTTP 服务器。

使用 Redis 管理您的 HTTP 会话

在处理分布式应用程序时,我们可能需要为前端用户实现无状态负载平衡。这样我们就可以将会话信息持久化存储在数据库或文件系统中,以便在服务器关闭或重新启动时识别用户并检索他们的信息。

我们将在这个配方的一部分中使用 Redis 作为持久存储来解决这个问题。

准备就绪...

由于我们已经在上一个配方中使用 Gorilla cookie 存储创建了一个会话变量,因此我们只需扩展此配方以将会话信息保存在 Redis 中,而不是在服务器上维护它。

Gorilla 会话存储有多种实现,您可以在https://github.com/gorilla/sessions#store-implementations找到。由于我们使用 Redis 作为后端存储,我们将使用https://github.com/boj/redistore,它依赖于 Redigo Redis 库来存储会话。

这个配方假设您已经在本地端口63794567上安装并运行了 Redis 和 Redis 浏览器。

如何做...

  1. 使用go get命令安装gopkg.in/boj/redistore.v1github.com/gorilla/sessions,如下所示:
$ go get gopkg.in/boj/redistore.v1
$ go get github.com/gorilla/sessions
  1. 创建http-session-redis.go,在那里我们将创建一个RedisStore来存储和检索会话变量,如下所示:
package main
import 
(
  "fmt"
  "log"
  "net/http"
  "github.com/gorilla/sessions"
  redisStore "gopkg.in/boj/redistore.v1"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
var store *redisStore.RediStore
var err error
func init() 
{
  store, err = redisStore.NewRediStore(10, "tcp", ":6379", "",
  []byte("secret-key"))
  if err != nil 
  {
    log.Fatal("error getting redis store : ", err)
  }
}
func home(w http.ResponseWriter, r *http.Request) 
{
  session, _ := store.Get(r, "session-name")
  var authenticated interface{} = session.Values["authenticated"]
  if authenticated != nil 
  {
    isAuthenticated := session.Values["authenticated"].(bool)
    if !isAuthenticated 
    {
      http.Error(w, "You are unauthorized to view the page",
      http.StatusForbidden)
      return
    }
    fmt.Fprintln(w, "Home Page")
  } 
  else 
  {
    http.Error(w, "You are unauthorized to view the page",
    http.StatusForbidden)
    return
  }
}
func login(w http.ResponseWriter, r *http.Request) 
{
  session, _ := store.Get(r, "session-name")
  session.Values["authenticated"] = true
  if err = sessions.Save(r, w); err != nil 
  {
    log.Fatalf("Error saving session: %v", err)
  }
  fmt.Fprintln(w, "You have successfully logged in.")
}
func logout(w http.ResponseWriter, r *http.Request) 
{
  session, _ := store.Get(r, "session-name")
  session.Values["authenticated"] = false
  session.Save(r, w)
  fmt.Fprintln(w, "You have successfully logged out.")
}
func main() 
{
  http.HandleFunc("/home", home)
  http.HandleFunc("/login", login)
  http.HandleFunc("/logout", logout)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  defer store.Close()
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-session-redis.go

它是如何工作的...

运行程序后,HTTP 服务器将在本地端口8080上开始监听。

接下来,我们将执行一些命令来看看会话是如何工作的。

首先,我们将通过执行以下命令访问/home

$ curl -X GET http://localhost:8080/home

这将导致服务器显示未经授权的访问消息,如下面的屏幕截图所示:

这是因为我们首先必须登录到一个应用程序,这将创建一个服务器将在提供对任何网页的访问之前验证的会话 ID。所以,让我们登录到应用程序:

$ curl -X GET -i http://localhost:8080/login

执行上一个命令将给我们一个Cookie,必须将其设置为请求头以访问任何网页:

一旦执行了上一个命令,将会创建一个Cookie并保存在 Redis 中,您可以通过从redis-cli执行命令或在 Redis 浏览器中查看,如下面的屏幕截图所示:

接下来,我们将使用提供的Cookie来访问/home,如下所示:

$ curl --cookie "session-name=MTUyMzEwNDUyM3xOd3dBTkV4T1JrdzNURFkyUkVWWlQxWklUekpKVUVOWE1saFRUMHBHVTB4T1RGVXlSRU5RVkZWWk5VeFNWVmRPVVZSQk4wTk1RMUU9fAlGgLGU-OHxoP78xzEHMoiuY0Q4rrbsXfajSS6HiJAm;" http://localhost:8080/home

这将导致服务器作为响应的主页:

让我们了解我们在这个配方中引入的更改:

  1. 使用var store *redisStore.RediStore,我们声明了一个私有的RediStore来在 Redis 中存储会话。

  2. 接下来,我们更新了init()函数,使用大小和最大空闲连接数为10创建NewRediStore,并将其分配给存储。如果在创建存储时出现错误,我们将记录错误并以状态码1退出。

  3. 最后,我们更新了main(),引入了defer store.Close()语句,一旦我们从函数返回,就会关闭 Redis 存储。

创建你的第一个 HTTP cookie

在客户端存储信息时,cookie 扮演着重要的角色,我们可以使用它们的值来识别用户。基本上,cookie 是为了解决记住用户信息或持久登录身份验证的问题而发明的,这指的是网站能够在会话之间记住主体的身份。

Cookie 是在互联网上访问网站时 Web 浏览器创建的简单文本文件。您的设备会在本地存储这些文本文件,允许您的浏览器访问 cookie 并将数据传递回原始网站,并以名称-值对的形式保存。

如何做到这一点...

  1. 使用go get命令安装github.com/gorilla/securecookie包,如下所示:
$ go get github.com/gorilla/securecookie
  1. 创建http-cookie.go,在其中我们将创建一个 Gorilla 安全 cookie 来存储和检索 cookie,如下所示:
package main
import 
(
  "fmt"
  "log"
  "net/http"
  "github.com/gorilla/securecookie"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
var cookieHandler *securecookie.SecureCookie
func init() 
{
  cookieHandler = securecookie.New(securecookie.
  GenerateRandomKey(64),
  securecookie.GenerateRandomKey(32))
}
func createCookie(w http.ResponseWriter, r *http.Request) 
{
  value := map[string]string
  {
    "username": "Foo",
  }
  base64Encoded, err := cookieHandler.Encode("key", value)
  if err == nil 
  {
    cookie := &http.Cookie
    {
      Name: "first-cookie",
      Value: base64Encoded,
      Path: "/",
    }
    http.SetCookie(w, cookie)
  }
  w.Write([]byte(fmt.Sprintf("Cookie created.")))
}
func readCookie(w http.ResponseWriter, r *http.Request) 
{
  log.Printf("Reading Cookie..")
  cookie, err := r.Cookie("first-cookie")
  if cookie != nil && err == nil 
  {
    value := make(map[string]string)
    if err = cookieHandler.Decode("key", cookie.Value, &value); 
    err == nil 
    {
      w.Write([]byte(fmt.Sprintf("Hello %v \n", 
      value["username"])))
    }
  } 
  else 
  {
    log.Printf("Cookie not found..")
    w.Write([]byte(fmt.Sprint("Hello")))
  }
}

func main() 
{
  http.HandleFunc("/create", createCookie)
  http.HandleFunc("/read", readCookie)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-cookie.go

它是如何工作的...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

浏览http://localhost:8080/read将在浏览器中显示 Hello,如下面的屏幕截图所示:

接下来,我们将访问http://localhost:8080/create,这将创建一个名为 first-cookie 的 cookie,并在浏览器中显示 Cookie created 消息:

现在,随后访问http://localhost:8080/read将使用first-cookie来显示 Hello,然后是first-cookie的值,如下所示:

让我们了解我们编写的程序:

  • 使用`import ("fmt" "log" "net/http" "github.com/gorilla

/securecookie"),我们引入了一个额外的包—github.com/gorilla/securecookie`,我们将使用它来对经过身份验证和加密的 cookie 值进行编码和解码。

  • 使用var cookieHandler *securecookie.SecureCookie,我们声明了一个私有的安全 cookie。

  • 接下来,我们更新了init()函数,创建了一个SecureCookie,传递了一个 64 字节的哈希密钥,用于使用 HMAC 对值进行身份验证,以及一个 32 字节的块密钥,用于加密值。

  • 接下来,我们定义了一个createCookie处理程序,在其中使用gorilla/securecookieEncode处理程序创建一个以username为键,Foo为值的Base64编码的 cookie。然后,我们向提供的ResponseWriter头部添加一个Set-Cookie头,并向 HTTP 响应中写入一个Cookie created.的消息。

  • 接下来,我们定义了一个readCookie处理程序,在其中我们从请求中检索一个 cookie,这在我们的代码中是first-cookie,为其获取一个值,并将其写入 HTTP 响应。

  • 最后,我们定义了main(),在其中将所有处理程序—createCookiereadCookie—映射到/create/read,并在localhost:8080上启动了 HTTP 服务器。

在 Go 中实现缓存

在 Web 应用程序中缓存数据有时是必要的,以避免反复从数据库或外部服务请求静态数据。Go 没有提供任何内置的包来缓存响应,但它通过外部包支持缓存。

有许多包,例如https://github.com/coocood/freecachehttps://github.com/patrickmn/go-cache,可以帮助实现缓存,在本教程中,我们将使用https://github.com/patrickmn/go-cache来实现它。

如何做到这一点...

  1. 使用go get命令安装github.com/patrickmn/go-cache包,如下所示:
$ go get github.com/patrickmn/go-cache
  1. 创建http-caching.go,在其中我们将在服务器启动时创建一个缓存并填充数据,如下所示:
package main
import 
(
  "fmt"
  "log"
  "net/http"
  "time"
  "github.com/patrickmn/go-cache"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
var newCache *cache.Cache
func init() 
{
  newCache = cache.New(5*time.Minute, 10*time.Minute)
  newCache.Set("foo", "bar", cache.DefaultExpiration)
}
func getFromCache(w http.ResponseWriter, r *http.Request) 
{
  foo, found := newCache.Get("foo")
  if found 
  {
    log.Print("Key Found in Cache with value as :: ", 
    foo.(string))
    fmt.Fprintf(w, "Hello "+foo.(string))
  } 
  else 
  {
    log.Print("Key Not Found in Cache :: ", "foo")
    fmt.Fprintf(w, "Key Not Found in Cache")
  }
}
func main() 
{
  http.HandleFunc("/", getFromCache)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-caching.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

在启动时,具有名称foo和值为bar的键将被添加到缓存中。

浏览http://localhost:8080/将从缓存中读取一个键值,并将其附加到 Hello,如下截图所示:

我们在程序中指定了缓存数据的过期时间为五分钟,这意味着我们在服务器启动时在缓存中创建的键在五分钟后将不再存在。因此,五分钟后再次访问相同的 URL 将从服务器返回缓存中找不到键的消息,如下所示:

让我们理解我们编写的程序:

  1. 使用var newCache *cache.Cache,我们声明了一个私有缓存。

  2. 接下来,我们更新了init()函数,在其中创建了一个具有五分钟过期时间和十分钟清理间隔的缓存,并向缓存中添加了一个键为foo,值为bar,过期值为0的项目,这意味着我们要使用缓存的默认过期时间。

如果过期持续时间小于一(或NoExpiration),则缓存中的项目永远不会过期(默认情况下),必须手动删除。如果清理间隔小于一,则在调用c.DeleteExpired()之前不会从缓存中删除过期的项目。

  1. 接下来,我们定义了getFromCache处理程序,从缓存中检索键的值。如果找到,我们将其写入 HTTP 响应;否则,我们将Key Not Found in Cache的消息写入 HTTP 响应。

在 Go 中实现 HTTP 错误处理

在任何 Web 应用程序中实现错误处理是主要方面之一,因为它有助于更快地进行故障排除和修复错误。错误处理意味着每当应用程序发生错误时,应该将其记录在某个地方,无论是在文件中还是在数据库中,都应该有适当的错误消息以及堆栈跟踪。

在 Go 中,可以以多种方式实现。一种方法是编写自定义处理程序,我们将在本教程中介绍。

如何做…

  1. 使用go get命令安装github.com/gorilla/mux包,如下所示:
$ go get github.com/gorilla/mux
  1. 创建http-error-handling.go,在其中我们将创建一个自定义处理程序,作为处理所有 HTTP 请求的包装器,如下所示:
package main
import 
(
  "errors"
  "fmt"
  "log"
  "net/http"
  "strings"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type NameNotFoundError struct 
{
  Code int
  Err error
}
func (nameNotFoundError NameNotFoundError) Error() string 
{
  return nameNotFoundError.Err.Error()
}
type WrapperHandler func(http.ResponseWriter, *http.Request) 
error
func (wrapperHandler WrapperHandler) ServeHTTP(w http.
ResponseWriter, r *http.Request) 
{
  err := wrapperHandler(w, r)
  if err != nil 
  {
    switch e := err.(type) 
    {
      case NameNotFoundError:
      log.Printf("HTTP %s - %d", e.Err, e.Code)
      http.Error(w, e.Err.Error(), e.Code)
      default:
      http.Error(w, http.StatusText(http.
      StatusInternalServerError),
      http.StatusInternalServerError)
    }
  }
}
func getName(w http.ResponseWriter, r *http.Request) error 
{
  vars := mux.Vars(r)
  name := vars["name"]
  if strings.EqualFold(name, "foo") 
  {
    fmt.Fprintf(w, "Hello "+name)
    return nil
  } 
  else 
  {
    return NameNotFoundError{500, errors.New("Name Not Found")}
  }
}
func main() 
{
  router := mux.NewRouter()
  router.Handle("/employee/get/{name}",
  WrapperHandler(getName)).Methods("GET")
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-error-handling.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,浏览http://localhost:8080/employee/get/foo将在浏览器中作为响应给我们 Hello,后跟员工姓名和状态码为200

另一方面,访问http://localhost:8080/employee/get/bar将返回一个带有消息 Name Not Found 和错误代码500的 HTTP 错误:

让我们理解我们编写的程序:

  1. 我们定义了一个NameNotFoundError结构,它有两个字段——类型为intCode和类型为errorErr,它表示一个带有关联 HTTP 状态码的错误,如下所示:
type NameNotFoundError struct 
{
  Code int
  Err error
}
  1. 然后,我们允许NameNotFoundError满足错误接口,如下所示:
func (nameNotFoundError NameNotFoundError) Error() string 
{
  return nameNotFoundError.Err.Error()
}
  1. 接下来,我们定义了一个用户定义类型WrapperHandler,它是一个接受任何接受func(http.ResponseWriter, *http.Request)作为输入参数并返回错误的处理程序的 Go 函数。

  2. 然后,我们定义了一个ServeHTTP处理程序,它调用我们传递给WrapperHandler的处理程序,将(http.ResponseWriter, *http.Request)作为参数传递给它,并检查处理程序是否返回任何错误。如果有错误,则使用 switch case 适当处理它们,如下所示:

if err != nil 
{
  switch e := err.(type) 
  {
    case NameNotFoundError:
    log.Printf("HTTP %s - %d", e.Err, e.Code)
    http.Error(w, e.Err.Error(), e.Code)
    default:
    http.Error(w, http.StatusText(http.
    StatusInternalServerError),
    http.StatusInternalServerError)
  }
}
  1. 接下来,我们定义了getName处理程序,它提取请求路径变量,获取name变量的值,并检查名称是否匹配foo。如果是,则将 Hello,后跟名称,写入 HTTP 响应;否则,它将返回一个Code字段值为500NameNotFoundError结构和一个err字段值为error的文本Name Not Found

  2. 最后,我们定义了main(),在其中将WrapperHandler注册为 URL 模式/get/{name}的处理程序。

在 Web 应用程序中实现登录和注销

每当我们希望应用程序只能被注册用户访问时,我们都必须实现一个机制,在允许他们查看任何网页之前要求用户提供凭据,这将在本示例中进行介绍。

准备工作…

由于我们已经在之前的示例中创建了一个 HTML 表单,我们只需更新它以使用gorilla/securecookie包实现登录和注销机制。

在第二章的使用模板、静态文件和 HTML 表单中查看在 Web 应用程序中实现登录和注销的示例。

如何做…

  1. 使用go get命令安装github.com/gorilla/muxgithub.com/gorilla/securecookie,如下所示:
$ go get github.com/gorilla/mux
$ go get github.com/gorilla/securecookie
  1. templates目录中创建home.html,如下所示:
$ mkdir templates && cd templates && touch home.html
  1. 将以下内容复制到home.html
<html>
  <head>
    <title></title>
  </head>
  <body>
    <h1>Welcome {{.userName}}!</h1>
    <form method="post" action="/logout">
      <button type="submit">Logout</button>
    </form>
  </body>
</html>

在上述模板中,我们定义了一个占位符{{.userName}},其值将在运行时由模板引擎替换,以及一个注销按钮。点击注销按钮后,客户端将对表单动作进行POST调用,这在我们的例子中是/logout

  1. 创建html-form-login-logout.go,在这里我们将解析登录表单,读取用户名字段,并在用户点击登录按钮时设置会话 cookie。用户点击注销按钮后,我们也会清除会话,如下所示:
package main
import 
(
  "html/template"
  "log"
  "net/http"
  "github.com/gorilla/mux"
  "github.com/gorilla/securecookie"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
var cookieHandler = securecookie.New
(
  securecookie.GenerateRandomKey(64),
  securecookie.GenerateRandomKey(32)
)
func getUserName(request *http.Request) (userName string) 
{
  cookie, err := request.Cookie("session")
  if err == nil 
  {
    cookieValue := make(map[string]string)
    err = cookieHandler.Decode("session", cookie.Value,
    &cookieValue)
    if err == nil 
    {
      userName = cookieValue["username"]
    }
  }
  return userName
}
func setSession(userName string, response http.ResponseWriter) 
{
  value := map[string]string
  {
    "username": userName,
  }
  encoded, err := cookieHandler.Encode("session", value)
  if err == nil 
  {
    cookie := &http.Cookie
    {
      Name: "session",
      Value: encoded,
      Path: "/",
    }
    http.SetCookie(response, cookie)
  }
}
func clearSession(response http.ResponseWriter) 
{
  cookie := &http.Cookie
  {
    Name: "session",
    Value: "",
    Path: "/",
    MaxAge: -1,
  }
  http.SetCookie(response, cookie)
}
func login(response http.ResponseWriter, request *http.Request) 
{
  username := request.FormValue("username")
  password := request.FormValue("password")
  target := "/"
  if username != "" && password != "" 
  {
    setSession(username, response)
    target = "/home"
  }
  http.Redirect(response, request, target, 302)
}
func logout(response http.ResponseWriter, request *http.Request) 
{
  clearSession(response)
  http.Redirect(response, request, "/", 302)
}
func loginPage(w http.ResponseWriter, r *http.Request) 
{
  parsedTemplate, _ := template.ParseFiles("templates/
  login-form.html")
  parsedTemplate.Execute(w, nil)
}
func homePage(response http.ResponseWriter, request *http.Request) 
{
  userName := getUserName(request)
  if userName != "" 
  {
    data := map[string]interface{}
    {
      "userName": userName,
    }
    parsedTemplate, _ := template.ParseFiles("templates/home.html")
    parsedTemplate.Execute(response, data)
  } 
  else 
  {
    http.Redirect(response, request, "/", 302)
  }
}
func main() 
{
  var router = mux.NewRouter()
  router.HandleFunc("/", loginPage)
  router.HandleFunc("/home", homePage)
  router.HandleFunc("/login", login).Methods("POST")
  router.HandleFunc("/logout", logout).Methods("POST")
  http.Handle("/", router)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}

一切就绪后,目录结构应如下所示:

  1. 使用以下命令运行程序:
$ go run html-form-login-logout.go

工作原理…

一旦我们运行程序,HTTP 服务器将在本地的 8080 端口上开始监听。

接下来,浏览http://localhost:8080将显示我们的登录表单,如下截图所示:

在输入用户名Foo和随机密码后提交表单将在浏览器中显示欢迎 Foo!消息,并创建一个名为 session 的 cookie,用于管理用户的登录/注销状态:

现在,直到名为 session 的 cookie 存在,对http://localhost:8080/home的每个后续请求都将在浏览器中显示欢迎 Foo!消息。

接下来,清除 cookie 后访问http://localhost:8080/home将重定向我们到http://localhost:8080/并显示登录表单:

让我们了解我们编写的程序。

  1. 使用`var cookieHandler = securecookie.New(securecookie.

使用GenerateRandomKey(64), securecookie.GenerateRandomKey(32)),我们创建了一个安全 cookie,将哈希密钥作为第一个参数,块密钥作为第二个参数。哈希密钥用于使用 HMAC 对值进行身份验证,块密钥用于加密值。

  1. 接下来,我们定义了getUserName处理程序,从 HTTP 请求中获取一个 cookie,初始化一个字符串到字符串cookieValue映射,解码一个 cookie,并获取用户名的值并返回。

  2. 接下来,我们定义了setSession处理程序,其中我们创建并初始化一个带有keyvalue的映射,将其序列化,使用消息认证码对其进行签名,使用cookieHandler.Encode处理程序对其进行编码,创建一个新的 HTTP cookie,并将其写入 HTTP 响应流。

  3. 接下来,我们定义了clearSession,它基本上将 cookie 的值设置为空,并将其写入 HTTP 响应流。

  4. 接下来,我们定义了一个login处理程序,在这里,我们从 HTTP 表单中获取用户名和密码,检查两者是否都不为空,然后调用setSession处理程序并重定向到/home,否则重定向到根 URL/

  5. 接下来,我们定义了一个logout处理程序,在这里,我们调用clearSession处理程序清除会话值,并重定向到根 URL。

  6. 接下来,我们定义了一个loginPage处理程序,在这里,我们解析login-form.html,返回一个具有名称和内容的新模板,调用已解析模板上的Execute处理程序,生成 HTML 输出,并将其写入 HTTP 响应流。

  7. 接下来,我们定义了一个homePage处理程序,该处理程序从调用getUserName处理程序的 HTTP 请求中获取用户名。然后,我们检查它是否不为空或是否存在 cookie 值。如果用户名不为空,我们解析home.html,将用户名注入数据映射,生成 HTML 输出,并将其写入 HTTP 响应流;否则,我们将其重定向到根 URL/

最后,我们定义了main()方法,我们在这里启动程序执行。由于这个方法做了很多事情,让我们逐行查看它:

  • var router = mux.NewRouter(): 在这里,我们创建了一个新的路由器实例。

  • router.HandleFunc("/", loginPage): 在这里,我们使用gorilla/mux包的HandleFunc注册了loginPageHandler处理程序,并使用/ URL 模式,这意味着每当我们访问具有/模式的 HTTP URL 时,loginPage处理程序将通过传递(http.ResponseWriter, *http.Request)作为参数来执行。

  • router.HandleFunc("/home", homePage): 在这里,我们使用gorilla/mux包的HandleFunc注册了homePageHandler处理程序,并使用/home URL 模式,这意味着每当我们访问具有/home模式的 HTTP URL 时,homePage处理程序将通过传递(http.ResponseWriter, *http.Request)作为参数来执行。

  • router.HandleFunc("/login", login).Methods("POST"): 在这里,我们使用gorilla/mux包的HandleFunc注册了loginHandler处理程序,并使用/login URL 模式,这意味着每当我们访问具有/login模式的 HTTP URL 时,login处理程序将通过传递(http.ResponseWriter, *http.Request)作为参数来执行。

  • router.HandleFunc("/logout", logout).Methods("POST"): 在这里,我们使用gorilla/mux包的HandleFunc注册了logoutHandler处理程序,并使用/logout URL 模式,这意味着每当我们访问具有/logout模式的 HTTP URL 时,logout处理程序将通过传递(http.ResponseWriter, *http.Request)作为参数来执行。

  • http.Handle("/", router): 在这里,我们使用net/http包的HandleFunc/ URL 模式注册了路由器,这意味着所有具有/ URL 模式的请求都由路由器处理。

  • err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil): 在这里,我们调用http.ListenAndServe来提供处理每个传入连接的 HTTP 请求的请求。ListenAndServe接受两个参数——服务器地址和处理程序,其中服务器地址为localhost:8080,处理程序为nil,这意味着我们要求服务器使用DefaultServeMux作为处理程序。

  • if err != nil { log.Fatal("error starting http server : ", err) return}: 在这里,我们检查是否有任何启动服务器的问题。如果有,记录错误并以状态码1退出。

第四章:在 Go 中编写和使用 RESTful Web 服务

在本章中,我们将涵盖以下内容:

  • 创建你的第一个 HTTP GET 方法

  • 创建你的第一个 HTTP POST 方法

  • 创建你的第一个 HTTP PUT 方法

  • 创建你的第一个 HTTP DELETE 方法

  • 对你的 REST API 进行版本控制

  • 创建你的第一个 REST 客户端

  • 创建你的第一个 AngularJS 客户端

  • 创建你的第一个 ReactJS 客户端

  • 创建你的第一个 VueJS 客户端

介绍

每当我们构建一个封装了对其他相关应用有帮助的逻辑的 Web 应用程序时,我们通常也会编写和使用 Web 服务。这是因为它们通过网络公开功能,可以通过 HTTP 协议访问,使应用程序成为唯一的真相来源。

在本章中,我们将编写一个支持GETPOSTPUTDELETE HTTP 方法的 RESTful API,然后我们将学习如何对 REST API 进行版本控制,这在我们创建公开使用的 API 时非常有帮助。最后,我们将编写 REST 客户端来消耗它们。

创建你的第一个 HTTP GET 方法

在编写 Web 应用程序时,我们经常需要将我们的服务暴露给客户端或 UI,以便它们可以消耗在不同系统上运行的代码。通过 HTTP 协议方法可以暴露服务。在许多 HTTP 方法中,我们将学习在本教程中实现 HTTP GET方法。

如何做...

  1. 使用go get命令安装github.com/gorilla/mux包,如下所示:
$ go get github.com/gorilla/mux
  1. 创建http-rest-get.go,在其中我们将定义两个路由—/employees/employee/{id}以及它们的处理程序。前者写入员工的静态数组,后者将为提供的 ID 写入相应 ID 的员工详情到 HTTP 响应流,如下所示:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Route struct 
{
  Name string
  Method string
  Pattern string
  HandlerFunc http.HandlerFunc
}
type Routes []Route
var routes = Routes
{
  Route
  {
    "getEmployees",
    "GET",
    "/employees",
    getEmployees,
  },
  Route
  {
    "getEmployee",
    "GET",
    "/employee/{id}",
    getEmployee,
  },
}
type Employee struct 
{
  Id string `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
    Employee{Id: "2", FirstName: "Baz", LastName: "Qux"},
  }
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  json.NewEncoder(w).Encode(employees)
}
func getEmployee(w http.ResponseWriter, r *http.Request) 
{
  vars := mux.Vars(r)
  id := vars["id"]
  for _, employee := range employees 
  {
    if employee.Id == id 
    {
      if err := json.NewEncoder(w).Encode(employee); err != nil 
      {
        log.Print("error getting requested employee :: ", err)
      }
    }
  }
}
func AddRoutes(router *mux.Router) *mux.Router 
{
  for _, route := range routes 
  {
    router.
    Methods(route.Method).
    Path(route.Pattern).
    Name(route.Name).
    Handler(route.HandlerFunc)
  }
  return router
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  router := AddRoutes(muxRouter)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-rest-get.go

它是如何工作的...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080上启动。

接下来,从命令行执行GET请求如下将给你一个员工列表:

$ curl -X GET http://localhost:8080/employees
[{"id":"1","firstName":"Foo","lastName":"Bar"},{"id":"2","firstName":"Baz","lastName":"Qux"}]

在这里,从命令行执行GET请求获取特定员工 ID,将为你提供相应 ID 的员工详情:

$ curl -X GET http://localhost:8080/employee/1
 {"id":"1","firstName":"Foo","lastName":"Bar"}

让我们了解我们编写的程序:

  1. 我们使用了import ("encoding/json" "log" "net/http" "strconv" "github.com/gorilla/mux")。在这里,我们导入了github.com/gorilla/mux来创建一个Gorilla Mux Router

  2. 接下来,我们声明了Route结构类型,具有四个字段—NameMethodPatternHandlerFunc,其中Name表示 HTTP 方法的名称,Method表示 HTTP 方法类型,可以是GETPOSTPUTDELETE等,Pattern表示 URL 路径,HandlerFunc表示 HTTP 处理程序。

  3. 接下来,我们为GET请求定义了两个路由,如下:

var routes = Routes
{
  Route
  {
    "getEmployees",
    "GET",
    "/employees",
    getEmployees,
  },
  Route
  {
    "getEmployee",
    "GET",
    "/employee/{id}",
    getEmployee,
  },
}
  1. 接下来,我们定义了一个静态的Employees数组,如下:
func init() 
{
  employees = Employees 
  {
    Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
    Employee{Id: "2", FirstName: "Baz", LastName: "Qux"},
  }
}
  1. 然后,我们定义了两个处理程序—getEmployeesgetEmployee,前者只是将员工的静态数组编组并将其写入 HTTP 响应流,后者从 HTTP 请求变量获取员工 ID,从数组中获取相应 ID 的员工,编组对象,并将其写入 HTTP 响应流。

  2. 在处理程序之后,我们定义了一个AddRoutes函数,它遍历我们定义的路由数组,将其添加到gorilla/mux路由器,并返回Router对象。

  3. 最后,我们定义了main(),在其中使用NewRouter()处理程序创建了一个gorilla/mux路由器实例,对于新路由的尾部斜杠行为为 true,这意味着应用程序将始终将路径视为路由中指定的路径。例如,如果路由路径是/path/,访问/path将重定向到前者,反之亦然。

创建你的第一个 HTTP POST 方法

每当我们需要通过异步调用或 HTML 表单将数据发送到服务器时,我们使用 HTTP POST方法的实现,这将在本教程中介绍。

如何做...

  1. 使用以下命令安装github.com/gorilla/mux包,如下所示:
$ go get github.com/gorilla/mux
  1. 创建http-rest-post.go,在其中我们将定义一个支持 HTTP POST方法的附加路由和一个处理程序,该处理程序将员工添加到初始静态数组的员工,并将更新后的列表写入 HTTP 响应流,如下所示:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Route struct 
{
  Name string
  Method string
  Pattern string
  HandlerFunc http.HandlerFunc
}
type Routes []Route
var routes = Routes
{
  Route
  {
    "getEmployees",
    "GET",
    "/employees",
    getEmployees,
  },
  Route
  {
    "addEmployee",
    "POST",
    "/employee/add",
    addEmployee,
  },
}
type Employee struct 
{
  Id string `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
    Employee{Id: "2", FirstName: "Baz", LastName: "Qux"},
  }
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  json.NewEncoder(w).Encode(employees)
}
func addEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  err := json.NewDecoder(r.Body).Decode(&employee)
  if err != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", err)
    return
  }
  log.Printf("adding employee id :: %s with firstName 
  as :: %s and lastName as :: %s ", employee.Id, 
  employee.FirstName, employee.LastName)
  employees = append(employees, Employee{Id: employee.Id, 
  FirstName: employee.FirstName, LastName: employee.LastName})
  json.NewEncoder(w).Encode(employees)
}
func AddRoutes(router *mux.Router) *mux.Router 
{
  for _, route := range routes 
  {
    router.
    Methods(route.Method).
    Path(route.Pattern).
    Name(route.Name).
    Handler(route.HandlerFunc)
  }
  return router
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  router := AddRoutes(muxRouter)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-rest-post.go

工作原理…

运行程序后,HTTP 服务器将在本地监听端口8080

接下来,使用以下命令从命令行执行POST请求将员工添加到具有ID3的列表,并将员工列表作为响应返回:

$ curl -H "Content-Type: application/json" -X POST -d '{"Id":"3", "firstName":"Quux", "lastName":"Corge"}' http://localhost:8080/employee/add

这可以在以下截图中看到:

让我们了解本节中引入的更改:

  1. 首先,我们添加了另一个名为addEmployee的路由,该路由为 URL 模式/employee/add的每个POST请求执行addEmployee处理程序。

  2. 然后,我们定义了一个addEmployee处理程序,它基本上解码了作为POST请求的一部分传递的员工数据,使用 Go 的内置encoding/json包的NewDecoder处理程序将其附加到员工的初始静态数组,并将其写入 HTTP 响应流。

创建您的第一个 HTTP PUT 方法

每当我们想要更新我们之前创建的记录或者如果记录不存在则创建新记录,通常称为Upsert,我们就会使用 HTTP PUT方法的实现,我们将在本节中介绍。

操作步骤…

  1. 使用go get命令安装github.com/gorilla/mux包,如下所示:
$ go get github.com/gorilla/mux
  1. 创建http-rest-put.go,在其中我们将定义一个支持 HTTP PUT方法的附加路由和一个处理程序,该处理程序要么更新提供的 ID 的员工详细信息,要么将员工添加到初始静态数组的员工;如果 ID 不存在,则将其编组为 JSON,并将其写入 HTTP 响应流,如下所示:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Route struct 
{
  Name string
  Method string
  Pattern string
  HandlerFunc http.HandlerFunc
}
type Routes []Route
var routes = Routes
{
  Route
  {
    "getEmployees",
    "GET",
    "/employees",
    getEmployees,
  },
  Route
  {
    "addEmployee",
    "POST",
    "/employee/add",
    addEmployee,
  },
  Route
  {
    "updateEmployee",
    "PUT",
    "/employee/update",
    updateEmployee,
  },
}
type Employee struct 
{
  Id string `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
    Employee{Id: "2", FirstName: "Baz", LastName: "Qux"},
  }
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  json.NewEncoder(w).Encode(employees)
}
func updateEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  err := json.NewDecoder(r.Body).Decode(&employee)
  if err != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", err)
    return
  }
  var isUpsert = true
  for idx, emp := range employees 
  {
    if emp.Id == employee.Id 
    {
      isUpsert = false
      log.Printf("updating employee id :: %s with 
      firstName as :: %s and lastName as:: %s ", 
      employee.Id, employee.FirstName, employee.LastName)
      employees[idx].FirstName = employee.FirstName
      employees[idx].LastName = employee.LastName
      break
    }
  }
  if isUpsert 
  {
    log.Printf("upserting employee id :: %s with 
    firstName as :: %s and lastName as:: %s ", 
    employee.Id, employee.FirstName, employee.LastName)
    employees = append(employees, Employee{Id: employee.Id,
    FirstName: employee.FirstName, LastName: employee.LastName})
  }
  json.NewEncoder(w).Encode(employees)
}
func addEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  err := json.NewDecoder(r.Body).Decode(&employee)
  if err != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", err)
    return
  }
  log.Printf("adding employee id :: %s with firstName 
  as :: %s and lastName as :: %s ", employee.Id, 
  employee.FirstName, employee.LastName)
  employees = append(employees, Employee{Id: employee.Id, 
  FirstName: employee.FirstName, LastName: employee.LastName})
  json.NewEncoder(w).Encode(employees)
}
func AddRoutes(router *mux.Router) *mux.Router 
{
  for _, route := range routes 
  {
    router.
    Methods(route.Method).
    Path(route.Pattern).
    Name(route.Name).
    Handler(route.HandlerFunc)
  }
  return router
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  router := AddRoutes(muxRouter)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-rest-put.go

工作原理…

运行程序后,HTTP 服务器将在本地监听端口8080

接下来,使用以下命令从命令行执行PUT请求,将为具有 ID 1的员工更新firstNamelastName

$ curl -H "Content-Type: application/json" -X PUT -d '{"Id":"1", "firstName":"Grault", "lastName":"Garply"}' http://localhost:8080/employee/update

这可以在以下截图中看到:

如果我们从命令行执行PUT请求,为具有 ID 3的员工添加另一个员工到数组中,因为没有 ID 为 3 的员工,这演示了 upsert 场景:

$ curl -H "Content-Type: application/json" -X PUT -d '{"Id":"3", "firstName":"Quux", "lastName":"Corge"}' http://localhost:8080/employee/update

这可以在以下截图中看到:

让我们了解本节中引入的更改:

  1. 首先,我们添加了另一个名为updateEmployee的路由,该路由为 URL 模式/employee/update的每个PUT请求执行updateEmployee处理程序。

  2. 然后,我们定义了一个updateEmployee处理程序,它基本上解码了作为PUT请求的一部分传递的员工数据,使用 Go 的内置encoding/json包的NewDecoder处理程序迭代员工数组以了解员工 ID 请求是否存在于员工的初始静态数组中,我们也可以称之为 UPDATE 或 UPSERT 场景,执行所需的操作,并将响应写入 HTTP 响应流。

创建您的第一个 HTTP DELETE 方法

每当我们想要删除不再需要的记录时,我们就会使用 HTTP DELETE方法的实现,我们将在本节中介绍。

工作原理…

  1. 使用go get命令安装github.com/gorilla/mux包,如下所示:
$ go get github.com/gorilla/mux
  1. 创建http-rest-delete.go,在其中我们将定义一个支持 HTTP DELETE方法的路由和一个处理程序,该处理程序从员工的静态数组中删除提供的 ID 的员工详细信息,将数组编组为 JSON,并将其写入 HTTP 响应流,如下所示:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Route struct 
{
  Name string
  Method string
  Pattern string
  HandlerFunc http.HandlerFunc
}
type Routes []Route
var routes = Routes
{
  Route
  {
    "getEmployees",
    "GET",
    "/employees",
    getEmployees,
  },
  Route
  {
    "addEmployee",
    "POST",
    "/employee/add/",
    addEmployee,
  },
  Route
  {
    "deleteEmployee",
    "DELETE",
    "/employee/delete",
    deleteEmployee,
  },
}
type Employee struct 
{
  Id string `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
    Employee{Id: "2", FirstName: "Baz", LastName: "Qux"},
  }
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  json.NewEncoder(w).Encode(employees)
}
func deleteEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  err := json.NewDecoder(r.Body).Decode(&employee)
  if err != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", err)
    return
  }
  log.Printf("deleting employee id :: %s with firstName 
  as :: %s and lastName as :: %s ", employee.Id, 
  employee.FirstName, employee.LastName)
  index := GetIndex(employee.Id)
  employees = append(employees[:index], employees[index+1:]...)
  json.NewEncoder(w).Encode(employees)
}
func GetIndex(id string) int 
{
  for i := 0; i < len(employees); i++ 
  {
    if employees[i].Id == id 
    {
      return i
    }
  }
  return -1
}
func addEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  err := json.NewDecoder(r.Body).Decode(&employee)
  if err != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", err)
    return
  }
  log.Printf("adding employee id :: %s with firstName 
  as :: %s and lastName as :: %s ", employee.Id, 
  employee.FirstName, employee.LastName)
  employees = append(employees, Employee{Id: employee.Id, 
  FirstName: employee.FirstName, LastName: employee.LastName})
  json.NewEncoder(w).Encode(employees)
}
func AddRoutes(router *mux.Router) *mux.Router 
{
  for _, route := range routes 
  {
    router.
    Methods(route.Method).
    Path(route.Pattern).
    Name(route.Name).
    Handler(route.HandlerFunc)
  }
  return router
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  router := AddRoutes(muxRouter)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-rest-delete.go

工作原理…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,从命令行执行DELETE请求,将删除 ID 为 1 的员工,并给我们更新后的员工列表:

$ curl -H "Content-Type: application/json" -X DELETE -d '{"Id":"1", "firstName": "Foo", "lastName": "Bar"}' http://localhost:8080/employee/delete

这可以在以下截图中看到:

让我们了解我们在这个示例中引入的更改:

  1. 首先,我们添加了另一个名为deleteEmployee的路由,它为 URL 模式/employee/delete的每个DELETE请求执行deleteEmployee处理程序。

  2. 然后,我们定义了一个deleteEmployee处理程序,基本上是使用 Go 内置的encoding/json包的NewDecoder处理程序解码作为DELETE请求的一部分传入的员工数据,使用GetIndex辅助函数获取请求的员工的索引,删除员工,并将更新后的数组以 JSON 格式写入 HTTP 响应流。

对 REST API 进行版本控制

当您创建一个 RESTful API 来为内部客户端提供服务时,您可能不必担心对 API 进行版本控制。更进一步,如果您可以控制访问您的 API 的所有客户端,情况可能是一样的。

然而,在您有一个公共 API 或者您无法控制每个使用它的客户端的 API 的情况下,可能需要对 API 进行版本控制,因为业务需要不断发展,我们将在这个示例中进行介绍。

如何做...

  1. 使用go get命令安装github.com/gorilla/mux包,如下所示:
$ go get github.com/gorilla/mux
  1. 创建http-rest-versioning.go,在其中我们将定义支持 HTTP GET方法的相同 URL 路径的两个版本,其中一个具有v1作为前缀,另一个具有v2作为前缀,如下所示:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "strings"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Route struct 
{
  Name string
  Method string
  Pattern string
  HandlerFunc http.HandlerFunc
}
type Routes []Route
var routes = Routes
{
  Route
  {
    "getEmployees",
    "GET",
    "/employees",
    getEmployees,
  },
}
type Employee struct 
{
  Id string `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
var employeesV1 []Employee
var employeesV2 []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
  }
  employeesV1 = Employees
  {
    Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
    Employee{Id: "2", FirstName: "Baz", LastName: "Qux"},
  }
  employeesV2 = Employees
  {
    Employee{Id: "1", FirstName: "Baz", LastName: "Qux"},
    Employee{Id: "2", FirstName: "Quux", LastName: "Quuz"},
  }
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  if strings.HasPrefix(r.URL.Path, "/v1") 
  {
    json.NewEncoder(w).Encode(employeesV1)
  } 
  else if strings.HasPrefix(r.URL.Path, "/v2") 
  {
    json.NewEncoder(w).Encode(employeesV2)
  } 
  else 
  {
    json.NewEncoder(w).Encode(employees)
  }
}
func AddRoutes(router *mux.Router) *mux.Router 
{
  for _, route := range routes 
  {
    router.
    Methods(route.Method).
    Path(route.Pattern).
    Name(route.Name).
    Handler(route.HandlerFunc)
  }
  return router
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  router := AddRoutes(muxRouter)
  // v1
  AddRoutes(muxRouter.PathPrefix("/v1").Subrouter())
  // v2
  AddRoutes(muxRouter.PathPrefix("/v2").Subrouter())
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-rest-versioning.go

它是如何工作的...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,从命令行执行带有路径前缀为/v1GET请求,将给您一个员工列表:

$ curl -X GET http://localhost:8080/v1/employees
[{"id":"1","firstName":"Foo","lastName":"Bar"},{"id":"2","firstName":"Baz","lastName":"Qux"}]

在这里,使用路径前缀为/v2执行GET请求将给您另一组员工的列表,如下所示:

$ curl -X GET http://localhost:8080/v2/employees
 [{"id":"1","firstName":"Baz","lastName":"Qux"},{"id":"2","firstName":"Quux","lastName":"Quuz"}]

有时,在设计 REST URL 时,如果客户端在不指定 URL 路径中的版本的情况下查询端点,我们更倾向于返回默认数据。为了实现这一点,我们修改了getEmployees处理程序,以检查 URL 中的前缀并相应地采取行动。因此,从命令行执行不带路径前缀的GET请求,将给您一个带有单个记录的列表,我们可以称之为 REST 端点的默认或初始响应:

$ curl -X GET http://localhost:8080/employees
 [{"id":"1","firstName":"Foo","lastName":"Bar"}]

让我们了解我们在这个示例中引入的更改:

  1. 首先,我们定义了一个名为getEmployees的单一路由,它为 URL 模式/employees的每个GET请求执行getEmployees处理程序。

  2. 然后,我们创建了三个数组,分别是employeesemployeesV1employeesV2,它们作为对 URL 模式/employees/v1/employees/v2/employees的 HTTP GET调用的响应返回。

  3. 接下来,我们定义了一个getEmployees处理程序,在其中我们检查 URL 路径中的前缀,并根据其执行操作。

  4. 然后,我们定义了一个AddRoutes辅助函数,它遍历我们定义的路由数组,将其添加到gorilla/mux路由器中,并返回Router对象。

  5. 最后,我们定义了main(),在其中我们使用NewRouter()处理程序创建一个带有尾部斜杠行为为 true 的gorilla/mux路由器实例,并通过调用AddRoutes辅助函数将路由添加到其中,传递默认路由器和两个子路由器,一个带有前缀v1,另一个带有前缀v2

创建您的第一个 REST 客户端

如今,大多数与服务器通信的应用程序都使用 RESTful 服务。根据我们的需求,我们通过 JavaScript、jQuery 或 REST 客户端来消费这些服务。

在这个食谱中,我们将使用https://gopkg.in/resty.v1包编写一个 REST 客户端,该包本身受到 Ruby rest 客户端的启发,用于消耗 RESTful 服务。

准备就绪…

在一个单独的终端中运行我们在之前的食谱中创建的http-rest-get.go,执行以下命令:

$ go run http-rest-get.go

参见创建您的第一个 HTTP GET 方法食谱。

通过执行以下命令验证/employees服务是否在本地端口8080上运行:

$ curl -X GET http://localhost:8080/employees

这应该返回以下响应:

[{"id":"1","firstName":"Foo","lastName":"Bar"},{"id":"2","firstName":"Baz","lastName":"Qux"}]

如何做…

  1. 使用go get命令安装github.com/gorilla/muxgopkg.in/resty.v1包,如下所示:
$ go get github.com/gorilla/mux
$ go get -u gopkg.in/resty.v1
  1. 创建http-rest-client.go,在其中我们将定义调用resty处理程序的处理程序,如GETPOSTPUTDELETE,从 REST 服务获取响应,并将其写入 HTTP 响应流,如下所示:
package main
import 
(
  "encoding/json"
  "fmt"
  "log"
  "net/http"
  "github.com/gorilla/mux"
  resty "gopkg.in/resty.v1"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8090"
)
const WEB_SERVICE_HOST string = "http://localhost:8080"
type Employee struct 
{
  Id string `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  response, err := resty.R().Get(WEB_SERVICE_HOST + 
  "/employees")
  if err != nil 
  {
    log.Print("error getting data from the web service :: ", err)
    return
  }
  printOutput(response, err)
  fmt.Fprintf(w, response.String())
}
func addEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  decodingErr := json.NewDecoder(r.Body).Decode(&employee)
  if decodingErr != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", decodingErr)
    return
  }
  log.Printf("adding employee id :: %s with firstName 
  as :: %s and lastName as :: %s ", employee.Id, 
  employee.FirstName, employee.LastName)
  response, err := resty.R().
  SetHeader("Content-Type", "application/json").
  SetBody(Employee{Id: employee.Id, FirstName: 
  employee.FirstName, LastName: employee.LastName}).
  Post(WEB_SERVICE_HOST + "/employee/add")
  if err != nil 
  {
    log.Print("error occurred while adding employee :: ", err)
    return
  }
  printOutput(response, err)
  fmt.Fprintf(w, response.String())
}
func updateEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  decodingErr := json.NewDecoder(r.Body).Decode(&employee)
  if decodingErr != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", decodingErr)
    return
  }
  log.Printf("updating employee id :: %s with firstName 
  as :: %s and lastName as :: %s ", employee.Id, 
  employee.FirstName, employee.LastName)
  response, err := resty.R().
  SetBody(Employee{Id: employee.Id, FirstName: 
  employee.FirstName, LastName: employee.LastName}).
  Put(WEB_SERVICE_HOST + "/employee/update")
  if err != nil 
  {
    log.Print("error occurred while updating employee :: ", err)
    return
  }
  printOutput(response, err)
  fmt.Fprintf(w, response.String())
}
func deleteEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  decodingErr := json.NewDecoder(r.Body).Decode(&employee)
  if decodingErr != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", decodingErr)
    return
  }
  log.Printf("deleting employee id :: %s with firstName 
  as :: %s and lastName as :: %s ", employee.Id, 
  employee.FirstName, employee.LastName)
  response, err := resty.R().
  SetBody(Employee{Id: employee.Id, FirstName: 
  employee.FirstName, LastName: employee.LastName}).
  Delete(WEB_SERVICE_HOST + "/employee/delete")
  if err != nil 
  {
    log.Print("error occurred while deleting employee :: ", err)
    return
  }
  printOutput(response, err)
  fmt.Fprintf(w, response.String())
}
func printOutput(resp *resty.Response, err error) 
{
  log.Println(resp, err)
}
func main() 
{
  router := mux.NewRouter().StrictSlash(false)
  router.HandleFunc("/employees", getEmployees).Methods("GET")
  employee := router.PathPrefix("/employee").Subrouter()
  employee.HandleFunc("/add", addEmployee).Methods("POST")
  employee.HandleFunc("/update", updateEmployee).Methods("PUT")
  employee.HandleFunc("/delete", deleteEmployee).Methods("DELETE")
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-rest-client.go

工作原理…

一旦我们运行程序,HTTP 服务器将在本地监听端口8090

接下来,通过执行以下命令向 REST 客户端发送GET请求,将会得到来自服务的所有员工的列表:

$ curl -X GET http://localhost:8090/employees
 [{"id":"1","firstName":"Foo","lastName":"Bar"},{"id":"2","firstName":"Baz","lastName":"Qux"}]

同样地,在一个单独的终端中运行我们在之前的食谱中创建的http-rest-post.go,执行以下命令:

$ go run http-rest-post.go

从命令行执行POST请求到 REST 客户端,如下所示:

$ curl -H "Content-Type: application/json" -X POST -d '{"Id":"3", "firstName":"Quux", "lastName":"Corge"}' http://localhost:8090/employee/add [{"id":"1","firstName":"Foo","lastName":"Bar"},{"id":"2","firstName":"Baz","lastName":"Qux"},{"id":"3","firstName":"Quux","lastName":"Corge"}]

这将向初始静态列表添加一个员工,并返回更新后的员工列表,如下截图所示:

让我们了解我们编写的程序:

  1. 使用import ("encoding/json" "fmt" "log" "net/http" "github.com/gorilla/mux" resty “gopkg.in/resty.v1"),我们导入了github.com/gorilla/mux来创建Gorilla Mux Router,并使用包别名resty导入了gopkg.in/resty.v1,它是 Go 的 REST 客户端,具有各种处理程序来消耗 RESTful web 服务。

  2. 使用const WEB_SERVICE_HOST string = "http://localhost:8080",我们声明了 RESTful web 服务主机的完整 URL。

根据项目大小,您可以将WEB_SERVICE_HOST字符串移动到常量文件或属性文件中,以帮助您在运行时覆盖其值。

  1. 接下来,我们定义了一个getEmployees处理程序,在其中我们创建一个新的resty请求对象调用其R()处理程序,调用Get方法,执行 HTTP GET请求,获取响应,并将其写入 HTTP 响应。

  2. 类似地,我们定义了另外三个处理程序,用于向 RESTful 服务发送POSTPUTDELETE请求,以及一个main(),在其中我们创建了一个gorilla/mux路由器实例,并使用getEmployees处理程序注册了/employees URL 路径,以及使用addEmployeeupdateEmployeedeleteEmployee处理程序分别注册了/employee/add/employee/update/employee/delete

创建您的第一个 AngularJS 客户端

AngularJS 是一个开源的 JavaScript Model-View-Whatever(MVW)框架,它让我们能够构建结构良好、易于测试和易于维护的基于浏览器的应用程序。

在这个食谱中,我们将学习创建一个 AngularJS 与 TypeScript 2 客户端,向本地运行的 HTTP 服务器发送POST请求。

准备就绪…

由于我们已经在之前的食谱中创建了一个接受GETPOST请求的 HTTP 服务器,我们将使用相同的代码库作为我们的 HTTP 服务器。

此外,此处的食谱假设您的机器上已安装了 Angular2 CLI。如果没有,请执行以下命令进行安装:

$ npm install -g @angular/cli

参见创建您的第一个 HTTP POST 方法食谱。

如何做…

  1. 通过执行以下命令创建一个新项目和骨架应用程序:
$ ng new angularjs-client
  1. 移动到angularjs-client目录,并通过执行以下命令创建server.go
$ cd angularjs-client && touch server.go
  1. 将以下代码复制到server.go中:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Route struct 
{
  Name string
  Method string
  Pattern string
  HandlerFunc http.HandlerFunc
}
type Routes []Route
var routes = Routes
{
  Route
  {
    "getEmployees",
    "GET",
    "/employees",
    getEmployees,
  },
  Route
  {
    "addEmployee",
    "POST",
    "/employee/add",
    addEmployee,
  },
}
type Employee struct 
{
  Id string `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
    Employee{Id: "2", FirstName: "Baz", LastName: "Qux"},
  }
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  json.NewEncoder(w).Encode(employees)
}
func addEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  err := json.NewDecoder(r.Body).Decode(&employee)
  if err != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", err)
    return
  }
  log.Printf("adding employee id :: %s with firstName 
  as :: %s and lastName as :: %s ", employee.Id, 
  employee.FirstName, employee.LastName)
  employees = append(employees, Employee{Id: employee.Id, 
  FirstName: employee.FirstName, LastName: employee.LastName})
  json.NewEncoder(w).Encode(employees)
}
func AddRoutes(router *mux.Router) *mux.Router 
{
  for _, route := range routes 
  {
    router.
    Methods(route.Method).
    Path(route.Pattern).
    Name(route.Name).
    Handler(route.HandlerFunc)
  }
  return router
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  router := AddRoutes(muxRouter)
  router.PathPrefix("/").Handler(http.FileServer
  (http.Dir("./dist/")))
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 移动到angularjs-client目录,并通过执行以下命令创建models/employee.tsservice/employee.service.ts
$ cd src/app/ && mkdir models && mkdir services && cd models && touch employee.ts && cd ../services && touch employee.service.ts
  1. 将以下代码复制到angularjs-client/src/app/models/employee.ts中:
export class Employee 
{
  constructor
  (
    public id: string,
    public firstName: string,
    public lastName: string
  ) {}
}
  1. 将以下代码复制到angularjs-client/src/app/services

/employee.service.ts`:

import { Injectable } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { Employee } from "app/models/employee";

@Injectable()
export class EmployeeService 
{
  constructor(private http: Http) { }
  getEmployees(): Observable<Employee[]> 
  {
    return this.http.get("http://localhost:8080/employees")
    .map((res: Response) => res.json())
    .catch((error: any) => Observable.throw(error.json().
    error || 'Server error'));
  }
  addEmployee(employee: Employee): Observable<Employee> 
  {
    let headers = new Headers({ 'Content-Type': 
    'application/json' });
    let options = new RequestOptions({ headers: headers });
    return this.http.post("http://localhost:8080/employee
    /add", employee, options)
    .map(this.extractData)
    .catch(this.handleErrorObservable);
  }
  private extractData(res: Response) 
  {
    let body = res.json();
    return body || {};
  }
  private handleErrorObservable(error: Response | any) 
  {
    console.error(error.message || error);
    return Observable.throw(error.message || error);
  }
}
  1. 用以下内容替换angularjs-client/src/app/app.component.html的代码:
<div class = "container" style="padding:5px">
  <form>
    <div class = "form-group">
      <label for = "id">ID</label>
      <input type = "text" class = "form-control" id = "id" 
      required [(ngModel)] = "employee.id" name = "id">
    </div>
    <div class = "form-group">
      <label for = "firstName">FirstName</label>
      <input type = "text" class = "form-control" id = 
      "firstName" [(ngModel)] = "employee.firstName" name =
      "firstName">
    </div>
    <div class = "form-group">
      <label for = "lastName">LastName</label>
      <input type = "text" class = "form-control" id = 
      "lastName" [(ngModel)] = "employee.lastName" name =
      "lastName">
    </div>
    <div>
      <button (click)="addEmployee()">Add</button>
    </div>
  </form>
</div>
<table>
  <thead>
    <th>ID</th>
    <th>FirstName</th>
    <th>LastName</th>
  </thead>
  <tbody>
    <tr *ngFor="let employee of employees">
      <td>{{employee.id}}</td>
      <td>{{employee.firstName}}</td>
      <td>{{employee.lastName}}</td>
    </tr>
  </tbody>
</table>
  1. 用以下内容替换angularjs-client/src/app/app.component.ts的代码:
import { Component, OnInit } from '@angular/core';
import { EmployeeService } from "app/services/employee.service";
import { Employee } from './models/employee';

@Component
({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit 
{
  title = 'app';
  employee = new Employee('', '', '');
  employees;
  constructor(private employeeService: EmployeeService) { }
  ngOnInit(): void 
  {
    this.getEmployees();
  }
  getEmployees(): void 
  {
    this.employeeService.getEmployees()
    .subscribe(employees => this.employees = employees);
  }
  addEmployee(): void 
  {
    this.employeeService.addEmployee(this.employee)
    .subscribe
    (
      employee => 
      {
        this.getEmployees();
        this.reset();
      }
    );
  }
  private reset() 
  {
    this.employee.id = null;
    this.employee.firstName = null;
    this.employee.lastName = null;
  }
}
  1. 用以下内容替换angularjs-client/src/app/app.module.ts的代码:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { EmployeeService } from "app/services/employee.service";
import { FormsModule } from '@angular/forms';

@NgModule
({
 declarations: 
 [
   AppComponent
 ],
 imports: 
 [
   BrowserModule, HttpModule, FormsModule
 ],
 providers: [EmployeeService],
 bootstrap: [AppComponent]
})
export class AppModule { }

一切就绪后,目录结构应如下所示:

  1. 移动到angularjs-client目录并执行以下命令来构建项目构件并运行程序:
$ ng build
$ go run server.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

浏览到http://localhost:8080将显示 AngularJS 客户端页面,其中有一个带有 Id、FirstName 和 LastName 字段的 HTML 表单,如下图所示:

在填写表单后点击“Add”按钮将向运行在端口8080上的 HTTP 服务器发送一个POST请求。一旦服务器处理了请求,它将返回所有静态员工的列表以及新添加的员工,并在浏览器中显示,如下图所示:

所有静态员工的列表以及新添加的员工

创建你的第一个 ReactJS 客户端

ReactJS 是一个声明式的 JavaScript 库,有助于高效构建用户界面。因为它基于虚拟 DOM 的概念工作,它提高了应用程序的性能,因为 JavaScript 虚拟 DOM 比常规 DOM 更快。

在这个教程中,我们将学习创建一个 ReactJS 客户端来向本地运行的 HTTP 服务器发送POST请求。

准备就绪…

由于我们已经在之前的教程中创建了一个接受GETPOST HTTP 请求的 HTTP 服务器,我们将使用相同的代码库作为我们的 HTTP 服务器。

此外,本教程假设您已在您的机器上安装了npm,并且对npmwebpack有基本的了解,它是一个 JavaScript 模块打包工具。

参见创建你的第一个 HTTP POST 方法教程。

如何做…

  1. 创建一个reactjs-client目录,我们将在其中保存所有我们的 ReactJS 源文件和一个 HTTP 服务器,如下所示:
$ mkdir reactjs-client && cd reactjs-client && touch server.go
  1. 将以下代码复制到server.go中:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Route struct 
{
  Name string
  Method string
  Pattern string
  HandlerFunc http.HandlerFunc
}
type Routes []Route
var routes = Routes
{
  Route
  {
    "getEmployees",
    "GET",
    "/employees",
    getEmployees,
  },
  Route
  {
    "addEmployee",
    "POST",
    "/employee/add",
    addEmployee,
  },
}
type Employee struct 
{
  Id string `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
    Employee{Id: "2", FirstName: "Baz", LastName: "Qux"},
  }
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  json.NewEncoder(w).Encode(employees)
}
func addEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  err := json.NewDecoder(r.Body).Decode(&employee)
  if err != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", err)
    return
  }
  log.Printf("adding employee id :: %s with firstName 
  as :: %s and lastName as :: %s ", employee.Id, 
  employee.FirstName, employee.LastName)
  employees = append(employees, Employee{Id: employee.Id, 
  FirstName: employee.FirstName, LastName: employee.LastName})
  json.NewEncoder(w).Encode(employees)
}
func AddRoutes(router *mux.Router) *mux.Router 
{
  for _, route := range routes 
  {
    router.
    Methods(route.Method).
    Path(route.Pattern).
    Name(route.Name).
    Handler(route.HandlerFunc)
  }
  return router
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  router := AddRoutes(muxRouter)
  router.PathPrefix("/").Handler(http.FileServer
  (http.Dir("./assets/")))
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 创建另一个名为assets的目录,其中将保存所有我们的前端代码文件,如.html.js.cssimages,如下所示:
$ mkdir assets && cd assets && touch index.html
  1. 将以下内容复制到index.html中:
<html>
  <head lang="en">
    <meta charset="UTF-8" />
    <title>ReactJS Client</title>
  </head> 
  <body>
    <div id="react"></div>
    <script src="img/script.js"></script>
  </body>
</html>
  1. 移动到reactjs-client目录并执行npm init来创建package.json,在其中我们指定构建我们的 react 客户端所需的所有依赖项,如ReactReact DOMWebpackBabel LoaderBabel CoreBabel Preset: ES2015Babel Preset: React,如下所示:
$ cd reactjs-client && touch npm init

用以下内容替换package.json的内容:

{
  "name": "reactjs-client",
  "version": "1.0.0",
  "description": "ReactJs Client",
  "keywords": 
  [
    "react"
  ],
  "author": "Arpit Aggarwal",
  "dependencies": 
  {
    "axios": "⁰.18.0",
    "react": "¹⁶.2.0",
    "react-dom": "¹⁶.2.0",
    "react-router-dom": "⁴.2.2",
    "webpack": "⁴.2.0",
    "webpack-cli": "².0.9",
    "lodash": "⁴.17.5"
  },
  "scripts": 
  {
    "build": "webpack",
    "watch": "webpack --watch -d"
  },
  "devDependencies": 
  {
    "babel-core": "⁶.18.2",
    "babel-loader": "⁷.1.4",
    "babel-polyfill": "⁶.16.0",
    "babel-preset-es2015": "⁶.18.0",
    "babel-preset-react": "⁶.16.0"
  }
}
  1. 创建webpack.config.js,在其中我们将配置webpack,如下所示:
$ cd reactjs-client && touch webpack.config.js

将以下内容复制到webpack.config.js中:

var path = require('path');
module.exports = 
{
  resolve: 
  {
    extensions: ['.js', '.jsx']
  },
  mode: 'development',
  entry: './app/main.js',
  cache: true,
  output: 
  {
    path: __dirname,
    filename: './assets/script.js'
  },
  module: 
  {
    rules: 
    [
      {
        test: path.join(__dirname, '.'),
        exclude: /(node_modules)/,
        loader: 'babel-loader',
        query: 
        {
          cacheDirectory: true,
          presets: ['es2015', 'react']
        }
      }
    ]
  }
};
  1. 通过执行以下命令为webpack创建入口点,即reactjs-client/app/main.js
$ cd reactjs-client && mkdir app && cd app && touch main.js

将以下内容复制到main.js中:

'use strict';
const React = require('react');
const ReactDOM = require('react-dom')
import EmployeeApp from './components/employee-app.jsx'
ReactDOM.render
(
  <EmployeeApp />,
  document.getElementById('react')
)
  1. 通过执行以下命令定义ReactApp以及它的子组件:
$ cd reactjs-client && mkdir components && cd components && touch react-app.jsx employee-list.jsx employee.jsx add-employee.jsx

将以下内容复制到reactjs-client/app/components/employee-app.jsx中:

'use strict';
const React = require('react');
var axios = require('axios');
import EmployeeList from './employee-list.jsx'
import AddEmployee from './add-employee.jsx'
export default class EmployeeApp extends React.Component 
{
  constructor(props) 
  {
    super(props);
    this.state = {employees: []};
    this.addEmployee = this.addEmployee.bind(this);
    this.Axios = axios.create
    (
      {
        headers: {'content-type': 'application/json'}
      }
    );
  }
  componentDidMount() 
  {
    let _this = this;
    this.Axios.get('/employees')
    .then
    (
      function (response) 
      {
        _this.setState({employees: response.data});
      }
    )
    .catch(function (error) { });
  }
  addEmployee(employeeName)
  {
    let _this = this;
    this.Axios.post
    (
      '/employee/add', 
      {
        firstName: employeeName
      }
    )
    .then
    (
      function (response) 
      {
        _this.setState({employees: response.data});
      }
    )
    .catch(function (error) { });
    }
    render() 
    {
      return 
      (
        <div>
          <AddEmployee addEmployee={this.addEmployee}/>
          <EmployeeList employees={this.state.employees}/>
        </div>
      )
   }
}

将以下内容复制到reactjs-client/app/components/employee.jsx中:

const React = require('react');
export default class Employee extends React.Component
{
  render() 
  {
    return 
    (
      <tr>
        <td>{this.props.employee.firstName}</td>
      </tr>
    )
  }
}

将以下内容复制到reactjs-client/app/components/employee-list.jsx中:

const React = require('react');
import Employee from './employee.jsx'
export default class EmployeeList extends React.Component
{
  render() 
  {
    var employees = this.props.employees.map
    (
      (employee, i) =>
      <Employee key={i} employee={employee}/>
    );
    return 
    (
      <table>
        <tbody>
          <tr>
            <th>FirstName</th>
          </tr>
          {employees}
        </tbody>
      </table>
    )
  }
}

将以下内容复制到reactjs-client/app/components/add-employee.jsx中:

import React, { Component, PropTypes } from 'react'
export default class AddEmployee extends React.Component 
{
  render()
  {
    return 
    (
      <div>
        <input type = 'text' ref = 'input' />
        <button onClick = {(e) => this.handleClick(e)}>
          Add
        </button>
      </div>
    )
  }
  handleClick(e) 
  {
    const node = this.refs.input
    const text = node.value.trim()
    this.props.addEmployee(text)
    node.value = ''
  }
}

一切就绪后,目录结构应如下所示:

目录结构

  1. 移动到reactjs-client目录并执行以下命令来安装node modules和构建webpack
$ npm install
$ npm run build
  1. 使用以下命令运行程序:
$ go run server.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

浏览到http://localhost:8080将会显示我们的 VueJS 客户端页面,如下截图所示:

ReactJS 客户端页面

在填写文本框后点击添加按钮将会向运行在端口8080上的 HTTP 服务器发送一个POST请求:

在填写文本框后点击添加按钮

接下来,从命令行执行一个GET请求将会给你一个所有静态员工的列表:

$ curl -X GET http://localhost:8080/employees

这将会和新添加的员工一起显示如下:

[{"id":"1","firstName":"Foo","lastName":"Bar"},{"id":"2","firstName":"Baz","lastName":"Qux"},{"id":"","firstName":"Arpit","lastName":""}]

创建你的第一个 VueJS 客户端

作为开源项目,VueJS 是逐步可采用和渐进式的 JavaScript 框架之一,公司正在采用它来构建他们的前端或面向客户的用户界面。

在这个教程中,我们将学习在 VueJS 中创建一个客户端,通过向本地运行的 HTTP 服务器发送一个 HTTP POST请求来添加一个员工。

准备好…

由于我们已经在之前的教程中创建了一个接受GETPOST请求的 HTTP 服务器,我们将使用相同的代码库作为我们的 HTTP 服务器。

参见创建你的第一个 HTTP POST 方法教程。

如何做…

  1. 创建一个vuejs-client目录,我们将在其中保存所有 VueJS 源文件和一个 HTTP 服务器,如下所示:
$ mkdir vuejs-client && cd vuejs-client && touch server.go
  1. 将以下代码复制到server.go中:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Route struct 
{
  Name string
  Method string
  Pattern string
  HandlerFunc http.HandlerFunc
}
type Routes []Route
var routes = Routes
{
  Route
  {
    "getEmployees",
    "GET",
    "/employees",
    getEmployees,
  },
  Route
  {
    "addEmployee",
    "POST",
    "/employee/add",
    addEmployee,
  },
}
type Employee struct 
{
  Id string `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
    Employee{Id: "2", FirstName: "Baz", LastName: "Qux"},
  }
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  json.NewEncoder(w).Encode(employees)
}
func addEmployee(w http.ResponseWriter, r *http.Request) 
{
  employee := Employee{}
  err := json.NewDecoder(r.Body).Decode(&employee)
  if err != nil 
  {
    log.Print("error occurred while decoding employee 
    data :: ", err)
    return
  }
  log.Printf("adding employee id :: %s with firstName 
  as :: %s and lastName as :: %s ", employee.Id, 
  employee.FirstName, employee.LastName)
  employees = append(employees, Employee{Id: employee.Id, 
  FirstName: employee.FirstName, LastName: employee.LastName})
  json.NewEncoder(w).Encode(employees)
}
func AddRoutes(router *mux.Router) *mux.Router 
{
  for _, route := range routes 
  {
    router.
    Methods(route.Method).
    Path(route.Pattern).
    Name(route.Name).
    Handler(route.HandlerFunc)
  }
  return router
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  router := AddRoutes(muxRouter)
  router.PathPrefix("/").Handler(http.FileServer
  (http.Dir("./assets/")))
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 创建另一个名为assets的目录,其中将保存所有我们的前端代码文件,如.html.js.cssimages,如下所示:
$ mkdir assets && cd assets && touch index.html && touch main.js
  1. 将以下内容复制到index.html中:
<html>
  <head>
    <title>VueJs Client</title>
    <script type = "text/javascript" src = "https://cdnjs.
    cloudflare.com/ajax/libs/vue/2.4.0/vue.js"></script>
    <script type = "text/javascript" src="img/vue-resource@1.5.0"></script>
  </head>
  <body>
    <div id = "form">
      <h1>{{ message }}</h1>
      <table>
        <tr>
          <td><label for="id">Id</label></td>
          <td><input type="text" value="" v-model="id"/></td>
        </tr>
        <tr>
          <td><label for="firstName">FirstName</label></td>
          <td><input type="text" value="" v-model="firstName"/>
          <td>
        </tr>
        <tr>
          <td><label for="lastName">LastName</label></td>
          <td> <input type="text" value="" v-model="lastName" />
          </td>
        </tr>
        <tr>
          <td><a href="#" class="btn" @click="addEmployee">Add
          </a></td>
        </tr>
      </table>
    </div>
    <script type = "text/javascript" src = "main.js"></script>
  </body>
</html>
  1. 将以下内容复制到main.js中:
var vue_det = new Vue
({
 el: '#form',
 data: 
 {
   message: 'Employee Dashboard',
   id: '',
   firstName:'',
   lastName:''
 },
 methods: 
 {
   addEmployee: function() 
   {
     this.$http.post
     (
       '/employee/add', 
       {
         id: this.id,
         firstName:this.firstName,
         lastName:this.lastName
       }
     )
     .then
     (
       response => 
       {
         console.log(response);
       }, 
       error => 
       {
         console.error(error);
       }
     );
   }
 }
});

一切就绪后,目录结构应该如下所示:

目录结构

  1. 用以下命令运行程序:
$ go run server.go

工作原理…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

浏览到http://localhost:8080将会显示我们的 VueJS 客户端页面,其中有一个包含 Id、FirstName 和 LastName 字段的 HTML 表单,如下截图所示:

VueJS 客户端页面

在填写表单后点击添加按钮将会向运行在端口8080上的 HTTP 服务器发送一个POST请求,如下截图所示:

在填写表单后点击添加按钮

接下来,从命令行执行一个GET请求,将会给你一个所有静态员工的列表:

$ curl -X GET http://localhost:8080/employees

这将会和新添加的员工一起显示如下:

[{"id":"1","firstName":"Foo","lastName":"Bar"},{"id":"2","firstName":"Baz","lastName":"Qux"},{"id":"5","firstName":"Arpit","lastName":"Aggarwal"}]

第五章:使用 SQL 和 NoSQL 数据库

在本章中,我们将涵盖以下内容:

  • 集成 MySQL 和 Go

  • 在 MySQL 中创建您的第一条记录

  • 从 MySQL 中读取记录

  • 更新您的第一条记录在 MySQL 中

  • 从 MySQL 中删除您的第一条记录

  • 集成 MongoDB 和 Go

  • 在 MongoDB 中创建您的第一个文档

  • 从 MongoDB 中读取文档

  • 在 MongoDB 中更新您的第一个文档

  • 从 MongoDB 中删除您的第一个文档

介绍

每当我们想要持久保存数据时,我们总是期待将其保存在数据库中,主要分为两类——SQLNoSQL。每个类别下都有许多可以根据业务用例使用的数据库,因为每个数据库都具有不同的特性并且服务于不同的目的。

在本章中,我们将把 Go Web 应用程序与最著名的开源数据库——MySQLMongoDB集成,并学习在它们上执行 CRUD 操作。由于我们将使用 MySQL 和 MongoDB,我假设这两个数据库都已安装并在您的本地机器上运行。

集成 MySQL 和 Go

假设您是一名开发人员,并且希望将应用程序数据保存在 MySQL 数据库中。作为第一步,您必须在应用程序和 MySQL 之间建立连接,我们将在本示例中介绍。

准备就绪...

通过执行以下命令验证本地端口3306上是否安装并运行了 MySQL:

$ ps -ef | grep 3306

这应该返回以下响应:

还要登录到 MySQL 数据库并创建一个 mydb 数据库,执行如下截图中显示的命令:

如何做...

  1. 使用go get命令安装github.com/go-sql-driver/mysql包,如下所示:
$ go get github.com/go-sql-driver/mysql
  1. 创建connect-mysql.go。然后我们连接到 MySQL 数据库并执行SELECT查询以获取当前数据库名称,如下所示:
package main
import 
(
  "database/sql"
  "fmt"
  "log"
  "net/http"
  "github.com/go-sql-driver/mysql"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  DRIVER_NAME = "mysql"
  DATA_SOURCE_NAME = "root:password@/mydb"
)
var db *sql.DB
var connectionError error
func init() 
{
  db, connectionError = sql.Open(DRIVER_NAME, DATA_SOURCE_NAME)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database :: ", connectionError)
  }
}
func getCurrentDb(w http.ResponseWriter, r *http.Request) 
{
  rows, err := db.Query("SELECT DATABASE() as db")
  if err != nil 
  {
    log.Print("error executing query :: ", err)
    return
  }
  var db string
  for rows.Next() 
  {
    rows.Scan(&db)
  }
  fmt.Fprintf(w, "Current Database is :: %s", db)
}
func main() 
{
  http.HandleFunc("/", getCurrentDb)
  defer db.Close()
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run connect-mysql.go

它是如何工作的...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

浏览到http://localhost:8080/将返回当前数据库名称,如下截图所示:

让我们了解我们编写的程序:

  1. 使用import ("database/sql" "fmt" "log" "net/http" _ "github.com/go-sql-driver/mysql"),我们导入了github.com/go-sql-driver/mysql以进行副作用或初始化,使用下划线在导入语句前面明确表示。

  2. 使用var db *sql.DB,我们声明了一个私有的DB实例。

根据项目大小,您可以全局声明一个 DB 实例,使用处理程序将其注入为依赖项,或将连接池指针放入x/net/context中。

  1. 接下来,我们定义了一个init()函数,在其中我们连接到数据库并将数据库驱动程序名称和数据源传递给它。

  2. 然后,我们定义了一个getCurrentDb处理程序,基本上在数据库上执行选择查询以获取当前数据库名称,遍历记录,将其值复制到变量中,最终将其写入 HTTP 响应流。

在 MySQL 中创建您的第一条记录

在数据库中创建或保存记录需要我们编写 SQL 查询并执行它们,实现对象关系映射ORM),或实现数据映射技术。

在这个示例中,我们将编写一个 SQL 查询,并使用database/sql包执行它来创建一条记录。为了实现这一点,您还可以使用 Go 中许多第三方库中可用的任何库来实现 ORM,例如https://github.com/jinzhu/gormhttps://github.com/go-gorp/gorphttps://github.com/jirfag/go-queryset

准备就绪...

由于我们在上一个示例中已经与 MySQL 数据库建立了连接,我们将扩展它以执行 SQL 查询来创建一条记录。

在创建记录之前,我们必须在 MySQL 数据库中创建一个表,我们将通过执行以下截图中显示的命令来完成:

操作步骤…

  1. 使用go get命令安装github.com/go-sql-driver/mysqlgithub.com/gorilla/mux包,如下所示:
$ go get github.com/go-sql-driver/mysql
$ go get github.com/gorilla/mux
  1. 创建create-record-mysql.go。然后我们连接到 MySQL 数据库并执行 INSERT 查询以创建员工记录,如下所示:
package main
import 
(
  "database/sql"
  "fmt"
  "log"
  "net/http"
  "strconv"
  "github.com/go-sql-driver/mysql"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  DRIVER_NAME = "mysql"
  DATA_SOURCE_NAME = "root:password@/mydb"
)
var db *sql.DB
var connectionError error
func init() 
{
  db, connectionError = sql.Open(DRIVER_NAME, DATA_SOURCE_NAME)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database : ", connectionError)
  }
}
func createRecord(w http.ResponseWriter, r *http.Request) 
{
  vals := r.URL.Query()
  name, ok := vals["name"]
  if ok 
  {
    log.Print("going to insert record in database for name : ",
    name[0])
    stmt, err := db.Prepare("INSERT employee SET name=?")
    if err != nil 
    {
      log.Print("error preparing query :: ", err)
      return
    }
    result, err := stmt.Exec(name[0])
    if err != nil 
    {
      log.Print("error executing query :: ", err)
      return
    }
    id, err := result.LastInsertId()
    fmt.Fprintf(w, "Last Inserted Record Id is :: %s",
    strconv.FormatInt(id, 10))
  } 
  else 
  {
    fmt.Fprintf(w, "Error occurred while creating record in 
    database for name :: %s", name[0])
  }
}
func main() 
{
  router := mux.NewRouter()
  router.HandleFunc("/employee/create", createRecord).
  Methods("POST")
  defer db.Close()
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run create-record-mysql.go

工作原理…

运行程序后,HTTP 服务器将在本地监听端口8080

从命令行执行POST请求以创建员工记录,将会给出最后创建的记录的 ID:

$ curl -X POST http://localhost:8080/employee/create?name=foo
Last created record id is :: 1

让我们理解我们编写的程序:

  1. 使用import ("database/sql" "fmt" "log" "net/http" "strconv" _ "github.com/go-sql-driver/mysql" "github.com/gorilla/mux"),我们导入了github.com/gorilla/mux来创建一个 Gorilla Mux 路由器,并初始化了 Go MySQL 驱动,导入了github.com/go-sql-driver/mysql包。

  2. 接下来,我们定义了一个createRecord处理程序,它从请求中获取姓名,将其分配给本地变量名,准备一个带有姓名占位符的INSERT语句,该占位符将动态替换为姓名,执行该语句,并最终将最后创建的 ID 写入 HTTP 响应流。

从 MySQL 中读取记录

在上一个示例中,我们在 MySQL 数据库中创建了一个员工记录。现在,在这个示例中,我们将学习如何通过执行 SQL 查询来读取它。

操作步骤…

  1. 使用go get命令安装github.com/go-sql-driver/mysqlgithub.com/gorilla/mux包,如下所示:
$ go get github.com/go-sql-driver/mysql
$ go get github.com/gorilla/mux
  1. 创建read-record-mysql.go,在其中我们连接到 MySQL 数据库,执行SELECT查询以获取数据库中的所有员工,遍历记录,将其值复制到结构体中,将所有记录添加到列表中,并将其写入 HTTP 响应流,如下所示:
package main
import 
(
  "database/sql" "encoding/json"
  "log"
  "net/http"
  "github.com/go-sql-driver/mysql"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  DRIVER_NAME = "mysql"
  DATA_SOURCE_NAME = "root:password@/mydb"
)
var db *sql.DB
var connectionError error
func init() 
{
  db, connectionError = sql.Open(DRIVER_NAME, DATA_SOURCE_NAME)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database :: ", connectionError)
  }
}
type Employee struct 
{
  Id int `json:"uid"`
  Name string `json:"name"`
}
func readRecords(w http.ResponseWriter, r *http.Request) 
{
  log.Print("reading records from database")
  rows, err := db.Query("SELECT * FROM employee")
  if err != nil 
  {
    log.Print("error occurred while executing select 
    query :: ",err)
    return
  }
  employees := []Employee{}
  for rows.Next() 
  {
    var uid int
    var name string
    err = rows.Scan(&uid, &name)
    employee := Employee{Id: uid, Name: name}
    employees = append(employees, employee)
  }
  json.NewEncoder(w).Encode(employees)
}
func main() 
{
  router := mux.NewRouter()
  router.HandleFunc("/employees", readRecords).Methods("GET")
  defer db.Close()
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run read-record-mysql.go

工作原理…

运行程序后,HTTP 服务器将在本地监听端口8080

浏览到http://localhost:8080/employees将列出员工表中的所有记录,如下截图所示:

让我们看一下我们编写的程序:

  1. 使用import ("database/sql" "encoding/json" "log" "net/http" _ "github.com/go-sql-driver/mysql" "github.com/gorilla/mux"),我们导入了一个额外的包encoding/json,它有助于将 Go 数据结构编组为JSON

  2. 接下来,我们声明了 Go 数据结构Person,它具有IdName字段。

请记住,在类型定义中字段名称应以大写字母开头,否则可能会出现错误。

  1. 接下来,我们定义了一个readRecords处理程序,它查询数据库以获取员工表中的所有记录,遍历记录,将其值复制到结构体中,将所有记录添加到列表中,将对象列表编组为 JSON,并将其写入 HTTP 响应流。

在 MySQL 中更新您的第一个记录

考虑这样一个情景,你在数据库中创建了一个员工的记录,包括姓名、部门、地址等所有细节,一段时间后员工更换了部门。在这种情况下,我们必须在数据库中更新他们的部门,以便他们的详细信息在整个组织中保持同步,这可以通过SQL UPDATE语句实现,在这个示例中,我们将学习如何在 Go 中实现它。

操作步骤…

  1. 使用go get命令安装github.com/go-sql-driver/mysqlgithub.com/gorilla/mux包,如下所示:
$ go get github.com/go-sql-driver/mysql
$ go get github.com/gorilla/mux
  1. 创建update-record-mysql.go。然后我们连接到 MySQL 数据库,更新员工的姓名,然后将更新的记录数量写入数据库到 HTTP 响应流中,如下所示:
package main
import 
(
  "database/sql"
  "fmt"
  "log"
  "net/http" 
  "github.com/go-sql-driver/mysql"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  DRIVER_NAME = "mysql"
  DATA_SOURCE_NAME = "root:password@/mydb"
)
var db *sql.DB
var connectionError error 
func init() 
{
  db, connectionError = sql.Open(DRIVER_NAME, DATA_SOURCE_NAME)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database :: ", connectionError)
  }
}
type Employee struct 
{
  Id   int    `json:"uid"`
  Name string `json:"name"`
}
func updateRecord(w http.ResponseWriter, r *http.Request) 
{
  vars := mux.Vars(r)
  id := vars["id"]
  vals := r.URL.Query()
  name, ok := vals["name"]
  if ok 
  {
    log.Print("going to update record in database 
    for id :: ", id)
    stmt, err := db.Prepare("UPDATE employee SET name=? 
    where uid=?")
    if err != nil 
    {
      log.Print("error occurred while preparing query :: ", err)
      return
    }
    result, err := stmt.Exec(name[0], id)
    if err != nil 
    {
      log.Print("error occurred while executing query :: ", err)
      return
    }
    rowsAffected, err := result.RowsAffected()
    fmt.Fprintf(w, "Number of rows updated in database 
    are :: %d",rowsAffected)
  } 
  else 
  {
    fmt.Fprintf(w, "Error occurred while updating record in 
    database for id :: %s", id)
  }
}
func main() 
{
  router := mux.NewRouter()
  router.HandleFunc("/employee/update/{id}",
  updateRecord).Methods("PUT")
  defer db.Close()
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run update-record-mysql.go

工作原理…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,从命令行执行PUT请求以更新 ID 为1的员工记录将给出数据库中更新的记录数作为响应:

$ curl -X PUT http://localhost:8080/employee/update/1?name\=bar
Number of rows updated in database are :: 1

让我们看一下我们编写的程序:

  1. 我们定义了一个updateRecord处理程序,它以 URL 路径变量路径中要更新的 ID 和请求变量中的新名称作为输入,准备一个带有名称和 UID 占位符的update语句,该占位符将动态替换,执行该语句,获取执行结果中更新的行数,并将其写入 HTTP 响应流。

  2. 接下来,我们注册了一个updateRecord处理程序,用于处理gorilla/mux路由器中/employee/update/{id}的 URL 模式的每个PUT请求,并在从main()函数返回时使用defer db.Close()语句关闭数据库。

从 MySQL 中删除您的第一条记录

考虑这样一个情景,员工已经离开组织,您想要从数据库中撤销他们的详细信息。在这种情况下,我们可以使用SQL DELETE语句,我们将在本教程中介绍。

如何做到这一点...

  1. 使用go get命令安装github.com/go-sql-driver/mysqlgithub.com/gorilla/mux包,如下所示:
$ go get github.com/go-sql-driver/mysql
$ go get github.com/gorilla/mux
  1. 创建delete-record-mysql.go。然后我们连接到 MySQL 数据库,从数据库中删除员工的名称,并将从数据库中删除的记录数写入 HTTP 响应流,如下所示:
package main
import 
(
  "database/sql"
  "fmt"
  "log"
  "net/http"
  "github.com/go-sql-driver/mysql"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  DRIVER_NAME = "mysql"
  DATA_SOURCE_NAME = "root:password@/mydb"
)
var db *sql.DB
var connectionError error
func init() 
{
  db, connectionError = sql.Open(DRIVER_NAME, DATA_SOURCE_NAME)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database :: ", connectionError)
  }
}
func deleteRecord(w http.ResponseWriter, r *http.Request) 
{
  vals := r.URL.Query()
  name, ok := vals["name"]
  if ok 
  {
    log.Print("going to delete record in database for 
    name :: ", name[0])
    stmt, err := db.Prepare("DELETE from employee where name=?")
    if err != nil 
    {
      log.Print("error occurred while preparing query :: ", err)
      return
    }
    result, err := stmt.Exec(name[0])
    if err != nil 
    {
      log.Print("error occurred while executing query :: ", err)
      return
    }
    rowsAffected, err := result.RowsAffected()
    fmt.Fprintf(w, "Number of rows deleted in database are :: %d",
    rowsAffected)
  } 
  else 
  {
    fmt.Fprintf(w, "Error occurred while deleting record in 
    database for name %s", name[0])
  }
}
func main() 
{
  router := mux.NewRouter()
  router.HandleFunc("/employee/delete",
  deleteRecord).Methods("DELETE")
  defer db.Close()
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run delete-record-mysql.go

它是如何工作的...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,从命令行执行DELETE请求以删除名称为bar的员工将给出从数据库中删除的记录数:

$ curl -X DELETE http://localhost:8080/employee/delete?name\=bar
Number of rows deleted in database are :: 1

让我们看一下我们编写的程序:

  1. 我们定义了一个deleteRecord处理程序,它以请求变量中要从数据库中删除的名称作为输入,准备一个带有名称占位符的DELETE语句,该占位符将动态替换,执行该语句,获取执行结果中删除的行数,并将其写入 HTTP 响应流。

  2. 接下来,我们注册了一个deleteRecord处理程序,用于处理gorilla/mux路由器中/employee/delete的 URL 模式的每个DELETE请求,并在从main()函数返回时使用defer db.Close()语句关闭数据库。

集成 MongoDB 和 Go

每当您想要在 MongoDB 数据库中持久保存数据时,您必须采取的第一步是在数据库和您的 Web 应用程序之间建立连接,在本教程中,我们将使用 Go 中最著名和常用的 MongoDB 驱动程序之一gopkg.in/mgo.v2

准备就绪...

通过执行以下命令验证MongoDB是否安装并在本地端口27017上运行:

$ mongo

这应该返回以下响应:

如何做到这一点...

  1. 使用go get命令安装gopkg.in/mgo.v包,如下所示:
$ go get gopkg.in/mgo.v
  1. 创建connect-mongodb.go。然后我们连接到MongoDB数据库,从集群中获取所有数据库名称,并将它们写入 HTTP 响应流,如下所示:
package main
import 
(
  "fmt"
  "log"
  "net/http"
  "strings"
  mgo "gopkg.in/mgo.v2"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  MONGO_DB_URL = "127.0.0.1"
)
var session *mgo.Session
var connectionError error
func init() 
{
  session, connectionError = mgo.Dial(MONGO_DB_URL)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database :: ", connectionError)
  }
  session.SetMode(mgo.Monotonic, true)
}
func getDbNames(w http.ResponseWriter, r *http.Request) 
{
  db, err := session.DatabaseNames()
  if err != nil 
  {
    log.Print("error getting database names :: ", err)
    return
  }
  fmt.Fprintf(w, "Databases names are :: %s", strings.Join
  (db, ", "))
}
func main() 
{
  http.HandleFunc("/", getDbNames)
  defer session.Close()
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run connect-mongodb.go

它是如何工作的...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

浏览到http://localhost:8080/将列出 MongoDB 集群中存在的所有数据库的名称,并显示如下屏幕截图所示:

让我们看一下我们编写的程序:

  1. 使用`import("fmt" "log" "net/http" "strings" mgo

"gopkg.in/mgo.v2"),我们导入了gopkg.in/mgo.v2并使用mgo`作为包别名。

  1. 使用var session *mgo.Session,我们声明了私有的 MongoDBSession实例,它作为与数据库的通信会话。

  2. 使用var connectionError error,我们声明了一个私有的error对象。

  3. 接下来,我们定义了init()函数,在这里我们连接到 MongoDB,传递主机为127.0.0.1,这意味着 MongoDB 和应用程序都在同一台机器上的端口27017上运行,可选择将会话切换到单调行为,以便在同一会话中的顺序查询中读取的数据将是一致的,并且在会话中进行的修改将在随后的查询中被观察到。

如果你的 MongoDB 运行在除27017之外的端口上,那么你必须传递主机和端口,用冒号分隔,如:mgo.Dial("localhost:27018")

  1. 接下来,我们定义了一个getDbNames处理程序,它基本上从 MongoDB 集群中获取所有数据库名称,并将它们作为逗号分隔的字符串写入 HTTP 响应流。

在 MongoDB 中创建你的第一个文档

在这个示例中,我们将学习如何在数据库中创建一个 BSON 文档(JSON 样式文档的二进制编码序列化),使用 Go 的 MongoDB 驱动程序(gopkg.in/mgo.v2)。

如何做...

  1. 使用以下命令,安装gopkg.in/mgo.v2github.com/gorilla/mux包:
$ go get gopkg.in/mgo.v2
$ go get github.com/gorilla/mux
  1. 创建create-record-mongodb.go。然后我们连接到 MongoDB 数据库,创建一个包含两个字段(ID 和姓名)的员工文档,并将最后创建的文档 ID 写入 HTTP 响应流,如下所示:
package main
import 
(
  "fmt"
  "log"
  "net/http"
  "strconv"
  "github.com/gorilla/mux"
  mgo "gopkg.in/mgo.v2"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  MONGO_DB_URL = "127.0.0.1"
)
var session *mgo.Session
var connectionError error
type Employee struct 
{
  Id int `json:"uid"`
  Name string `json:"name"`
}
func init() 
{
  session, connectionError = mgo.Dial(MONGO_DB_URL)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database :: ", connectionError)
  }
  session.SetMode(mgo.Monotonic, true)
}
func createDocument(w http.ResponseWriter, r *http.Request) 
{
  vals := r.URL.Query()
  name, nameOk := vals["name"]
  id, idOk := vals["id"]
  if nameOk && idOk 
  {
    employeeId, err := strconv.Atoi(id[0])
    if err != nil 
    {
      log.Print("error converting string id to int :: ", err)
      return
    }
    log.Print("going to insert document in database for name 
    :: ", name[0])
    collection := session.DB("mydb").C("employee")
    err = collection.Insert(&Employee{employeeId, name[0]})
    if err != nil 
    {
      log.Print("error occurred while inserting document in 
      database :: ", err)
      return
    }
    fmt.Fprintf(w, "Last created document id is :: %s", id[0])
  } 
  else 
  {
    fmt.Fprintf(w, "Error occurred while creating document in
    database for name :: %s", name[0])
  }
}
func main() 
{
  router := mux.NewRouter()
  router.HandleFunc("/employee/create",
  createDocument).Methods("POST")
  defer session.Close()
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run create-record-mongodb.go

它是如何工作的...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,执行以下命令行中的POST请求来创建一个员工文档将会给你在 MongoDB 中创建的文档的 ID:

$ curl -X POST http://localhost:8080/employee/create?name=foo\&id=1
Last created document id is :: 1

让我们来看一下我们编写的程序:

  1. 使用import ("fmt" "log" "net/http" "strconv" "github.com/gorilla/mux" mgo "gopkg.in/mgo.v2"),我们导入了github.com/gorilla/mux来创建一个 Gorilla Mux 路由器,以及gopkg.in/mgo.v2,包别名为mgo,它将作为 MongoDB 驱动程序。

  2. 接下来,我们定义了一个createDocument处理程序,它从 HTTP 请求中获取员工的姓名和 ID。因为请求变量的类型是string,我们将string类型的变量 ID 转换为int类型。然后,我们从 MongoDB 获取员工集合,并调用collection.Insert处理程序将Employee结构类型的实例保存到数据库中。

从 MongoDB 中读取文档

在上一个示例中,我们在 MongoDB 中创建了一个 BSON 文档。现在,在这个示例中,我们将学习如何使用gopkg.in/mgo.v2/bson包来读取它,该包有助于查询 MongoDB 集合。

如何做...

  1. 使用以下命令,安装gopkg.in/mgo.v2gopkg.in/mgo.v2/bsongithub.com/gorilla/mux包:
$ go get gopkg.in/mgo.v2
$ go get gopkg.in/mgo.v2/bson
$ go get github.com/gorilla/mux
  1. 创建read-record-mongodb.go。然后我们连接到 MongoDB 数据库,读取员工集合中的所有文档,将列表编组为 JSON,并将其写入 HTTP 响应流,如下所示:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "github.com/gorilla/mux"
  mgo "gopkg.in/mgo.v2"
  "gopkg.in/mgo.v2/bson"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  MONGO_DB_URL = "127.0.0.1"
)
var session *mgo.Session
var connectionError error
func init() 
{
  session, connectionError = mgo.Dial(MONGO_DB_URL)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database :: ", connectionError)
  }
  session.SetMode(mgo.Monotonic, true)
}
type Employee struct 
{
  Id int `json:"uid"`
  Name string `json:"name"`
}
func readDocuments(w http.ResponseWriter, r *http.Request) 
{
  log.Print("reading documents from database")
  var employees []Employee
  collection := session.DB("mydb").C("employee")
  err := collection.Find(bson.M{}).All(&employees)
  if err != nil 
  {
    log.Print("error occurred while reading documents from 
    database :: ", err)
    return
  }
  json.NewEncoder(w).Encode(employees)
}
func main() 
{
  router := mux.NewRouter()
  router.HandleFunc("/employees", readDocuments).Methods("GET")
  defer session.Close()
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run read-record-mongodb.go

它是如何工作的...

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,浏览到http://localhost:8080/employees将会给你 MongoDB 员工集合中所有员工的列表:

让我们来看一下我们在程序中引入的更改:

  1. 使用import ("encoding/json" "log" "net/http" "github.com/gorilla/mux" mgo "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson"),我们导入了额外的gopkg.in/mgo.v2/bson包,它是 Go 的 BSON 规范,以及encoding/json包,我们用它来将我们从 MongoDB 获取的对象列表编组为JSON

  2. 接下来,我们定义了一个readDocuments处理程序,在这里我们首先从 MongoDB 获取员工集合,查询其中的所有文档,遍历文档将其映射到Employee结构的数组中,最后将其编组为JSON

在 MongoDB 中更新您的第一个文档

一旦创建了一个 BSON 文档,我们可能需要更新其中的一些字段。在这种情况下,我们必须在 MongoDB 集合上执行update/upsert查询,这将在本教程中介绍。

如何做…

  1. 使用go get命令安装gopkg.in/mgo.v2gopkg.in/mgo.v2/bsongithub.com/gorilla/mux包,如下所示:
$ go get gopkg.in/mgo.v2
$ go get gopkg.in/mgo.v2/bson
$ go get github.com/gorilla/mux
  1. 创建update-record-mongodb.go。然后我们连接到 MongoDB 数据库,更新 ID 的员工的名称,并将在 HTTP 响应流中写入在 MongoDB 中更新的记录数量,如下所示:
package main
import 
(
  "fmt"
  "log"
  "net/http"
  "strconv"
  "github.com/gorilla/mux"
  mgo "gopkg.in/mgo.v2"
  "gopkg.in/mgo.v2/bson"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  MONGO_DB_URL = "127.0.0.1"
)
var session *mgo.Session
var connectionError error
type Employee struct 
{
  Id int `json:"uid"`
  Name string `json:"name"`
}
func init() 
{
  session, connectionError = mgo.Dial(MONGO_DB_URL)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database :: ", 
    connectionError)
  }
  session.SetMode(mgo.Monotonic, true)
}
func updateDocument(w http.ResponseWriter, r *http.Request) 
{
  vars := mux.Vars(r)
  id := vars["id"]
  vals := r.URL.Query()
  name, ok := vals["name"]
  if ok 
  {
    employeeId, err := strconv.Atoi(id)
    if err != nil 
    {
      log.Print("error converting string id to int :: ", err)
      return
    }
    log.Print("going to update document in database 
    for id :: ", id)
    collection := session.DB("mydb").C("employee")
    var changeInfo *mgo.ChangeInfo
    changeInfo, err = collection.Upsert(bson.M{"id": employeeId},
    &Employee{employeeId, name[0]})
    if err != nil 
    {
      log.Print("error occurred while updating record in 
      database :: ", err)
      return
    }
    fmt.Fprintf(w, "Number of documents updated in database 
    are :: %d", changeInfo.Updated)
  } 
  else 
  {
    fmt.Fprintf(w, "Error occurred while updating document
    in database for id :: %s", id)
  }
}
func main() 
{
  router := mux.NewRouter()
  router.HandleFunc("/employee/update/{id}",
  updateDocument).Methods("PUT")
  defer session.Close()
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run update-record-mongodb.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,通过命令行执行PUT请求来更新员工文档,如下所示,将会给出在 MongoDB 中更新的文档数量:

$ curl -X PUT http://localhost:8080/employee/update/1\?name\=bar
Number of documents updated in database are :: 1

让我们来看一下我们写的程序:

  1. 我们定义了一个updateDocument处理程序,它从 URL 路径变量中获取要在 MongoDB 中更新的 ID 和作为 HTTP 请求变量的新名称。由于请求变量是字符串类型,我们将string类型的变量 ID 转换为int类型。然后,我们从 MongoDB 获取员工集合,并调用collection.Upsert处理程序,以插入(如果不存在)或更新具有新名称的员工文档的 ID。

  2. 接下来,我们注册了一个updateDocument处理程序,用于处理/employee/update/{id}的 URL 模式,对于每个使用gorilla/mux路由器的PUT请求,并在我们从main()函数返回时使用defer session.Close()语句关闭 MongoDB 会话。

从 MongoDB 中删除您的第一个文档

每当我们想要清理数据库或删除不再需要的文档时,我们可以使用 Go 的 MongoDB 驱动程序(gopkg.in/mgo.v2)轻松地删除它们,这将在本教程中介绍。

如何做…

  1. 使用go get命令安装gopkg.in/mgo.v2gopkg.in/mgo.v2/bsongithub.com/gorilla/mux包,如下所示:
$ go get gopkg.in/mgo.v2
$ go get gopkg.in/mgo.v2/bson
$ go get github.com/gorilla/mux
  1. 创建delete-record-mongodb.go。然后我们连接到 MongoDB,从数据库中获取要删除的员工的名称作为 HTTP 请求变量,获取命名集合,并按如下方式删除文档:
package main
import 
(
  "fmt"
  "log"
  "net/http"
  "github.com/gorilla/mux"
  mgo "gopkg.in/mgo.v2"
  "gopkg.in/mgo.v2/bson"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  MONGO_DB_URL = "127.0.0.1"
)
var session *mgo.Session
var connectionError error
type Employee struct 
{
  Id int `json:"uid"`
  Name string `json:"name"`
}
func init() 
{
  session, connectionError = mgo.Dial(MONGO_DB_URL)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database :: ", 
    connectionError)
  }
  session.SetMode(mgo.Monotonic, true)
}
func deleteDocument(w http.ResponseWriter, r *http.Request) 
{
  vals := r.URL.Query()
  name, ok := vals["name"]
  if ok 
  {
    log.Print("going to delete document in database for 
    name :: ", name[0])
    collection := session.DB("mydb").C("employee")
    removeErr := collection.Remove(bson.M{"name": name[0]})
    if removeErr != nil 
    {
      log.Print("error removing document from 
      database :: ", removeErr)
      return
    }
    fmt.Fprintf(w, "Document with name %s is deleted from 
    database", name[0])
  } 
  else 
  {
    fmt.Fprintf(w, "Error occurred while deleting document 
    in database for name :: %s", name[0])
  }
}
func main() 
{
  router := mux.NewRouter()
  router.HandleFunc("/employee/delete",
  deleteDocument).Methods("DELETE")
  defer session.Close()
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run delete-record-mongodb.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,通过命令行执行DELETE请求来删除 BSON 文档,如下所示,将会给出从数据库中删除的文档的名称:

$ curl -X DELETE http://localhost:8080/employee/delete?name\=bar
Document with name bar is deleted from database

让我们来看一下我们写的程序:

  1. 我们定义了一个deleteDocument处理程序,它从 MongoDB 获取要删除的名称作为请求变量,从 MongoDB 获取员工集合,并调用collection.Remove处理程序来删除给定名称的文档。

  2. 然后,我们注册了一个deleteDocument处理程序,用于处理/employee/delete的 URL 模式,对于每个使用gorilla/mux路由器的DELETE请求,并在我们从main()函数返回时使用defer session.Close()语句关闭 MongoDB 会话。

第六章:使用 Micro 编写 Go 中的微服务-微服务工具包

在本章中,我们将涵盖以下内容:

  • 创建您的第一个协议缓冲

  • 启动微服务发现客户端

  • 创建您的第一个微服务

  • 创建您的第二个微服务

  • 创建您的微服务 API

  • 使用命令行界面和 Web UI 与微服务进行交互

介绍

随着组织现在转向 DevOps,微服务也开始变得流行起来。由于这些服务具有独立的性质,并且可以用任何语言开发,这使得组织能够专注于它们的开发。通过掌握本章涵盖的概念,我们将能够以相当简单的方式使用 Go Micro 编写微服务。

在本章中,我们将首先编写协议缓冲。然后我们将学习如何启动 Consul,这是一个微服务发现客户端,最终转向创建微服务并通过命令行和 Web 仪表板与它们进行交互。

创建您的第一个协议缓冲

协议缓冲是 Go 支持的一种灵活、高效和自动化的编码和序列化结构化数据的机制。在本教程中,我们将学习如何编写我们的第一个协议缓冲。

准备就绪…

  1. 验证是否通过执行以下命令安装了protoc
$ protoc --version
 libprotoc 3.3.2
  1. 通过以下方式安装protobuf
$ git clone https://github.com/google/protobuf
$ cd protobuf
$ ./autogen.sh
$ ./configure
$ make
$ make check
$ make install

如何做…

  1. proto目录中创建hello.proto并定义一个名为Sayservice接口,其中包含两种数据类型-RequestResponse,如下所示:
syntax = "proto3";
service Say 
{
  rpc Hello(Request) returns (Response) {}
}
message Request 
{
  string name = 1;
}
message Response 
{
  string msg = 1;
}
  1. 使用以下命令编译hello.proto
$ protoc --go_out=plugins=micro:. hello.proto

它是如何工作的…

一旦命令成功执行,hello.pb.go将在proto目录中创建,其外观如下截图所示:

让我们了解我们编写的.proto文件:

  • syntax = "proto3";:在这里,我们指定我们使用proto3语法,这使得编译器了解协议缓冲必须使用版本 3 进行编译。如果我们不明确指定语法,则编译器会假定我们使用proto2

  • service Say { rpc Hello(Request) returns (Response) {} }:在这里,我们定义了一个名为Say的 RPC 服务和一个接受Request并返回ResponseHello方法。

  • message Request { string name = 1; }:在这里,我们定义了具有name字段的Request数据类型。

  • message Response { string msg = 1; }:在这里,我们定义了具有msg字段的Response数据类型。

启动微服务发现客户端

在部署了多个服务的微服务架构中,服务发现客户端帮助应用程序找到它们依赖的服务,可以通过 DNS 或 HTTP 进行。当我们谈论服务发现客户端时,最常见和著名的之一是 HashiCorp 的Consul,我们将在本教程中启动它。

准备就绪…

通过执行以下命令验证是否安装了Consul

$ consul version
 Consul v0.8.5
 Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

如何做…

通过执行以下命令以服务器模式启动consul agent

$ consul agent -dev

它是如何工作的…

一旦命令成功执行,Consul 代理将以服务器模式运行,给我们以下输出:

我们还可以通过执行以下命令列出 Consul 集群的成员:

$ consul members

这将给我们以下结果:

由于 Consul 可以在服务器模式或客户端模式下运行,至少需要一个服务器,为了保持最低限度的设置,我们已经以服务器模式启动了我们的代理,尽管这并不推荐,因为在故障情况下存在数据丢失的可能性。

此外,浏览到http://localhost:8500/ui/将显示 Consul Web UI,我们可以在其中查看所有服务和节点,如下所示:

创建您的第一个微服务

微服务只是作为唯一进程运行并通过明确定义的轻量级机制进行通信以服务于业务目标的代码片段,我们将在这个示例中使用https://github.com/micro/micro编写,尽管还有许多其他库可用,如https://github.com/go-kit/kithttps://github.com/grpc/grpc-go,它们具有相同的目的。

准备就绪…

  1. 通过执行以下命令启动consul agent
$ consul agent -dev
  1. 通过执行以下命令安装和运行micro
$ go get github.com/micro/micro
$ micro api
 2018/02/06 00:03:36 Registering RPC Handler at /rpc
 2018/02/06 00:03:36 Registering API Default Handler at /
 2018/02/06 00:03:36 Listening on [::]:8080
 2018/02/06 00:03:36 Listening on [::]:54814
 2018/02/06 00:03:36 Broker Listening on [::]:54815
 2018/02/06 00:03:36 Registering node: go.micro.api-a6a82a54-0aaf-11e8-8d64-685b35d52676

如何做…

  1. 通过执行命令$ mkdir services && cd services && touch first-greeting-service.goservices目录中创建first-greeting-service.go

  2. 将以下内容复制到first-greeting-service.go

package main
import 
(
  "log"
  "time"
  hello "../proto"
  "github.com/micro/go-micro"
)
type Say struct{}
func (s *Say) Hello(ctx context.Context, req *hello.Request, 
rsp *hello.Response) error 
{
  log.Print("Received Say.Hello request - first greeting service")
  rsp.Msg = "Hello " + req.Name
  return nil
}
func main() 
{
  service := micro.NewService
  (
    micro.Name("go.micro.service.greeter"),
    micro.RegisterTTL(time.Second*30),
    micro.RegisterInterval(time.Second*10),
  )
  service.Init()
  hello.RegisterSayHandler(service.Server(), new(Say))
  if err := service.Run(); err != nil 
  {
    log.Fatal("error starting service : ", err)
    return
  }
}

一切就绪后,目录结构应如下所示:

  1. 转到services目录并使用以下命令运行程序:
$ go run first-greeting-service.go

它是如何工作的…

一旦我们运行程序,RPC 服务器将在本地监听端口8080

接下来,从命令行执行POST请求,如下所示:

$ curl -X POST -H 'Content-Type: application/json' -d '{"service": "go.micro.service.greeter", "method": "Say.Hello", "request": {"name": "Arpit Aggarwal"}}' http://localhost:8080/rpc

这将使我们从服务器获得 Hello,然后是名称作为响应,如下所示的屏幕截图:

查看first-greeting-service.go的日志将向我们展示请求是由第一个问候服务提供的,如下所示:

让我们看一下我们编写的程序:

  • 使用import ("log" "time" hello "../proto" "github.com/micro/go-micro" "golang.org/x/net/context"),我们导入了"hello "../proto",一个包含协议缓冲区源代码和已编译协议缓冲区后缀.pb.go的目录。此外,我们导入了github.com/micro/go-micro包,其中包含编写微服务所需的所有库。

  • 接下来,我们定义了一个main()处理程序,在其中使用micro.NewService()创建一个名为go.micro.service.greeter的新服务,初始化它,注册处理程序,并最终启动它。

创建您的第二个微服务

在这个示例中,我们将使用go-micro创建另一个微服务,它是first-greeting-service.go的副本,除了在控制台上打印的日志消息之外,它演示了两个具有相同名称的服务的客户端负载平衡的概念。

如何做…

  1. 通过执行命令$ cd services && touch second-greeting-service.goservices目录中创建second-greeting-service.go

  2. 将以下内容复制到second-greeting-service.go

package main
import 
(
  "context"
  "log"
  "time"
  hello "../proto"
  "github.com/micro/go-micro"
)
type Say struct{}
func (s *Say) Hello(ctx context.Context, req *hello.Request, 
rsp *hello.Response) error 
{
  log.Print("Received Say.Hello request - second greeting
  service")
  rsp.Msg = "Hello " + req.Name
  return nil
}
func main() 
{
  service := micro.NewService
  (
    micro.Name("go.micro.service.greeter"),
    micro.RegisterTTL(time.Second*30),
    micro.RegisterInterval(time.Second*10),
  )
  service.Init()
  hello.RegisterSayHandler(service.Server(), new(Say))
  if err := service.Run(); err != nil 
  {
    log.Fatal("error starting service : ", err)
    return
  }
}

一切就绪后,目录结构应如下所示:

  1. 转到services目录并使用以下命令运行程序:
$ go run second-greeting-service.go

它是如何工作的…

一旦我们运行程序,RPC 服务器将在本地监听端口8080

接下来,从命令行执行POST请求,如下所示:

$ curl -X POST -H 'Content-Type: application/json' -d '{"service": "go.micro.service.greeter", "method": "Say.Hello", "request": {"name": "Arpit Aggarwal"}}' http://localhost:8080/rpc

这将使我们从服务器获得 Hello,然后是名称作为响应,如下所示:

查看second-greeting-service.go的日志将向我们展示请求是由第二个问候服务提供的:

现在,如果我们再次执行POST请求,它将在first-greeting-service.go控制台中打印日志,这是因为 Go Micro 提供的智能客户端负载平衡构建在发现之上的服务。

创建您的 Micro API

到目前为止,我们已经通过名称显式调用了后端服务和访问它的方法。在这个示例中,我们将学习如何使用 Go Micro API 访问服务,该 API 实现了 API 网关模式,提供了微服务的单一入口点。使用 Go Micro API 的优势在于它通过 HTTP 提供服务,并使用 HTTP 处理程序动态路由到适当的后端服务。

准备就绪…

通过执行以下命令在单独的终端中启动 consul agentmicro APIfirst-greeting-service.gosecond-greeting-service.go

$ consul agent -dev
$ micro api
$ go run first-greeting-service.go
$ go run second-greeting-service.go

操作步骤…

  1. 通过执行命令 $ mkdir api && cd api && touch greeting-api.go 在 api 目录中创建 greeting-api.go

  2. 将以下内容复制到 greeting-api.go

package main
import 
(
  "context"
  "encoding/json"
  "log"
  "strings"
  hello "../proto"
  "github.com/micro/go-micro"
  api "github.com/micro/micro/api/proto"
)
type Say struct 
{
  Client hello.SayClient
}
func (s *Say) Hello(ctx context.Context, req *api.Request, 
rsp *api.Response) error 
{
  log.Print("Received Say.Hello request - Micro Greeter API")
  name, ok := req.Get["name"]
  if ok 
  {
    response, err := s.Client.Hello
    (
      ctx, &hello.Request
      {
        Name: strings.Join(name.Values, " "),
      }
    )
    if err != nil 
    {
      return err
    }
    message, _ := json.Marshal
    (
      map[string]string
      {
        "message": response.Msg,
      }
    )
    rsp.Body = string(message)
  }
  return nil
}
func main() 
{
  service := micro.NewService
  (
    micro.Name("go.micro.api.greeter"),
  )
  service.Init()
  service.Server().Handle
  (
    service.Server().NewHandler
    (
      &Say{Client: hello.NewSayClient("go.micro.service.
      greeter", service.Client())},
    ),
  )
  if err := service.Run(); err != nil 
  {
    log.Fatal("error starting micro api : ", err)
    return
  }
}

一切就绪后,目录结构应该如下所示:

  1. 转到 api 目录并使用以下命令运行程序:
$ go run greeting-api.go

工作原理…

一旦我们运行程序,HTTP 服务器将在本地监听端口 8080

接下来,按照以下步骤浏览至 http://localhost:8080/greeter/say/hello?name=Arpit+Aggarwal

这将给出响应 Hello,后跟作为 HTTP 请求变量接收到的名称。此外,查看 second-greeting-service.go 的日志将显示请求是由第二个问候服务提供的,如下所示:

现在,如果我们再次执行 GET 请求,它将在 first-greeting-service.go 控制台中打印日志,这是因为 Go Micro 提供的发现功能上构建的服务的智能客户端负载平衡:

使用命令行界面和 web UI 与微服务交互

到目前为止,我们已经使用命令行执行了 GET 和 POST HTTP 请求来访问服务。这也可以通过 Go Micro web 用户界面来实现。我们只需要启动 micro web,这将在本示例中介绍。

操作步骤…

  1. 使用以下命令安装 go get github.com/micro/micro 包:
$ go get github.com/micro/micro
  1. 使用以下命令运行 web UI:
$ micro web

工作原理…

一旦命令成功执行,浏览至 http://localhost:8082/registry 将列出所有已注册的服务,如下截图所示:

使用 web UI 查询我们的 greeter 服务,请求为 {"name" : "Arpit Aggarwal"},将会得到响应 {"msg": "Hello Arpit Aggarwal"} 

使用 CLI 命令查询相同的 greeter 服务,命令为 query go.micro.service.greeter Say.Hello {"name" : "Arpit Aggarwal"},将会得到响应 {"msg": "Hello Arpit Aggarwal"}

第七章:在 Go 中使用 WebSocket

在本章中,我们将涵盖以下示例:

  • 创建你的第一个 WebSocket 服务器

  • 创建你的第一个 WebSocket 客户端

  • 调试你的第一个本地 WebSocket 服务器

  • 调试你的第一个远程 WebSocket 服务器

  • 单元测试你的第一个 WebSocket 服务器

介绍

WebSocket 提供了服务器和客户端之间的双向、单一套接字、全双工连接,使实时通信比其他方式如长轮询和服务器发送事件更加高效。

使用 WebSocket,客户端和服务器可以独立通信,每个都能在初始握手后同时发送和接收信息,重复使用从客户端到服务器和服务器到客户端的相同连接,最终大大减少延迟和服务器负载,使 Web 应用程序能够以最有效的方式执行现代任务。WebSocket 协议得到大多数主流浏览器的支持,包括 Google Chrome、Microsoft Edge、Internet Explorer、Firefox、Safari 和 Opera。因此没有兼容性问题。

在本章中,我们将学习如何创建 WebSocket 服务器和客户端,编写单元测试并调试运行在本地或远程的服务器。

创建你的第一个 WebSocket 服务器

在这个示例中,我们将学习如何编写一个 WebSocket 服务器,它是一个 TCP 应用程序,监听在端口8080上,允许连接的客户端彼此发送消息。

如何做…

  1. 使用go get命令安装github.com/gorilla/websocket包,如下所示:
$ go get github.com/gorilla/websocket
  1. 创建websocket-server.go,我们将在其中将 HTTP 请求升级为 WebSocket,从客户端读取 JSON 消息,并将其广播给所有连接的客户端,如下所示:
package main 
import 
(
  "log"
  "net/http"
  "github.com/gorilla/websocket"
)
var clients = make(map[*websocket.Conn]bool)
var broadcast = make(chan Message) 
var upgrader = websocket.Upgrader{}
type Message struct 
{
  Message string `json:"message"`
}
func HandleClients(w http.ResponseWriter, r *http.Request) 
{
  go broadcastMessagesToClients()
  websocket, err := upgrader.Upgrade(w, r, nil)
  if err != nil 
  {
    log.Fatal("error upgrading GET request to a 
    websocket :: ", err)
  }
  defer websocket.Close()
  clients[websocket] = true
  for 
  {
    var message Message
    err := websocket.ReadJSON(&message)
    if err != nil 
    {
      log.Printf("error occurred while reading 
      message : %v", err)
      delete(clients, websocket)
      break
    }
    broadcast <- message
  }
}
func main() 
{
  http.HandleFunc
  (
    "/", func(w http.ResponseWriter, 
    r *http.Request) 
    {
      http.ServeFile(w, r, "index.html")
    }
  )
  http.HandleFunc("/echo", HandleClients)
  err := http.ListenAndServe(":8080", nil)
  if err != nil 
  {
    log.Fatal("error starting http server :: ", err)
    return
  }
}
func broadcastMessagesToClients() 
{
  for 
  {
    message := <-broadcast
    for client := range clients 
    {
      err := client.WriteJSON(message)
      if err != nil 
      {
        log.Printf("error occurred while writing 
        message to client: %v", err)
        client.Close()
        delete(clients, client)
      }
    }
  }
}
  1. 使用以下命令运行程序:
$ go run websocket-server.go

工作原理…

一旦我们运行程序,WebSocket 服务器将在本地监听端口8080

让我们了解我们编写的程序:

  1. 我们使用了import ("log" "net/http" "github.com/gorilla/websocket"),这是一个预处理命令,告诉 Go 编译器包括所有来自lognet/httpgithub.com/gorilla/websocket包的文件。

  2. 使用var clients = make(map[*websocket.Conn]bool),我们创建了一个表示连接到 WebSocket 服务器的客户端的映射,KeyType 为 WebSocket 连接对象,ValueType 为布尔值。

  3. 使用var broadcast = make(chan Message),我们创建了一个通道,所有接收到的消息都会被写入其中。

  4. 接下来,我们定义了一个HandleClients处理程序,当收到HTTP GET请求时,将其升级为WebSocket,将客户端注册到套接字服务器,读取请求的 JSON 消息,并将其写入广播通道。

  5. 然后,我们定义了一个 Go 函数broadcastMessagesToClients,它抓取写入广播通道的消息,并将其发送给当前连接到 WebSocket 服务器的每个客户端。

创建你的第一个 WebSocket 客户端

在这个示例中,我们将创建一个简单的客户端来开始 WebSocket 握手过程。客户端将向 WebSocket 服务器发送一个相当标准的HTTP GET请求,服务器通过响应中的 Upgrade 头将其升级。

如何做…

  1. 创建index.html,我们将在页面加载时打开到非安全 WebSocket 服务器的连接,如下所示:
<html>
  <title>WebSocket Server</title>
  <input id="input" type="text" />
  <button onclick="send()">Send</button>
  <pre id="output"></pre>
  <script>
    var input = document.getElementById("input");
    var output = document.getElementById("output");
    var socket = new WebSocket("ws://" + window.
    location.host + "/echo");
    socket.onopen = function () 
    {
      output.innerHTML += "Status: Connected\n";
    };
    socket.onmessage = function (e) 
    {
      output.innerHTML += "Message from Server: " + 
      e.data + "\n";
    };
    function send() 
    {
      socket.send
      (
        JSON.stringify
        (
          {
            message: input.value
          }
        )
      );
      input.value = "";
    }
  </script>
</html>

一切就绪后,目录结构应该如下所示:

  1. 使用以下命令运行程序:
$ go run websocket-server.go

工作原理…

一旦我们运行程序,WebSocket 服务器将在本地监听端口8080

浏览到http://localhost:8080将显示带有文本框和发送按钮的 WebSocket 客户端页面,如下截图所示:

调试你的第一个本地 WebSocket 服务器

调试 Web 应用程序是开发人员学习的最重要的技能之一,因为它有助于识别问题、隔离问题的来源,然后要么纠正问题,要么确定解决问题的方法。在这个示例中,我们将学习如何使用 GoLand IDE 调试在本地运行的 WebSocket 服务器。

准备...

本示例假定您已经安装并配置了 GoLand IDE 以在您的机器上运行 Go 应用程序。

如何做...

  1. 单击 GoLand IDE 中的 Open Project 以打开我们在以前的示例中编写的websocket-server.go,如下截图所示:

  1. 一旦项目打开,单击 Edit Configurations,如下截图所示:

  1. 通过单击+号显示如下截图所示的 Add New Configuration 来选择 Add New Configuration:

  1. 选择 Go Build,将配置重命名为WebSocket Local Debug,将运行类型更改为目录,然后单击应用和确定,如下截图所示:

  1. 放置一些断点并单击调试按钮:

它是如何工作的...

一旦我们运行程序,WebSocket 服务器将在本地以调试模式启动,监听端口8080

浏览到http://localhost:8080将显示带有文本框和发送按钮的 WebSocket 客户端页面,如下截图所示:

输入文本并单击发送按钮,以查看程序执行停在我们在 GoLand IDE 中放置的断点处,如下所示:

调试您的第一个远程 WebSocket 服务器

在以前的示例中,我们学习了如何调试在本地运行的 WebSocket 服务器。在这个示例中,我们将学习如何在另一台或远程机器上调试它。

这些步骤与我们在以前的示例中所采取的步骤基本相同,只是在调试配置部分,我们将把本地主机更改为远程机器 IP 或 DNS,并启动 Delve 服务器,这是 Go 编程语言在远程机器上的调试器。

如何做...

  1. 通过单击 Edit Configurations...添加另一个配置,如下截图所示:

  1. 单击+号添加新配置,然后选择 Go Remote:

  1. 将调试配置重命名为WebSocket Remote Debug,将主机更改为remote-machine-IPDNS,然后单击应用和确定,如下截图所示:

  1. 通过执行以下命令在目标或远程机器上运行无头 Delve 服务器:
dlv debug --headless --listen=:2345 --api-version=2

上述命令将启动一个监听端口2345的 API 服务器。

  1. 选择 WebSocket Remote Debug 配置,然后单击调试按钮:

它是如何工作的...

浏览到远程可用的 WebSocket 客户端页面,输入一些文本,然后单击发送按钮,以查看程序执行停在我们放置的断点处:

单元测试您的第一个 WebSocket 服务器

单元测试或测试驱动开发有助于开发人员设计松散耦合的代码,重点放在代码的可重用性上。它还帮助我们意识到何时停止编码并快速进行更改。

在这个示例中,我们将学习如何为我们在以前的示例中已经编写的 WebSocket 服务器编写单元测试。

参见创建您的第一个 WebSocket 服务器示例。

如何做...

  1. 使用go get命令安装github.com/gorilla/websocketgithub.com/stretchr/testify/assert包,如下所示:
$ go get github.com/gorilla/websocket
$ go get github.com/stretchr/testify/assert
  1. 创建websocket-server_test.go,我们将在其中创建一个测试服务器,使用 Gorilla 客户端连接到它,并最终读取和编写消息以测试连接,如下所示:
package main
import 
(
  "net/http"
  "net/http/httptest"
  "strings"
  "testing"
  "github.com/gorilla/websocket"
  "github.com/stretchr/testify/assert"
)
func TestWebSocketServer(t *testing.T) 
{
  server := httptest.NewServer(http.HandlerFunc
  (HandleClients))
  defer server.Close()
  u := "ws" + strings.TrimPrefix(server.URL, "http")
  socket, _, err := websocket.DefaultDialer.Dial(u, nil)
  if err != nil 
  {
    t.Fatalf("%v", err)
  }
  defer socket.Close()
  m := Message{Message: "hello"}
  if err := socket.WriteJSON(&m); err != nil 
  {
    t.Fatalf("%v", err)
  }
  var message Message
  err = socket.ReadJSON(&message)
  if err != nil 
  {
    t.Fatalf("%v", err)
  }
  assert.Equal(t, "hello", message.Message, "they 
  should be equal")
}

工作原理…

从命令行执行go test如下:

$ go test websocket-server_test.go websocket-server.go
ok  command-line-arguments 0.048s

它将给我们响应ok,这意味着测试已成功编译和执行。

让我们看看当 Go 测试失败时会是什么样子。将assert语句中的预期输出更改为其他内容。在以下示例中,hello已更改为hi

...
assert.Equal(t, "hi", message.Message, "they should be equal")
...

通过运行go test命令再次执行测试:

$ go test websocket-server_test.go websocket-server.go

它将给我们失败的响应,以及如下截图所示的错误跟踪:

第八章:使用 Go Web 应用程序框架-Beego

在本章中,我们将涵盖以下内容:

  • 使用 Beego 创建你的第一个项目

  • 创建你的第一个控制器和路由器

  • 创建你的第一个视图

  • 创建你的第一个会话变量

  • 创建你的第一个过滤器

  • 在 Beego 中处理 HTTP 错误

  • 在 Beego 中实现缓存

  • 监视 Beego 应用程序

  • 在本地机器上部署 Beego 应用程序

  • 使用 Nginx 部署 Beego 应用程序

介绍

无论何时我们开发一个应用程序,Web 应用程序框架都是必不可少的,因为它通过消除编写大量重复代码的需要并提供模型、API 和其他元素等功能,显著加快和简化了我们的工作。使用应用程序框架,我们可以享受其架构模式的好处,并加速应用程序的开发。

一种流行的 Web 应用程序框架类型是模型-视图-控制器MVC),Go 语言有许多 MVC 框架可用,如 Revel、Utron 和 Beego。

在本章中,我们将学习 Beego,这是一个最受欢迎和常用的 Web MVC 框架之一。我们将从创建项目开始,然后转向创建控制器、视图和过滤器。我们还将看看如何实现缓存,监视和部署应用程序。

使用 Beego 创建你的第一个项目

开始一个项目的第一件事是设置其基本架构。在 Beego 中,可以使用一个叫做bee的工具轻松实现这一点,我们将在这个示例中介绍。

如何做…

  1. 使用go get命令安装github.com/beego/bee包,如下所示:
$ go get github.com/beego/bee
  1. 打开终端到你的$GOPATH/src目录,并使用bee new命令创建一个项目,如下所示:
$ cd $GOPATH/src
$ bee new my-first-beego-project

一旦命令成功执行,它将创建一个新的 Beego 项目,并在控制台上的创建步骤将如下屏幕截图所示:

  1. 转到新创建的项目路径,输入bee run编译和运行项目,如下所示:
$ cd $GOPATH/src/my-first-beego-project
$ bee run

一旦命令成功执行,bee将构建项目并启动应用程序,如下面的屏幕截图所示:

它是如何工作的…

一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080上运行,并浏览http://localhost:8080/将呈现应用程序的欢迎页面,如下面的屏幕截图所示:

创建你的第一个控制器和路由器

Web 应用程序的一个主要组件是控制器,它充当视图和模型之间的协调者,并处理用户的请求,这可能是按钮点击、菜单选择或 HTTP GETPOST请求。在这个示例中,我们将学习如何在 Beego 中创建一个控制器。

如何做…

  1. 转到$GOPATH/src/my-first-beego-project/controllers并创建firstcontroller.go,如下所示:
package controllers
import "github.com/astaxie/beego"
type FirstController struct 
{
  beego.Controller
}
type Employee struct 
{
  Id int `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: 1, FirstName: "Foo", LastName: "Bar"},
    Employee{Id: 2, FirstName: "Baz", LastName: "Qux"},
  }
}
func (this *FirstController) GetEmployees() 
{
  this.Ctx.ResponseWriter.WriteHeader(200)
  this.Data["json"] = employees
  this.ServeJSON()
}
  1. 转到$GOPATH/src/my-first-beego-project/routers并编辑router.go以添加GET映射/employees,由FirstController中定义的GetEmployees处理程序处理,如下所示:
package routers
import 
(
  "my-first-beego-project/controllers"
  "github.com/astaxie/beego"
)
func init() 
{
  beego.Router("/", &controllers.MainController{})
  beego.Router("/employees", &controllers.FirstController{},
  "get:GetEmployees")
}
  1. 使用以下命令运行项目:
$ bee run

它是如何工作的…

一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080上运行。

接下来,从命令行执行GET请求将给你列出所有员工的列表:

$ curl -X GET http://localhost:8080/employees
[
 {
 "id": 1,
 "firstName": "Foo",
 "lastName": "Bar"
 },
 {
 "id": 2,
 "firstName": "Baz",
 "lastName": "Qux"
 }
]

让我们理解我们编写的程序:

  • 导入“github.com/astaxie/beego”:在这里,我们导入了 Beego。

  • type FirstController struct { beego.Controller }:在这里,我们定义了FirstController结构类型,它包含了一个匿名的beego.Controller类型的结构字段,因此FirstController自动获取了beego.Controller的所有方法。

  • func (this *FirstController) GetEmployees() { this.Ctx.ResponseWriter.WriteHeader(200) this.Data["json"] = employees this.ServeJSON() }:在这里,我们定义了GetEmployees处理程序,它将为 URL 模式/employees的每个GET请求执行。

在 Go 中,以大写字母开头的函数或处理程序是导出函数,这意味着它们是公共的,并且可以在程序外部使用。这就是我们在程序中定义所有函数时都使用大写字母而不是驼峰命名法的原因。

创建你的第一个视图

视图是模型的可视表示。它通过模型访问数据,并指定数据应该如何呈现。当模型发生变化时,它保持其呈现的一致性,这可以通过推模型或拉模型来实现。在推模型中,视图向模型注册自己以获取更改通知,而在拉模型中,视图负责在需要检索最新数据时调用模型。在本示例中,我们将学习如何创建我们的第一个视图来呈现员工列表。

如何做…

  1. 移动到$GOPATH/src/my-first-beego-project/views并创建dashboard.tpl,并复制以下内容:
<!DOCTYPE html>
<html>
  <body>
    <table border= "1" style="width:100%;">
      {{range .employees}}
      <tr>
        <td>{{.Id}}</td>
        <td>{{.FirstName}}</td>
        <td>{{.LastName}}</td>
      </tr>
      {{end}}
    </table>
  </body>
</html>
  1. 移动到$GOPATH/src/my-first-beego-project/controllers并编辑firstcontroller.go,添加Dashboard处理程序,如下所示:
package controllers
import "github.com/astaxie/beego"
type FirstController struct 
{
  beego.Controller
}
type Employee struct 
{
  Id int `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: 1, FirstName: "Foo", LastName: "Bar"},
    Employee{Id: 2, FirstName: "Baz", LastName: "Qux"},
  }
}
...
func (this *FirstController) Dashbaord() 
{
  this.Data["employees"] = employees
  this.TplName = "dashboard.tpl"
}
  1. 移动到$GOPATH/src/my-first-beego-project/routers并编辑router.go,添加GET映射/dashboard,由FirstController中定义的Dashboard处理程序处理,如下所示:
package routers
import 
(
  "my-first-beego-project/controllers"
  "github.com/astaxie/beego"
)
func init() 
{
  beego.Router("/", &controllers.MainController{})
  beego.Router("/employees", &controllers.FirstController{},
  "get:GetEmployees")
  beego.Router("/dashboard", &controllers.FirstController{},
  "get:Dashbaord")
}

  1. 使用以下命令运行项目:
$ bee run

它是如何工作的…

一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080上运行。

浏览http://localhost:8080/dashboard将呈现员工仪表板,如下截图所示:

创建你的第一个会话变量

每当我们需要将用户数据从一个 HTTP 请求传递到另一个 HTTP 请求时,我们可以使用 HTTP 会话,我们将在本示例中介绍。

准备好…

此示例假定您已经在本地端口6379上安装并运行了Redis

如何做…

  1. 使用go get命令安装github.com/astaxie/beego/session/redis包,如下所示:
$ go get -u github.com/astaxie/beego/session/redis
  1. 移动到$GOPATH/src/my-first-beego-project/controllers并创建sessioncontroller.go,在这里我们将定义处理程序,确保只有经过身份验证的用户才能查看主页,如下所示:
package controllers 
import "github.com/astaxie/beego"
type SessionController struct 
{
  beego.Controller
}
func (this *SessionController) Home() 
{
  isAuthenticated := this.GetSession("authenticated")
  if isAuthenticated == nil || isAuthenticated == false 
  {
    this.Ctx.WriteString("You are unauthorized to 
    view the page.")
    return
  }
  this.Ctx.ResponseWriter.WriteHeader(200)
  this.Ctx.WriteString("Home Page")
}
func (this *SessionController) Login() 
{
  this.SetSession("authenticated", true)
  this.Ctx.ResponseWriter.WriteHeader(200)
  this.Ctx.WriteString("You have successfully logged in.")
}
func (this *SessionController) Logout() 
{
  this.SetSession("authenticated", false)
  this.Ctx.ResponseWriter.WriteHeader(200)
  this.Ctx.WriteString("You have successfully logged out.")
}
  1. 移动到$GOPATH/src/my-first-beego-project/routers并编辑router.go,添加GET映射/home/login/logout,分别由FirstController中定义的HomeLoginLogout处理程序处理,如下所示:
package routers
import 
(
  "my-first-beego-project/controllers"
  "github.com/astaxie/beego"
)
func init() 
{
  beego.Router("/", &controllers.MainController{})
  beego.Router("/employees", &controllers.FirstController{},
  "get:GetEmployees")
  beego.Router("/dashboard", &controllers.FirstController{}, 
  "get:Dashbaord")
  beego.Router("/home", &controllers.SessionController{},
  "get:Home")
  beego.Router("/login", &controllers.SessionController{}, 
  "get:Login")
  beego.Router("/logout", &controllers.SessionController{}, 
  "get:Logout")
}
  1. 移动到$GOPATH/src/my-first-beego-project并编辑main.go,导入github.com/astaxie/beego/session/redis,如下所示:
package main
import 
(
  _ "my-first-beego-project/routers"
  "github.com/astaxie/beego"
  _ "github.com/astaxie/beego/session/redis"
)
func main() 
{
  beego.BConfig.WebConfig.DirectoryIndex = true
  beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
  beego.Run()
}
  1. $GOPATH/src/my-first-beego-project/conf/app.conf中打开session的使用,如下所示:
SessionOn = true
SessionProvider = "redis"
SessionProviderConfig = "127.0.0.1:6379"
  1. 使用以下命令运行程序:
$ bee run 

它是如何工作的…

一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080上运行。

接下来,我们将执行一些命令来看会话是如何工作的。首先,我们将通过执行以下命令访问/home

$ curl -X GET http://localhost:8080/home 

这将导致我们从服务器收到未经授权的访问消息:

You are unauthorized to view the page.

显然,我们无法访问它,因为我们必须首先登录到应用程序,这将创建一个beegosessionID。现在让我们通过执行以下命令登录到应用程序:

$ curl -X GET -i http://localhost:8080/login

这将导致服务器返回以下响应:

现在我们将使用作为/login请求的一部分创建的 cookiebeegosessionID来访问/home,如下所示:

$ curl --cookie "beegosessionID=6e1c6f60141811f1371d7ea044f1c194" http://localhost:8080/home Home Page

创建你的第一个过滤器

有时,我们可能希望在调用操作方法之前或之后执行逻辑。在这种情况下,我们使用过滤器,我们将在本示例中介绍。

过滤器基本上是封装常见功能或横切关注点的处理程序。我们只需定义它们一次,然后将它们应用于不同的控制器和操作方法。

操作步骤…

  1. 使用go get命令安装github.com/astaxie/beego/context包,如下所示:
$ go get github.com/astaxie/beego/context
  1. 移动到$GOPATH/src/my-first-beego-project/filters并创建firstfilter.go,在Controller之前运行,并记录 IP 地址和当前时间戳,如下所示:
package filters 
import 
(
  "fmt"
  "time"
  "github.com/astaxie/beego/context"
)
var LogManager = func(ctx *context.Context) 
{ 
  fmt.Println("IP :: " + ctx.Request.RemoteAddr + ", 
  Time :: " + time.Now().Format(time.RFC850))
}
  1. 移动到$GOPATH/src/my-first-beego-project/routers并编辑router.go以添加GET映射/*,将由LogManager过滤器处理,如下所示:
package routers 
import 
(
  "my-first-beego-project/controllers"
  "my-first-beego-project/filters"
  "github.com/astaxie/beego"
)
func init() 
{
  beego.Router("/", &controllers.MainController{})
  ...
  beego.InsertFilter("/*", beego.BeforeRouter, 
  filters.LogManager)
}
  1. 使用以下命令运行程序:
$ bee run

工作原理…

一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080上运行。

接下来,我们将执行一个请求,通过执行以下命令获取所有员工:

$ curl -X GET http://localhost:8080/employees
[
 {
 "id": 1,
 "firstName": "Foo",
 "lastName": "Bar"
 },
 {
 "id": 2,
 "firstName": "Baz",
 "lastName": "Qux"
 }
]

一旦命令成功执行,我们可以在控制台的应用程序日志中看到打印的 IP 和时间戳,如下所示:

使用beego.InsertFilter("/*", beego.BeforeRouter, filters.LogManager),我们在应用程序中插入了一个过滤器,该过滤器在找到路由器之前执行 URL 模式/*,并由LogManager处理。类似于beego.BeforeRouter,还有四个其他位置可以放置过滤器:beego.BeforeStaticbeego.BeforeExecbeego.AfterExecbeego.FinishRouter

在 Beego 中处理 HTTP 错误

错误处理是 Web 应用程序设计中最重要的方面之一,因为它在两个方面有所帮助。首先,它以相对友好的方式让应用程序用户知道出了问题,他们应该联系技术支持部门或者应该通知技术支持部门的人员。其次,它允许程序员添加一些细节来帮助调试问题。在本示例中,我们将学习如何在 Beego 中实现错误处理。

操作步骤…

  1. 移动到$GOPATH/src/my-first-beego-project/controllers并创建errorcontroller.go,在其中我们将定义处理404500 HTTP 错误的处理程序,以及处理应用程序中任何通用错误的处理程序,如下所示:
package controllers
import "github.com/astaxie/beego"
type ErrorController struct 
{
  beego.Controller
}
func (c *ErrorController) Error404() 
{
  c.Data["content"] = "Page Not Found"
  c.TplName = "404.tpl"
}
func (c *ErrorController) Error500() 
{
  c.Data["content"] = "Internal Server Error"
  c.TplName = "500.tpl"
}
func (c *ErrorController) ErrorGeneric() 
{
  c.Data["content"] = "Some Error Occurred"
  c.TplName = "genericerror.tpl"
}
  1. 移动到$GOPATH/src/my-first-beego-project/controllers并编辑firstcontroller.go以添加GetEmployee处理程序,该处理程序将从 HTTP 请求参数中获取 ID,从静态员工数组中获取员工详细信息,并将其作为响应返回,或者如果请求的 ID 不存在,则抛出通用错误,如下所示:
package controllers
import "github.com/astaxie/beego"
type FirstController struct 
{
  beego.Controller
}
type Employee struct 
{
  Id int `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: 1, FirstName: "Foo", LastName: "Bar"},
    Employee{Id: 2, FirstName: "Baz", LastName: "Qux"},
  }
}
...
func (this *FirstController) GetEmployee() 
{
  var id int
  this.Ctx.Input.Bind(&id, "id")
  var isEmployeeExist bool
  var emps []Employee
  for _, employee := range employees 
  {
    if employee.Id == id 
    {
      emps = append(emps, Employee{Id: employee.Id, 
      FirstName: employee.FirstName, LastName: 
      employee.LastName})
      isEmployeeExist = true
      break
    }
  }
  if !isEmployeeExist 
  {
    this.Abort("Generic")
  } 
  else 
  {
    this.Data["employees"] = emps
    this.TplName = "dashboard.tpl"
  }
}
  1. 移动到$GOPATH/src/my-first-beego-project/views并创建genericerror.tpl,内容如下:
<!DOCTYPE html>
<html>
  <body>
    {{.content}}
  </body>
</html>
  1. 使用以下命令运行程序:
$ bee run 

工作原理…

一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080上运行。

接下来,浏览http://localhost:8080/employee?id=2将会给出员工的详细信息,如下面的屏幕截图所示:

当浏览http://localhost:8080/employee?id=4时:

它将给出错误消息,如“发生了一些错误”。这是因为我们要求获取 ID 为4的员工的详细信息,而在静态员工数组中不存在,因此服务器抛出通用错误,由errorcontroller.go中定义的ErrorGeneric处理程序处理。

在 Beego 中实现缓存

在 Web 应用程序中缓存数据有时是必要的,以避免反复请求数据库或外部服务的静态数据。在本示例中,我们将学习如何在 Beego 应用程序中实现缓存。

Beego 支持四种缓存提供程序:fileMemcachememoryRedis。在本示例中,我们将使用框架默认的memory缓存提供程序。

操作步骤…

  1. 使用go get命令安装github.com/astaxie/beego/cache包,如下所示:
$ go get github.com/astaxie/beego/cache
  1. 移动到$GOPATH/src/my-first-beego-project/controllers并创建cachecontroller.go,在其中我们将定义GetFromCache处理程序,该处理程序将从缓存中获取键的值并将其写入 HTTP 响应,如下所示:
package controllers
import 
(
  "fmt"
  "time"
  "github.com/astaxie/beego"
  "github.com/astaxie/beego/cache"
)
type CacheController struct 
{
  beego.Controller
}
var beegoCache cache.Cache
var err error
func init() 
{
  beegoCache, err = cache.NewCache("memory",
  `{"interval":60}`)
  beegoCache.Put("foo", "bar", 100000*time.Second)
}
func (this *CacheController) GetFromCache() 
{
  foo := beegoCache.Get("foo")
  this.Ctx.WriteString("Hello " + fmt.Sprintf("%v", foo))
}
  1. 移动到$GOPATH/src/my-first-beego-project/routers并编辑router.go以添加GET映射/getFromCache,该映射将由CacheController中定义的GetFromCache处理程序处理,如下所示:
package routers
import 
(
  "my-first-beego-project/controllers"
  "my-first-beego-project/filters"
  "github.com/astaxie/beego"
)
func init() 
{
  beego.Router("/", &controllers.MainController{})
  ... 
  beego.Router("/getFromCache", &controllers.
  CacheController{}, "get:GetFromCache")
}
  1. 使用以下命令运行程序:
$ bee run

它是如何工作的…

一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080上运行。

在应用程序启动时,将使用名称为foo且值为bar的键添加到缓存中。接下来,浏览http://localhost:8080/getFromCache将从缓存中读取foo键值,将其附加到 Hello,并在浏览器上显示,如下面的屏幕截图所示:

监控 Beego 应用程序

一旦 Beego 应用程序启动并运行,我们可以轻松地通过其管理仪表板监视应用程序请求统计信息、性能、健康检查、任务和配置状态。我们将在本教程中学习如何做到这一点。

如何做到这一点…

  1. 通过在$GOPATH/src/my-first-beego-project/conf/app.conf中添加EnableAdmin = true来启用应用程序实时监视,如下所示:
appname = my-first-beego-project
...
EnableAdmin = true
..

可选地,通过在$GOPATH/src/my-first-beego-project/conf/app.conf中添加字段来更改其监听的端口:

AdminAddr = "localhost"
AdminPort = 8088
  1. 使用以下命令运行程序:
$ bee run

它是如何工作的…

一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080上运行,并且浏览http://localhost:8088/将呈现管理仪表板,如下面的屏幕截图所示:

浏览http://localhost:8088/qps将显示应用程序的请求统计信息,如下面的屏幕截图所示:

在本地机器上部署 Beego 应用程序

一旦应用程序开发结束,我们必须部署它以供最终用户使用,这可以在本地或远程进行。在本教程中,我们将学习如何在本地机器上部署我们的 Beego 应用程序。

如何做到这一点…

  1. 因为bee创建的应用程序默认处于开发模式,并且在公共服务器上运行应用程序时,始终以生产模式运行应用程序是最佳实践,因此我们必须在$GOPATH/src/my-first-beego-project/conf/app.conf中将RunMode更改为prod,如下所示:
beego.RunMode = "prod"
  1. 通过执行以下命令将静态文件、配置文件和模板作为 Beego 应用程序的字节码文件的一部分包含在一个单独的目录中:
$ mkdir $GOPATH/my-first-beego-app-deployment
$ cp my-first-beego-project $GOPATH/my-first-beego-app-deployment
$ cp -fr views $GOPATH/my-first-beego-app-deployment
$ cp -fr static $GOPATH/my-first-beego-app-deployment
$ cp -fr conf $GOPATH/my-first-beego-app-deployment
  1. 移动到$GOPATH/my-first-beego-app-deployment并使用nohup命令将应用程序作为后台进程运行,如下所示:
$ cd $GOPATH/my-first-beego-app-deployment
$ nohup ./my-first-beego-project &

它是如何工作的…

一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080上运行,浏览http://localhost:8080/将呈现应用程序的欢迎页面,如下面的屏幕截图所示:

使用 Nginx 部署 Beego 应用程序

在上一个教程中,我们学习了如何在本地运行 Beego 应用程序。在本教程中,我们将使用Nginx部署相同的应用程序。

准备就绪…

这个教程假设您已经安装并在端口80上运行了Nginx。对我来说,它安装在/Users/ArpitAggarwal/nginx

如何做到这一点…

  1. 打开/Users/ArpitAggarwal/nginx/conf/nginx.conf中的 Nginx 配置文件,并将server下的location块替换为以下内容:
location / 
{
 # root html;
 # index index.html index.htm;
 proxy_pass http://localhost:8080/;
}
  1. 通过执行以下命令启动 Nginx:
$ cd /Users/ArpitAggarwal/nginx/sbin
$ ./nginx
  1. 通过执行以下命令运行 Beego 应用程序:
$ bee run

它是如何工作的…

一旦命令成功执行,浏览http://localhost:80/将呈现应用程序的欢迎页面,如下截图所示:

第九章:使用 Go 和 Docker

在本章中,我们将涵盖以下内容:

  • 构建你的第一个 Go Docker 镜像

  • 运行你的第一个 Go Docker 容器

  • 将你的 Docker 镜像推送到 Docker 注册表

  • 创建你的第一个用户定义的桥接网络

  • 在用户定义的桥接网络上运行 MySQL Docker 镜像

  • 构建一个 Go web 应用的 Docker 镜像

  • 在用户定义的桥接网络上运行一个与 MySQL Docker 容器链接的 web 应用 Docker 容器

介绍

随着组织向 DevOps 迈进,Docker 也开始变得流行起来。Docker 允许将应用程序及其所有依赖项打包成标准化的软件开发单元。如果该单元在您的本地机器上运行,我们可以保证它将在任何地方,从 QA 到暂存,再到生产环境中以完全相同的方式运行。通过本章涵盖的概念,我们将能够轻松编写 Docker 镜像并部署 Docker 容器。

在本章中,我们将学习如何创建一个 Docker 镜像和 Docker 容器来部署一个简单的 Go web 应用,之后我们将看看如何将容器保存为镜像并将其推送到 Docker 注册表,以及一些 Docker 网络的基本概念。

由于我们将要使用 Docker,我假设它已经安装并在您的本地机器上运行。

构建你的第一个 Go Docker 镜像

Docker 镜像是我们应用程序的文件系统和配置,进一步用于创建 Docker 容器。有两种方式可以创建 Docker 镜像,即从头开始或从父镜像创建。在这个示例中,我们将学习如何从父镜像创建 Docker 镜像。这意味着基本上创建的镜像是指其父级的内容,并且Dockerfile中的后续声明修改了父镜像的内容。

准备就绪…

通过执行以下命令验证DockerDocker Machine是否已安装:

$ docker --version
Docker version 18.03.0-ce, build 0520e24  $ docker-machine --version
docker-machine version 0.14.0, build 89b8332

操作步骤如下…

  1. 创建http-server.go,在这里我们将创建一个简单的 HTTP 服务器,它将在浏览http://docker-machine-ip:8080或从命令行执行curl -X GET http://docker-machine-ip:8080时呈现 Hello World!
package main
import 
(
  "fmt"
  "log"
  "net/http"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
func helloWorld(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Hello World!")
}
func main() 
{
  http.HandleFunc("/", helloWorld)
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 创建一个DockerFile,这是一个包含构建镜像所需的所有命令的文本文件。我们将使用golang:1.9.2作为基础或父镜像,我们在Dockerfile中使用FROM指令指定了这一点,如下所示:
FROM golang:1.9.2
 ENV SRC_DIR=/go/src/github.com/arpitaggarwal/
 ENV GOBIN=/go/bin

 WORKDIR $GOBIN

 # Add the source code:
 ADD . $SRC_DIR

 RUN cd /go/src/;

 RUN go install github.com/arpitaggarwal/;
 ENTRYPOINT ["./arpitaggarwal"]

 EXPOSE 8080

一切就绪后,目录结构应该如下所示:

  1. 使用-t标志执行docker build命令构建一个名为golang-image的 Docker 镜像,如下所示:
$ docker build --no-cache=true -t golang-image .

一旦前面的命令成功执行,它将产生以下输出:

如果您在公司代理后面构建镜像,您可能需要提供代理设置。您可以通过在Dockerfile中使用ENV语句添加环境变量来实现这一点,我们通常称之为运行时定制,如下所示:

FROM golang:1.9.2
....
ENV http_proxy "http://proxy.corp.com:80"
ENV https_proxy "http://proxy.corp.com:80"
...

我们还可以使用--build-arg <varname>=<value>标志在构建时将代理设置传递给构建器,这被称为构建时定制。

$ docker build --no-cache=true --build-arg http_proxy="http://proxy.corp.com:80" -t golang-image.

工作原理…

通过执行以下命令验证 Docker 镜像是否已成功创建:

$ docker images

这将列出所有顶级镜像,它们的仓库、标签和大小,如下截图所示:

让我们了解我们创建的Dockerfile

  • FROM golang:1.9.2: FROM指令指定了基础镜像,对我们来说是golang:1.9.2

  • ENV SRC_DIR=/go/src/github.com/arpitaggarwal/:在这里,我们使用ENV语句将 Go 源代码目录设置为环境变量

  • ENV GOBIN=/go/bin:在这里,我们使用ENV语句将GOBIN或生成可执行二进制文件的目录设置为环境变量。

  • WORKDIR $GOBINWORKDIR指令为我们的镜像设置了任何RUNCMDENTRYPOINTCOPYADD语句的工作目录,对于我们的镜像来说,这个目录是/go/bin

  • ADD . $SRC_DIR:在这里,我们使用ADD语句将当前目录中的http-server.go复制到golang-image/go/src/github.com/arpitaggarwal/目录中。

  • RUN cd /go/src/:在这里,我们使用RUN语句将当前目录更改为/go/src/中的golang-image

  • RUN go install github.com/arpitaggarwal/:在这里,我们编译/go/src/github.com/arpitaggarwal/http-server.go,并在/go/bin目录中生成可执行二进制文件。

  • ENTRYPOINT ["./arpitaggarwal"]:在这里,我们指定要作为可执行文件运行的可执行二进制文件。

  • EXPOSE 8080EXPOSE指令通知 Docker,我们将从镜像创建的容器在运行时监听网络端口8080

运行您的第一个 Go Docker 容器

Docker 容器包括一个应用程序及其所有依赖项。它与其他容器共享内核,并作为主机操作系统上用户空间中的隔离进程运行。要运行实际的应用程序,我们必须从镜像创建和运行容器,这将在本教程中介绍。

如何做…

执行docker run命令从golang-image创建并运行一个 Docker 容器,使用-name标志将容器命名为golang-container,如下所示:

$ docker run -d -p 8080:8080 --name golang-container -it golang-image
 9eb53d8d41a237ac216c9bb0f76b4b47d2747fab690569ef6ff4b216e6aab486

docker run命令中指定的-d标志以守护进程模式启动容器,末尾的哈希字符串代表golang-container的 ID。

工作原理…

通过执行以下命令验证 Docker 容器是否已创建并成功运行:

$ docker ps

一旦上述命令成功执行,它将给我们正在运行的 Docker 容器的详细信息,如下面的屏幕截图所示:

要列出所有 Docker 容器,无论它们是否正在运行,我们必须传递一个额外的标志-a,如docker ps -a

浏览http://localhost:8080/或从命令行执行GET调用,如下所示:

$ curl -X GET http://localhost:8080/
 Hello World!

这将给我们一个 Hello World!的响应,这意味着 HTTP 服务器在 Docker 容器内的端口8080上监听。

将您的 Docker 镜像推送到 Docker 注册表

一旦创建了 Docker 镜像,最佳做法是存储或保存该镜像,这样下次您要从自定义镜像启动容器时,就不必再去烦恼或记住之前创建它时执行的步骤。

您可以将镜像保存在本地计算机上,也可以保存在艺术工厂或任何公共或私有的 Docker 注册表中,例如 Docker Hub、Quay、Google 容器注册表、AWS 容器注册表等。在本教程中,我们将学习如何将我们在之前的教程中创建的镜像保存或推送到 Docker Hub。

查看构建您的第一个 Go Docker 镜像教程.

如何做…

  1. 在 Docker Hub(https://hub.docker.com/)上创建您的帐户。

  2. 通过执行docker login命令从命令行登录到 Docker Hub,如下所示:

$ docker login --username arpitaggarwal --password XXXXX
 Login Succeeded
  1. golang-image打标签:
$ docker tag golang-image arpitaggarwal/golang-image
  1. 通过执行docker images命令验证镜像是否已成功标记:
$ docker images

执行上述命令将列出所有 Docker 镜像,如下面的屏幕截图所示:

  1. 通过执行docker push命令将标记的镜像推送到 Docker Hub,如下所示:
$ docker push arpitaggarwal/golang-image
 The push refers to a repository [docker.io/arpitaggarwal
 /golang-image]
 4db0afeaa6dd: Pushed
 4e648ebe6cf2: Pushed
 6bfc813a3812: Mounted from library/golang
 e1e44e9665b9: Mounted from library/golang
 1654abf914f4: Mounted from library/golang
 2a55a2194a6c: Mounted from library/golang
 52c175f1a4b1: Mounted from library/golang
 faccc7315fd9: Pushed
 e38b8aef9521: Mounted from library/golang
 a75caa09eb1f: Mounted from library/golang
 latest: digest: sha256:ca8f0a1530d3add72ad4e328e51235ef70c5fb8f38bde906a378d74d2b75c8a8 size: 2422

工作原理…

要验证图像是否已成功推送到 Docker Hub,请浏览https://hub.docker.com/,使用您的凭据登录,一旦登录,您将看到已标记的图像,如下面的屏幕截图所示:

如果对 Docker 容器进行了任何更改,并且希望将其作为图像的一部分进行持久化,那么首先必须使用docker commit命令将更改提交到新图像或相同图像,然后将其标记并推送到 Docker Hub,如下所示:

$ docker commit <container-id> golang-image-new

$ docker tag golang-image-new arpitaggarwal/golang-image

$ docker push arpitaggarwal/golang-image

创建您的第一个用户定义的桥接网络

每当我们想要通过容器名称将一个 Docker 容器连接到另一个 Docker 容器时,首先我们必须创建一个用户定义的网络。这是因为 Docker 不支持在默认桥接网络上的自动服务发现。在本教程中,我们将学习如何创建自己的桥接网络。

如何做…

执行docker network命令创建一个名为my-bridge-network的桥接网络,如下所示:

$ docker network create my-bridge-network
 325bca66cc2ccb98fb6044b1da90ed4b6b0f29b54c4588840e259fb7b6505331

它是如何工作的…

通过执行以下命令验证my-bridge-network是否已成功创建:

$ docker network ls
 NETWORK ID NAME DRIVER
 20dc090404cb bridge bridge
 9fa39d9bb674 host host
 325bca66cc2c my-bridge-network bridge
 f36203e11372 none null

要查看有关my-bridge-network的详细信息,请运行docker network inspect命令,然后输入网络名称,如下所示:

$ docker network inspect my-bridge-network
 [
 {
 "Name": "my-bridge-network",
 "Id": "325bca66cc2ccb98fb6044b1da90ed4b6b0
     f29b54c4588840e259fb7b6505331",
 "Scope": "local",
 "Driver": "bridge",
 "EnableIPv6": false,
 "IPAM": 
     {
 "Driver": "default",
 "Options": {},
 "Config": 
       [
 {
 "Subnet": "172.18.0.0/16",
 "Gateway": "172.18.0.1"
 }
 ]
 },
 "Internal": false,
 "Containers": {},
 "Options": {},
 "Labels": {}
 }
 ]

在用户定义的桥接网络上运行 MySQL Docker 图像

每当我们运行 Docker 图像创建和启动容器时,它都会使用默认的桥接网络,Docker 在安装期间创建。要在特定网络上运行图像,该网络可以是用户定义的,也可以是 Docker 自动创建的另外两个网络之一,即主机或无网络,我们必须在docker run命令的一部分中提供附加的--net标志,并将值作为网络名称。

在本教程中,我们将在上一个教程中创建的用户定义的桥接网络上运行 MySQL 图像,将--net标志值传递为my-bridge-network

如何做…

执行docker run命令,从mysql:latest图像创建和运行 MySQL Docker 容器,并使用--name标志将容器名称分配为mysql-container,如下所示:

$ docker run --net=my-bridge-network -p 3306:3306 --name mysql-container -e MYSQL_ROOT_PASSWORD=my-pass -d mysql:latest
 c3ca3e6f253efa40b1e691023155ab3f37eb07b767b1744266ac4ae85fca1722

docker run命令中指定的--net标志将mysql-container连接到my-bridge-networkdocker run命令中指定的-p标志将容器的3306端口发布到主机的3306端口。docker run命令中指定的-e标志将MYSQL_ROOT_PASSWORD值设置为my-pass,这是mysql:latest图像的环境变量。docker run命令中指定的-d标志以守护进程模式启动容器,末尾的哈希字符串表示mysql-container的 ID。

它是如何工作…

通过执行以下命令验证 Docker 容器是否已成功创建并正在运行:

$ docker ps
 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
 f2ec80f82056 mysql:latest "docker-entrypoint.sh" 8 seconds ago Up 6 seconds 0.0.0.0:3306->3306/tcp mysql-container

再次检查my-bridge-network将在Containers部分显示mysql-container的详细信息,如下所示:

$ docker network inspect my-bridge-network
[
 {
 "Name": "my-bridge-network",
 "Id": "325bca66cc2ccb98fb6044b1da90ed
    4b6b0f29b54c4588840e259fb7b6505331",
 "Scope": "local",
 "Driver": "bridge",
 "EnableIPv6": false,
 "IPAM": 
    {
 "Driver": "default",
 "Options": {},
 "Config": 
      [
 {
 "Subnet": "172.18.0.0/16",
 "Gateway": "172.18.0.1"
 }
 ]
 },
 "Internal": false,
 "Containers": 
    {
 "f2ec80f820566707ba7b18ce12ca7a65
      c87fa120fd4221e11967131656f68e59": 
      {
 "Name": "mysql-container",
 "EndpointID": "58092b80bd34135d94154e4d8a8f5806bad
        601257cfbe28e53b5d7161da3b350",
 "MacAddress": "02:42:ac:12:00:02",
 "IPv4Address": "172.18.0.2/16",
 "IPv6Address": ""
 }
 },
 "Options": {},
 "Labels": {}
 }
]

构建 Go Web 应用程序 Docker 图像

在本教程中,我们将构建一个 Docker 图像,该图像连接到单独运行的 MySQL 数据库实例的 Docker 容器。

如何做…

  1. 创建http-server.go,在其中我们将创建一个简单的 HTTP 服务器和一个处理程序,该处理程序将为我们提供当前数据库详细信息,例如机器 IP、主机名、端口和所选数据库,如下所示:
package main
import 
(
  "bytes"
  "database/sql"
  "fmt"
  "log"
  "net/http"
  "github.com/go-sql-driver/mysql"
  "github.com/gorilla/mux"
)
var db *sql.DB
var connectionError error
const 
(
  CONN_PORT = "8080"
  DRIVER_NAME = "mysql"
  DATA_SOURCE_NAME = "root:my-pass@tcp(mysql-container:3306)/mysql"
)
func init() 
{
  db, connectionError = sql.Open(DRIVER_NAME, DATA_SOURCE_NAME)
  if connectionError != nil 
  {
    log.Fatal("error connecting to database : ", connectionError)
  }
}
func getDBInfo(w http.ResponseWriter, r *http.Request) 
{
  rows, err := db.Query("SELECT SUBSTRING_INDEX(USER(), 
  '@', -1) AS ip, @@hostname as hostname, @@port as port,
  DATABASE() as current_database;")
  if err != nil 
  {
    log.Print("error executing database query : ", err)
    return
  }
  var buffer bytes.Buffer
  for rows.Next() 
  {
    var ip string
    var hostname string
    var port string
    var current_database string
    err = rows.Scan(&ip, &hostname, &port, &current_database)
    buffer.WriteString("IP :: " + ip + " | HostName :: " + 
    hostname + " | Port :: " + port + " | Current 
    Database :: " + current_database)
  }
  fmt.Fprintf(w, buffer.String())
}
func main() 
{
  router := mux.NewRouter()
  router.HandleFunc("/", getDBInfo).Methods("GET")
  defer db.Close()
  err := http.ListenAndServe(":"+CONN_PORT, router)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 创建一个DockerFile,这是一个包含构建图像所需的所有命令的文本文件,如下所示:
FROM golang:1.9.2

 ENV SRC_DIR=/go/src/github.com/arpitaggarwal/
 ENV GOBIN=/go/bin

 WORKDIR $GOBIN

 ADD . $SRC_DIR

 RUN cd /go/src/;
 RUN go get github.com/go-sql-driver/mysql;
 RUN go get github.com/gorilla/mux;

 RUN go install github.com/arpitaggarwal/;
 ENTRYPOINT ["./arpitaggarwal"]

 EXPOSE 8080

一切就绪后,目录结构应如下所示:

  1. Dockerfile构建 Docker 图像,使用-t标志将图像名称设置为web-application-image,如下所示:
$ docker build --no-cache=true -t web-application-image .

一旦上述命令成功执行,它将呈现以下输出:

工作原理…

通过执行以下命令验证 Docker 镜像是否已成功创建:

$ docker images

这将列出所有顶级镜像,它们的存储库、标签和大小,如下截图所示:

我们在这个教程中创建的Dockerfile与我们在之前的教程中创建的完全相同,除了在构建镜像时安装 Go MySQL Driver 和 Gorilla Mux URL 路由器的两个额外命令,如下:

...
RUN go get github.com/go-sql-driver/mysql;
RUN go get github.com/gorilla/mux;
...

参见构建您的第一个 Go Docker 镜像教程。

在用户定义的桥接网络上运行与 MySQL Docker 容器链接的 Web 应用程序 Docker 容器

在这个教程中,我们将学习如何运行一个 Go Web 应用程序 Docker 镜像,创建一个容器,该容器将与在单独的 Docker 容器中运行的 MYSQL 数据库实例进行通信。

由于我们知道 Docker 不支持默认桥接网络上的自动服务发现,我们将使用我们在之前的教程中创建的用户定义网络来运行 Go Web 应用程序 Docker 镜像。

如何做…

执行docker run命令,从web-application-image创建一个 Web 应用程序 Docker 容器,使用--name标志将容器名称指定为web-application-container,命令如下:

$ docker run --net=my-bridge-network -p 8090:8080 --name web-application-container -d web-application-image
 ef9c73396e9f9e04c94b7327e8f02cf57ce5f0cd674791e2805c86c70e5b9564

docker run命令中指定的--net标志将mysql-container连接到my-bridge-networkdocker run命令中指定的-p标志将容器的8080端口发布到主机的8080端口。docker run命令中指定的-d标志以守护进程模式启动容器,末尾的哈希字符串表示web-application-container的 ID。

工作原理…

通过执行以下命令验证 Docker 容器是否已成功创建并正在运行:

$ docker ps

这将呈现以下输出:

浏览http://localhost:8090/将会给我们返回机器 IP、主机名、端口和当前数据库详情:

此外,再次检查my-bridge-network将显示mysql-containerweb-application-container的详细信息在Containers部分,如下:

$ docker network inspect my-bridge-network
[
 {
 "Name": "my-bridge-network",
 "Id": "325bca66cc2ccb98fb6044b1da90ed4b6b0
    f29b54c4588840e259fb7b6505331",
 "Scope": "local",
 "Driver": "bridge",
 "EnableIPv6": false,
 "IPAM": 
    {
 "Driver": "default",
 "Options": {},
 "Config": 
      [
 {
 "Subnet": "172.18.0.0/16",
 "Gateway": "172.18.0.1"
 }
 ]
 },
 "Internal": false,
 "Containers": 
    {
 "08ce8f20c3205fa3e421083fa1077b
      673cdd10fd5be34f5ef431fead06219019": 
      {
 "Name": "web-application-container",
 "EndpointID": "d22f7076cf037ef0f0057ffb9fec
        0a07e07b44b442182544731db1ad10db87e4",
 "MacAddress": "02:42:ac:12:00:03",
 "IPv4Address": "172.18.0.3/16",
 "IPv6Address": ""
 },
 "f2ec80f820566707ba7b18ce12ca7a65
      c87fa120fd4221e11967131656f68e59": 
      {
 "Name": "mysql-container",
 "EndpointID": "58092b80bd34135d94154e4d8
        a8f5806bad601257cfbe28e53b5d7161da3b350",
 "MacAddress": "02:42:ac:12:00:02",
 "IPv4Address": "172.18.0.2/16",
 "IPv6Address": ""
 }
 },
 "Options": {},
 "Labels": {}
 }
]

第十章:保护 Go Web 应用程序

在本章中,我们将涵盖以下内容:

  • 使用 OpenSSL 创建私钥和 SSL 证书

  • 将 HTTP 服务器移动到 HTTPS

  • 定义 REST API 和路由

  • 创建 JSON Web 令牌

  • 使用 JSON Web 令牌保护 RESTful 服务

  • 在 Go Web 应用程序中防止跨站点请求伪造

介绍

保护 Web 应用程序是本章中我们将学习的最重要的方面之一,除了创建应用程序。应用程序安全是一个非常广泛的主题,可以以超出本章范围的各种方式实现。

在本章中,我们将专注于如何将我们的 Go Web 应用程序从 HTTP 协议移动到 HTTPS,通常称为HTTP + TLS (传输层安全),以及使用JSON Web 令牌 (JWTs)保护 Go Web 应用程序 REST 端点,并保护我们的应用程序免受跨站点请求伪造(CSRF)攻击。

使用 OpenSSL 创建私钥和 SSL 证书

将运行在 HTTP 上的服务器移动到 HTTPS,我们首先要做的是获取 SSL 证书,这可能是自签名的,也可能是由受信任的证书颁发机构(如 Comodo、Symantec 或 GoDaddy)签名的证书。

要获得由受信任的证书颁发机构签名的 SSL 证书,我们必须向他们提供证书签名请求CSR),主要包括密钥对的公钥和一些附加信息,而自签名证书是您可以自行签发的证书,用自己的私钥签名。

自签名证书可以用于加密数据,也可以用 CA 签名的证书,但用户将收到一个警告,说证书未被他们的计算机或浏览器信任。因此,您不应该在生产或公共服务器上使用它们。

在这个教程中,我们将学习如何创建私钥、证书签名请求和自签名证书。

准备工作…

本教程假设您的机器上已安装了openssl。要验证是否已安装,请执行以下命令:

$ openssl
OpenSSL> exit

如何做…

  1. 使用openssl执行以下命令生成私钥和证书签名请求:
$ openssl req -newkey rsa:2048 -nodes -keyout domain.key -out domain.csr -subj "/C=IN/ST=Mumbai/L=Andheri East/O=Packt/CN=packtpub.com"

这将产生以下输出:

  1. 通过执行以下命令生成证书并用刚创建的私钥签名:
$ openssl req -key domain.key -new -x509 -days 365 -out domain.crt -subj "/C=IN/ST=Mumbai/L=Andheri East/O=Packt/CN=packtpub.com"

工作原理…

一旦命令成功执行,我们可以看到生成了domain.keydomain.csrdomain.crt,其中domain.key是用于签署 SSL 证书的 2,048 位 RSA 私钥,而domain.crtdomain.csr是证书签名请求,包含了密钥对的公钥和一些附加信息,这些信息在签署证书时被插入。

让我们了解我们执行的生成证书签名请求的命令:

  • -newkey rsa:2048选项创建一个新的证书请求和一个新的私钥,应该是使用 RSA 算法生成的 2,048 位私钥。

  • -nodes选项指定创建的私钥不会使用密码短语加密。

  • -keyout domain.key选项指定要将新创建的私钥写入的文件名。

  • -out domain.csr选项指定要写入的输出文件名,或者默认情况下为标准输出。

  • -subj选项用指定的数据替换输入请求的主题字段,并输出修改后的请求。如果我们不指定此选项,则必须通过OpenSSL回答 CSR 信息提示以完成该过程。

接下来,我们将了解我们执行的生成证书并用私钥签名的命令,如下所示:

openssl req -key domain.key -new -x509 -days 365 -out domain.crt -subj "/C=IN/ST=Mumbai/L=Andheri East/O=Packt/CN=packtpub.com"

-key选项指定从中读取私钥的文件。-x509选项输出自签名证书而不是证书请求。-days 365选项指定证书的认证天数。默认值为 30 天。

将 HTTP 服务器移动到 HTTPS

一旦 Web 应用程序开发结束,我们很可能会将其部署到服务器上。在部署时,建议始终在公开暴露的服务器上使用 HTTPS 协议运行 Web 应用程序,而不是 HTTP。在本教程中,我们将学习如何在 Go 中实现这一点。

如何做…

  1. 创建https-server.go,在其中我们将定义一个处理程序,该处理程序将仅为所有 HTTPS 请求向 HTTP 响应流写入 Hello World!,如下所示:
package main
import 
(
  "fmt"
  "log"
  "net/http"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8443"
  HTTPS_CERTIFICATE = "domain.crt"
  DOMAIN_PRIVATE_KEY = "domain.key"
)
func helloWorld(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Hello World!")
}
func main() 
{
  http.HandleFunc("/", helloWorld)
  err := http.ListenAndServeTLS(CONN_HOST+":"+CONN_PORT,
  HTTPS_CERTIFICATE, DOMAIN_PRIVATE_KEY, nil)
  if err != nil 
  {
    log.Fatal("error starting https server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run https-server.go

工作原理…

一旦我们运行程序,HTTPS 服务器将在本地监听端口8443上启动。

浏览https://localhost:8443/将从服务器获得 Hello World!作为响应:

此外,使用curl从命令行执行GET请求并传递--insecure标志将跳过证书验证,因为我们使用的是自签名证书:

$ curl -X GET https://localhost:8443/ --insecure
 Hello World!

让我们了解我们编写的程序:

  • const (CONN_HOST = "localhost" CONN_PORT = "8443" HTTPS_CERTIFICATE = "domain.crt" DOMAIN_PRIVATE_KEY = "domain.key"):在这里,我们声明了四个常量—CONN_HOST的值为localhostCONN_PORT的值为8443HTTPS_CERTIFICATE的值为domain.crt或自签名证书,DOMAIN_PRIVATE_KEY的值为domain.key或我们在上一个教程中创建的私钥。

  • func helloWorld(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello World!") }:这是一个 Go 函数,它以ResponseWriterRequest作为输入参数,并在 HTTP 响应流上写入Hello World!

接下来,我们声明了main(),程序从这里开始执行。由于这个方法做了很多事情,让我们逐行理解它:

  • http.HandleFunc("/", helloWorld): 在这里,我们使用net/http包的HandleFunchelloWorld函数注册到 URL 模式/,这意味着每当我们访问 HTTPS URL 模式/时,helloWorld都会被执行,并将(http.ResponseWriter, *http.Request)作为输入传递给它。

  • err := http.ListenAndServeTLS(CONN_HOST+":"+CONN_PORT, HTTPS_CERTIFICATE, DOMAIN_PRIVATE_KEY, nil): 在这里,我们调用http.ListenAndServeTLS来提供处理每个传入连接的 HTTPS 请求的请求。ListenAndServeTLS接受四个参数—服务器地址、SSL 证书、私钥和处理程序。在这里,我们将服务器地址传递为localhost:8443,我们的自签名证书、私钥和处理程序为nil,这意味着我们要求服务器使用DefaultServeMux作为处理程序。

  • if err != nil { log.Fatal("error starting https server : ", err) return}:在这里,我们检查启动服务器时是否有任何问题。如果有问题,则记录错误并以状态码 1 退出。

定义 REST API 和路由

在编写 RESTful API 时,很常见的是在允许用户访问之前对用户进行身份验证。身份验证用户的先决条件是创建 API 路由,我们将在本教程中介绍。

如何做…

  1. 使用go get命令安装github.com/gorilla/muxgithub.com/gorilla/handlers包,如下所示:
$ go get github.com/gorilla/mux
$ go get github.com/gorilla/handlers
  1. 创建http-rest-api.go,在其中我们将定义三个路由—/status/get-token/employees—以及它们的处理程序,如下所示:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "os"
  "github.com/gorilla/handlers"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
)
type Employee struct 
{
  Id int `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: 1, FirstName: "Foo", LastName: "Bar"},
    Employee{Id: 2, FirstName: "Baz", LastName: "Qux"},
  }
}
func getStatus(w http.ResponseWriter, r *http.Request) 
{
  w.Write([]byte("API is up and running"))
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  json.NewEncoder(w).Encode(employees)
}
func getToken(w http.ResponseWriter, r *http.Request) 
{ 
  w.Write([]byte("Not Implemented"))
}
func main() 
{
  router := mux.NewRouter().StrictSlash(true)
  router.HandleFunc("/status", getStatus).Methods("GET")
  router.HandleFunc("/get-token", getToken).Methods("GET")
  router.HandleFunc("/employees", getEmployees).Methods("GET")
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT,
  handlers.LoggingHandler(os.Stdout, router))
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-rest-api.go

工作原理…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080上启动。

接下来,您可以从命令行执行GET请求,如下所示:

$ curl -X GET http://localhost:8080/status
 API is up and running

这将给您 REST API 的状态。您可以从命令行执行GET请求,如下所示:

$ curl -X GET http://localhost:8080/employees
 [{"id":1,"firstName":"Foo","lastName":"Bar"},{"id":2,"firstName":"Baz","lastName":"Qux"}]

这将给你一个所有员工的列表。我们可以尝试通过命令行获取访问令牌:

$ curl -X GET http://localhost:8080/get-token

我们将从服务器获取“Not Implemented”消息。

让我们了解我们编写的程序:

  • import ("encoding/json" "log" "net/http" "os" “github.com/gorilla/handlers" "github.com/gorilla/mux"):在这里,我们导入了github.com/gorilla/mux来创建一个 Gorilla Mux 路由器,以及github.com/gorilla/handlers来创建一个 Gorilla 日志处理程序,以 Apache Common Log Format 记录 HTTP 请求。

  • func getStatus(w http.ResponseWriter, r *http.Request) { w.Write([]byte("API is up and running"))}:这是一个处理程序,它只是向 HTTP 响应流写入 API 正在运行。

  • func getEmployees(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(employees)}:这是一个处理程序,它将一个静态员工数组写入 HTTP 响应流。

  • 这是一个处理程序,它只是向 HTTP 响应流写入“Not Implemented”。

  • 然后,我们定义了main(),在其中我们使用NewRouter()处理程序创建了一个gorilla/mux路由器实例,对新路由的尾随斜杠行为设置为true,添加路由并向其注册处理程序,最后调用http.ListenAndServe来处理每个传入连接的 HTTP 请求,每个连接在单独的 Goroutine 中处理。ListenAndServe接受两个参数——服务器地址和处理程序。在这里,我们将服务器地址传递为localhost:8080,处理程序为 Gorilla LoggingHandler,它以 Apache Common Log Format 记录 HTTP 请求。

创建 JSON Web 令牌

要保护您的 REST API 或服务端点,您必须编写一个在 Go 中生成 JSON Web 令牌或JWT的处理程序。

在这个示例中,我们将使用https://github.com/dgrijalva/jwt-go来生成JWT,尽管您可以在 Go 中实现许多第三方库中提供的任何库,例如https://github.com/square/go-josehttps://github.com/tarent/loginsrv

如何做…

  1. 使用go get命令安装github.com/dgrijalva/jwt-gogithub.com/gorilla/muxgithub.com/gorilla/handlers包,如下所示:
$ go get github.com/dgrijalva/jwt-go
$ go get github.com/gorilla/handlers
$ go get github.com/gorilla/mux
  1. 创建create-jwt.go,在其中我们将定义getToken处理程序来生成JWT,如下所示:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "os"
  "time"
  jwt "github.com/dgrijalva/jwt-go"
  "github.com/gorilla/handlers"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  CLAIM_ISSUER = "Packt"
  CLAIM_EXPIRY_IN_HOURS = 24
)
type Employee struct 
{
  Id int `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: 1, FirstName: "Foo", LastName: "Bar"},
    Employee{Id: 2, FirstName: "Baz", LastName: "Qux"},
  }
}
var signature = []byte("secret")
func getToken(w http.ResponseWriter, r *http.Request) 
{
  claims := &jwt.StandardClaims
  {
    ExpiresAt: time.Now().Add(time.Hour *
    CLAIM_EXPIRY_IN_HOURS).Unix(),
    Issuer: CLAIM_ISSUER,
  }
  token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  tokenString, _ := token.SignedString(signature)
  w.Write([]byte(tokenString))
}
func getStatus(w http.ResponseWriter, r *http.Request) 
{
  w.Write([]byte("API is up and running"))
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  json.NewEncoder(w).Encode(employees)
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  muxRouter.HandleFunc("/status", getStatus).Methods("GET")
  muxRouter.HandleFunc("/get-token", getToken).Methods("GET")
  muxRouter.HandleFunc("/employees", getEmployees).Methods("GET")
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT,
  handlers.LoggingHandler(os.Stdout, muxRouter))
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run create-jwt.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,我们从命令行执行一个GET请求:

$ curl -X GET http://localhost:8080/status
 API is up and running

它将给你 API 的状态。接下来,我们从命令行执行一个GET请求:

$ curl -X GET http://localhost:8080/employees
 [{"id":1,"firstName":"Foo","lastName":"Bar"},{"id":2,"firstName":"Baz","lastName":"Qux"}]

它将给你一个所有员工的列表。接下来,让我们尝试通过命令行获取 REST API 的访问令牌:

$ curl -X GET http://localhost:8080/get-token

它将给我们生成的 JWT 令牌:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTM1MDY4ODEsImlzcyI6IlBhY2t0In0.95vuiR7lpWt4AIBDasBzOffL_Xv78_J9rcrKkeqSW08

接下来,浏览到https://jwt.io/,并将生成的令牌粘贴到 Encoded 部分,以查看其解码值,如下面的屏幕截图所示:

让我们了解我们在这个示例中引入的更改:

  • import ( "encoding/json" "log" "net/http" "os" "time" jwt "github.com/dgrijalva/jwt-go" "github.com/gorilla/handlers" "github.com/gorilla/mux"):在这里,我们导入了一个额外的包——github.com/dgrijalva/jwt-go,它具有 JWT 的 Go 实现。

  • const ( CONN_HOST = "localhost" CONN_PORT = "8080" CLAIM_ISSUER = "Packt" CLAIM_EXPIRY_IN_HOURS = 24 ):在这里,我们引入了两个额外的常量——一个是CLAIM_ISSUER,用于标识发出 JWT 的主体,另一个是CLAIM_EXPIRY_IN_HOURS,用于标识 JWT 必须在到期时间之后多长时间内不被接受进行处理。

  • var signature = []byte("secret"):这是服务器保存的签名。使用这个签名,服务器将能够验证现有令牌并签发新令牌。

接下来,我们定义了一个getToken处理程序,在其中我们首先使用JWT StandardClaims处理程序准备了一个声明对象,然后使用jwt NewWithClaims处理程序生成了一个 JWT 令牌,并最终使用服务器签名对其进行签名,并将其写入 HTTP 响应流。

使用 JSON Web Token 保护 RESTful 服务

一旦我们有了 REST API 端点和 JWT 令牌生成处理程序,我们就可以轻松地使用 JWT 保护我们的端点,我们将在本教程中介绍。

如何做…

  1. 使用go get命令安装github.com/auth0/go-jwt-middlewaregithub.com/dgrijalva/jwt-gogithub.com/gorilla/muxgithub.com/gorilla/handlers包,如下所示:
$ go get github.com/auth0/go-jwt-middleware
$ go get github.com/dgrijalva/jwt-go
$ go get github.com/gorilla/handlers
$ go get github.com/gorilla/mux
  1. 创建http-rest-api-secured.go,在其中我们将定义 JWT 中间件以检查 HTTP 请求中的 JWT,并将/employees路由包装在其中,如下所示:
package main
import 
(
  "encoding/json"
  "log"
  "net/http"
  "os"
  "time"
  jwtmiddleware "github.com/auth0/go-jwt-middleware"
  jwt "github.com/dgrijalva/jwt-go"
  "github.com/gorilla/handlers"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8080"
  CLAIM_ISSUER = "Packt"
  CLAIM_EXPIRY_IN_HOURS = 24
)
type Employee struct 
{
  Id int `json:"id"`
  FirstName string `json:"firstName"`
  LastName string `json:"lastName"`
}
type Employees []Employee
var employees []Employee
func init() 
{
  employees = Employees
  {
    Employee{Id: 1, FirstName: "Foo", LastName: "Bar"},
    Employee{Id: 2, FirstName: "Baz", LastName: "Qux"},
  }
}
var signature = []byte("secret")
var jwtMiddleware = jwtmiddleware.New
(
  jwtmiddleware.Options
  {
    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) 
    {
      return signature, nil
    },
    SigningMethod: jwt.SigningMethodHS256,
  }
)
func getToken(w http.ResponseWriter, r *http.Request) 
{
  claims := &jwt.StandardClaims
  {
    ExpiresAt: time.Now().Add(time.Hour *
    CLAIM_EXPIRY_IN_HOURS).Unix(),
    Issuer: CLAIM_ISSUER,
  }
  token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  tokenString, _ := token.SignedString(signature)
  w.Write([]byte(tokenString))
}
func getStatus(w http.ResponseWriter, r *http.Request) 
{
  w.Write([]byte("API is up and running"))
}
func getEmployees(w http.ResponseWriter, r *http.Request) 
{
  json.NewEncoder(w).Encode(employees)
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  muxRouter.HandleFunc("/status", getStatus).Methods("GET")
  muxRouter.HandleFunc("/get-token", getToken).Methods("GET")
  muxRouter.Handle("/employees", jwtMiddleware.Handler
  (http.HandlerFunc(getEmployees))).Methods("GET")
  err := http.ListenAndServe(CONN_HOST+":"+CONN_PORT,
  handlers.LoggingHandler(os.Stdout, muxRouter))
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}
  1. 使用以下命令运行程序:
$ go run http-rest-api-secured.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8080

接下来,我们从命令行执行GET请求,如下所示:

$ curl -X GET http://localhost:8080/status
 API is up and running

它将向我们显示 API 的状态。接下来,我们从命令行执行GET请求,如下所示:

$ curl -X GET http://localhost:8080/employees
 Required authorization token not found

它将向我们显示 JWT 未在请求中找到的消息。因此,要获取所有员工的列表,我们必须获取 API 的访问令牌,可以通过执行以下命令获取:

$ curl -X GET http://localhost:8080/get-token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTM1MTI2NTksImlzcyI6IlBhY2t0In0.2r_q_82erdOmt862ofluiMGr3O5x5_c0_sMyW7Pi5XE

现在,再次调用员工 API,将 JWT 作为 HTTPAuthorization请求头传递,如下所示:

$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTM1MTI2NTksImlzcyI6IlBhY2t0In0.2r_q_82erdOmt862ofluiMGr3O5x5_c0_sMyW7Pi5XE" http://localhost:8080/employees

它将为您提供所有员工的列表,如下所示:

[{"id":1,"firstName":"Foo","lastName":"Bar"},{"id":2,"firstName":"Baz","lastName":"Qux"}]

让我们了解本教程中引入的更改:

  1. 使用import("encoding/json" "log" "net/http" "os" "time" jwtmiddleware "github.com/auth0/go-jwt-middleware" jwt "github.com/dgrijalva/jwt-go" "github.com/gorilla/handlers" "github.com/gorilla/mux"),我们导入了一个额外的包,github.com/auth0/go-jwt-middleware,别名为jwtmiddleware,它在 HTTP 请求中检查 JWT。

  2. 然后,我们构建了一个新的安全实例jwtmiddleware,将SigningMethod设置为HS256,并将ValidationKeyGetter选项设置为一个返回用于验证 JWT 的密钥的 Go 函数。在这里,服务器签名被用作验证 JWT 的密钥。

  3. 最后,我们在main()中使用jwtmiddleware处理程序包装了/employees路由,这意味着对于每个 URL 模式为/employees的请求,我们在提供响应之前检查并验证 JWT。

在 Go Web 应用程序中防止跨站点请求伪造

从恶意网站、电子邮件、博客、即时消息或程序攻击受信任的站点,用户当前已经认证,以防止不必要的操作,这是一种常见的做法。我们经常称之为跨站点请求伪造。

在 Go 中实现跨站点请求伪造非常容易,使用 Gorilla CSRF 包,我们将在本教程中介绍。

如何做…

  1. 使用go get命令安装github.com/gorilla/csrfgithub.com/gorilla/mux包,如下所示:
$ go get github.com/gorilla/csrf
$ go get github.com/gorilla/mux
  1. 创建sign-up.html,其中包含名称和电子邮件输入文本字段,以及一个在提交 HTML 表单时调用的操作,如下所示:
<html>
  <head>
    <title>Sign Up!</title>
  </head>
  <body>
    <form method="POST" action="/post" accept-charset="UTF-8">
      <input type="text" name="name">
      <input type="text" name="email">
      {{ .csrfField }}
      <input type="submit" value="Sign up!">
    </form>
  </body>
</html>
  1. 创建prevent-csrf.go,在其中创建一个signUp处理程序,用于呈现注册 HTML 表单,以及一个post处理程序,每当提交 HTML 表单并且请求具有有效的 CSRF 令牌时执行,如下所示:
package main
import 
(
  "fmt"
  "html/template"
  "log"
  "net/http"
  "github.com/gorilla/csrf"
  "github.com/gorilla/mux"
)
const 
(
  CONN_HOST = "localhost"
  CONN_PORT = "8443"
  HTTPS_CERTIFICATE = "domain.crt"
  DOMAIN_PRIVATE_KEY = "domain.key"
)
var AUTH_KEY = []byte("authentication-key")
func signUp(w http.ResponseWriter, r *http.Request) 
{
  parsedTemplate, _ := template.ParseFiles("sign-up.html")
  err := parsedTemplate.Execute
  (
    w, map[string]interface{}
    {
      csrf.TemplateTag: csrf.TemplateField(r),
    }
  )
  if err != nil 
  {
    log.Printf("Error occurred while executing the 
    template : ", err)
    return
  }
}
func post(w http.ResponseWriter, r *http.Request) 
{
  err := r.ParseForm()
  if err != nil 
  {
    log.Print("error occurred while parsing form ", err)
  }
  name := r.FormValue("name")
  fmt.Fprintf(w, "Hi %s", name)
}
func main() 
{
  muxRouter := mux.NewRouter().StrictSlash(true)
  muxRouter.HandleFunc("/signup", signUp)
  muxRouter.HandleFunc("/post", post)
  http.ListenAndServeTLS(CONN_HOST+":"+CONN_PORT, 
  HTTPS_CERTIFICATE, DOMAIN_PRIVATE_KEY, csrf.Protect
  (AUTH_KEY)(muxRouter))
}
  1. 使用以下命令运行程序:
$ go run prevent-csrf.go

它是如何工作的…

一旦我们运行程序,HTTP 服务器将在本地监听端口8443

接下来,从命令行执行POST请求,如下所示:

$ curl -X POST --data "name=Foo&email=aggarwalarpit.89@gmail.com" https://localhost:8443/post --insecure

它将向您显示Forbidden - CSRF token invalid消息作为服务器的响应,并禁止您提交 HTML 表单,因为服务器在请求中找不到有效的 CSRF 令牌:

因此,要提交表单,首先我们必须注册,通过执行以下命令生成有效的 CSRF 令牌:

$ curl -i -X GET https://localhost:8443/signup --insecure

这将给你一个 HTTP X-CSRF-Token,如下面的截图所示:

现在,您必须将其作为 HTTP X-CSRF-Token请求头和 HTTP cookie 一起传递,以提交 HTML 表单,如下所示:

$ curl -X POST --data "name=Foo&email=aggarwalarpit.89@gmail.com" -H "X-CSRF-Token: M9gqV7rRcXERvSJVRSYprcMzwtFmjEHKXRm6C8cDC4EjTLIt4OiNzVrHfYNB12nEx280rrKs8fqOgvfcJgQiFA==" --cookie "_gorilla_csrf=MTUyMzQzMjg0OXxJa1ZLVTFsbGJHODFMMHg0VEdWc0wxZENVRVpCWVZGU1l6bHVMMVZKVEVGM01EVjBUakVyUlVoTFdsVTlJZ289fJI5dumuyObaHVp97GN_CiZBCCpnbO0wlIwgSgvHL7-C;" https://localhost:8443/post --insecure

Hi Foo

让我们了解一下我们编写的程序:

  • const (CONN_HOST = "localhost" CONN_PORT = "8443" HTTPS_CERTIFICATE = "domain.crt" DOMAIN_PRIVATE_KEY = "domain.key"):在这里,我们声明了四个常量 - CONN_HOST的值为localhostCONN_PORT的值为8443HTTPS_CERTIFICATE的值为domain.crt或自签名证书,以及DOMAIN_PRIVATE_KEY的值为domain.key或我们在上一个示例中创建的私钥。

  • var AUTH_KEY = []byte("authentication-key"):这是用于生成 CSRF 令牌的身份验证密钥。

  • signUp:这是一个处理程序,解析sign-up.html并在表单中用 CSRF 令牌替换{{ .csrfField }}提供一个<input>字段。

  • post:这是一个处理程序,解析提交的表单,获取名称输入字段的值,并将其写入 HTTP 响应流。

最后,我们定义了main(),在这里我们使用NewRouter()处理程序创建了一个gorilla/mux路由器实例,对于新路由的尾随斜杠行为设置为true,注册了/signup路由与signUp处理程序以及/post路由与post处理程序,并调用了http.ListenAndServeTLS,将处理程序传递为csrf.Protect(AUTH_KEY)(muxRouter),这样可以确保所有没有有效令牌的POST请求都会返回HTTP 403 Forbidden

第十一章:将 Go Web 应用程序和 Docker 容器部署到 AWS

在本章中,我们将涵盖以下内容:

  • 创建您的第一个 EC2 实例以运行 Go Web 应用程序

  • 与您的第一个 EC2 实例交互

  • 在您的第一个 EC2 实例上创建、复制和运行 Go Web 应用程序

  • 设置 EC2 实例以运行 Docker 容器

  • 在 AWS EC2 实例上从 Docker Hub 拉取 Docker 镜像

  • 在 EC2 实例上运行您的 Go Docker 容器

介绍

如今,每个组织都在向 DevOps 转变,每个人都在谈论持续集成和持续部署,通常称为 CI 和 CD,这已经成为开发人员必须学习的技能。当我们谈论 CI/CD 时,我们在很高的层面上谈论通过持续集成工具(如 Jenkins 和 Bamboo)将容器部署到公共/私有云中。

在本章中,我们将学习如何将简单的 Go Web 应用程序和 Go Docker 容器部署到手动配置的 EC2 实例上。由于我们将使用 Docker 和 AWS,我假设您具有 Docker 和 AWS 的基本知识。

创建您的第一个 EC2 实例以运行 Go Web 应用程序

在 AWS 上创建 EC2 实例与获取新机器并安装所需软件以运行 Web 应用程序是一样的。在本教程中,我们将创建一个 EC2 实例,对其进行配置,并运行一个简单的 Go Web 应用程序。

准备工作…

要开始在 AWS EC2 实例上创建和部署,首先必须创建和激活 AWS 账户。由于这与本教程无关,我们将不在此处进行操作。

您可以按照以下链接中提供的详细说明来创建和激活 AWS 账户:https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/

操作步骤…

  1. 登录到 AWS,转到 EC2 管理控制台,并在“创建实例”部分点击“启动实例”,如下截图所示:

  1. 选择 Amazon Linux AMI 2017.09.1(HVM),SSD 卷类型,如下截图所示:

  1. 选择 t2.micro 实例类型,然后点击“下一步:配置实例详细信息”:

  1. 在“配置实例详细信息”部分启用“自动分配公共 IP”,如下截图所示:

  1. 不要对添加存储和添加标签部分进行任何更改。

  2. 添加 HTTP 和 HTTPS 规则,然后在配置安全组部分点击“Review and Launch”按钮,如下截图所示:

  1. 从下拉菜单中选择“创建新的密钥对”,为密钥对命名,然后点击“下载密钥对”按钮。保存my-first-ec2-instance.pem文件,然后点击“启动实例”,如下截图所示:

工作原理…

点击“启动实例”后,它将在 AWS 上创建并启动一个 Linux 机器,并为实例分配 ID、公共 DNS 和公共 IP,通过这些信息我们可以访问它。

转到 EC2 仪表板的实例部分,您可以看到正在运行的实例,如下截图所示:

与您的第一个 EC2 实例交互

要在 EC2 实例上部署应用程序,我们首先必须登录并安装必要的软件包/软件,这可以通过 SSH 客户端(如 MobaXterm,Putty 等)轻松完成。在本教程中,我们将登录到之前创建的 EC2 实例,并使用 Red Hat 软件包管理器安装 Go。

操作步骤…

  1. 将私钥文件my-first-ec2-instance.pem的权限设置为400,这意味着用户/所有者可以读取,但不能写入,也不能执行,而组和其他人都不能读取,不能写入,也不能执行,通过执行chmod命令,如下所示:
$ chmod 400 my-first-ec2-instance.pem
  1. 获取 EC2 实例的公共 DNS,并使用私钥文件作为ec2-user连接到它,如下所示执行ssh命令:
$ ssh -i my-first-ec2-instance.pem ec2-user@ec2-172-31-34-99.compute-1.amazonaws.com

一旦命令成功执行,我们将登录到 EC2 实例,并且输出将如下所示:

  1. 通过执行sudo命令从ec2-user切换到root用户:
[ec2-user@ip-172-31-34-99 ~]$ sudo su
  1. 使用 Red Hat 软件包管理器yum安装Go,如下所示:
[root@ip-172-31-34-99 ~]$ yum install -y go

工作原理…

通过执行go version命令验证ec2-user是否成功安装了Go,如下所示:

[ec2-user@ip-172-31-34-99 ~]$ go version
go version go1.8.4 linux/amd64

在第一个 EC2 实例上创建、复制和运行 Go Web 应用程序

一旦我们准备好具有所需库的 EC2 实例,我们可以使用安全拷贝协议简单地复制应用程序,然后使用go run命令运行它,这将在本教程中介绍。

如何做…

  1. 创建http-server.go,我们将创建一个简单的 HTTP 服务器,它将在http://ec2-instance-public-dns:80上呈现 Hello World!,或者从命令行执行curl -X GET http://ec2-instance-public-dns:80,如下所示:
package main
import 
(
  "fmt"
  "log"
  "net/http"
)
const 
(
  CONN_PORT = "80"
)
func helloWorld(w http.ResponseWriter, r *http.Request) 
{
  fmt.Fprintf(w, "Hello World!")
}
func main() 
{ 
  http.HandleFunc("/", helloWorld)
  err := http.ListenAndServe(":"+CONN_PORT, nil)
  if err != nil 
  {
    log.Fatal("error starting http server : ", err)
    return
  }
}

一切就绪后,目录结构应如下所示:

  1. 使用安全拷贝或scp命令将http-server.go从本地机器目录复制到 EC2 用户主目录(/home/ec2-user),如下所示:
$ scp -i my-first-ec2-instance.pem http-server.go ec2-user@ec2-172-31-34-99.compute-1.amazonaws.com:/home/ec2-user
  1. 使用私钥文件和公共 DNS 名称登录 EC2 实例,如下所示:
$ ssh -i my-first-ec2-instance.pem ec2-user@ec2-172-31-34-99.compute-1.amazonaws.com
  1. 在后台运行http-server.go,执行无挂起或nohup命令,如下所示:
[ec2-user@ip-172-31-34-99 ~] $ nohup go run http-server.go &

工作原理…

一旦在 EC2 实例上运行程序,HTTP 服务器将在本地监听端口80

接下来,从命令行执行GET请求:

$ curl -i -X GET http://ec2-172-31-34-99.compute-1.amazonaws.com:80/

这将作为响应给出“Hello World!”,将给出以下输出:

HTTP/1.1 200 OK
Date: Sat, 06 Jan 2018 10:59:38 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8

Hello World!

设置 EC2 实例以运行 Docker 容器

要在 EC2 实例上运行 Docker 容器,我们首先必须设置一个带有 Docker 安装的实例,并将ec2-user添加到 Docker 组,以便我们可以以ec2-user而不是root用户执行 Docker 命令,这将在本教程中介绍。

如何做…

  1. 通过执行以下命令从ec2-user用户切换到root用户:
[ec2-user@ip-172-31-34-99 ~]$ sudo su
[root@ip-172-31-34-99 ec2-user]#
  1. 安装Docker并通过执行以下命令更新 EC2 实例:
[root@ip-172-31-34-99 ec2-user] yum install -y docker
[root@ip-172-31-34-99 ec2-user] yum update -y
  1. 通过执行以下命令在 EC2 实例上启动Docker服务:
[root@ip-172-31-34-99 ec2-user] service docker start
  1. ec2-user添加到docker组,以便您可以在不使用sudo的情况下执行 Docker 命令,如下所示:
[root@ip-172-31-34-99 ec2-user] usermod -a -G docker ec2-user
  1. 通过执行以下命令退出 EC2 实例:
[root@ip-172-31-34-99 ec2-user]# exit
 exit
[ec2-user@ip-172-31-34-99 ~]$ exit
 logout
Connection to ec2-172-31-34-99.compute-1.amazonaws.com closed.
  1. 通过执行以下命令再次登录以获取新的 Docker 组权限:
$ ssh -i my-first-ec2-instance.pem ec2-user@ec2-172-31-34-99.compute-1.amazonaws.com

这将在控制台上给出输出,如下截图所示:

工作原理…

登录 EC2 实例并通过执行以下命令验证ec2-user是否可以在不使用sudo的情况下运行 Docker 命令:

[ec2-user@ip-54-196-74-162 ~]$ docker info

这将显示有关 Docker 安装的系统范围信息,如下输出所示:

 Containers: 1
 Running: 1
 Paused: 0
 Stopped: 0
 Images: 1
 ...
 Kernel Version: 4.9.62-21.56.amzn1.x86_64
 Operating System: Amazon Linux AMI 2017.09
 ...
 Live Restore Enabled: false

从 Docker Hub 在 AWS EC2 实例上拉取 Docker 镜像

要运行 Docker 容器,我们需要有一个 Docker 镜像,可以从DockerFile构建,也可以从任何公共或私有 Docker 注册表中拉取,例如 Docker Hub、Quay、Google 容器注册表、AWS 容器注册表等等。

由于我们已经学习了如何从DockerFile创建 Docker 镜像并在第九章“使用 Go 和 Docker”中将其推送到 Docker Hub,因此我们不会在本教程中再次构建镜像。相反,我们将在 EC2 实例上从 Docker Hub 拉取预构建的镜像。

在第九章中查看构建您的第一个 Go Docker 镜像教程,与 Go 和 Docker 一起工作。

如何做…

  1. 使用您的凭据从命令行登录到 Docker Hub,执行以下命令:
$ docker login --username arpitaggarwal --password XXXXX
 Login Succeeded
  1. 执行docker pull命令从 Docker Hub 拉取arpitaggarwal/golang-image,如下所示:
$ docker pull arpitaggarwal/golang-image

这将导致以下输出:

工作原理…

登录到 EC2 实例并通过执行以下命令验证是否成功从 Docker Hub 拉取了arpitaggarwal/golang-image

$ docker images

这将列出所有顶级镜像、它们的存储库、标签和大小,如下截图所示:

在 EC2 实例上运行您的 Go Docker 容器

一旦我们在 EC2 实例上安装了 Docker 镜像和 Docker,那么您可以通过执行docker run命令来简单地运行 Docker 容器,我们将在本教程中介绍这一点。

如何做…

登录到 EC2 实例并执行docker run命令,从arpitaggarwal/golang-image创建和运行一个 Docker 容器,使用--name标志将容器名称分配为golang-container,如下所示:

$ docker run -d -p 80:8080 --name golang-container -it arpitaggarwal/golang-image
 8a9256fcbffc505ad9406f5a8b42ae33ab3951fffb791502cfe3ada42aff781e

docker run命令中指定的-d标志以守护进程模式启动容器,末尾的哈希字符串表示golang-container的 ID。

docker run命令中指定的-p标志将容器的端口发布到主机。由于我们在 Docker 容器内的端口8080上运行 HTTP 服务器,并且我们为 E2C 实例的入站流量打开了端口80,因此我们将其映射为80:8080

工作原理…

登录到 EC2 实例并通过执行以下命令验证 Docker 容器是否已创建并成功运行:

$ docker ps

一旦前面的命令成功执行,它将给我们运行中的 Docker 容器的详细信息,如下截图所示:

获取 EC2 实例的公共 DNS,并从命令行执行GET请求:

$ curl -i -X GET http://ec2-172-31-34-99.compute-1.amazonaws.com/

这将作为响应给出“Hello World!”,如下输出所示:

 HTTP/1.1 200 OK
 Date: Sat, 06 Jan 2018 12:49:28 GMT
 Content-Length: 12
 Content-Type: text/plain; charset=utf-8
 Hello World!
posted @ 2024-05-04 22:32  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报