如何使用 Go kit 编写微服务
我在网上搜索了很多内容(我认为我的 google-fu 相当不错),寻找一些关于使用 Go kit 在 Go 中编写简单的“RESTful”微服务的精心编写的教程。
我找不到任何...
在我看来, Go kit 存储库中的示例很好,但文档很枯燥。
然后我决定买一本叫做《Go 编程蓝图,第二版》的书,这本书还不错,但只有两章专门讲解 Go Kit(一章讲微服务的实际开发,一章讲实际部署),而且我现在不太在意gRPC,虽然本书第十章的例子也实现了 gRPC。要我说,脚手架代码太多了 :P
所以,我决定回馈社区,写一篇教程,希望能够“边做边学”。这篇教程很大程度上受到了上面提到的那本书的启发,并且可能在很多方面有所改进。
请随时提供反馈!
您可以在我的博客(本文的原始来源)上找到微服务完整源代码的链接。coding.napolux.com
什么是 Go kit?
来自Go kit README.md:
Go Kit 是一款用于构建 Go 微服务(或优雅单体式应用)的编程工具包。我们致力于解决分布式系统和应用架构中的常见问题,让您专注于交付业务价值。
[...]
Go 是一门优秀的通用语言,但微服务需要一定程度的专业支持。RPC 安全性、系统可观察性、基础设施集成,甚至程序设计——Go Kit 填补了标准库留下的空白,使 Go 成为任何组织编写微服务的一流语言。
我不想对此进行过多讨论:我对 Go 还太新,无法发表强烈的意见。社区当然分为喜欢和不喜欢 Go 的两派。你也可以在这里找到一篇关于 Go 微服务框架差异的优秀文章。
我们要做什么?
我们将创建一个非常基础的微服务,用于返回并验证日期……我们的目标是理解 Go Kit 的工作原理,仅此而已。即使没有 Go Kit,你也可以轻松复制所有逻辑,但我来这里是为了学习,所以……
我希望您能对您的下一个项目有一个大致的了解,并有一个良好的起点!
我们的微服务将有一些端点。
- 该
GET
端点/status
将返回一个简单的答案,以确认微服务已启动并正在运行 - 其中一个
GET
端点/get
将返回今天的日期 - 其中一个
POST
端点/validate
将接收一个日期字符串dd/mm/yyyy
(如果你问我,这是唯一现有的日期格式,请采用美国格式!)并根据简单的正则表达式对其进行验证...
再次,您可以在我的博客(本文的原始来源)上找到微服务完整源代码的链接。coding.napolux.com
开始吧!!!
先决条件
你的机器上应该已经安装并运行了Golang。我发现官方下载的软件包比我的 Macbook 上的Homebrew 安装效果更好(我的环境变量设置出了点问题) 。
struct
另外,你应该了解 Go 语言,例如,我不会解释什么是 a 。
napodate 微服务
好的,让我们首先在$GOPATH
文件夹中创建一个名为的新文件夹napodate
。这也将是我们的包的名称。
在那里放一个service.go
文件。让我们在文件顶部添加我们的服务接口。
package napodate
import "context"
// Service provides some "date capabilities" to your application
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,我们开发了一个 context 包,可以轻松地将请求范围的值、取消信号和截止日期跨 API 边界传递给处理请求所涉及的所有 goroutine
基本上,这是必要的,因为我们的微服务应该从一开始就处理并发请求,并且每个请求的上下文都是强制性的。
你肯定不想把东西弄混。本教程后面会详细介绍。我们现在用得不多,但慢慢习惯吧!:P 我们现在有了微服务接口。
实施我们的服务
你可能知道,如果没有实现,接口就毫无意义。所以,让我们来实现我们的服务。让我们添加一些代码service.go
。
type dateService struct{}
// NewService makes a new Service.
func NewService() Service {
return dateService{}
}
// Status only tell us that our service is ok!
func (dateService) Status(ctx context.Context) (string, error) {
return "ok", nil
}
// Get will return today's date
func (dateService) Get(ctx context.Context) (string, error) {
now := time.Now()
return now.Format("02/01/2006"), nil
}
// Validate will check if the date today's date
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)
}
// testing 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")
// testing today's date
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)
}
// testing that the date is valid
if !b {
t.Errorf("date should be valid")
}
// testing an invalid date
b, err = srv.Validate(ctx, "31/31/2019")
if b {
t.Errorf("date should be invalid")
}
// testing a USA date date
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()
}
我让测试更加易读,但您确实应该使用Subtests 来编写它们,以获得更为最新的语法。
测试已通过!(!)但请关注一下方法。对于每个测试,我们都会使用上下文setup()
返回一个服务实例。NewService()
交通
我们的服务将使用 HTTP 公开。现在,我们将对已接受的 HTTP 请求和响应进行建模。请transport.go
在 的同一文件夹中创建一个名为 的文件service.go
。
package napodate
import (
"context"
"encoding/json"
"net/http"
)
// In the first part of the file we are mapping requests and responses to their JSON payload.
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"`
}
// In the second part we will write "decoders" for our incoming requests
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
}
// Last but not least, we have the encoder for the response output
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
return json.NewEncoder(w).Encode(response)
}
您可以在我的博客(本文的原始来源)上找到微服务完整源代码的链接。coding.napolux.com
如果您问我,我会给出一些代码,但您会在transport.go
repo 文件中找到可以帮助您导航的注释。
在文件的第一部分,statusRequest
我们将请求和响应映射到它们的 JSON 负载。对于 和,getRequest
我们不需要太多,因为没有负载发送到服务器。对于 ,validateRequest
我们将传递一个要验证的日期,因此这里是date
字段。
答案也相当直接。
在第二部分中,我们将为传入的请求编写“解码器”,告诉服务如何转换请求并将其映射到正确的请求结构体。我知道get
和status
是空的,但它们是为了完整性而存在的。记住,我是边做边学的……
最后但并非最不重要的一点是,我们有响应输出的编码器,这是一个简单的 JSON 编码器:给定一个对象,我们将从中返回一个 JSON 对象。
这就是传输,让我们创建我们的端点!
端点
让我们创建一个新文件endpoint.go
。该文件将包含我们的端点,用于将来自客户端的请求映射到我们的内部服务
package napodate
import (
"context"
"errors"
"github.com/go-kit/kit/endpoint"
)
// Endpoints are exposed
type Endpoints struct {
GetEndpoint endpoint.Endpoint
StatusEndpoint endpoint.Endpoint
ValidateEndpoint endpoint.Endpoint
}
// MakeGetEndpoint returns the response from our service "get"
func MakeGetEndpoint(srv Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
_ = request.(getRequest) // we really just need the request, we don't use any value from it
d, err := srv.Get(ctx)
if err != nil {
return getResponse{d, err.Error()}, nil
}
return getResponse{d, ""}, nil
}
}
// MakeStatusEndpoint returns the response from our service "status"
func MakeStatusEndpoint(srv Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
_ = request.(statusRequest) // we really just need the request, we don't use any value from it
s, err := srv.Status(ctx)
if err != nil {
return statusResponse{s}, err
}
return statusResponse{s}, nil
}
}
// MakeValidateEndpoint returns the response from our service "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 endpoint mapping
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 endpoint mapping
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 endpoint mapping
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...
的。它们将接收服务作为参数,然后使用类型断言将请求类型“强制”为特定类型,并使用它来调用该类型的服务方法。
Make...
在文件中使用这些方法之后main.go
,我们将编写符合服务接口的端点
type Endpoints struct {
GetEndpoint endpoint.Endpoint
StatusEndpoint endpoint.Endpoint
ValidateEndpoint endpoint.Endpoint
}
我们来举个例子:
// Status endpoint mapping
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 is a good little server
func NewHTTPServer(ctx context.Context, endpoints Endpoints) http.Handler {
r := mux.NewRouter()
r.Use(commonMiddleware) // @see 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)
}()
// mapping endpoints
endpoints := napodate.Endpoints{
GetEndpoint: napodate.MakeGetEndpoint(srv),
StatusEndpoint: napodate.MakeStatusEndpoint(srv),
ValidateEndpoint: napodate.MakeValidateEndpoint(srv),
}
// HTTP transport
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
但是由于该标志,我们可以用我们不想用的任何端口来使用它。
接下来是服务器的设置:我们创建一个上下文(上下文的解释见上文),并获取我们的服务。此外,还设置了一个错误通道。
通道是连接并发 Goroutine 的管道。你可以从一个 Goroutine 向通道发送值,并在另一个 Goroutine 中接收这些值。
然后我们创建两个 goroutines。一个用于按下时停止服务器CTRL+C
,另一个用于实际监听传入的请求。
看看handler := napodate.NewHTTPServer(ctx, endpoints)
这个处理程序将映射我们的服务端点(你还记得Make...
上面的方法吗?)并返回正确的答案。
你之前在哪见过NewHTTPServer()
?
一旦通道收到错误消息,服务器就会停止并死亡。
让我们服务吧!
如果你正确完成了所有操作,请运行
go run cmd/main.go
从您的项目文件夹中,您应该能够卷曲您的微服务!
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 和 Go 编程语言的好方法。
希望您和我一样喜欢本教程!
您可以在我的博客(本文的原始来源)上找到微服务完整源代码的链接。coding.napolux.com
文章来源:https://dev.to/napolux/how-to-write-a-microservice-in-go-with-go-kit-a66