Go 教程:Go Kit 微服务实战

Golang

什么是 Go kit?

来自 Go kit README.md:

Go kit 是用于在 Go 中构建微服务的编程工具包。我们解决了分布式系统和应用程序体系结构中的常见问题,因此您可以专注于交付业务价值。

[...]

Go是一种很棒的通用语言,但是微服务需要一定量的专业支持。 RPC安全性,系统可观察性,基础结构集成甚至程序设计。Go kit 填补了标准库留下的空白,并使 Go 成为在任何组织中编写微服务的一流语言。

我们会做什么?

我们将创建一个非常基本的微服务,该微服务将返回并验证日期。目标是了解 Go kit 的工作原理,仅此而已。不用 Go kit 就能轻松复制所有逻辑,但我们是来这里学习 Go kit 的,所以...

希望这能对你的下一个项目有一个良好的开端!

我们的微服务将有一些端点。

  • /status 处的一个 GET 端点,该端点将返回一个简单的回复,以确认微服务已启动并正在运行
  • /get 处的一个 GET 端点,它将返回今天的日期
  • /validate 处的一个 POST 端点,它将收到一个 dd/mm/yyyy 的日期字符串 (美国的日期格式) 并根据一个简单的正则表达式对其进行验证…

源代码:https://github.com/napolux/go-kit-microser...

先决条件

您应该已经在计算机上安装并运行了 Golang。我发现在我的 Macbook 上 官方下载包 (我在配置环境变量时遇到一些问题) 安装的 Golang 比 Homebrew 安装 运行得更好。

另外,你应该了解 Go 语言。例如,我不会解释什么是 struct

napodate 微服务

好的,让我们在我们的 $GOPATH 文件夹中创建一个名为 napodate 的新文件夹。它也将会是我们的包名。

将 service.go 文件放在此处。让我们在文件顶部添加服务接口。

package napodate

import "context"

// 服务为您的应用程序提供了一些「日期功能」
type Service interface {
    Status(ctx context.Context) (string, error)
    Get(ctx context.Context) (string, error)
    Validate(ctx context.Context, date string) (bool, error)
}

在这里,我们为服务定义了「蓝图」:在 Go kit 中,您必须将服务建模为接口。如上所述,我们将需要三个端点,这些端点将被映射到此接口。
为什么我们要使用 context 包? 请阅读 https://blog.golang.org/context:

在 Google,我们开发了一个上下文包,可轻松将跨 API 边界的请求范围的值,取消信号和截止日期传递给处理请求的所有协程

基本上,这是必需的,因为我们的微服务应该从一开始就处理并发请求,并且必须为每个请求提供上下文。

不要把事情搞混。本教程后面的内容将对此进行详细介绍。我们现在用的不多,但要习惯它! :P 现在我们有了微服务接口。

实现我们的服务

正如你知道的,没有实现,接口就什么都不是,所以让我们实现我们的服务吧。让我们为 service.go 添加更多代码。

type dateService struct{}

// NewService 创建一个新的服务
func NewService() Service {
    return dateService{}
}

// Status 仅仅告诉我们我们的服务是正常的!
func (dateService) Status(ctx context.Context) (string, error) {
    return "ok", nil
}

// Get 将会返回今天的日期
func (dateService) Get(ctx context.Context) (string, error) {
    now := time.Now()
    return now.Format("02/01/2006"), nil
}

// Validate 将会检查日期是否为今天的日期
func (dateService) Validate(ctx context.Context, date string) (bool, error) {
    _, err := time.Parse("02/01/2006", date)
    if err != nil {
        return false, err
    }
    return true, nil
}

新定义的类型 dateService (一个空结构) 是我们为了将服务的方法组合在一起,同时以某种方式向其他地方「隐藏」实现的方式。

请参阅 NewService() 作为我们「对象」的构造函数。这就是我们要获得服务实例的调用,同时要像优秀程序员一样隐藏内部的逻辑。

让我们写一个测试

在我们的服务测试中可以看到一个关于如何使用 NewService() 的好例子。继续并创建一个 service_test.go 文件。

package napodate

import (
    "context"
    "testing"
    "time"
)

func TestStatus(t *testing.T) {
    srv, ctx := setup()

    s, err := srv.Status(ctx)
    if err != nil {
        t.Errorf("Error: %s", err)
    }

    // 测试 status
    ok := s == "ok"
    if !ok {
        t.Errorf("expected service to be ok")
    }
}

func TestGet(t *testing.T) {
    srv, ctx := setup()
    d, err := srv.Get(ctx)
    if err != nil {
        t.Errorf("Error: %s", err)
    }

    time := time.Now()
    today := time.Format("02/01/2006")

    // 测试今天的日期
    ok := today == d
    if !ok {
        t.Errorf("expected dates to be equal")
    }
}

func TestValidate(t *testing.T) {
    srv, ctx := setup()
    b, err := srv.Validate(ctx, "31/12/2019")
    if err != nil {
        t.Errorf("Error: %s", err)
    }

    // 测试日期是否有效
    if !b {
        t.Errorf("date should be valid")
    }

    // 测试无效日期
    b, err = srv.Validate(ctx, "31/31/2019")
    if b {
        t.Errorf("date should be invalid")
    }

    // 测试美国日期
    b, err = srv.Validate(ctx, "12/31/2019")
    if b {
        t.Errorf("USA date should be invalid")
    }
}

func setup() (srv Service, ctx context.Context) {
    return NewService(), context.Background()
}

使测试更具可读性,但实际上应该使用 子测试,以获得最新的语法 来编写它们。

测试是绿色的(!),但重点关注一下 setup() 方法。对于每个测试,我们使用 NewService() 和上下文返回一个服务实例。

传输协议

我们的服务将使用 HTTP 公开。我们将对接受的 HTTP 请求和响应进行建模。在 service.go 的同一文件夹中创建一个名为 transport.go 的文件。

package napodate

import (
    "context"
    "encoding/json"
    "net/http"
)

// 在文件的第一部分中,我们将请求和响应映射到它们的 JSON 有效负载。
type getRequest struct{}

type getResponse struct {
    Date string `json:"date"`
    Err  string `json:"err,omitempty"`
}

type validateRequest struct {
    Date string `json:"date"`
}

type validateResponse struct {
    Valid bool   `json:"valid"`
    Err   string `json:"err,omitempty"`
}

type statusRequest struct{}

type statusResponse struct {
    Status string `json:"status"`
}

// 在第二部分中,我们将为传入的请求编写「解码器」
func decodeGetRequest(ctx context.Context, r *http.Request) (interface{}, error) {
    var req getRequest
    return req, nil
}

func decodeValidateRequest(ctx context.Context, r *http.Request) (interface{}, error) {
    var req validateRequest
    err := json.NewDecoder(r.Body).Decode(&req)
    if err != nil {
        return nil, err
    }
    return req, nil
}

func decodeStatusRequest(ctx context.Context, r *http.Request) (interface{}, error) {
    var req statusRequest
    return req, nil
}

// 最后,我们有响应输出的编码器
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
    return json.NewEncoder(w).Encode(response)
}

源代码: https://github.com/napolux/go-kit-microser...

如果你想要问我一些代码相关的问题,你可以在 repo 文件的 transport.go 中找到注释,这将有助于你找到它。

在文件的第一部分中,我们将请求和响应映射到它们的 JSON 有效负载。对于 statusRequestgetRequest 我们不需要太多,因为没有有效负载被发送到服务器。对于 validateRequest 我们将传递一个要验证的日期,因此这里是 date 字段。

响应也非常简单。

在第二部分 我们将为传入的请求编写「解码器」,告诉服务如何翻译请求并将它们映射到正确的请求结构。我知道 getstatus 是空的,但它们是为了完整性而存在的。记住,我们在实践中学习...

最后但并非最不重要, 我们有用于响应输出的编码器,它是一个简单的 JSON 编码器:给定一个对象,我们将从中返回一个 JSON 对象。

以上就是传输协议,现在让我们去创建端点!

端点

让我们创建 endpoint.go 文件。该文件包含我们的端点,这些端点会将来自客户端的请求映射到我们的内部服务

package napodate

import (
    "context"
    "errors"

    "github.com/go-kit/kit/endpoint"
)

// 公开端点
type Endpoints struct {
    GetEndpoint      endpoint.Endpoint
    StatusEndpoint   endpoint.Endpoint
    ValidateEndpoint endpoint.Endpoint
}

// MakeGetEndpoint 返回 「get」服务的响应
func MakeGetEndpoint(srv Service) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        _ = request.(getRequest) // 我们只需要请求,不需要使用它的值
        d, err := srv.Get(ctx)
        if err != nil {
            return getResponse{d, err.Error()}, nil
        }
        return getResponse{d, ""}, nil
    }
}

// MakeStatusEndpoint 返回 「status」服务的响应
func MakeStatusEndpoint(srv Service) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        _ = request.(statusRequest) // 我们只需要请求,不需要使用它的值
        s, err := srv.Status(ctx)
        if err != nil {
            return statusResponse{s}, err
        }
        return statusResponse{s}, nil
    }
}

// MakeValidateEndpoint 返回「validate」服务的响应
func MakeValidateEndpoint(srv Service) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(validateRequest)
        b, err := srv.Validate(ctx, req.Date)
        if err != nil {
            return validateResponse{b, err.Error()}, nil
        }
        return validateResponse{b, ""}, nil
    }
}

// get 端点映射
func (e Endpoints) Get(ctx context.Context) (string, error) {
    req := getRequest{}
    resp, err := e.GetEndpoint(ctx, req)
    if err != nil {
        return "", err
    }
    getResp := resp.(getResponse)
    if getResp.Err != "" {
        return "", errors.New(getResp.Err)
    }
    return getResp.Date, nil
}

// Status 端点映射
func (e Endpoints) Status(ctx context.Context) (string, error) {
    req := statusRequest{}
    resp, err := e.StatusEndpoint(ctx, req)
    if err != nil {
        return "", err
    }
    statusResp := resp.(statusResponse)
    return statusResp.Status, nil
}

// Validate 端点映射
func (e Endpoints) Validate(ctx context.Context, date string) (bool, error) {
    req := validateRequest{Date: date}
    resp, err := e.ValidateEndpoint(ctx, req)
    if err != nil {
        return false, err
    }
    validateResp := resp.(validateResponse)
    if validateResp.Err != "" {
        return false, errors.New(validateResp.Err)
    }
    return validateResp.Valid, nil
}

让我们深入探讨...为了将我们所有的服务方法 Get()Status()Validate() 作为端点公开, 我们将要编写处理传入请求的函数,调用相应的服务方法,并根据响应构建并返回适当的对象。

这些方法都是 Make...。它们接收 service 作为参数,然后使用类型断言将请求类型「强制」为特定类型,并使用它来为其调用服务方法。

在这些 Make... 方法(将在 main.go 文件中使用)之后,我们将编写符合服务接口的端点。

type Endpoints struct {
    GetEndpoint      endpoint.Endpoint
    StatusEndpoint   endpoint.Endpoint
    ValidateEndpoint endpoint.Endpoint
}

让我们来看一个例子:

// Status 端点映射
func (e Endpoints) Status(ctx context.Context) (string, error) {
    req := statusRequest{}
    resp, err := e.StatusEndpoint(ctx, req)
    if err != nil {
        return "", err
    }
    statusResp := resp.(statusResponse)
    return statusResp.Status, nil
}

这个方法允许我们以 Go 的方法来使用端点。

HTTP 服务器

我们的微服务需要一个 HTTP 服务器。Go 对此很有帮助, 但是我选择了 https://github.com/gorilla/mux 作为我们的路由,因为它的语法看起来非常简洁,所以让我们创建一个映射到我们端点的 HTTP 服务器。

在你的项目中创建 server.go 文件。

package napodate

import (
    "context"
    "net/http"

    httptransport "github.com/go-kit/kit/transport/http"
    "github.com/gorilla/mux"
)

// NewHTTPServer 是一个很好的服务器
func NewHTTPServer(ctx context.Context, endpoints Endpoints) http.Handler {
    r := mux.NewRouter()
    r.Use(commonMiddleware) // @请参阅 https://stackoverflow.com/a/51456342

    r.Methods("GET").Path("/status").Handler(httptransport.NewServer(
        endpoints.StatusEndpoint,
        decodeStatusRequest,
        encodeResponse,
    ))

    r.Methods("GET").Path("/get").Handler(httptransport.NewServer(
        endpoints.GetEndpoint,
        decodeGetRequest,
        encodeResponse,
    ))

    r.Methods("POST").Path("/validate").Handler(httptransport.NewServer(
        endpoints.ValidateEndpoint,
        decodeValidateRequest,
        encodeResponse,
    ))

    return r
}

func commonMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Add("Content-Type", "application/json")
        next.ServeHTTP(w, r)
    })
}

端点将从 main.go 文件和 commonMiddleware() 传递到服务器,请注意为未公开的端点的每个响应添加特殊的头信息。

最后,我们的 main.go 文件

让我们来完成我们的微服务吧!现在我们有一个带有端点的服务和一个 HTTP 服务器。我们只需要一个可以打包它们的地方,这就是我们的 main.go 文件。将它放入名为 cmd 的文件夹中。

package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"

    "napodate"
)

func main() {
    var (
        httpAddr = flag.String("http", ":8080", "http listen address")
    )
    flag.Parse()
    ctx := context.Background()
    // our napodate service
    srv := napodate.NewService()
    errChan := make(chan error)

    go func() {
        c := make(chan os.Signal, 1)
        signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
        errChan <- fmt.Errorf("%s", <-c)
    }()

    // 映射端点
    endpoints := napodate.Endpoints{
        GetEndpoint:      napodate.MakeGetEndpoint(srv),
        StatusEndpoint:   napodate.MakeStatusEndpoint(srv),
        ValidateEndpoint: napodate.MakeValidateEndpoint(srv),
    }

    // HTTP 传输
    go func() {
        log.Println("napodate is listening on port:", *httpAddr)
        handler := napodate.NewHTTPServer(ctx, endpoints)
        errChan <- http.ListenAndServe(*httpAddr, handler)
    }()

    log.Fatalln(<-errChan)
}

让我们一起分析一下这个文件。我们声明 main 包并导入我们需要的内容。

我们使用一个标志让监听端口可配置, 我们服务的默认端口是经典的 8080 ,但是我们要感谢标志让我们在任何端口上都可以使用我们的服务。

下面是我们服务器的设置:我们创建一个 Context(Context的说明见上面)),然后我们得到我们的服务。 同时也设置了一个错误通道.

通道(Channels)是连接并发 goroutine 的管道。您可以将值从一个 goroutine发送到通道,并使用另一个 goroutine 接收。

我们然后创建两个 goroutines . 一个用于在我们按 CTRL+C 时停止服务器,另一个将实际侦听传入请求。

看一下 handler := napodate.NewHTTPServer(ctx, endpoints) 此处理程序将映射我们的 endpoints (还记得上面使用 Make...方法不?)并返回正确的方法回答。

您以前在哪里看到过 NewHTTPServer() ?

一旦通道收到错误消息,服务器将停止并死机。

让我们启动服务!

如果你正确的执行了前文的所有操作,请运行下面的命令

go run cmd/main.go

在你的项目文件夹目录下,你应该可以 curl 你的微服务了!

curl http://localhost:8080/get
{"date":"14/04/2019"}

curl http://localhost:8080/status
{"status":"ok"}

curl -XPOST -d '{"date":"32/12/2020"}' http://localhost:8080/validate
{"valid":false,"err":"parsing time \"32/12/2020\": day out of range"}

curl -XPOST -d '{"date":"12/12/2021"}' http://localhost:8080/validate
{"valid":true}

结束

我们使用 Go kit 从零开始创建了一个简单的微服务。

希望你和我一样喜欢本教程!

源代码: https://github.com/napolux/go-kit-microser...

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://dev.to/napolux/how-to-write-a-mi...

译文地址:https://learnku.com/go/t/38417

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!