第 5 章:在 Go 中构建微服务
黄金法则:你可以在不更改任何其他代码的前提下更改服务并重新部署吗?——Sam Newman,Building Microservices作者
微服务架构强调服务的独立性和可替换性。本章将以 API First 和测试驱动开发为核心,带你实践在 Go 中构建微服务的全过程,并介绍云端部署的最佳实践。
API First 开发理念与实践
在微服务开发中,API First 是确保服务可扩展、易维护的关键。我们将以 GoGo 游戏服务器为例,展示如何从接口设计到代码实现,逐步构建高质量微服务。
设计 Matches API
首先,需要定义 match 资源集合,确保能够创建和查询比赛。下表展示了 Matches API 的主要接口:
资源 | 方法 | 描述 |
---|---|---|
/matches | GET | 查询所有可用的 match 列表 |
/matches | POST | 创建并开始一个新的 match |
/matches/{id} | GET | 查询指定 match 的详情 |
设计 Moves API
为支持比赛过程中的玩家操作,还需设计 Moves API:
资源 | 方法 | 描述 |
---|---|---|
/matches/{id}/moves | GET | 返回比赛中所有移动的时间排序列表 |
/matches/{id}/moves | POST | 玩家进行移动,若无位置信息则为略过 |
API Blueprint 文档与工具
推荐使用 API Blueprint Markdown 格式记录接口规范,便于团队协作与自动化测试。可通过 API Blueprint 官网 了解详细语法。
示例文档片段如下:
### 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 }
]
}
通过 Apiary 可在线测试和发布 API 文档,支持模拟服务器和多语言客户端代码生成。
微服务基础框架搭建
在 Go 中搭建微服务框架,推荐使用 Negroni 和 Gorilla Mux 等中间件库,提升路由和中间件扩展能力。
主程序入口
主函数应保持简洁,负责服务启动和端口绑定:
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)
}
服务框架实现
服务框架通过 NewServer 方法配置路由和中间件:
package service
import (
"net/http"
"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
"github.com/unrolled/render"
)
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"})
}
}
通过访问 /test
路径可验证服务是否正常运行。
测试驱动开发(TDD)实践
TDD 强调先编写测试,再实现功能代码。通过不断迭代测试和实现,确保服务质量和可维护性。
编写第一个失败测试
以创建 match 的 POST 接口为例,先编写测试用例:
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(`{
"gridsize": 19,
"players": [
{"color": "white", "name": "bob"},
{"color": "black", "name": "alfred"}
]
}`)
req, err := http.NewRequest("POST", server.URL, bytes.NewBuffer(body))
if err != nil {
t.Errorf("Error in creating POST request: %v", err)
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
t.Errorf("Error in POST: %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 status 201, got %s", res.Status)
}
fmt.Printf("Payload: %s", string(payload))
}
初始实现返回 200,测试失败。调整处理函数返回 201 状态码即可通过测试。
迭代完善测试与实现
每次新增断言,先让测试失败,再补充最小实现使其通过。例如:
- 检查 Location header 是否设置
- 验证 header 格式和内容
- 校验响应体中的 match ID 与 header GUID 一致
- 检查存储库是否保存新 match
- 验证玩家信息和 match 大小
- 检查无效请求返回 Bad Request
最终实现如下:
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,
})
}
}
TDD 的关键在于每次只实现通过当前测试所需的最小代码,确保测试覆盖所有边界和异常情况。
云端部署与运行微服务
完成微服务开发后,推荐将服务部署到云平台。以 Cloud Foundry 的 PCF Dev 和 Pivotal Web Services(PWS)为例,介绍部署流程。
配置与部署步骤
注册 PWS 账户,安装 Cloud Foundry CLI。
本地可使用 PCF Dev 进行开发和测试,启动虚拟机后通过 CLI 管理应用。
创建 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
通过命令行推送应用:
cf push
可结合 Wercker 等 CI 工具实现自动化部署。
关于 Buildpack:Go Buildpack 用于集成运行环境,但建议优先采用 Docker 镜像部署,确保工件不可变和环境一致性。
总结
本章系统介绍了在 Go 中构建微服务的完整流程,包括 API First 设计、测试驱动开发、服务框架搭建及云端部署。通过规范化接口设计和严格测试,提升服务质量与可维护性。建议读者结合实际项目,实践 TDD 和自动化部署,夯实云原生微服务开发能力。
参考文献
- API Blueprint 官网 - apiblueprint.org
- Apiary 在线文档 - docs.gogame.apiary.io
- Negroni 中间件库 - github.com
- Gorilla Mux 路由库 - github.com
- Cloud Foundry CLI 文档 - docs.run.pivotal.io
- PCF Dev 官方文档 - docs.pivotal.io
- Building Microservices - oreilly.com
- Continuous Delivery - Jez Humble & David Farley