使用 Go kit 在 Go 中实现微服务
在《微服务模式:Java示例》一书的其中一章中,作者提到了“微服务底盘”模式:
使用微服务底盘框架构建微服务,该框架可处理横切关注点,例如异常跟踪、日志记录、健康检查、外部化配置和分布式跟踪。
他更进一步,举例说明了在 Java 和 Go 中实现这些概念的框架:
经过一番研究,我选择了 Go 套件,因为它是最受欢迎的套件之一,更新速度也很快,而且我喜欢它提出的架构。
建筑学
服务
服务是实现所有业务逻辑的地方。在 Go Kit 中,服务通常被建模为接口,而这些接口的实现则包含业务逻辑。Go Kit 服务应力求遵循整洁架构或六边形架构。也就是说,业务逻辑不应该了解传输域的概念:你的服务不应该知道任何关于 HTTP 标头或 gRPC 错误代码的信息。
端点
端点就像控制器上的动作/处理程序;安全性和反脆弱性逻辑就存在于其中。如果您实现了两种传输方式(HTTP 和 gRPC),则可能需要两种方法向同一个端点发送请求。
运输
传输域绑定到具体的传输协议,例如 HTTP 或 gRPC。在微服务可以支持一种或多种传输协议的情况下,这非常强大;你可以在单个微服务中同时支持传统的 HTTP API 和较新的 RPC 服务。
例子
让我们用这种架构创建一个微服务示例。目录结构如下所示:
服务
本示例中的服务层代码非常简单:
package user
import (
"auth/security"
"context"
"errors"
)
type Service interface {
ValidateUser(ctx context.Context, mail, password string) (string, error)
ValidateToken(ctx context.Context, token string) (string, error)
}
var (
ErrInvalidUser = errors.New("Invalid user")
ErrInvalidToken = errors.New("Invalid token")
)
type service struct{}
func NewService() *service {
return &service{}
}
func (s *service) ValidateUser(ctx context.Context, email, password string) (string, error) {
//@TODO create validation rules, using databases or something else
if email == "eminetto@gmail.com" && password != "1234567" {
return "nil", ErrInvalidUser
}
token, err := security.NewToken(email)
if err != nil {
return "", err
}
return token, nil
}
func (s *service) ValidateToken(ctx context.Context, token string) (string, error) {
t, err := security.ParseToken(token)
if err != nil {
return "", ErrInvalidToken
}
tData, err := security.GetClaims(t)
if err != nil {
return "", ErrInvalidToken
}
return tData["email"].(string), nil
}
正如 Go 工具包文档所建议的,第一步是interface为我们的服务创建一个接口,该接口将用于实现我们的业务逻辑。很快,当我们在应用程序中添加日志记录和指标监控功能时,创建接口的这一决定就会显得非常有用。
由于服务层测试只包含业务规则,因此也非常简单:
package user_test
import (
"auth/user"
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidateUser(t *testing.T) {
service := user.NewService()
t.Run("invalid user", func(t *testing.T) {
_, err := service.ValidateUser(context.Background(), "eminetto@gmail.com", "invalid")
assert.NotNil(t, err)
assert.Equal(t, "Invalid user", err.Error())
})
t.Run("valid user", func(t *testing.T) {
token, err := service.ValidateUser(context.Background(), "eminetto@gmail.com", "1234567")
assert.Nil(t, err)
assert.NotEmpty(t, token)
})
}
端点
现在我们将把函数暴露给外部。在这个例子中,两个函数都可以从外部访问,因此我们将创建两个端点。但这并非总是如此。根据具体情况,您可以只暴露部分函数,而将其他函数保留在服务层内部访问。
package user
import (
"context"
"github.com/go-kit/kit/endpoint"
)
//definition of endpoint input and output structures
type validateUserRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
type validateUserResponse struct {
Token string `json:"token,omitempty"`
Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}
//the endpoint will receive a request, convert to the desired
//format, invoke the service and return the response structure
func makeValidateUserEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(validateUserRequest)
token, err := svc.ValidateUser(ctx, req.Email, req.Password)
if err != nil {
return validateUserResponse{"", err.Error()}, err
}
return validateUserResponse{token, ""}, err
}
}
//definition of endpoint input and output structures
type validateTokenRequest struct {
Token string `json:"token"`
}
type validateTokenResponse struct {
Email string `json:"email,omitempty"`
Err string `json:"err,omitempty"`
}
//the endpoint will receive a request, convert to the desired
//format, invoke the service and return the response structure
func makeValidateTokenEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(validateTokenRequest)
email, err := svc.ValidateToken(ctx, req.Token)
if err != nil {
return validateTokenResponse{"", err.Error()}, err
}
return validateTokenResponse{email, ""}, err
}
}
端点的作用是接收请求,将其转换为预期的结构体,调用服务层,并返回另一个结构体。端点层对上层一无所知,因为无论端点是通过 HTTP、gRPC 还是其他传输方式被调用,都无关紧要。
由于其结构简单,测试这一层也同样容易实现:
package user
import (
"context"
"testing"
)
func TestMakeValidateUserEndpoint(t *testing.T) {
s := NewService()
endpoint := makeValidateUserEndpoint(s)
t.Run("valid user", func(t *testing.T) {
req := validateUserRequest{
Email: "eminetto@gmail.com",
Password: "1234567",
}
_, err := endpoint(context.Background(), req)
if err != nil {
t.Errorf("expected %v received %v", nil, err)
}
})
t.Run("invalid user", func(t *testing.T) {
req := validateUserRequest{
Email: "eminetto@gmail.com",
Password: "123456",
}
_, err := endpoint(context.Background(), req)
if err == nil {
t.Errorf("expected %v received %v", ErrInvalidUser, err)
}
})
}
可以通过将服务的使用替换为实现相同Service接口的模拟服务来改进此测试,从而使测试更加高效。
运输
在这一层,我们可以实现多种协议,例如 HTTP、gRPC、AMPQ、NATS 等。在本例中,我们将以 HTTP API 的形式公开我们的端点。因此,我们将创建以下文件transpor_http.go:
package user
import (
"context"
"encoding/json"
"net/http"
"github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/gorilla/mux"
)
func NewHttpServer(svc Service, logger log.Logger) *mux.Router {
//options provided by the Go kit to facilitate error control
options := []httptransport.ServerOption{
httptransport.ServerErrorLogger(logger),
httptransport.ServerErrorEncoder(encodeErrorResponse),
}
//definition of a handler
validateUserHandler := httptransport.NewServer(
makeValidateUserEndpoint(svc), //use the endpoint
decodeValidateUserRequest, //converts the parameters received via the request body into the struct expected by the endpoint
encodeResponse, //converts the struct returned by the endpoint to a json response
options...,
)
validateTokenHandler := httptransport.NewServer(
makeValidateTokenEndpoint(svc),
decodeValidateTokenRequest,
encodeResponse,
options...,
)
r := mux.NewRouter() //I'm using Gorilla Mux, but it could be any other library, or even the stdlib
r.Methods("POST").Path("/v1/auth").Handler(validateUserHandler)
r.Methods("POST").Path("/v1/validate-token").Handler(validateTokenHandler)
return r
}
func encodeErrorResponse(_ context.Context, err error, w http.ResponseWriter) {
if err == nil {
panic("encodeError with nil error")
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(codeFrom(err))
json.NewEncoder(w).Encode(map[string]interface{}{
"error": err.Error(),
})
}
func codeFrom(err error) int {
switch err {
case ErrInvalidUser:
return http.StatusNotFound
case ErrInvalidToken:
return http.StatusUnauthorized
default:
return http.StatusInternalServerError
}
}
//converts the parameters received via the request body into the struct expected by the endpoint
func decodeValidateUserRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var request validateUserRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
//converts the parameters received via the request body into the struct expected by the endpoint
func decodeValidateTokenRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var request validateTokenRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
//converts the struct returned by the endpoint to a json response
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
return json.NewEncoder(w).Encode(response)
}
这段代码看起来像是一系列设置,用于指示每个 API 地址将使用哪个端点。我尝试在代码注释中描述其行为。这一层的测试代码如下:
package user
import (
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/go-kit/kit/log"
)
func TestHTTP(t *testing.T) {
var logger log.Logger
logger = log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "listen", "8081", "caller", log.DefaultCaller)
s := NewService()
r := NewHttpServer(s, logger)
srv := httptest.NewServer(r)
for _, testcase := range []struct {
method, url, body string
want int
}{
{"POST", srv.URL + "/v1/auth", `{"email": "eminetto@gmail.com", "password":"1234567"}`, http.StatusOK},
{"GET", srv.URL + "/v1/auth", `{"email": "eminetto@gmail.com", "password":"1234567"}`, http.StatusMethodNotAllowed},
{"POST", srv.URL + "/v1/auth", `{"email": "eminetto@gmail.com", "password":"invalid"}`, http.StatusNotFound},
{"POST", srv.URL + "/v1/validate-token", `{"token": "invalid"}`, http.StatusUnauthorized},
} {
req, _ := http.NewRequest(testcase.method, testcase.url, strings.NewReader(testcase.body))
resp, _ := http.DefaultClient.Do(req)
if testcase.want != resp.StatusCode {
t.Errorf("%s %s %s: want %d have %d", testcase.method, testcase.url, testcase.body, testcase.want, resp.StatusCode)
}
}
}
就像测试端点层一样,我们可以使用服务的模拟来改进此测试。
主要的
在这个main.go文件中,我们将使用所有图层:
package main
import (
"auth/user"
"net/http"
"os"
"github.com/go-kit/kit/log"
)
func main() {
var logger log.Logger
logger = log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "listen", "8081", "caller", log.DefaultCaller)
svc := user.NewLoggingMiddleware(logger, user.NewService())
r := user.NewHttpServer(svc, logger)
logger.Log("msg", "HTTP", "addr", "8081")
logger.Log("err", http.ListenAndServe(":8081", r))
}
这里我们可以看到为服务创建接口的另一个优势。该user.NewHttpServer函数需要一个实现了该接口的对象作为第一个参数Service。该user.NewLoggingMiddleware函数会创建一个实现了该接口的结构体,并将我们原始的服务包含在其中。该文件的代码logging.go如下所示:
package user
import (
"context"
"time"
"github.com/go-kit/kit/log"
)
func NewLoggingMiddleware(logger log.Logger, next Service) logmw {
return logmw{logger, next}
}
type logmw struct {
logger log.Logger
Service
}
func (mw logmw) ValidateUser(ctx context.Context, email, password string) (token string, err error) {
defer func(begin time.Time) {
_ = mw.logger.Log(
"method", "validateUser",
"input", email,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
token, err = mw.Service.ValidateUser(ctx, email, password)
return
}
func (mw logmw) ValidateToken(ctx context.Context, token string) (email string, err error) {
defer func(begin time.Time) {
_ = mw.logger.Log(
"method", "validateToken",
"input", token,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
email, err = mw.Service.ValidateToken(ctx, token)
return
}
它实现了接口的所有功能,并增加了在调用实际服务代码之前记录每次函数调用的功能。同样的方法也可用于实现指标管理、限制 API 访问等。官方教程中提供了一些示例。
如果我们的微服务需要以更多格式(例如 gRPC 或 NATS)交付逻辑,我们只需在传输层实现这些代码,并指定将使用哪些端点即可。这为功能扩展提供了极大的灵活性,而不会增加复杂性。
在这篇文章中,我更多地关注了 Go 工具包提供的架构,但在官方文档中,你可以看到它提供的其他chassis功能,例如身份验证、熔断器、日志、指标、速率限制、服务发现、跟踪等。
我喜欢它的架构和功能,我相信它能以快速、简洁、高效的方式创建服务。
本示例的代码位于此存储库中。
文章来源:https://dev.to/eminetto/microservices-in-go-using-the-go-kit-jjf



