Go-Web-开发秘籍(全)
Go Web 开发秘籍(全)
原文:
zh.annas-archive.org/md5/6712F93A50A8E516D2DB7024F42646AC
译者:飞龙
前言
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并注册,以便将文件直接发送到您的邮箱。
您可以按照以下步骤下载代码文件:
-
登录或注册www.packtpub.com。
-
选择“支持”选项卡。
-
单击“代码下载和勘误”。
-
在搜索框中输入书名,然后按照屏幕上的说明操作。
下载文件后,请确保使用最新版本的以下软件解压缩或提取文件夹:
-
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!执行以下步骤:
- 创建
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
}
}
- 使用以下命令运行程序:
$ go run http-server.go
它是如何工作的…
一旦我们运行程序,一个 HTTP 服务器将在本地监听端口8080
。在浏览器中打开http://localhost:8080
将显示来自服务器的 Hello World!,如下面的屏幕截图所示:
你好,世界!
让我们理解程序中每一行的含义:
-
package main
: 这定义了程序的包名称。 -
import ( "fmt" "log" "net/http" )
: 这是一个预处理命令,告诉 Go 编译器包括fmt
、log
和net/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 函数,它以ResponseWriter
和Request
作为输入,并在 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 服务器。执行以下步骤:
- 创建
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
}
}
- 使用以下命令运行程序:
$ go run http-server-basic-authentication.go
它是如何工作的…
一旦我们运行程序,HTTP 服务器将在本地监听端口8080
上启动。
一旦服务器启动,在浏览器中访问http://localhost:8080
将提示您输入用户名和密码。提供admin
,admin
将在屏幕上呈现 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_USER
和ADMIN_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
处理程序,而不是nil
或DefaultServeMux
来处理所有带有 URL 模式为/
的传入请求。
使用 GZIP 压缩优化 HTTP 服务器响应
GZIP 压缩意味着从服务器以.gzip
格式向客户端发送响应,而不是发送纯文本响应,如果客户端/浏览器支持的话,发送压缩响应总是一个好习惯。
通过发送压缩响应,我们节省了网络带宽和下载时间,最终使页面加载更快。 GZIP 压缩的原理是浏览器发送一个请求标头,告诉服务器它接受压缩内容(.gzip
和.deflate
),如果服务器有能力以压缩形式发送响应,则发送压缩形式的响应。如果服务器支持压缩,则它将设置Content-Encoding: gzip
作为响应标头,否则它将向客户端发送一个纯文本响应,这清楚地表示要求压缩响应只是浏览器的请求,而不是要求。我们将使用 Gorilla 的 handlers 包在这个配方中实现它。
如何做…
在本教程中,我们将创建一个带有单个处理程序的 HTTP 服务器,该处理程序将在 HTTP 响应流上写入 Hello World!并使用 Gorilla CompressHandler
以.gzip
格式将所有响应发送回客户端。执行以下步骤:
- 使用大猩猩处理程序,首先我们需要使用
go get
命令安装包,或者手动将其复制到$GOPATH/src
或$GOPATH
,如下所示:
$ go get github.com/gorilla/handlers
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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 编译器包括来自io
、net/http
和github.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!")}
: 这是一个接受ResponseWriter
和Request
作为输入参数并在 HTTP 响应流上写入Hello World!
的 Go 函数。
接下来,我们声明了main()
方法,程序的执行从这里开始。由于这个方法做了很多事情,让我们逐行理解它:
-
mux := http.NewServeMux()
: 这将分配并返回一个新的 HTTP 请求多路复用器(ServeMux
),它将匹配每个传入请求的 URL 与已注册模式列表,并调用最接近 URL 的模式的处理程序。使用它的好处之一是程序完全控制与服务器一起使用的处理程序,尽管任何使用DefaultServeMux
注册的处理程序都将被忽略。 -
http.HandleFunc("/", helloWorld)
: 在这里,我们使用net/http
包的HandleFunc
将helloWorld
函数注册到/
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
上接受连接。执行以下步骤:
- 创建
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)
}
}
- 使用以下命令运行程序:
$ go run tcp-server.go
工作原理…
运行程序后,TCP 服务器将在本地监听端口8080
。
让我们理解程序中每一行的含义:
-
package main
: 这定义了程序的包名称。 -
import ( "log" "net")
: 这是一个预处理命令,告诉 Go 编译器包括log
和net
包中的所有文件。 -
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
方法并传递连接对象以读取和打印服务器控制台上的数据。执行以下步骤:
- 创建
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()
}
- 使用以下命令运行程序:
$ go run tcp-server-read-data.go
工作原理…
一旦我们运行程序,TCP 服务器将在本地端口8080
上开始监听。从命令行执行echo
命令将向 TCP 服务器发送消息:
$ echo -n "Hello to TCP server\n" | nc localhost 8080
这显然会将其记录到服务器控制台,如下面的屏幕截图所示:
让我们理解这个示例中引入的变化:
- 首先,我们使用
go
关键字从main()
方法中调用handleRequest
,这意味着我们在 Goroutine 中调用函数,如下所示:
func main()
{
...
go handleRequest(conn)
...
}
- 接下来,我们定义了
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
方法,以便向客户端写入数据。执行以下步骤:
- 创建
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()
}
- 使用以下命令运行程序:
$ 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
,以及它们的处理程序。执行以下步骤:
- 创建
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
}
}
- 使用以下命令运行程序:
$ go run http-server-basic-routing.go
它是如何工作的…
一旦我们运行程序,HTTP 服务器将在本地监听端口8080
,并且从浏览器或命令行访问http://localhost:8080/
、http://localhost:8080/login
和http://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
这将为我们提供来自服务器的以下响应:
让我们了解我们编写的程序:
- 我们首先定义了三个处理程序或 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!
。
- 接下来,我们使用
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
。我们在这个示例中也会这样做。执行以下步骤:
- 使用
go get
命令安装github.com/gorilla/mux
,如下所示:
$ go get github.com/gorilla/mux
- 创建
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)
}
- 使用以下命令运行程序:
$ go run http-server-gorilla-mux-routing.go
它是如何工作…
一旦我们运行程序,HTTP 服务器将在本地监听端口8080
,并且从浏览器或命令行访问http://localhost:8080/
、http://localhost:8080/post
和http://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
这将为我们提供来自服务器的以下响应:
让我们了解我们在这个示例中所做的代码更改:
- 首先,我们定义了
GetRequestHandler
和PostRequestHandler
,它们只是在 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!"))
}
)
- 接下来,我们定义了
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))
}
)
- 然后,我们将所有这些处理程序注册到
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 处理程序实现日志记录。执行以下步骤:
- 使用
go get
命令安装github.com/gorilla/handler
和github.com/gorilla/mux
包,如下所示:
$ go get github.com/gorilla/handlers
$ go get github.com/gorilla/mux
- 创建
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)
}
- 运行程序,使用以下命令:
$ 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 日志格式记录请求的详细信息,如下面的屏幕截图所示:
让我们了解一下在这个示例中我们做了什么:
- 首先,我们导入了两个额外的包,一个是
os
,我们用它来打开一个文件。另一个是github.com/gorilla/handlers
,我们用它来导入用于记录 HTTP 请求的日志处理程序,如下所示:
import ( "net/http" "os" "github.com/gorilla/handlers" "github.com/gorilla/mux" )
- 接下来,我们修改了
main()
方法。使用router.Handle("/", handlers.LoggingHandler(os.Stdout,
http.HandlerFunc(GetRequestHandler))).Methods("GET")
,我们用 Gorilla 日志处理程序包装了GetRequestHandler
,并将标准输出流作为写入器传递给它,这意味着我们只是要求在控制台上以 Apache Common 日志格式记录每个 URL 路径为/
的请求。
- 接下来,我们以只写模式创建一个名为
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
}
-
使用
router.Handle("/post", handlers.LoggingHandler(logFile, PostRequestHandler)).Methods("POST")
,我们用 Gorilla 日志处理程序包装了GetRequestHandler
,并将文件作为写入器传递给它,这意味着我们只是要求在名为/hello/{name}
的文件中以 Apache Common 日志格式记录每个 URL 路径为/post
的请求。 -
使用
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
、.css
和images
,最终创建、读取和验证 HTML 表单,并将文件上传到服务器。
创建您的第一个模板
模板允许我们定义动态内容的占位符,可以由模板引擎在运行时替换为值。然后可以将它们转换为 HTML 文件并发送到客户端。在 Go 中创建模板非常容易,使用 Go 的html/template
包,我们将在本示例中介绍。
如何做…
在这个示例中,我们将创建一个first-template.html
,其中包含一些占位符,其值将在运行时由模板引擎注入。执行以下步骤:
- 通过执行以下 Unix 命令在
templates
目录中创建first-template.html
:
$ mkdir templates && cd templates && touch first-template.html
- 将以下内容复制到
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}}
,它们的值将由模板引擎在运行时替换或注入。
- 创建
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
}
}
一切就绪后,目录结构应如下所示:
- 使用以下命令运行程序:
$ 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
结构类型,具有Id
和Name
字段。
类型定义中的字段名称应以大写字母开头;否则,将导致错误并且不会在模板中被替换。
接下来,我们定义了一个renderTemplate()
处理程序,它执行了许多操作。
-
person := Person{Id: "1", Name: "Foo"}
: 在这里,我们初始化了一个person
结构类型,其中Id
为1
,Name
为Foo
。 -
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
、.css
和images
,而不是从 Web 服务器提供。这是因为所有这些类型的文件都是静态的,不需要处理;那么为什么我们要给服务器增加额外的负载呢?此外,它有助于提高应用程序的性能,因为所有对静态文件的请求都将从外部来源提供,并因此减少了对服务器的负载。
Go 的net/http
包足以通过FileServer
从文件系统中提供静态资源,我们将在本教程中介绍。
准备就绪…
由于我们已经在上一个教程中创建了一个模板,我们将扩展它以从static/css
目录中提供静态.css
文件。
如何做…
在本教程中,我们将创建一个文件服务器,它将从文件系统中提供静态资源。执行以下步骤:
- 在
static/css
目录中创建main.css
,如下所示:
$ mkdir static && cd static && mkdir css && cd css && touch main.css
- 将以下内容复制到
main.css
中:
body {color: #00008B}
- 创建
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
}
}
- 更新
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>
一切就绪后,目录结构应如下所示:
- 使用以下命令运行程序:
$ 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
包的HandleFunc
将http.StripPrefix("/static/", fileServer)
处理程序注册到/static
URL 模式,这意味着每当我们访问带有/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 路由器。
如何做…
- 使用
go get
命令安装github.com/gorilla/mux
包,如下所示:
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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 表单,其中包含两个输入字段和一个提交表单的按钮。执行以下步骤:
- 在
templates
目录中创建login-form.html
,如下所示:
$ mkdir templates && cd templates && touch login-form.html
- 将以下内容复制到
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
。
- 创建
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
}
}
一切就绪后,目录结构应如下所示:
- 使用以下命令运行程序:
$ 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) }:这是一个接受
ResponseWriter和
Request作为输入参数的 Go 函数,解析
login-form.html`并返回一个新模板。 -
http.HandleFunc("/", login):在这里,我们使用
net/http包的
HandleFunc将登录函数注册到
/URL 模式,这意味着每次访问
/模式的 HTTP URL 时,登录函数都会被执行,传递
ResponseWriter和
Request`作为参数。 -
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 表单,我们只需扩展该示例以读取其字段值。
如何做...
- 使用以下命令安装
github.com/gorilla/schema
包:
$ go get github.com/gorilla/schema
- 创建
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
}
}
- 使用以下命令运行程序:
$ go run html-form-read.go
工作原理...
一旦我们运行程序,HTTP 服务器将在本地监听端口8080
。浏览http://localhost:8080
将显示一个 HTML 表单,如下面的屏幕截图所示:
一旦我们输入用户名和密码并单击登录按钮,我们将在服务器的响应中看到 Hello,后跟用户名,如下面的屏幕截图所示:
让我们了解一下我们在这个配方中引入的更改:
-
使用
import ("fmt" "html/template" "log" "net/http" "github.com/gorilla/schema")
,我们导入了两个额外的包——fmt
和github.com/gorilla/schema
——它们有助于将structs
与Form
值相互转换。 -
接下来,我们定义了
User struct
类型,它具有Username
和Password
字段,如下所示:
type User struct
{
Username string
Password string
}
- 接下来,我们定义了
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.PostForm
和r.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.v9
、gopkg.in/validator.v2
和github.com/asaskevich/govalidator
。
在这个配方中,我们将使用最著名和常用的验证器github.com/asaskevich/govalidator
来验证我们的 HTML 表单。
准备工作...
由于我们已经在上一个配方中创建并读取了一个 HTML 表单,我们只需扩展它以验证其字段值。
如何做...
- 使用以下命令安装
github.com/asaskevich/govalidator
和github.com/gorilla/schema
包:
$ go get github.com/asaskevich/govalidator
$ go get github.com/gorilla/schema
- 创建
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
}
}
- 使用以下命令运行程序:
$ go run html-form-validation.go
工作原理...
一旦我们运行程序,HTTP 服务器将在本地监听端口8080
。浏览http://localhost:8080
将显示一个 HTML 表单,如下面的屏幕截图所示:
然后提交具有有效值的表单:
它将在浏览器屏幕上显示 Hello,后跟用户名,如下面的屏幕截图所示:
在任何字段中提交值为非字母的表单将显示错误消息。例如,提交用户名值为1234
的表单:
它将在浏览器上显示错误消息,如下面的屏幕截图所示:
此外,我们可以从命令行提交 HTML 表单,如下所示:
$ curl --data "username=Foo&password=password" http://localhost:8080/
这将给我们在浏览器中得到的相同输出:
让我们了解一下我们在这个示例中引入的更改:
-
使用
import ("fmt", "html/template", "log", "net/http" "github.com/asaskevich/govalidator" "github.com/gorilla/schema" )
,我们导入了一个额外的包——github.com/asaskevich/govalidator
,它可以帮助我们验证结构。 -
接下来,我们更新了
User struct
类型,包括一个字符串字面标签,key
为valid
,value
为alpha, required
,如下所示:
type User struct
{
Username string `valid:"alpha,required"`
Password string
valid:"alpha,required"
}
-
接下来,我们定义了一个
validateUser
处理程序,它接受ResponseWriter
、Request
和User
作为输入,并返回bool
和string
,分别是结构的有效状态和验证错误消息。在这个处理程序中,我们调用govalidator
的ValidateStruct
处理程序来验证结构标签。如果在验证字段时出现错误,我们将调用govalidator
的ErrorByField
处理程序来获取错误,并将结果与验证错误消息一起返回。 -
接下来,我们更新了
login
处理程序,调用validateUser
并将(w http.ResponseWriter, r *http.Request, user *User)
作为输入参数传递给它,并检查是否有任何验证错误。如果有错误,我们将在 HTTP 响应流中写入错误消息并返回它。
上传您的第一个文件
在任何 Web 应用程序中,最常见的情景之一就是上传文件或文件夹到服务器。例如,如果我们正在开发一个求职门户网站,那么我们可能需要提供一个选项,申请人可以上传他们的个人资料/简历,或者,比如说,我们需要开发一个电子商务网站,其中客户可以使用文件批量上传他们的订单。
在 Go 中实现上传文件的功能非常容易,使用其内置的包,我们将在本示例中进行介绍。
如何做…
在这个示例中,我们将创建一个带有file
类型字段的 HTML 表单,允许用户选择一个或多个文件通过表单提交上传到服务器。执行以下步骤:
- 在
templates
目录中创建upload-file.html
,如下所示:
$ mkdir templates && cd templates && touch upload-file.html
- 将以下内容复制到
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
。
- 创建
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
}
}
一切就绪后,目录结构应该如下所示:
- 使用以下命令运行程序:
$ 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 来实现一个会话。
如何做…
- 使用
go get
命令安装github.com/gorilla/sessions
包,如下所示:
$ go get github.com/gorilla/sessions
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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 响应流;否则,我们将写入一个403
HTTP 代码以及消息 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()
,在那里我们将所有处理程序home
,login
和logout
映射到/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 库来存储会话。
这个配方假设您已经在本地端口6379
和4567
上安装并运行了 Redis 和 Redis 浏览器。
如何做...
- 使用
go get
命令安装gopkg.in/boj/redistore.v1
和github.com/gorilla/sessions
,如下所示:
$ go get gopkg.in/boj/redistore.v1
$ go get github.com/gorilla/sessions
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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
这将导致服务器作为响应的主页:
让我们了解我们在这个配方中引入的更改:
-
使用
var store *redisStore.RediStore
,我们声明了一个私有的RediStore
来在 Redis 中存储会话。 -
接下来,我们更新了
init()
函数,使用大小和最大空闲连接数为10
创建NewRediStore
,并将其分配给存储。如果在创建存储时出现错误,我们将记录错误并以状态码1
退出。 -
最后,我们更新了
main()
,引入了defer store.Close()
语句,一旦我们从函数返回,就会关闭 Redis 存储。
创建你的第一个 HTTP cookie
在客户端存储信息时,cookie 扮演着重要的角色,我们可以使用它们的值来识别用户。基本上,cookie 是为了解决记住用户信息或持久登录身份验证的问题而发明的,这指的是网站能够在会话之间记住主体的身份。
Cookie 是在互联网上访问网站时 Web 浏览器创建的简单文本文件。您的设备会在本地存储这些文本文件,允许您的浏览器访问 cookie 并将数据传递回原始网站,并以名称-值对的形式保存。
如何做到这一点...
- 使用
go get
命令安装github.com/gorilla/securecookie
包,如下所示:
$ go get github.com/gorilla/securecookie
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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/securecookie
的Encode
处理程序创建一个以username
为键,Foo
为值的Base64
编码的 cookie。然后,我们向提供的ResponseWriter
头部添加一个Set-Cookie
头,并向 HTTP 响应中写入一个Cookie created.
的消息。 -
接下来,我们定义了一个
readCookie
处理程序,在其中我们从请求中检索一个 cookie,这在我们的代码中是first-cookie
,为其获取一个值,并将其写入 HTTP 响应。 -
最后,我们定义了
main()
,在其中将所有处理程序—createCookie
和readCookie
—映射到/create
和/read
,并在localhost:8080
上启动了 HTTP 服务器。
在 Go 中实现缓存
在 Web 应用程序中缓存数据有时是必要的,以避免反复从数据库或外部服务请求静态数据。Go 没有提供任何内置的包来缓存响应,但它通过外部包支持缓存。
有许多包,例如https://github.com/coocood/freecache
和https://github.com/patrickmn/go-cache
,可以帮助实现缓存,在本教程中,我们将使用https://github.com/patrickmn/go-cache
来实现它。
如何做到这一点...
- 使用
go get
命令安装github.com/patrickmn/go-cache
包,如下所示:
$ go get github.com/patrickmn/go-cache
- 创建
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
}
}
- 使用以下命令运行程序:
$ go run http-caching.go
它是如何工作的…
一旦我们运行程序,HTTP 服务器将在本地监听端口8080
。
在启动时,具有名称foo
和值为bar
的键将被添加到缓存中。
浏览http://localhost:8080/
将从缓存中读取一个键值,并将其附加到 Hello,如下截图所示:
我们在程序中指定了缓存数据的过期时间为五分钟,这意味着我们在服务器启动时在缓存中创建的键在五分钟后将不再存在。因此,五分钟后再次访问相同的 URL 将从服务器返回缓存中找不到键的消息,如下所示:
让我们理解我们编写的程序:
-
使用
var newCache *cache.Cache
,我们声明了一个私有缓存。 -
接下来,我们更新了
init()
函数,在其中创建了一个具有五分钟过期时间和十分钟清理间隔的缓存,并向缓存中添加了一个键为foo
,值为bar
,过期值为0
的项目,这意味着我们要使用缓存的默认过期时间。
如果过期持续时间小于一(或NoExpiration
),则缓存中的项目永远不会过期(默认情况下),必须手动删除。如果清理间隔小于一,则在调用c.DeleteExpired()
之前不会从缓存中删除过期的项目。
- 接下来,我们定义了
getFromCache
处理程序,从缓存中检索键的值。如果找到,我们将其写入 HTTP 响应;否则,我们将Key Not Found in Cache
的消息写入 HTTP 响应。
在 Go 中实现 HTTP 错误处理
在任何 Web 应用程序中实现错误处理是主要方面之一,因为它有助于更快地进行故障排除和修复错误。错误处理意味着每当应用程序发生错误时,应该将其记录在某个地方,无论是在文件中还是在数据库中,都应该有适当的错误消息以及堆栈跟踪。
在 Go 中,可以以多种方式实现。一种方法是编写自定义处理程序,我们将在本教程中介绍。
如何做…
- 使用
go get
命令安装github.com/gorilla/mux
包,如下所示:
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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 错误:
让我们理解我们编写的程序:
- 我们定义了一个
NameNotFoundError
结构,它有两个字段——类型为int
的Code
和类型为error
的Err
,它表示一个带有关联 HTTP 状态码的错误,如下所示:
type NameNotFoundError struct
{
Code int
Err error
}
- 然后,我们允许
NameNotFoundError
满足错误接口,如下所示:
func (nameNotFoundError NameNotFoundError) Error() string
{
return nameNotFoundError.Err.Error()
}
-
接下来,我们定义了一个用户定义类型
WrapperHandler
,它是一个接受任何接受func(http.ResponseWriter, *http.Request)
作为输入参数并返回错误的处理程序的 Go 函数。 -
然后,我们定义了一个
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)
}
}
-
接下来,我们定义了
getName
处理程序,它提取请求路径变量,获取name
变量的值,并检查名称是否匹配foo
。如果是,则将 Hello,后跟名称,写入 HTTP 响应;否则,它将返回一个Code
字段值为500
的NameNotFoundError
结构和一个err
字段值为error
的文本Name Not Found
。 -
最后,我们定义了
main()
,在其中将WrapperHandler
注册为 URL 模式/get/{name}
的处理程序。
在 Web 应用程序中实现登录和注销
每当我们希望应用程序只能被注册用户访问时,我们都必须实现一个机制,在允许他们查看任何网页之前要求用户提供凭据,这将在本示例中进行介绍。
准备工作…
由于我们已经在之前的示例中创建了一个 HTML 表单,我们只需更新它以使用gorilla/securecookie
包实现登录和注销机制。
在第二章的使用模板、静态文件和 HTML 表单中查看在 Web 应用程序中实现登录和注销的示例。
如何做…
- 使用
go get
命令安装github.com/gorilla/mux
和github.com/gorilla/securecookie
,如下所示:
$ go get github.com/gorilla/mux
$ go get github.com/gorilla/securecookie
- 在
templates
目录中创建home.html
,如下所示:
$ mkdir templates && cd templates && touch home.html
- 将以下内容复制到
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
。
- 创建
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
}
}
一切就绪后,目录结构应如下所示:
- 使用以下命令运行程序:
$ 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/
并显示登录表单:
让我们了解我们编写的程序。
- 使用`var cookieHandler = securecookie.New(securecookie.
使用GenerateRandomKey(64), securecookie.GenerateRandomKey(32))
,我们创建了一个安全 cookie,将哈希密钥作为第一个参数,块密钥作为第二个参数。哈希密钥用于使用 HMAC 对值进行身份验证,块密钥用于加密值。
-
接下来,我们定义了
getUserName
处理程序,从 HTTP 请求中获取一个 cookie,初始化一个字符串键
到字符串值
的cookieValue
映射,解码一个 cookie,并获取用户名的值并返回。 -
接下来,我们定义了
setSession
处理程序,其中我们创建并初始化一个带有key
和value
的映射,将其序列化,使用消息认证码对其进行签名,使用cookieHandler.Encode
处理程序对其进行编码,创建一个新的 HTTP cookie,并将其写入 HTTP 响应流。 -
接下来,我们定义了
clearSession
,它基本上将 cookie 的值设置为空,并将其写入 HTTP 响应流。 -
接下来,我们定义了一个
login
处理程序,在这里,我们从 HTTP 表单中获取用户名和密码,检查两者是否都不为空,然后调用setSession
处理程序并重定向到/home
,否则重定向到根 URL/
。 -
接下来,我们定义了一个
logout
处理程序,在这里,我们调用clearSession
处理程序清除会话值,并重定向到根 URL。 -
接下来,我们定义了一个
loginPage
处理程序,在这里,我们解析login-form.html
,返回一个具有名称和内容的新模板,调用已解析模板上的Execute
处理程序,生成 HTML 输出,并将其写入 HTTP 响应流。 -
接下来,我们定义了一个
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 协议访问,使应用程序成为唯一的真相来源。
在本章中,我们将编写一个支持GET
,POST
,PUT
和DELETE
HTTP 方法的 RESTful API,然后我们将学习如何对 REST API 进行版本控制,这在我们创建公开使用的 API 时非常有帮助。最后,我们将编写 REST 客户端来消耗它们。
创建你的第一个 HTTP GET 方法
在编写 Web 应用程序时,我们经常需要将我们的服务暴露给客户端或 UI,以便它们可以消耗在不同系统上运行的代码。通过 HTTP 协议方法可以暴露服务。在许多 HTTP 方法中,我们将学习在本教程中实现 HTTP GET
方法。
如何做...
- 使用
go get
命令安装github.com/gorilla/mux
包,如下所示:
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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"}
让我们了解我们编写的程序:
-
我们使用了
import ("encoding/json" "log" "net/http" "strconv" "github.com/gorilla/mux")
。在这里,我们导入了github.com/gorilla/mux
来创建一个Gorilla Mux Router
。 -
接下来,我们声明了
Route
结构类型,具有四个字段—Name
,Method
,Pattern
和HandlerFunc
,其中Name
表示 HTTP 方法的名称,Method
表示 HTTP 方法类型,可以是GET
,POST
,PUT
,DELETE
等,Pattern
表示 URL 路径,HandlerFunc
表示 HTTP 处理程序。 -
接下来,我们为
GET
请求定义了两个路由,如下:
var routes = Routes
{
Route
{
"getEmployees",
"GET",
"/employees",
getEmployees,
},
Route
{
"getEmployee",
"GET",
"/employee/{id}",
getEmployee,
},
}
- 接下来,我们定义了一个静态的
Employees
数组,如下:
func init()
{
employees = Employees
{
Employee{Id: "1", FirstName: "Foo", LastName: "Bar"},
Employee{Id: "2", FirstName: "Baz", LastName: "Qux"},
}
}
-
然后,我们定义了两个处理程序—
getEmployees
和getEmployee
,前者只是将员工的静态数组编组并将其写入 HTTP 响应流,后者从 HTTP 请求变量获取员工 ID,从数组中获取相应 ID 的员工,编组对象,并将其写入 HTTP 响应流。 -
在处理程序之后,我们定义了一个
AddRoutes
函数,它遍历我们定义的路由数组,将其添加到gorilla/mux
路由器,并返回Router
对象。 -
最后,我们定义了
main()
,在其中使用NewRouter()
处理程序创建了一个gorilla/mux
路由器实例,对于新路由的尾部斜杠行为为 true,这意味着应用程序将始终将路径视为路由中指定的路径。例如,如果路由路径是/path/
,访问/path
将重定向到前者,反之亦然。
创建你的第一个 HTTP POST 方法
每当我们需要通过异步调用或 HTML 表单将数据发送到服务器时,我们使用 HTTP POST
方法的实现,这将在本教程中介绍。
如何做...
- 使用以下命令安装
github.com/gorilla/mux
包,如下所示:
$ go get github.com/gorilla/mux
- 创建
http-rest-post.go
,在其中我们将定义一个支持 HTTPPOST
方法的附加路由和一个处理程序,该处理程序将员工添加到初始静态数组的员工,并将更新后的列表写入 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
}
}
- 使用以下命令运行程序:
$ go run http-rest-post.go
工作原理…
运行程序后,HTTP 服务器将在本地监听端口8080
。
接下来,使用以下命令从命令行执行POST
请求将员工添加到具有ID
为3
的列表,并将员工列表作为响应返回:
$ curl -H "Content-Type: application/json" -X POST -d '{"Id":"3", "firstName":"Quux", "lastName":"Corge"}' http://localhost:8080/employee/add
这可以在以下截图中看到:
让我们了解本节中引入的更改:
-
首先,我们添加了另一个名为
addEmployee
的路由,该路由为 URL 模式/employee/add
的每个POST
请求执行addEmployee
处理程序。 -
然后,我们定义了一个
addEmployee
处理程序,它基本上解码了作为POST
请求的一部分传递的员工数据,使用 Go 的内置encoding/json
包的NewDecoder
处理程序将其附加到员工的初始静态数组,并将其写入 HTTP 响应流。
创建您的第一个 HTTP PUT 方法
每当我们想要更新我们之前创建的记录或者如果记录不存在则创建新记录,通常称为Upsert,我们就会使用 HTTP PUT
方法的实现,我们将在本节中介绍。
操作步骤…
- 使用
go get
命令安装github.com/gorilla/mux
包,如下所示:
$ go get github.com/gorilla/mux
- 创建
http-rest-put.go
,在其中我们将定义一个支持 HTTPPUT
方法的附加路由和一个处理程序,该处理程序要么更新提供的 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
}
}
- 使用以下命令运行程序:
$ go run http-rest-put.go
工作原理…
运行程序后,HTTP 服务器将在本地监听端口8080
。
接下来,使用以下命令从命令行执行PUT
请求,将为具有 ID 1
的员工更新firstName
和lastName
:
$ 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
这可以在以下截图中看到:
让我们了解本节中引入的更改:
-
首先,我们添加了另一个名为
updateEmployee
的路由,该路由为 URL 模式/employee/update
的每个PUT
请求执行updateEmployee
处理程序。 -
然后,我们定义了一个
updateEmployee
处理程序,它基本上解码了作为PUT
请求的一部分传递的员工数据,使用 Go 的内置encoding/json
包的NewDecoder
处理程序迭代员工数组以了解员工 ID 请求是否存在于员工的初始静态数组中,我们也可以称之为 UPDATE 或 UPSERT 场景,执行所需的操作,并将响应写入 HTTP 响应流。
创建您的第一个 HTTP DELETE 方法
每当我们想要删除不再需要的记录时,我们就会使用 HTTP DELETE
方法的实现,我们将在本节中介绍。
工作原理…
- 使用
go get
命令安装github.com/gorilla/mux
包,如下所示:
$ go get github.com/gorilla/mux
- 创建
http-rest-delete.go
,在其中我们将定义一个支持 HTTPDELETE
方法的路由和一个处理程序,该处理程序从员工的静态数组中删除提供的 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
}
}
- 使用以下命令运行程序:
$ 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
这可以在以下截图中看到:
让我们了解我们在这个示例中引入的更改:
-
首先,我们添加了另一个名为
deleteEmployee
的路由,它为 URL 模式/employee/delete
的每个DELETE
请求执行deleteEmployee
处理程序。 -
然后,我们定义了一个
deleteEmployee
处理程序,基本上是使用 Go 内置的encoding/json
包的NewDecoder
处理程序解码作为DELETE
请求的一部分传入的员工数据,使用GetIndex
辅助函数获取请求的员工的索引,删除员工,并将更新后的数组以 JSON 格式写入 HTTP 响应流。
对 REST API 进行版本控制
当您创建一个 RESTful API 来为内部客户端提供服务时,您可能不必担心对 API 进行版本控制。更进一步,如果您可以控制访问您的 API 的所有客户端,情况可能是一样的。
然而,在您有一个公共 API 或者您无法控制每个使用它的客户端的 API 的情况下,可能需要对 API 进行版本控制,因为业务需要不断发展,我们将在这个示例中进行介绍。
如何做...
- 使用
go get
命令安装github.com/gorilla/mux
包,如下所示:
$ go get github.com/gorilla/mux
- 创建
http-rest-versioning.go
,在其中我们将定义支持 HTTPGET
方法的相同 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
}
}
- 使用以下命令运行程序:
$ go run http-rest-versioning.go
它是如何工作的...
一旦我们运行程序,HTTP 服务器将在本地监听端口8080
。
接下来,从命令行执行带有路径前缀为/v1
的GET
请求,将给您一个员工列表:
$ 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"}]
让我们了解我们在这个示例中引入的更改:
-
首先,我们定义了一个名为
getEmployees
的单一路由,它为 URL 模式/employees
的每个GET
请求执行getEmployees
处理程序。 -
然后,我们创建了三个数组,分别是
employees
,employeesV1
和employeesV2
,它们作为对 URL 模式/employees
,/v1/employees
和/v2/employees
的 HTTPGET
调用的响应返回。 -
接下来,我们定义了一个
getEmployees
处理程序,在其中我们检查 URL 路径中的前缀,并根据其执行操作。 -
然后,我们定义了一个
AddRoutes
辅助函数,它遍历我们定义的路由数组,将其添加到gorilla/mux
路由器中,并返回Router
对象。 -
最后,我们定义了
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"}]
如何做…
- 使用
go get
命令安装github.com/gorilla/mux
和gopkg.in/resty.v1
包,如下所示:
$ go get github.com/gorilla/mux
$ go get -u gopkg.in/resty.v1
- 创建
http-rest-client.go
,在其中我们将定义调用resty
处理程序的处理程序,如GET
、POST
、PUT
和DELETE
,从 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
}
}
- 使用以下命令运行程序:
$ 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"}]
这将向初始静态列表添加一个员工,并返回更新后的员工列表,如下截图所示:
让我们了解我们编写的程序:
-
使用
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 服务。 -
使用
const WEB_SERVICE_HOST string = "http://localhost:8080"
,我们声明了 RESTful web 服务主机的完整 URL。
根据项目大小,您可以将WEB_SERVICE_HOST
字符串移动到常量文件或属性文件中,以帮助您在运行时覆盖其值。
-
接下来,我们定义了一个
getEmployees
处理程序,在其中我们创建一个新的resty
请求对象调用其R()
处理程序,调用Get
方法,执行 HTTPGET
请求,获取响应,并将其写入 HTTP 响应。 -
类似地,我们定义了另外三个处理程序,用于向 RESTful 服务发送
POST
、PUT
和DELETE
请求,以及一个main()
,在其中我们创建了一个gorilla/mux
路由器实例,并使用getEmployees
处理程序注册了/employees
URL 路径,以及使用addEmployee
、updateEmployee
和deleteEmployee
处理程序分别注册了/employee/add
、/employee/update
和/employee/delete
。
创建您的第一个 AngularJS 客户端
AngularJS 是一个开源的 JavaScript Model-View-Whatever(MVW)框架,它让我们能够构建结构良好、易于测试和易于维护的基于浏览器的应用程序。
在这个食谱中,我们将学习创建一个 AngularJS 与 TypeScript 2 客户端,向本地运行的 HTTP 服务器发送POST
请求。
准备就绪…
由于我们已经在之前的食谱中创建了一个接受GET
和POST
请求的 HTTP 服务器,我们将使用相同的代码库作为我们的 HTTP 服务器。
此外,此处的食谱假设您的机器上已安装了 Angular2 CLI。如果没有,请执行以下命令进行安装:
$ npm install -g @angular/cli
参见创建您的第一个 HTTP POST 方法食谱。
如何做…
- 通过执行以下命令创建一个新项目和骨架应用程序:
$ ng new angularjs-client
- 移动到
angularjs-client
目录,并通过执行以下命令创建server.go
:
$ cd angularjs-client && touch server.go
- 将以下代码复制到
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
}
}
- 移动到
angularjs-client
目录,并通过执行以下命令创建models/employee.ts
和service/employee.service.ts
:
$ cd src/app/ && mkdir models && mkdir services && cd models && touch employee.ts && cd ../services && touch employee.service.ts
- 将以下代码复制到
angularjs-client/src/app/models/employee.ts
中:
export class Employee
{
constructor
(
public id: string,
public firstName: string,
public lastName: string
) {}
}
- 将以下代码复制到
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);
}
}
- 用以下内容替换
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>
- 用以下内容替换
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;
}
}
- 用以下内容替换
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 { }
一切就绪后,目录结构应如下所示:
- 移动到
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
请求。
准备就绪…
由于我们已经在之前的教程中创建了一个接受GET
和POST
HTTP 请求的 HTTP 服务器,我们将使用相同的代码库作为我们的 HTTP 服务器。
此外,本教程假设您已在您的机器上安装了npm
,并且对npm
和webpack
有基本的了解,它是一个 JavaScript 模块打包工具。
参见创建你的第一个 HTTP POST 方法教程。
如何做…
- 创建一个
reactjs-client
目录,我们将在其中保存所有我们的 ReactJS 源文件和一个 HTTP 服务器,如下所示:
$ mkdir reactjs-client && cd reactjs-client && touch server.go
- 将以下代码复制到
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
}
}
- 创建另一个名为
assets
的目录,其中将保存所有我们的前端代码文件,如.html
、.js
、.css
和images
,如下所示:
$ mkdir assets && cd assets && touch index.html
- 将以下内容复制到
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>
- 移动到
reactjs-client
目录并执行npm init
来创建package.json
,在其中我们指定构建我们的 react 客户端所需的所有依赖项,如React
、React DOM
、Webpack
、Babel Loader
、Babel Core
、Babel Preset: ES2015
和Babel 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"
}
}
- 创建
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']
}
}
]
}
};
- 通过执行以下命令为
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')
)
- 通过执行以下命令定义
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 = ''
}
}
一切就绪后,目录结构应如下所示:
目录结构
- 移动到
reactjs-client
目录并执行以下命令来安装node modules
和构建webpack
:
$ npm install
$ npm run build
- 使用以下命令运行程序:
$ 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
请求来添加一个员工。
准备好…
由于我们已经在之前的教程中创建了一个接受GET
和POST
请求的 HTTP 服务器,我们将使用相同的代码库作为我们的 HTTP 服务器。
参见创建你的第一个 HTTP POST 方法教程。
如何做…
- 创建一个
vuejs-client
目录,我们将在其中保存所有 VueJS 源文件和一个 HTTP 服务器,如下所示:
$ mkdir vuejs-client && cd vuejs-client && touch server.go
- 将以下代码复制到
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
}
}
- 创建另一个名为
assets
的目录,其中将保存所有我们的前端代码文件,如.html
、.js
、.css
和images
,如下所示:
$ mkdir assets && cd assets && touch index.html && touch main.js
- 将以下内容复制到
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>
- 将以下内容复制到
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);
}
);
}
}
});
一切就绪后,目录结构应该如下所示:
目录结构
- 用以下命令运行程序:
$ 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 中删除您的第一个文档
介绍
每当我们想要持久保存数据时,我们总是期待将其保存在数据库中,主要分为两类——SQL和NoSQL。每个类别下都有许多可以根据业务用例使用的数据库,因为每个数据库都具有不同的特性并且服务于不同的目的。
在本章中,我们将把 Go Web 应用程序与最著名的开源数据库——MySQL和MongoDB集成,并学习在它们上执行 CRUD 操作。由于我们将使用 MySQL 和 MongoDB,我假设这两个数据库都已安装并在您的本地机器上运行。
集成 MySQL 和 Go
假设您是一名开发人员,并且希望将应用程序数据保存在 MySQL 数据库中。作为第一步,您必须在应用程序和 MySQL 之间建立连接,我们将在本示例中介绍。
准备就绪...
通过执行以下命令验证本地端口3306
上是否安装并运行了 MySQL:
$ ps -ef | grep 3306
这应该返回以下响应:
还要登录到 MySQL 数据库并创建一个 mydb 数据库,执行如下截图中显示的命令:
如何做...
- 使用
go get
命令安装github.com/go-sql-driver/mysql
包,如下所示:
$ go get github.com/go-sql-driver/mysql
- 创建
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
}
}
- 使用以下命令运行程序:
$ go run connect-mysql.go
它是如何工作的...
一旦我们运行程序,HTTP 服务器将在本地监听端口8080
。
浏览到http://localhost:8080/
将返回当前数据库名称,如下截图所示:
让我们了解我们编写的程序:
-
使用
import ("database/sql" "fmt" "log" "net/http" _ "github.com/go-sql-driver/mysql")
,我们导入了github.com/go-sql-driver/mysql
以进行副作用或初始化,使用下划线在导入语句前面明确表示。 -
使用
var db *sql.DB
,我们声明了一个私有的DB
实例。
根据项目大小,您可以全局声明一个 DB 实例,使用处理程序将其注入为依赖项,或将连接池指针放入x/net/context
中。
-
接下来,我们定义了一个
init()
函数,在其中我们连接到数据库并将数据库驱动程序名称和数据源传递给它。 -
然后,我们定义了一个
getCurrentDb
处理程序,基本上在数据库上执行选择查询以获取当前数据库名称,遍历记录,将其值复制到变量中,最终将其写入 HTTP 响应流。
在 MySQL 中创建您的第一条记录
在数据库中创建或保存记录需要我们编写 SQL 查询并执行它们,实现对象关系映射(ORM),或实现数据映射技术。
在这个示例中,我们将编写一个 SQL 查询,并使用database/sql
包执行它来创建一条记录。为了实现这一点,您还可以使用 Go 中许多第三方库中可用的任何库来实现 ORM,例如https://github.com/jinzhu/gorm
,https://github.com/go-gorp/gorp
和https://github.com/jirfag/go-queryset
。
准备就绪...
由于我们在上一个示例中已经与 MySQL 数据库建立了连接,我们将扩展它以执行 SQL 查询来创建一条记录。
在创建记录之前,我们必须在 MySQL 数据库中创建一个表,我们将通过执行以下截图中显示的命令来完成:
操作步骤…
- 使用
go get
命令安装github.com/go-sql-driver/mysql
和github.com/gorilla/mux
包,如下所示:
$ go get github.com/go-sql-driver/mysql
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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
让我们理解我们编写的程序:
-
使用
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
包。 -
接下来,我们定义了一个
createRecord
处理程序,它从请求中获取姓名,将其分配给本地变量名,准备一个带有姓名占位符的INSERT
语句,该占位符将动态替换为姓名,执行该语句,并最终将最后创建的 ID 写入 HTTP 响应流。
从 MySQL 中读取记录
在上一个示例中,我们在 MySQL 数据库中创建了一个员工记录。现在,在这个示例中,我们将学习如何通过执行 SQL 查询来读取它。
操作步骤…
- 使用
go get
命令安装github.com/go-sql-driver/mysql
和github.com/gorilla/mux
包,如下所示:
$ go get github.com/go-sql-driver/mysql
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ go run read-record-mysql.go
工作原理…
运行程序后,HTTP 服务器将在本地监听端口8080
。
浏览到http://localhost:8080/employees
将列出员工表中的所有记录,如下截图所示:
让我们看一下我们编写的程序:
-
使用
import ("database/sql" "encoding/json" "log" "net/http" _ "github.com/go-sql-driver/mysql" "github.com/gorilla/mux")
,我们导入了一个额外的包encoding/json
,它有助于将 Go 数据结构编组为JSON
。 -
接下来,我们声明了 Go 数据结构
Person
,它具有Id
和Name
字段。
请记住,在类型定义中字段名称应以大写字母开头,否则可能会出现错误。
- 接下来,我们定义了一个
readRecords
处理程序,它查询数据库以获取员工表中的所有记录,遍历记录,将其值复制到结构体中,将所有记录添加到列表中,将对象列表编组为 JSON,并将其写入 HTTP 响应流。
在 MySQL 中更新您的第一个记录
考虑这样一个情景,你在数据库中创建了一个员工的记录,包括姓名、部门、地址等所有细节,一段时间后员工更换了部门。在这种情况下,我们必须在数据库中更新他们的部门,以便他们的详细信息在整个组织中保持同步,这可以通过SQL UPDATE
语句实现,在这个示例中,我们将学习如何在 Go 中实现它。
操作步骤…
- 使用
go get
命令安装github.com/go-sql-driver/mysql
和github.com/gorilla/mux
包,如下所示:
$ go get github.com/go-sql-driver/mysql
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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
让我们看一下我们编写的程序:
-
我们定义了一个
updateRecord
处理程序,它以 URL 路径变量路径中要更新的 ID 和请求变量中的新名称作为输入,准备一个带有名称和 UID 占位符的update
语句,该占位符将动态替换,执行该语句,获取执行结果中更新的行数,并将其写入 HTTP 响应流。 -
接下来,我们注册了一个
updateRecord
处理程序,用于处理gorilla/mux
路由器中/employee/update/{id}
的 URL 模式的每个PUT
请求,并在从main()
函数返回时使用defer db.Close()
语句关闭数据库。
从 MySQL 中删除您的第一条记录
考虑这样一个情景,员工已经离开组织,您想要从数据库中撤销他们的详细信息。在这种情况下,我们可以使用SQL DELETE
语句,我们将在本教程中介绍。
如何做到这一点...
- 使用
go get
命令安装github.com/go-sql-driver/mysql
和github.com/gorilla/mux
包,如下所示:
$ go get github.com/go-sql-driver/mysql
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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
让我们看一下我们编写的程序:
-
我们定义了一个
deleteRecord
处理程序,它以请求变量中要从数据库中删除的名称作为输入,准备一个带有名称占位符的DELETE
语句,该占位符将动态替换,执行该语句,获取执行结果中删除的行数,并将其写入 HTTP 响应流。 -
接下来,我们注册了一个
deleteRecord
处理程序,用于处理gorilla/mux
路由器中/employee/delete
的 URL 模式的每个DELETE
请求,并在从main()
函数返回时使用defer db.Close()
语句关闭数据库。
集成 MongoDB 和 Go
每当您想要在 MongoDB 数据库中持久保存数据时,您必须采取的第一步是在数据库和您的 Web 应用程序之间建立连接,在本教程中,我们将使用 Go 中最著名和常用的 MongoDB 驱动程序之一gopkg.in/mgo.v2
。
准备就绪...
通过执行以下命令验证MongoDB
是否安装并在本地端口27017
上运行:
$ mongo
这应该返回以下响应:
如何做到这一点...
- 使用
go get
命令安装gopkg.in/mgo.v
包,如下所示:
$ go get gopkg.in/mgo.v
- 创建
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
}
}
- 使用以下命令运行程序:
$ go run connect-mongodb.go
它是如何工作的...
一旦我们运行程序,HTTP 服务器将在本地监听端口8080
。
浏览到http://localhost:8080/
将列出 MongoDB 集群中存在的所有数据库的名称,并显示如下屏幕截图所示:
让我们看一下我们编写的程序:
- 使用`import("fmt" "log" "net/http" "strings" mgo
"gopkg.in/mgo.v2"),我们导入了
gopkg.in/mgo.v2并使用
mgo`作为包别名。
-
使用
var session *mgo.Session
,我们声明了私有的 MongoDBSession
实例,它作为与数据库的通信会话。 -
使用
var connectionError error
,我们声明了一个私有的error
对象。 -
接下来,我们定义了
init()
函数,在这里我们连接到 MongoDB,传递主机为127.0.0.1
,这意味着 MongoDB 和应用程序都在同一台机器上的端口27017
上运行,可选择将会话切换到单调行为,以便在同一会话中的顺序查询中读取的数据将是一致的,并且在会话中进行的修改将在随后的查询中被观察到。
如果你的 MongoDB 运行在除27017
之外的端口上,那么你必须传递主机和端口,用冒号分隔,如:mgo.Dial("localhost:27018")
。
- 接下来,我们定义了一个
getDbNames
处理程序,它基本上从 MongoDB 集群中获取所有数据库名称,并将它们作为逗号分隔的字符串写入 HTTP 响应流。
在 MongoDB 中创建你的第一个文档
在这个示例中,我们将学习如何在数据库中创建一个 BSON 文档(JSON 样式文档的二进制编码序列化),使用 Go 的 MongoDB 驱动程序(gopkg.in/mgo.v2)。
如何做...
- 使用以下命令,安装
gopkg.in/mgo.v2
和github.com/gorilla/mux
包:
$ go get gopkg.in/mgo.v2
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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
让我们来看一下我们编写的程序:
-
使用
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 驱动程序。 -
接下来,我们定义了一个
createDocument
处理程序,它从 HTTP 请求中获取员工的姓名和 ID。因为请求变量的类型是string
,我们将string
类型的变量 ID 转换为int
类型。然后,我们从 MongoDB 获取员工集合,并调用collection.Insert
处理程序将Employee
结构类型的实例保存到数据库中。
从 MongoDB 中读取文档
在上一个示例中,我们在 MongoDB 中创建了一个 BSON 文档。现在,在这个示例中,我们将学习如何使用gopkg.in/mgo.v2/bson
包来读取它,该包有助于查询 MongoDB 集合。
如何做...
- 使用以下命令,安装
gopkg.in/mgo.v2
、gopkg.in/mgo.v2/bson
和github.com/gorilla/mux
包:
$ go get gopkg.in/mgo.v2
$ go get gopkg.in/mgo.v2/bson
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ go run read-record-mongodb.go
它是如何工作的...
一旦我们运行程序,HTTP 服务器将在本地监听端口8080
。
接下来,浏览到http://localhost:8080/employees
将会给你 MongoDB 员工集合中所有员工的列表:
让我们来看一下我们在程序中引入的更改:
-
使用
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
。 -
接下来,我们定义了一个
readDocuments
处理程序,在这里我们首先从 MongoDB 获取员工集合,查询其中的所有文档,遍历文档将其映射到Employee
结构的数组中,最后将其编组为JSON
。
在 MongoDB 中更新您的第一个文档
一旦创建了一个 BSON 文档,我们可能需要更新其中的一些字段。在这种情况下,我们必须在 MongoDB 集合上执行update/upsert
查询,这将在本教程中介绍。
如何做…
- 使用
go get
命令安装gopkg.in/mgo.v2
、gopkg.in/mgo.v2/bson
和github.com/gorilla/mux
包,如下所示:
$ go get gopkg.in/mgo.v2
$ go get gopkg.in/mgo.v2/bson
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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
让我们来看一下我们写的程序:
-
我们定义了一个
updateDocument
处理程序,它从 URL 路径变量中获取要在 MongoDB 中更新的 ID 和作为 HTTP 请求变量的新名称。由于请求变量是字符串类型,我们将string
类型的变量 ID 转换为int
类型。然后,我们从 MongoDB 获取员工集合,并调用collection.Upsert
处理程序,以插入(如果不存在)或更新具有新名称的员工文档的 ID。 -
接下来,我们注册了一个
updateDocument
处理程序,用于处理/employee/update/{id}
的 URL 模式,对于每个使用gorilla/mux
路由器的PUT
请求,并在我们从main()
函数返回时使用defer session.Close()
语句关闭 MongoDB 会话。
从 MongoDB 中删除您的第一个文档
每当我们想要清理数据库或删除不再需要的文档时,我们可以使用 Go 的 MongoDB 驱动程序(gopkg.in/mgo.v2)轻松地删除它们,这将在本教程中介绍。
如何做…
- 使用
go get
命令安装gopkg.in/mgo.v2
、gopkg.in/mgo.v2/bson
和github.com/gorilla/mux
包,如下所示:
$ go get gopkg.in/mgo.v2
$ go get gopkg.in/mgo.v2/bson
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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
让我们来看一下我们写的程序:
-
我们定义了一个
deleteDocument
处理程序,它从 MongoDB 获取要删除的名称作为请求变量,从 MongoDB 获取员工集合,并调用collection.Remove
处理程序来删除给定名称的文档。 -
然后,我们注册了一个
deleteDocument
处理程序,用于处理/employee/delete
的 URL 模式,对于每个使用gorilla/mux
路由器的DELETE
请求,并在我们从main()
函数返回时使用defer session.Close()
语句关闭 MongoDB 会话。
第六章:使用 Micro 编写 Go 中的微服务-微服务工具包
在本章中,我们将涵盖以下内容:
-
创建您的第一个协议缓冲
-
启动微服务发现客户端
-
创建您的第一个微服务
-
创建您的第二个微服务
-
创建您的微服务 API
-
使用命令行界面和 Web UI 与微服务进行交互
介绍
随着组织现在转向 DevOps,微服务也开始变得流行起来。由于这些服务具有独立的性质,并且可以用任何语言开发,这使得组织能够专注于它们的开发。通过掌握本章涵盖的概念,我们将能够以相当简单的方式使用 Go Micro 编写微服务。
在本章中,我们将首先编写协议缓冲。然后我们将学习如何启动 Consul,这是一个微服务发现客户端,最终转向创建微服务并通过命令行和 Web 仪表板与它们进行交互。
创建您的第一个协议缓冲
协议缓冲是 Go 支持的一种灵活、高效和自动化的编码和序列化结构化数据的机制。在本教程中,我们将学习如何编写我们的第一个协议缓冲。
准备就绪…
- 验证是否通过执行以下命令安装了
protoc
:
$ protoc --version
libprotoc 3.3.2
- 通过以下方式安装
protobuf
:
$ git clone https://github.com/google/protobuf
$ cd protobuf
$ ./autogen.sh
$ ./configure
$ make
$ make check
$ make install
如何做…
- 在
proto
目录中创建hello.proto
并定义一个名为Say
的service
接口,其中包含两种数据类型-Request
和Response
,如下所示:
syntax = "proto3";
service Say
{
rpc Hello(Request) returns (Response) {}
}
message Request
{
string name = 1;
}
message Response
{
string msg = 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
并返回Response
的Hello
方法。 -
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/kit
和https://github.com/grpc/grpc-go
,它们具有相同的目的。
准备就绪…
- 通过执行以下命令启动
consul agent
:
$ consul agent -dev
- 通过执行以下命令安装和运行
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
如何做…
-
通过执行命令
$ mkdir services && cd services && touch first-greeting-service.go
在services
目录中创建first-greeting-service.go
。 -
将以下内容复制到
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
}
}
一切就绪后,目录结构应如下所示:
- 转到
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
的副本,除了在控制台上打印的日志消息之外,它演示了两个具有相同名称的服务的客户端负载平衡的概念。
如何做…
-
通过执行命令
$ cd services && touch second-greeting-service.go
在services
目录中创建second-greeting-service.go
。 -
将以下内容复制到
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
}
}
一切就绪后,目录结构应如下所示:
- 转到
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 agent
、micro API
、first-greeting-service.go
和 second-greeting-service.go
:
$ consul agent -dev
$ micro api
$ go run first-greeting-service.go
$ go run second-greeting-service.go
操作步骤…
-
通过执行命令
$ mkdir api && cd api && touch greeting-api.go
在api
目录中创建greeting-api.go
。 -
将以下内容复制到
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
}
}
一切就绪后,目录结构应该如下所示:
- 转到
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
,这将在本示例中介绍。
操作步骤…
- 使用以下命令安装
go get github.com/micro/micro
包:
$ go get github.com/micro/micro
- 使用以下命令运行 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
上,允许连接的客户端彼此发送消息。
如何做…
- 使用
go get
命令安装github.com/gorilla/websocket
包,如下所示:
$ go get github.com/gorilla/websocket
- 创建
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)
}
}
}
}
- 使用以下命令运行程序:
$ go run websocket-server.go
工作原理…
一旦我们运行程序,WebSocket 服务器将在本地监听端口8080
。
让我们了解我们编写的程序:
-
我们使用了
import ("log" "net/http" "github.com/gorilla/websocket")
,这是一个预处理命令,告诉 Go 编译器包括所有来自log
、net/http
和github.com/gorilla/websocket
包的文件。 -
使用
var clients = make(map[*websocket.Conn]bool)
,我们创建了一个表示连接到 WebSocket 服务器的客户端的映射,KeyType 为 WebSocket 连接对象,ValueType 为布尔值。 -
使用
var broadcast = make(chan Message)
,我们创建了一个通道,所有接收到的消息都会被写入其中。 -
接下来,我们定义了一个
HandleClients
处理程序,当收到HTTP GET
请求时,将其升级为WebSocket
,将客户端注册到套接字服务器,读取请求的 JSON 消息,并将其写入广播通道。 -
然后,我们定义了一个 Go 函数
broadcastMessagesToClients
,它抓取写入广播通道的消息,并将其发送给当前连接到 WebSocket 服务器的每个客户端。
创建你的第一个 WebSocket 客户端
在这个示例中,我们将创建一个简单的客户端来开始 WebSocket 握手过程。客户端将向 WebSocket 服务器发送一个相当标准的HTTP GET
请求,服务器通过响应中的 Upgrade 头将其升级。
如何做…
- 创建
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>
一切就绪后,目录结构应该如下所示:
- 使用以下命令运行程序:
$ go run websocket-server.go
工作原理…
一旦我们运行程序,WebSocket 服务器将在本地监听端口8080
。
浏览到http://localhost:8080
将显示带有文本框和发送按钮的 WebSocket 客户端页面,如下截图所示:
调试你的第一个本地 WebSocket 服务器
调试 Web 应用程序是开发人员学习的最重要的技能之一,因为它有助于识别问题、隔离问题的来源,然后要么纠正问题,要么确定解决问题的方法。在这个示例中,我们将学习如何使用 GoLand IDE 调试在本地运行的 WebSocket 服务器。
准备...
本示例假定您已经安装并配置了 GoLand IDE 以在您的机器上运行 Go 应用程序。
如何做...
- 单击 GoLand IDE 中的 Open Project 以打开我们在以前的示例中编写的
websocket-server.go
,如下截图所示:
- 一旦项目打开,单击 Edit Configurations,如下截图所示:
- 通过单击+号显示如下截图所示的 Add New Configuration 来选择 Add New Configuration:
- 选择 Go Build,将配置重命名为
WebSocket Local Debug
,将运行类型更改为目录,然后单击应用和确定,如下截图所示:
- 放置一些断点并单击调试按钮:
它是如何工作的...
一旦我们运行程序,WebSocket 服务器将在本地以调试模式启动,监听端口8080
。
浏览到http://localhost:8080
将显示带有文本框和发送按钮的 WebSocket 客户端页面,如下截图所示:
输入文本并单击发送按钮,以查看程序执行停在我们在 GoLand IDE 中放置的断点处,如下所示:
调试您的第一个远程 WebSocket 服务器
在以前的示例中,我们学习了如何调试在本地运行的 WebSocket 服务器。在这个示例中,我们将学习如何在另一台或远程机器上调试它。
这些步骤与我们在以前的示例中所采取的步骤基本相同,只是在调试配置部分,我们将把本地主机更改为远程机器 IP 或 DNS,并启动 Delve 服务器,这是 Go 编程语言在远程机器上的调试器。
如何做...
- 通过单击 Edit Configurations...添加另一个配置,如下截图所示:
- 单击+号添加新配置,然后选择 Go Remote:
- 将调试配置重命名为
WebSocket Remote Debug
,将主机更改为remote-machine-IP
或DNS
,然后单击应用和确定,如下截图所示:
- 通过执行以下命令在目标或远程机器上运行无头 Delve 服务器:
dlv debug --headless --listen=:2345 --api-version=2
上述命令将启动一个监听端口2345
的 API 服务器。
- 选择 WebSocket Remote Debug 配置,然后单击调试按钮:
它是如何工作的...
浏览到远程可用的 WebSocket 客户端页面,输入一些文本,然后单击发送按钮,以查看程序执行停在我们放置的断点处:
单元测试您的第一个 WebSocket 服务器
单元测试或测试驱动开发有助于开发人员设计松散耦合的代码,重点放在代码的可重用性上。它还帮助我们意识到何时停止编码并快速进行更改。
在这个示例中,我们将学习如何为我们在以前的示例中已经编写的 WebSocket 服务器编写单元测试。
参见创建您的第一个 WebSocket 服务器示例。
如何做...
- 使用
go get
命令安装github.com/gorilla/websocket
和github.com/stretchr/testify/assert
包,如下所示:
$ go get github.com/gorilla/websocket
$ go get github.com/stretchr/testify/assert
- 创建
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
的工具轻松实现这一点,我们将在这个示例中介绍。
如何做…
- 使用
go get
命令安装github.com/beego/bee
包,如下所示:
$ go get github.com/beego/bee
- 打开终端到你的
$GOPATH/src
目录,并使用bee new
命令创建一个项目,如下所示:
$ cd $GOPATH/src
$ bee new my-first-beego-project
一旦命令成功执行,它将创建一个新的 Beego 项目,并在控制台上的创建步骤将如下屏幕截图所示:
- 转到新创建的项目路径,输入
bee run
编译和运行项目,如下所示:
$ cd $GOPATH/src/my-first-beego-project
$ bee run
一旦命令成功执行,bee
将构建项目并启动应用程序,如下面的屏幕截图所示:
它是如何工作的…
一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080
上运行,并浏览http://localhost:8080/
将呈现应用程序的欢迎页面,如下面的屏幕截图所示:
创建你的第一个控制器和路由器
Web 应用程序的一个主要组件是控制器,它充当视图和模型之间的协调者,并处理用户的请求,这可能是按钮点击、菜单选择或 HTTP GET
和POST
请求。在这个示例中,我们将学习如何在 Beego 中创建一个控制器。
如何做…
- 转到
$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()
}
- 转到
$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")
}
- 使用以下命令运行项目:
$ 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 中,以大写字母开头的函数或处理程序是导出函数,这意味着它们是公共的,并且可以在程序外部使用。这就是我们在程序中定义所有函数时都使用大写字母而不是驼峰命名法的原因。
创建你的第一个视图
视图是模型的可视表示。它通过模型访问数据,并指定数据应该如何呈现。当模型发生变化时,它保持其呈现的一致性,这可以通过推模型或拉模型来实现。在推模型中,视图向模型注册自己以获取更改通知,而在拉模型中,视图负责在需要检索最新数据时调用模型。在本示例中,我们将学习如何创建我们的第一个视图来呈现员工列表。
如何做…
- 移动到
$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>
- 移动到
$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"
}
- 移动到
$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")
}
- 使用以下命令运行项目:
$ bee run
它是如何工作的…
一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080
上运行。
浏览http://localhost:8080/dashboard
将呈现员工仪表板,如下截图所示:
创建你的第一个会话变量
每当我们需要将用户数据从一个 HTTP 请求传递到另一个 HTTP 请求时,我们可以使用 HTTP 会话,我们将在本示例中介绍。
准备好…
此示例假定您已经在本地端口6379
上安装并运行了Redis
。
如何做…
- 使用
go get
命令安装github.com/astaxie/beego/session/redis
包,如下所示:
$ go get -u github.com/astaxie/beego/session/redis
- 移动到
$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.")
}
- 移动到
$GOPATH/src/my-first-beego-project/routers
并编辑router.go
,添加GET
映射/home
,/login
和/logout
,分别由FirstController
中定义的Home
,Login
和Logout
处理程序处理,如下所示:
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")
}
- 移动到
$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()
}
- 在
$GOPATH/src/my-first-beego-project/conf/app.conf
中打开session
的使用,如下所示:
SessionOn = true
SessionProvider = "redis"
SessionProviderConfig = "127.0.0.1:6379"
- 使用以下命令运行程序:
$ 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
创建你的第一个过滤器
有时,我们可能希望在调用操作方法之前或之后执行逻辑。在这种情况下,我们使用过滤器,我们将在本示例中介绍。
过滤器基本上是封装常见功能或横切关注点的处理程序。我们只需定义它们一次,然后将它们应用于不同的控制器和操作方法。
操作步骤…
- 使用
go get
命令安装github.com/astaxie/beego/context
包,如下所示:
$ go get github.com/astaxie/beego/context
- 移动到
$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))
}
- 移动到
$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)
}
- 使用以下命令运行程序:
$ 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.BeforeStatic
,beego.BeforeExec
,beego.AfterExec
和beego.FinishRouter
。
在 Beego 中处理 HTTP 错误
错误处理是 Web 应用程序设计中最重要的方面之一,因为它在两个方面有所帮助。首先,它以相对友好的方式让应用程序用户知道出了问题,他们应该联系技术支持部门或者应该通知技术支持部门的人员。其次,它允许程序员添加一些细节来帮助调试问题。在本示例中,我们将学习如何在 Beego 中实现错误处理。
操作步骤…
- 移动到
$GOPATH/src/my-first-beego-project/controllers
并创建errorcontroller.go
,在其中我们将定义处理404
和500
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"
}
- 移动到
$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"
}
}
- 移动到
$GOPATH/src/my-first-beego-project/views
并创建genericerror.tpl
,内容如下:
<!DOCTYPE html>
<html>
<body>
{{.content}}
</body>
</html>
- 使用以下命令运行程序:
$ 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 支持四种缓存提供程序:file
,Memcache
,memory
和Redis
。在本示例中,我们将使用框架默认的memory
缓存提供程序。
操作步骤…
- 使用
go get
命令安装github.com/astaxie/beego/cache
包,如下所示:
$ go get github.com/astaxie/beego/cache
- 移动到
$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))
}
- 移动到
$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")
}
- 使用以下命令运行程序:
$ bee run
它是如何工作的…
一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080
上运行。
在应用程序启动时,将使用名称为foo
且值为bar
的键添加到缓存中。接下来,浏览http://localhost:8080/getFromCache
将从缓存中读取foo
键值,将其附加到 Hello,并在浏览器上显示,如下面的屏幕截图所示:
监控 Beego 应用程序
一旦 Beego 应用程序启动并运行,我们可以轻松地通过其管理仪表板监视应用程序请求统计信息、性能、健康检查、任务和配置状态。我们将在本教程中学习如何做到这一点。
如何做到这一点…
- 通过在
$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
- 使用以下命令运行程序:
$ bee run
它是如何工作的…
一旦命令成功执行,Web 应用程序将在默认的 Beego 端口8080
上运行,并且浏览http://localhost:8088/
将呈现管理仪表板,如下面的屏幕截图所示:
浏览http://localhost:8088/qps
将显示应用程序的请求统计信息,如下面的屏幕截图所示:
在本地机器上部署 Beego 应用程序
一旦应用程序开发结束,我们必须部署它以供最终用户使用,这可以在本地或远程进行。在本教程中,我们将学习如何在本地机器上部署我们的 Beego 应用程序。
如何做到这一点…
- 因为
bee
创建的应用程序默认处于开发模式,并且在公共服务器上运行应用程序时,始终以生产模式运行应用程序是最佳实践,因此我们必须在$GOPATH/src/my-first-beego-project/conf/app.conf
中将RunMode
更改为prod
,如下所示:
beego.RunMode = "prod"
- 通过执行以下命令将静态文件、配置文件和模板作为 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
- 移动到
$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
。
如何做到这一点…
- 打开
/Users/ArpitAggarwal/nginx/conf/nginx.conf
中的 Nginx 配置文件,并将server
下的location
块替换为以下内容:
location /
{
# root html;
# index index.html index.htm;
proxy_pass http://localhost:8080/;
}
- 通过执行以下命令启动 Nginx:
$ cd /Users/ArpitAggarwal/nginx/sbin
$ ./nginx
- 通过执行以下命令运行 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
中的后续声明修改了父镜像的内容。
准备就绪…
通过执行以下命令验证Docker
和Docker Machine
是否已安装:
$ docker --version
Docker version 18.03.0-ce, build 0520e24 $ docker-machine --version
docker-machine version 0.14.0, build 89b8332
操作步骤如下…
- 创建
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
}
}
- 创建一个
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
一切就绪后,目录结构应该如下所示:
- 使用
-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 $GOBIN
:WORKDIR
指令为我们的镜像设置了任何RUN
、CMD
、ENTRYPOINT
、COPY
和ADD
语句的工作目录,对于我们的镜像来说,这个目录是/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 8080
:EXPOSE
指令通知 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 镜像教程.
如何做…
-
在 Docker Hub(
https://hub.docker.com/
)上创建您的帐户。 -
通过执行
docker login
命令从命令行登录到 Docker Hub,如下所示:
$ docker login --username arpitaggarwal --password XXXXX
Login Succeeded
- 为
golang-image
打标签:
$ docker tag golang-image arpitaggarwal/golang-image
- 通过执行
docker images
命令验证镜像是否已成功标记:
$ docker images
执行上述命令将列出所有 Docker 镜像,如下面的屏幕截图所示:
- 通过执行
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-network
。docker 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 容器。
如何做…
- 创建
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, ¤t_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
}
}
- 创建一个
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
一切就绪后,目录结构应如下所示:
- 从
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-network
。docker run
命令中指定的-p
标志将容器的8080
端口发布到主机的8080
端口。docker run
命令中指定的-d
标志以守护进程模式启动容器,末尾的哈希字符串表示web-application-container
的 ID。
工作原理…
通过执行以下命令验证 Docker 容器是否已成功创建并正在运行:
$ docker ps
这将呈现以下输出:
浏览http://localhost:8090/
将会给我们返回机器 IP、主机名、端口和当前数据库详情:
此外,再次检查my-bridge-network
将显示mysql-container
和web-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
如何做…
- 使用
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"
这将产生以下输出:
- 通过执行以下命令生成证书并用刚创建的私钥签名:
$ 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.key
、domain.csr
和domain.crt
,其中domain.key
是用于签署 SSL 证书的 2,048 位 RSA 私钥,而domain.crt
和domain.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 中实现这一点。
如何做…
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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
的值为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!") }
:这是一个 Go 函数,它以ResponseWriter
和Request
作为输入参数,并在 HTTP 响应流上写入Hello World!
。
接下来,我们声明了main()
,程序从这里开始执行。由于这个方法做了很多事情,让我们逐行理解它:
-
http.HandleFunc("/", helloWorld)
: 在这里,我们使用net/http
包的HandleFunc
将helloWorld
函数注册到 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 路由,我们将在本教程中介绍。
如何做…
- 使用
go get
命令安装github.com/gorilla/mux
和github.com/gorilla/handlers
包,如下所示:
$ go get github.com/gorilla/mux
$ go get github.com/gorilla/handlers
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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
,处理程序为 GorillaLoggingHandler
,它以 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-jose
和https://github.com/tarent/loginsrv
。
如何做…
- 使用
go get
命令安装github.com/dgrijalva/jwt-go
、github.com/gorilla/mux
和github.com/gorilla/handlers
包,如下所示:
$ go get github.com/dgrijalva/jwt-go
$ go get github.com/gorilla/handlers
$ go get github.com/gorilla/mux
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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 保护我们的端点,我们将在本教程中介绍。
如何做…
- 使用
go get
命令安装github.com/auth0/go-jwt-middleware
、github.com/dgrijalva/jwt-go
、github.com/gorilla/mux
和github.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
- 创建
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
}
}
- 使用以下命令运行程序:
$ 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"}]
让我们了解本教程中引入的更改:
-
使用
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。 -
然后,我们构建了一个新的安全实例
jwtmiddleware
,将SigningMethod
设置为HS256
,并将ValidationKeyGetter
选项设置为一个返回用于验证 JWT 的密钥的 Go 函数。在这里,服务器签名被用作验证 JWT 的密钥。 -
最后,我们在
main()
中使用jwtmiddleware
处理程序包装了/employees
路由,这意味着对于每个 URL 模式为/employees
的请求,我们在提供响应之前检查并验证 JWT。
在 Go Web 应用程序中防止跨站点请求伪造
从恶意网站、电子邮件、博客、即时消息或程序攻击受信任的站点,用户当前已经认证,以防止不必要的操作,这是一种常见的做法。我们经常称之为跨站点请求伪造。
在 Go 中实现跨站点请求伪造非常容易,使用 Gorilla CSRF 包,我们将在本教程中介绍。
如何做…
- 使用
go get
命令安装github.com/gorilla/csrf
和github.com/gorilla/mux
包,如下所示:
$ go get github.com/gorilla/csrf
$ go get github.com/gorilla/mux
- 创建
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>
- 创建
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))
}
- 使用以下命令运行程序:
$ 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
的值为localhost
,CONN_PORT
的值为8443
,HTTPS_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/
操作步骤…
- 登录到 AWS,转到 EC2 管理控制台,并在“创建实例”部分点击“启动实例”,如下截图所示:
- 选择 Amazon Linux AMI 2017.09.1(HVM),SSD 卷类型,如下截图所示:
- 选择 t2.micro 实例类型,然后点击“下一步:配置实例详细信息”:
- 在“配置实例详细信息”部分启用“自动分配公共 IP”,如下截图所示:
-
不要对添加存储和添加标签部分进行任何更改。
-
添加 HTTP 和 HTTPS 规则,然后在配置安全组部分点击“Review and Launch”按钮,如下截图所示:
- 从下拉菜单中选择“创建新的密钥对”,为密钥对命名,然后点击“下载密钥对”按钮。保存
my-first-ec2-instance.pem
文件,然后点击“启动实例”,如下截图所示:
工作原理…
点击“启动实例”后,它将在 AWS 上创建并启动一个 Linux 机器,并为实例分配 ID、公共 DNS 和公共 IP,通过这些信息我们可以访问它。
转到 EC2 仪表板的实例部分,您可以看到正在运行的实例,如下截图所示:
与您的第一个 EC2 实例交互
要在 EC2 实例上部署应用程序,我们首先必须登录并安装必要的软件包/软件,这可以通过 SSH 客户端(如 MobaXterm,Putty 等)轻松完成。在本教程中,我们将登录到之前创建的 EC2 实例,并使用 Red Hat 软件包管理器安装 Go。
操作步骤…
- 将私钥文件
my-first-ec2-instance.pem
的权限设置为400
,这意味着用户/所有者可以读取,但不能写入,也不能执行,而组和其他人都不能读取,不能写入,也不能执行,通过执行chmod
命令,如下所示:
$ chmod 400 my-first-ec2-instance.pem
- 获取 EC2 实例的公共 DNS,并使用私钥文件作为
ec2-user
连接到它,如下所示执行ssh
命令:
$ ssh -i my-first-ec2-instance.pem ec2-user@ec2-172-31-34-99.compute-1.amazonaws.com
一旦命令成功执行,我们将登录到 EC2 实例,并且输出将如下所示:
- 通过执行
sudo
命令从ec2-user
切换到root
用户:
[ec2-user@ip-172-31-34-99 ~]$ sudo su
- 使用 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
命令运行它,这将在本教程中介绍。
如何做…
- 创建
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
}
}
一切就绪后,目录结构应如下所示:
- 使用安全拷贝或
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
- 使用私钥文件和公共 DNS 名称登录 EC2 实例,如下所示:
$ ssh -i my-first-ec2-instance.pem ec2-user@ec2-172-31-34-99.compute-1.amazonaws.com
- 在后台运行
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 命令,这将在本教程中介绍。
如何做…
- 通过执行以下命令从
ec2-user
用户切换到root
用户:
[ec2-user@ip-172-31-34-99 ~]$ sudo su
[root@ip-172-31-34-99 ec2-user]#
- 安装
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
- 通过执行以下命令在 EC2 实例上启动
Docker
服务:
[root@ip-172-31-34-99 ec2-user] service docker start
- 将
ec2-user
添加到docker
组,以便您可以在不使用sudo
的情况下执行 Docker 命令,如下所示:
[root@ip-172-31-34-99 ec2-user] usermod -a -G docker ec2-user
- 通过执行以下命令退出 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.
- 通过执行以下命令再次登录以获取新的 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 一起工作。
如何做…
- 使用您的凭据从命令行登录到 Docker Hub,执行以下命令:
$ docker login --username arpitaggarwal --password XXXXX
Login Succeeded
- 执行
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!