grpc api

API design is hard. Often with new projects, we are limited by the information or knowledge of the problem we are trying to solve. Once consumers come on board, insights which were previously overlooked can become known, resulting in the evolution of our API.

API设计很难。 通常在新项目中,我们会受到所要解决问题的信息或知识的限制。 消费者加入后,就可以了解以前被忽略的见解,从而导致我们API的发展。

gRPC and Protocol Buffers do a great job in helping us evolve our API in a way which maintains compatibility in both ways:

gRPC和协议缓冲区在帮助我们以两种方式都保持兼容性的方式发展我们的API方面做得很好:

  • Backwards compatibility can be achieved by ensuring all existing message fields, and service definitions remain unchanged. 向后兼容性可以通过确保所有现有消息字段和服务定义保持不变来实现。
  • Forwards compatibility is achieved by the way protocol buffers are serialised and referenced by the field index on the message type. 通过消息类型上的字段索引对协议缓冲区进行序列化和引用的方式,可以实现前向兼容性 。

However, sometimes backwards compatibility cannot be achieved. gRPC makes it easy for us to expose different versions of a services’ API concurrently on the same server. Doing this allows our consumers to migrate to the new version in their own time, without forcing a breaking change on their end.

但是,有时无法实现向后兼容。 gRPC使我们可以轻松地在同一服务器上同时公开服务API的不同版本。 这样一来,我们的消费者就可以在自己的时间内迁移到新版本,而不必强求最终改变。

The chances are, you might have found yourself in a similar position if you have ever built a service with a public facing API. The question I have for you is this:

如果您曾经使用面向公众的API构建服务,则很有可能会遇到类似的情况。 我对您的问题是:

Did the evolution of your API version require a complete or partial rewrite of your existing code?

API版本的演变是否需要完全或部分重写现有代码?

(Problem)

The go programming language allows us to structure code however we want. This is fantastic, but when done incorrectly your code can quickly turn into spaghetti.

Go编程语言允许我们按需构建代码。 这太棒了,但是如果操作不正确,您的代码很快就会变成意大利面条。

When building APIs using gRPC and go, the implications of a poor project layout can be devastating to your service. This can be amplified even further if you haven’t taken into consideration how you are going to support concurrent versions of the same service, for when this use case arises.

使用gRPC and go构建API时,不良的项目布局可能会给您的服务带来灾难性的后果。 如果您没有考虑如何支持同一服务的并发版本(在此用例出现时),则可以将其进一步放大。

Effective project structures for go / gRPC services is a topic which I have found little information on, and is always a point of conjecture. To avoid the pains of dependency cycles, or having to maintain different database layers for the different versions of your service, I’m going to highlight an approach which I use to solve this problem. Read on to find out more.

go / gRPC服务的有效项目结构是一个主题,我几乎没有发现任何信息,并且总是一个猜测点。 为了避免依赖周期带来的麻烦,或者为服务的不同版本维护不同的数据库层,我将重点介绍一种用于解决此问题的方法。 请继续阅读以了解更多信息。

(Solution)

To aid the explanation of the following solution, assume there is a Greeter service which has two API versions exposed on the same server.

为了帮助说明以下解决方案, 请假设有一个Greeter服务 ,该服务在同一服务器上公开了两个API版本。

package main


import (
	"log"
	"net"


	apiv1 "github.com/rokane/grpc-demo/pkg/api/greeter/v1"
	apiv2 "github.com/rokane/grpc-demo/pkg/api/greeter/v2"
	greeterv1 "github.com/rokane/grpc-demo/pkg/service/greeter/v1"
	greeterv2 "github.com/rokane/grpc-demo/pkg/service/greeter/v2"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)


const (
	port = ":8080"
)


func main() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatal("unable to listen on port ", port)
	}
	server := grpc.NewServer()
	reflection.Register(server)


	// Register API v1
	greeterV1, err := greeterv1.NewService()
	if err != nil {
		log.Fatal("unable to initialise v1 service")
	}
	apiv1.RegisterGreeterService(server, greeterV1)


	// Register API v2
	greeterV2, err := greeterv2.NewService()
	if err != nil {
		log.Fatal("unable to initialise v2 service")
	}
	apiv2.RegisterGreeterService(server, greeterV2)


	log.Printf("listening on port %s", port)


	if err := server.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

For a project like this, I would normally structure the internal or pkg directory such that it contains the following packages.

对于这样的项目,我通常会构建internal目录或pkg目录,使其包含以下软件包。

pkg/
    database/
    serializer/
    service/
        greeter/
            v1/
            v2/

The service package will hold the implementation of each service which is registered against your server. Each service will be contained within its own package and have an import path matching that of the API version it is implementing. Doing this provides a clear distinction between what services exist in the project, and the underlying API version the package is implementing.

该service包将保存在您的服务器上注册的每个服务的实现。 每个服务都将包含在其自己的程序包中,并且具有与其正在实现的API版本相匹配的导入路径。 这样做可以清楚区分项目中存在哪些服务,以及程序包正在实现的基础API版本。

The database package will define an interface to your database layer, and will hold implementations of that interface inside it. The database layer should not know anything about the service layer. If it does, it likely means your database is tightly coupled to an individual service. If that is the case, introducing more services, or different versions of a service will inevitably require a rewrite of the database package.

database包将定义数据库层的接口,并在其中保留该接口的实现。 数据库层不应该对服务层一无所知。 如果是这样,则可能意味着您的数据库已与单个服务紧密耦合。 在这种情况下,引入更多服务或服务的不同版本将不可避免地需要重写数据库包。

package database


// Storer defines an interface for interacting with the database layer
type Storer interface {
	RegisterDetails(context.Context, RegisterDetailsCriteria) (*RegisterDetailsResp, error)
	DeleteUser(context.Context, DeleteUserCriteria) (*DeleteUserResp, error)
}

My golden rule: Structs defined inside your service layer, or from your generated API code, should never be seen inside your database layer.

我的黄金法则 :在服务层内部定义的结构或从生成的API代码定义的结构,永远都不应在数据库层内部看到。

The serializer package will then act as a translation layer between your service and database layers. It will define an interface which mimics that of the database layer, but returns response types that implement their own interface. The methods contained inside this interface allow the response to be transformed into the response object which can then be sent over the wire.

然后, serializer程序包将充当service层和database层之间的转换层。 它将定义一个模仿数据库层的接口,但返回实现自己接口的响应类型。 该接口中包含的方法允许将响应转换为响应对象,然后可以通过电线发送该对象。

package serializer


import (
	"context"


	v1 "github.com/rokane/grpc-demo/pkg/api/greeter/v1"
	v2 "github.com/rokane/grpc-demo/pkg/api/greeter/v2"
	db "github.com/rokane/grpc-demo/pkg/database"
)


type SayHelloSerializer interface {
	ToV1() (*v1.SayHelloResponse, error)
	ToV2() (*v2.SayHelloResponse, error)
}


type SayGoodbyeSerializer interface {
	ToV1() (*v1.SayGoodbyeResponse, error)
	ToV2() (*v2.SayGoodbyeResponse, error)
}


type DatabaseSerializer interface {
	RegisterDetails(context.Context, db.RegisterDetailsCriteria) (SayHelloSerializer, error)
	DeleteUser(context.Context, db.DeleteUserCriteria) (SayGoodbyeSerializer, error)
}

Our services will then hold reference to a serializer rather than a reference to the database itself. The serializer will be held responsible for delegating the call to the database, and returning the response in a wrapper which implements the required serializer interface. It is inside this wrapper where the mapping from database response to service response is defined.

然后,我们的服务将保留对serializer的引用,而不是对database本身的引用。 序列化器将负责将调用委派给数据库,并在实现所需序列化器接口的wrapper返回响应。 在此包装器内部,定义了从数据库响应到服务响应的映射。

package serializer


import (
	"context"
	"fmt"
  
	v1 "github.com/rokane/grpc-demo/pkg/api/greeter/v1"
	v2 "github.com/rokane/grpc-demo/pkg/api/greeter/v2"
	db "github.com/rokane/grpc-demo/pkg/database"
)


// Wraps around the database repsonse and implements the 
// SayHelloSerializer interface.
type registerDetailsWrapper struct {
	*db.RegisterDetailsResp
}


// ToV1 returns a message type suitable to return for the greeter v1 service
func (w *registerDetailsWrapper) ToV1() (*v1.SayHelloResponse, error) {
	if w.Exists {
		return &v1.SayHelloResponse{
			Message: fmt.Sprintf("Welcome back %s", w.User.FirstName),
		}, nil
	}
	return &v1.SayHelloResponse{
		Message: fmt.Sprintf("Nice to meet you %s", w.User.FirstName),
	}, nil
}


// ToV1 returns a message type suitable to return for the greeter v2 service
func (w *registerDetailsWrapper) ToV2() (*v2.SayHelloResponse, error) {
	if w.Exists {
		return &v2.SayHelloResponse{
			Message: fmt.Sprintf("Welcome back %s %s", w.User.FirstName, w.User.LastName),
		}, nil
	}
	return &v2.SayHelloResponse{
		Message: fmt.Sprintf("Nice to meet you %s %s", w.User.FirstName, w.User.LastName),
	}, nil
}


// RegisterDetails delgates the call to the database and wraps the response.
func (dbs *dbserializer) RegisterDetails(ctx context.Context, criteria db.RegisterDetailsCriteria) (SayHelloSerializer, error) {
	resp, err := dbs.storer.RegisterDetails(ctx, criteria)
	if err != nil {
		return nil, err
	}
	return ®isterDetailsWrapper{resp}, nil
}

Our service then calls what appears to be the same interface as the database layer, but has a response which exposes a set of methods allowing it to map to the response needed to be sent across the wire.

然后,我们的服务将调用与数据库层相同的接口,但是具有一个响应,该响应公开了一组方法,使其可以映射到需要通过网络发送的响应。

package v1


import (
	"context"
	db "github.com/rokane/grpc-demo/pkg/database"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"log"


	v1 "github.com/rokane/grpc-demo/pkg/api/greeter/v1"
)


func (s *service) SayHello(ctx context.Context, req *v1.SayHelloRequest) (*v1.SayHelloResponse, error) {
	criteria := db.RegisterDetailsCriteria{
		First: req.Name,
	}
	log.Printf("processing SayHello request: %v", req)


	resp, err := s.serializer.RegisterDetails(ctx, criteria)
	if err != nil {
		return nil, status.Error(codes.Internal, "unable to register user")
	}
	return resp.ToV1()
}

(Conclusion)

Structuring your project like this will set you up for success when the time arises that you have to support another version of your service, or introduce another service to your gRPC server.

当您不得不支持另一版本的服务或将其他服务引入gRPC服务器时,以这种方式构造项目将使您获得成功。

A single database layer can be defined which serves the needs of all the services within your project. Instead of rewriting this layer, simply extend the interfaces inside the serializer package to provide transformations to your new version or service.

可以定义一个数据库层,以满足项目中所有服务的需求。 无需重写此层,只需扩展序列化程序包内的接口即可提供对新版本或服务的转换。

You will find yourself spending less time untangling the code as your project evolves in size and complexity, and more time building out your services by extending and implementing the already defined interfaces.

随着项目规模和复杂性的发展,您将发现花费更少的时间来整理代码,而通过扩展和实现已经定义的接口来花费更多的时间来构建服务。

Finally, if you would like to take a deeper look into the code accompanying this post, feel free to clone the repo below. Also feel free to leave any comments for ideas / suggestions you might have, or if you find anything wrong with the example code, thanks!

最后,如果您想更深入地了解本文附带的代码,请随时克隆以下存储库。 也可以随意发表任何意见或建议,或者如果您发现示例代码有任何问题,谢谢!

https://github.com/rokane/grpc-demo

https://github.com/rokane/grpc-demo

翻译自: https://medium.com/swlh/building-apis-with-grpc-and-go-9a6d369d7ce

grpc api