黄金法则:你可以在不更改任何其他代码的前提下更改服务并重新部署吗?——Sam Newman,Building Microservices作者
我们构建的每个服务都应该是微服务,正如本书前面提到的,我不赞同使用前缀“micro”。本章将要构建一个服务,不只关注结果,也同样关注过程。
我们会采用API First的方式,在编写代码之前首先设计服务的RESTful接口。在开始编写代码时,首先编写测试,通过编写从失败到通过的小测试逐步建立服务。
本章中构建的示例的功能是实现Go游戏服务器。此服务允许任何类型的客户参加比赛,不论是通过iPhone、浏览器还是其他方式。
首先,这个服务需要一个名字。一个使用Go实现的服务,又是用于进行Go游戏比赛的服务,没有比GoGo这个名字更适合的了。
本章将介绍以下几个方面。
在下一节中,我们将设计微服务。软件开发的过程中有一个经典问题:开发结果很少符合最初的设计。文档、需求和实现之间始终存在差距。
幸运的是,下一节大家将看到一些适用于微服务开发的工具,这些工具可以实现设计文档化,并可以最终集成到开发过程中。
为了创建一个管理match的服务,要做的第一件事是定义match的资源集合。要想使用集合,应该能够创建新的match同时列出当前由服务器管理的所有match,如表5.1所示。
表5.1match API
资源 | 方法 | 描述 |
---|---|---|
/matches | GET | 查询所有可用的match列表 |
/matches | POST | 创建和开始一个新的match |
/matches/{id} | GET | 查询一个match的详情 |
如果开发的是一款需要真实货币购买的Go游戏,而不是一个样品,则还需要实现一些方法用来支持在UI上查询如chains和liberties之类的基本概念,从而确定Go中的合法移动。
设计move API
一旦服务开始管理match,我们便需要提供一个API,它可以让玩家进行移动。需要添加以下HTTP方法到moves子资源中,如表5.2所示。
表5.2 move API
资源 | 方法 | 描述 |
---|---|---|
/matches/{id}/moves | GET | 返回一个按照时间排序的比赛中所有移动的列表 |
/matches/{id}/moves | POST | 进行移动。移动若没有包含位置信息,则视为略过 |
为了简化所做的一切工作,以前我们会尝试抛弃复杂或烦琐的文档。那么我们真的需要分享那些具有苛刻兼容性要求的骇人听闻的文档吗?
对我们来说,Markdown是编写文档和其他文件的首选工具。它是一个简单的纯文本格式,不需要使用IDE或其他臃肿的编辑工具,同时可以被转换成PDF、网页等无数的格式。实际上,关于文档格式的争论已经引发了大规模的“血腥”的办公室战争。
我们通常习惯在服务中使用Markdown文档。这样能够允许其他开发人员快速获得所有服务的REST资源、URI模式和请求/响应有效荷载的列表。就像Go代码一样,我们仍然想要一种简单的方法来记录服务协议,而不是让别人通过路由代码进行查询。
事实证明,有一种Markdown的特殊使用方式可以专门用于记录RESTful API:API Blueprint。大家可以在API Blueprint网站https://apiblueprint.org/上阅读关于此格式的资料。
如果查看本章的GitHub仓库(https://github.com/cloudnativego/gogo-service),可以看到一个名为apiary.apib的文件。此文件包含Markdown格式的内容,表示GoGo服务支持的RESTful接口的文档和规范。
代码清单5.1为Markdown格式的示例,大家可以看到它是如何描述REST资源、HTTP方法和JSON有效数据的。
代码清单5.1 Blueprint Markdown示例
### Start a New Match [POST]
You can create a new match with this action. It takes information about the players
and will set up a new game. The game will start at round 1, and it will be
**black**'s turn to play. Per standard Go rules, **black** plays first.
+ Request (application/json)
{
"gridsize" : 19,
"players" : [
{
"color" : "white",
"name" : "bob"
},
{
"color" : "black",
"name" : "alfred"
}
]
}
+ Response 201 (application/json)
+ Headers
Location: /matches/5a003b78-409e-4452-b456-a6f0dcee05bd
+ Body
{
"id" : "5a003b78-409e-4452-b456-a6f0dcee05bd",
"started_at": "2015-08-05T08:40:51.620Z",
"gridsize" : 19,
"turn" : 0,
"players" : [
{
"color" : "white",
"name" : "bob",
"score" : 10
},
{
"color" : "black",
"name" : "alfred",
"score" : 22
}
]
}
在第1章“云之道”中,我们提到过不要过度依赖工具。
工具应该使生活变得更轻松,但它们永远都不应该成为强制性的。包含服务的文档和规范的API Blueprint Markdown仅仅是一个简单的文本文件,我们可以使用一个工具使生活变得更加轻松。
Apiary是一个网站,它能实现以交互方式设计RESTful API。可以把它想象成支持API Blueprint Markdown语法的WYSIWYG(所见即所得)编辑器,但这只是一个开始。Apiary可以生成返回JSON有效数据的模拟服务器,这样节省了必须自己搭建模拟服务器的时间,并同时允许保持API First方式,直到通过制定API的草稿阶段。
除提供模拟服务器外,还可以在编写服务器代码之前使用生成的多种编程语言的客户端代码进一步协助团队来验证API。
GoGo服务的API Blueprint文档可以在GitHub仓库以及Apiary上进行查看,具体请访问http://docs.gogame.apiary.io/。本书中不会存储整套文档,我们会把大部分细节保留在blueprint文档和Apiary上,供各位读者自行阅读。
本章的目的不是教大家如何编写一个游戏服务器,而是让大家学会如何在Go语言中构建一个服务。因此,相比测试驱动开发和设置服务框架,一些技术细节(如Go语法规则和实际的游戏实现)是相对次要的部分,我们将在后面的章节中介绍。
在理想环境下,可以从一块白板开始直接进入测试。但很少存在理想与完美的世界。在示例中,我们希望能够从RESTful接口开始编写测试。
而现实情况是,我们并不能够真正为RESTful接口编写测试,除非知道将要为每个接口编写什么样的函数。为了解决这个问题,同时配置服务的基本框架,需要创建两个文件。
第一个文件是main.go(见代码清单5.2),其中包含主要功能,创建和运行一个新的服务器。这种情况下需要保持主函数尽可能小,因为主函数通常很难单独测试。
代码清单5.2 main.go
package main
import (
"os"
service "github.com/cloudnativego/gogo-service/service"
)
func main() {
port := os.Getenv("PORT")
if len(port) == 0 {
port = "3000"
}
server := service.NewServer()
server.Run(":" + port)
}
代码清单5.2中的代码调用了一个名为NewServer的函数。此函数返回一个指向Negroni结构体的指针。Negroni是一个第三方库,用于在Go内置的net/ http包上构建路由接口。
注意,粗体的代码行很重要。外部配置对于构建云端应用程序的能力至关重要。允许应用程序从环境变量接受绑定端口,是迈出构建云端运行服务的第一步。一些云提供商也会使用确定的环境变量来自动注入应用程序端口。
代码清单5.2为服务器实现程序。这段代码以经典模式创建和配置Negroni,使用Gorilla Mux作为路由库。我们通常谨慎地对待任何第三方依赖,必须确保其内部的功能不是Go语言的核心库所提供的。
Negroni和Mux可以完美地运行在Gonet/http上,它们是可扩展的中间件,不会干扰将来所做的任何工作。没有事情是强制性的,没有“魔法”,只有一些便利的库使我们不用花费太多时间就可以编写每个服务的模版。
有关Negroni的信息,请查看GitHub repo https://github.com/codegangsta/negroni。有关Gorilla Mux的信息,请查看https://github.com/gorilla/mux上的仓库。请注意,以上是直接导入代码的地址,非常容易追踪第三方包的文档和源码。
代码清单5.3显示了main函数引用的NewServer函数和一些实用函数中的内容。请注意,NewServer由于首字母是大写的,所以是可导出的,而initRoutes和testHandler则不可以。
代码清单5.3 server.go
package service
import (
"net/http"
"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
"github.com/unrolled/render"
)
// NewServer configures and returns a Server.
func NewServer() *negroni.Negroni {
formatter := render.New(render.Options{
IndentJSON: true
})
n := negroni.Classic()
mx := mux.NewRouter()
initRoutes(mx, formatter)
n.UseHandler(mx)
return n
}
func initRoutes(mx *mux.Router, formatter *render.Render) {
mx.HandleFunc("/test", testHandler(formatter)).Methods("GET")
}
func testHandler(formatter *render.Render) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
formatter.JSON(w, http.StatusOK,
struct{ Test string }{"This is a test"})
}
}
在这个框架中,最重要的是理解testHandler函数。与我们一直使用的常规函数不同,此函数返回了一个匿名函数。
此匿名函数反过来返回类型为http.HandlerFunc的函数,其定义如下。
type HandlerFunc func(ResponseWriter, *Request)
这种类型定义实际上允许将具有此签名的任何函数都视为HTTP处理程序。
可以发现此类模式在Go的核心包和许多第三方包中被广泛使用。
在简单框架里,我们通过调用formatter.JSON方法返回一个将匿名结构传递给响应写入器的函数(这就是将formatter传递给wrapper函数的理由)。
这样做很重要的原因是,所有的RESTful接口都是返回http.HandlerFunc类型函数的包装函数。
在编写测试之前,需要确保框架可以正常工作,以便测试我们实现的资源。可以通过以下命令进行构建(过程可能与Windows不同)。
$ go build
执行以上代码将构建文件夹中的所有Go文件。一旦创建了可执行文件,便可以运行GoGo服务了。
$ ./gogo-service
[negroni] listening on :3000
当访问http//localhost:3000/test时,可以在浏览器中获取JSON测试数据。由于使用了Negroni的经典配置,所以我们已经实现了一些HTTP请求处理功能。
[negroni] Started GET /test
[negroni] Completed 200 OK in 212.121µs
框架已经可以正常工作了,Web服务器至少能够处理简单的请求,现在是时候进行一些真正的测试驱动开发了。
只讨论TDD是很容易的。尽管有无数的博客和书籍赞颂其优点,却很少有人能规范地使用它,更少有人能完整使用它。使用不完整的TDD会导致在TDD上花费大量时间和精力,却没有收获代码质量和功能信心的提升。
在本章的这一节中,我们将以test-first方式为服务编写一个方法。正常情况下,我们应该会花费95%的时间编写测试,花费5%的时间编写代码。测试代码应该明显多于被测试的代码,这是因为我们需要更多的代码来覆盖测试函数所有可能的执行路径,而不只是编写测试函数本身。关于此概念的更多细节,请查阅由Jez Humble和David Farley所著的Continuous Delivery一书。
许多组织认为编写测试是在浪费时间,因为它不会增加任何价值,并且实际上会延长发布的时间。这种目光短浅的看法存在很多问题。
TDD确实会在初期降低开发效率,然而当我们理解一个新的关于团队开发的定义后便不会这样想了。
Development(n):应用程序新增功能特性处于没有上线压力的时期。
——Dan Nemeth
考虑以上定义,当审视应用程序的整个生命周期时,会发现应用程序只有很少一部分时期是处在这种“开发”状态下的。
测试的投入将在应用程序的整个生命周期内产生红利,特别是在生产环境中。
为了开始TDD服务的创建之旅,我们首先新建一个名为handlers_test.go的文件(如代码清单5.4所示)。这个文件将测试handlers.go文件中编写的函数。如果文本编辑器支持并行或分屏模式,这将非常有用。
我们将为POST请求匹配的HTTP处理程序编写一个测试。如果回过头查看Apiary文档,会发现此函数在成功时会返回201(已创建)的HTTP状态码。
开始编写测试。我们将调用函数TestCreateMatch,和所有Go的单元测试一样,它将作为一个指向testing.T结构体的指针。
为了测试服务器创建match的功能,需要调用HTTP处理程序。可以通过构建HTTP管道的所有组件(包括请求、响应流和头文件等)来手动调用。幸运的是,Go为我们提供了一个测试HTTP服务器,它并不需要开启套接字,但可以让我们调用HTTP处理程序。
这里需要完成很多工作,以下是在一次迭代中测试文件的完整代码,与TDD思想保持一致,这是一个失败测试。
代码清单5.4 handlers_test.go
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/unrolled/render"
)
var (
formatter = render.New(render.Options{
IndentJSON: true
})
)
func TestCreateMatch(t *testing.T) {
client := &http.Client{}
server := httptest.NewServer(
http.HandlerFunc(createMatchHandler(formatter)))
defer server.Close()
body := []byte("{\n \"gridsize\": 19,\n \"players\": [\n {\n
\"color\": \"white\",\n \"name\": \"bob\"\n },\n {\n
\"color\": \"black\",\n \"name\": \"alfred\"\n }\n ]\n}")
req, err := http.NewRequest("POST",
server.URL, bytes.NewBuffer(body))
if err != nil {
t.Errorf("Error in creating POST request for createMatchHandler: %v", err)
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
t.Errorf("Error in POST to createMatchHandler: %v", err)
}
defer res.Body.Close()
payload, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Errorf("Error reading response body: %v", err)
}
if res.StatusCode != http.StatusCreated {
t.Errorf("Expected response status 201, received %s", res.Status)
}
fmt.Printf("Payload: %s", string(payload))
}
使用Apiary的另一个原因是,如果查看关于create match功能的文档,点击该方法,它会自动生成Go客户端的代码示例。大部分生成的代码都会在代码清单5.3前面部分的测试方法中使用到。
第一步是调用httptest.NewServer,它创建了一个HTTP服务器监听自定义URL,并提供指定的方法。之后,使用Apiary生成的示例客户端代码来调用此方法。
此处为大家提供两个主要断言。
如果尝试运行以上测试,将看到编译失败。这才是真正的TDD,因为我们还没有编写测试方法(createMatchHandler还不存在)。为了使编译通过,可以将之前的测试方法添加到handlers.go文件中,如代码清单5.5所示。
代码清单5.5 handlers.go
package main
import (
"net/http"
"github.com/unrolled/render"
)
func createMatchHandler(formatter *render.Render) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
formatter.JSON(w,
http.StatusOK,
struct{ Test string }{"This is a test"})
}
}
现在尝试编译。首先,可以输入以下命令进行测试。
$ go test -v $(glide novendor)
应该能看到以下输出。
Expected response status 201, received 200 OK
目前,我们写了第一个失败测试!可能仅凭这一点,一些人便会开始怀疑这种方式。请相信我们,本章结束前一定会让大家看到曙光浮现。
为了使测试通过,需要在HTTP处理程序处返回201状态码。不写完整的实现,也不添加复杂的逻辑,唯一要做的就是让测试通过。这个过程至关重要,我们只写让测试通过的最小代码,如果添加了额外代码,就不再遵循Test First模式了。
为了使测试通过,将formatter的所在行改为以下形式。
formatter.JSON(w, http.StatusCreated, struct{ Test string }{"This is a test"})
更改http.StatusCreated的第二个参数。运行测试,可以看到如下所示的输出。
$ go test -v $(glide novendor)
=== RUN TestCreateMatch
--- PASS: TestCreateMatch (0.00s)
PASS
ok github.com/cloudnativego/gogo-service 0.011s
接下来要编写响应create match的请求(如Apiary文档中所述),并在HTTP响应中设置Locationheader。按照惯例,当RESTful服务新建一些资源时,Locationheader应该被设置为新创建资源的URL。
和往常一样,从一个失败测试开始,然后使测试通过。在测试中添加以下断言。
if _, ok := res.Header["Location"]; !ok {
t.Error("Location header is not set")
}
重新运行测试,结果会失败,同时显示以上错误信息。为了使测试通过,在handlers.go中修改createMatchHandler方法,代码如下。
func createMatchHandler(formatter *render.Render) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
w.Header().Add("Location", "some value")
formatter.JSON(w, http.StatusCreated,
struct{ Test string }{"This is atest"})
}
}
请注意,我们没有为Location添加实际值,相反,只是添加了“some value”。接下来,我们将添加一个会导致失败的测试条件。获取一个包含matches资源的有效location header,它有足够的长度,以便知道它还包含新创建match的GUID。修改先前针对location header编写的测试,代码如下所示。
loc, headerOk := res.Header["Location"]
if !headerOk {
t.Error("Location header is not set")
} else {
if !strings.Contains(loc[0], "/matches/") {
t.Errorf("Location header should contain '/matches/'")
}
if len(loc[0]) != len(fakeMatchLocationResult) {
t.Errorf("Location value does not contain guid of new match")
}
}
我们还在测试中声明了一个名为fakeMatchLocationResult的常量,这只是一个字符串,在Apiary中也标明了location header的测试值。下面将使用它来测试断言和模拟值。定义如下。
const(
fakeMatchLocationResult ="/matches/5a003b78-409e-4452-b456-a6f0dcee05bd"
)
由于本书篇幅有限,因此不会提供在测试迭代期间从红灯(失败)到绿灯(通过)更改的所有代码。
我们只会描述为了使TDD通过而进行的工作。
如果想查看操作历史,可以逐行翻阅我们在GitHub中的提交历史,以获取每一行的代码更改。搜索所有被标记为“TDD GoGo service Pass n”的提交,其中n是测试迭代次数。
总结针对每个失败测试所采用的方法,建议大家伴随着好莱坞黑客电影的蒙太奇背景音乐,阅读以下内容。
让我们拭目以待,看看在这组迭代之后会发生什么变化。代码清单5.6显示了使用TDD开发的一个处理程序,迭代测试失败,然后编写代码通过测试。值得一提的是,我们不会编写任何代码,除非它可以使测试通过。这在最大程度上保证了测试和可信度的覆盖。
对于许多开发人员和组织来说,这是艰难的一步,但这是值得的。这种开发方式给很多部署在云端的真实应用程序带来了非常多的帮助。
代码清单5.6 handlers.go (8轮TDD迭代后)
package service
import (
"encoding/json"
"io/ioutil"
"net/http"
"github.com/cloudnativego/gogo-engine"
"github.com/unrolled/render"
)
func createMatchHandler(formatter *render.Render, repo matchRepository)
http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
payload, _ := ioutil.ReadAll(req.Body)
var newMatchRequest newMatchRequest
err := json.Unmarshal(payload, &newMatchRequest)
if err != nil {
formatter.Text(w, http.StatusBadRequest,
"Failed to parse create match request")
return
}
if !newMatchRequest.isValid() {
formatter.Text(w, http.StatusBadRequest,
"Invalid new match request")
return
}
newMatch := gogo.NewMatch(newMatchRequest.GridSize,
newMatchRequest.PlayerBlack, newMatchRequest.PlayerWhite)
repo.addMatch(newMatch)
w.Header().Add("Location", "/matches/"+newMatch.ID)
formatter.JSON(w, http.StatusCreated,
&newMatchResponse{ID: newMatch.ID,
GridSize: newMatch.GridSize,
playerBlack: newMatchRequest.PlayerBlack,
PlayerWhite: newMatchRequest.PlayerWhite})
}
}
虽然Go编码指南建议采用包含8个字符的tab,但实际上进行压缩后可以使内容更加可读。
单个函数中包含了大约20行代码,该函数的两个测试函数中大约有120行代码。这正是我们希望达到的比率。甚至在使用HTTP测试工具测试服务之前,我们就希望拥有100%的信心,并清楚地知道服务应该如何运行。
基于以上编写的测试和代码清单5.6中的代码,大家能发现任何测试缺陷吗?能否找到任何可能绕过代码的场景或边界情况是我们还未在测试里考虑到的?
对此,我们发现了以下两点。
我们会在本书中不断更正一些内容,但是类似防范边界攻击的这类情况,应该是大家在建立企业级服务时考虑的。
现在我们已经使用Go构建了一个微服务,同时遵循云之道,我们可以在云端很好地使用和部署服务。首先要做的是寻找一个云环境。虽然有许多选择,但在本书中,我们选择Cloud Foundry的PCF Dev和Pivotal Web Services(PWS)作为部署对象,因为它们都非常容易上手,并且PWS有一个试用版本,无须绑定信用卡即可使用。
访问http://run.pivotal.io/,使用Pivotal Web Service创建账户。Pivotal Web服务由Cloud Foundry提供平台支持,可以实现在云端部署应用程序,并使用其市场中的一些免费和付费服务。
创建账户并登录后,可以看到所在组织的信息中心。组织是安全和部署的逻辑单元,可以邀请其他人加入自己的组织,以便在云项目中进行协作,或完成其他工作。
在组织的首页或资讯主页上,可以看到一个包含有用信息的方块,其中有指向Cloud Foundry CLI的链接。这是一个命令行接口,可以使用它在任何Cloud Foundry(而不只是PWS)中推送和配置应用程序。
下载并安装CF CLI,运行一些测试命令(如cf apps或cf spaces)以验证其是否已连接并正常工作。请记住,PWS有60天的试用期,在此期间不必绑定信用卡,因此请合理使用。
有关使用CF CLI的详细信息,请参阅http://docs.run.pivotal.io/devguide/cf-cli/中的文档。
如果可以承担风险,或者只是想简单地做一些小改动,那么可以使用PCF Dev。本质上讲,PCF Dev是Cloud Foundry的一个简化版本,为应用程序开发人员提供了将应用程序部署到CF上所需的全部基础功能,但不包含所有生产级别的功能。PCF Dev可以实现将云运行在自己的笔记本中。
PCF Dev利用虚拟化(可以在VMware或VirtualBox之间进行选择)和一个名为vagrant的工具来启动一台独立的虚拟机,该虚拟机将作为PCF Dev和应用程序的托管主机。
可以在本地使用PCF Dev测试应用程序在云端的运行情况,而无须推送到PWS上。我们发现,它对于如服务绑定、自动化集成测试和完全验收测试是非常有帮助的。
在编写本书的时候,PCF Dev仍处于早期阶段,因此现在的安装和配置说明可能会有所改动。
想了解更多关于PCF Dev的信息,请访问https://docs.pivotal.io/pcf-dev/。
PCF Dev的优点是,一旦完成安装,就可以简单地发出启动指令,它会在本地虚拟化环境中提供我们所需的一切。例如在OS X上,可以使用./start-osx脚本开始创建基础环境。
使用相同的Cloud Foundry CLI,可以将它指向MicroPCF环境。
$ cf api api.local.pcfdev.io --skip-ssl-validation
Setting api endpoint to api.local.pcfdev.io...
OK
API endpoint: https://api.local.pcfdev.io (API version: 2.44.0)
Not logged in. Use 'cf login' to log in.
确保按照提示登录(默认用户名和密码为admin和admin),可以使用Cloud Foundry CLI标准命令与本地新创建的私有CF通信。
$ cf apps
Getting apps in org local.pcfdev.io-org / space kev as admin...
OK
现在已经安装了CF CLI,并且可以选择让CLI以PWS云或本地PCF Dev环境为目标,下面便可以将应用程序推送到云端运行了。
虽然可以通过手动方式配置所有选项将应用程序推送到云端,但是采用创建manifest的方式会更简单一些(随后会进行更多配合CD管道的工作),如代码清单5.7所示。
代码清单5.7 manifest.yml
applications:
-path: .
memory: 512MB
instances: 1
name: your-app-name
disk_quota: 1024M
command: your-app-binary-name
buildpack: https://github.com/cloudfoundry/go-buildpack.git
将此manifest文件放在应用程序的主目录中,只需键入以下命令,应用程序便会部署在云端。
$ cf push
正如我们将在本书后面提到的,甚至可以配置Wercker管道,以便在持续交付的构建成功时自动将应用程序部署到我们所选择的Cloud Foundry中。
buildpack旨在将应用程序代码与运行应用程序所需的基础环境相整合。 Java buildpack包含JDK和JRE,Node buildpack包含node等。但是Go buildpack太容易违背“单一不可变部件”原则,可能会有人向buildpack提交一个破坏代码或管道的更改。正如本书后面将讨论的,当我们部署真正的应用程序时,更倾向于将Docker镜像直接从Docker Hub部署到云端。至于选择buildback还是Docker完全取决于个人和公司,这通常是一种个人喜好。
在本章中,我们向大家介绍了关于在Go中构建微服务的基础知识。讨论了一些建立基础路由和处理函数的代码,更重要的是,介绍了如何构建代码的测试。
此外,我们还将代码部署到了云端。本书的其余部分将涉及更多的技术细节,探索更深入的主题,所以希望大家在继续后面的学习之前,花一点时间来回顾本章中难以理解的内容。
现在是进行一些小的调整并创建自己的hello world服务的好时机,可以将它们部署到PWS上,启动、停止扩展应用程序。还可以浏览PWS中的市场,了解部署在其中的一些强大的应用程序,包括数据库、消息队列和监控等。
备注:本文节选自《Cloud Native Go:构建基于Go和React的云原生Web应用与微服务》