第 5 章:在 Go 中构建微服务

黄金法则:你可以在不更改任何其他代码的前提下更改服务并重新部署吗?——Sam Newman,Building Microservices作者

微服务架构强调服务的独立性和可替换性。本章将以 API First 和测试驱动开发为核心,带你实践在 Go 中构建微服务的全过程,并介绍云端部署的最佳实践。

API First 开发理念与实践

在微服务开发中,API First 是确保服务可扩展、易维护的关键。我们将以 GoGo 游戏服务器为例,展示如何从接口设计到代码实现,逐步构建高质量微服务。

设计 Matches API

首先,需要定义 match 资源集合,确保能够创建和查询比赛。下表展示了 Matches API 的主要接口:

资源方法描述
/matchesGET查询所有可用的 match 列表
/matchesPOST创建并开始一个新的 match
/matches/{id}GET查询指定 match 的详情
表 1: Matches API 资源与方法说明

设计 Moves API

为支持比赛过程中的玩家操作,还需设计 Moves API:

资源方法描述
/matches/{id}/movesGET返回比赛中所有移动的时间排序列表
/matches/{id}/movesPOST玩家进行移动,若无位置信息则为略过
表 2: Moves API 资源与方法说明

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)为例,介绍部署流程。

配置与部署步骤

  1. 注册 PWS 账户,安装 Cloud Foundry CLI。

  2. 本地可使用 PCF Dev 进行开发和测试,启动虚拟机后通过 CLI 管理应用。

  3. 创建 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
    
  4. 通过命令行推送应用:

    cf push
    
  5. 可结合 Wercker 等 CI 工具实现自动化部署。

关于 Buildpack:Go Buildpack 用于集成运行环境,但建议优先采用 Docker 镜像部署,确保工件不可变和环境一致性。

总结

本章系统介绍了在 Go 中构建微服务的完整流程,包括 API First 设计、测试驱动开发、服务框架搭建及云端部署。通过规范化接口设计和严格测试,提升服务质量与可维护性。建议读者结合实际项目,实践 TDD 和自动化部署,夯实云原生微服务开发能力。

参考文献

文章导航

独立页面

这是书籍中的独立页面。

书籍首页

评论区