竞技新闻网 互联网 我做了一个 Go 语言的微服务工具包

我做了一个 Go 语言的微服务工具包

原标题:我做了一个 Go 语言的微服务工具包

作者 | George Francis Jr

译者 | 刘雅梦

策划 | 田晓旭

多年以来,我一直认为自己是一名语言无关的软件开发人员,因为在编程语言方面,我总是把掌握基础知识和学习新概念放在首位,而不是“玩最爱”。在我 15 年的职业生涯中,我已经用多种语言(例如 Java、Scala、Go 等)编写了数千行代码。直到我精通 Go 之后,我才意识到:选择正确的语言很重要。我成为了一名真正的忠实主义者;今天,它无疑是我最喜欢的语言。它的简单、优雅以及强大的并发范式使其非常适用于下一代的分布式服务。

为了表达我对这种语言的热爱,我开发了一个工具包,以帮助希望使用 Go 来增强微服务的其他开发人员。

1REST + gRPC: 打造完美的婚姻

微服务通常由 HTTP 或 RPC 框架(如 REST 和 gRPC)支持。

REST 来自于人们熟悉的面向实体(entry) 设计——设计方法是 HTTP 协议的一 构建块。CRUD(Create、Read、Update、Delete)操作定义了实体的一组行为。REST API 使用 HTTP 方法的子集在通常表示 / 序列化为 JSON 的实体上执行 CRUD 操作。

gRPC 是一个高性能的 RPC 框架(备注:RPC API 允许开发人员访问分布式的过程或方法,这些过程或方法在语法上与集中式的过程或方法没有区别,从而隐藏了通过网络进行数据序列化 / 传输的复杂性)。它提供了客户端、服务端和双向流。

在底层,gRPC 使用 HTTP/2(用于传输)和 Protocol Buffers(用于高效的序列化)来实现比 REST+JSON 更高的性能。它为代码自动生成提供了一流的支持。protobuf 编译器生成客户端和服务端的代码,从而促进了应用程序的快速开发,并减少了发布新服务所需的工作量。

通过将 REST+gRPC 相结合,我们可以创建高性能的分布式服务,为客户提供双向访问模式,同时还能保留面向实体设计方法的优点。

下面是上述介绍的一个示例,在这个例子中,我们首先定义了一个 gRPC 服务,使用 protobuf 规范以面向实体的方式操作 orders。使用 order 作为实体,我们需要定义该实体能够支持的服务,即与 CRUD 操作相对应的 RPC 方法。我们将添加一个额外的 RPC 方法 List,以支持列出 / 过滤现有的订单。

syntax = “proto3”; package orders; import “google/protobuf/timestamp.proto”; // 使用 CRUD + List rpc 方法定义 Order 服务 service OrderService {

// 创建订单rpc Create (CreateOrderRequest) returns (CreateOrderResponse);

// 检索现有的订单rpc Retrieve (RetrieveOrderRequest) returns (RetrieveOrderResponse);

// 修改现有订单rpc Update (UpdateOrderRequest) returns (UpdateOrderResponse);

// 删除现有订单rpc Delete (DeleteOrderRequest) returns (DeleteOrderResponse);

// 现有订单的 List 列表rpc List (ListOrderRequest) returns (ListOrderResponse);}// 订单详细信息的 message(这是我们的实体)message Order {// 订单可能存在的状态enum Status {PENDING = 0;PAID = 1;SHIPPED = 2;DELIVERED = 3;CANCELLED = 4;}int64 order_id = 1;repeated Item items = 2;float total = 3;google.protobuf.Timestamp order_date = 5;Status status = 6;}// 支付信息的 messagemessage PaymentMethod {enum Type {NOT_DEFINED = 0;VISA = 1;MASTERCARD = 2;PAYPAL = 3;APPLEPAY = 4;}Type payment_type = 1;string pre_authorization_token = 2; }// 包含在订单中的商品的详细信息的 messagemessage Item {string deion = 1;float price = 2;}// 创建订单的请求message CreateOrderRequest {repeated Item items = 1;PaymentMethod payment_method = 2;}// 订单创建的响应message CreateOrderResponse {Order order = 1;}// 检索订单的请求message RetrieveOrderRequest {int64 order_id = 1;}// 检索订单的响应message RetrieveOrderResponse {Order order = 1;}// 更新现有订单的请求message UpdateOrderRequest {int64 order_id = 1;repeated Item items = 2;PaymentMethod payment_method = 3;}// 更新现有订单的响应message UpdateOrderResponse {Order order = 1;}// 删除现有订单的请求message DeleteOrderRequest {int64 order_id = 1;repeated Item items = 2;}// 删除现有订单的响应message DeleteOrderResponse {Order order = 1;}// 获取现有订单列表的请求message ListOrderRequest {repeated int64 ids = 1;Order.Status statuses = 2;}// 获取现有订单列表的响应message ListOrderResponse {repeated Order order = 1;}

order.proto 接下来,我们使用带有必要 Go 选项的protoc来编译order.proto。

编译 order.proto

运行上面的命令将生成两个文件:order.pb.go和order_grpc.pb.go。order.pb.go包含了针对order.proto中定义的每种 protobuf 的message类型的结构体。

Order 的结构体(生成的代码)

order_grpc.pb.go提供了用于与订单服务交互的客户端 / 服务端代码。这个文件中包括了OrderServiceServer——OrderService的接口转换(为了与“婚姻”进行类比,可以将它看作是司仪)。

OrderServiceServer 接口(生成的代码)

为了启动并运行 gRPC 服务,我们需要实现OrderServiceServer接口。在本练习中,我们可以使用UnimplementedOrderServiceServer(生成的代码中提供的基本的实现)。

UnimplementedOrderServiceServer(生成的代码)

RegisterOrderServiceServer方法接受grpc.Server以及OrderServiceServer接口;此方法基于我们订单服务接口实现封装了一个grpc.Server,并且必须要在调用服务的Serve方法之前调用它。请参见下面的示例。

import(“log””net””google.golang.org/grpc”)const (grpcPort = “50051”)func main {grpcServer := grpc.NewServerorderService := UnimplementedOrderServiceServer{}RegisterOrderServiceServer(grpcServer, orderService)lis, err := net.Listen(“tcp”, “:” + grpcPort)if err != nil {log.Fatalf(“failed to listen: %v”, err)}if err := grpcServer.Serve(lis); err != nil {log.Fatalf(“failed to start gRPC server: %v”, err)}}

初始化 gRPC 服务

通过这个步骤,gRPC 订单服务只需要几行代码就可以完成了。最后一步是开发一个 REST 服务。通过将OrderServiceServer接口注入到 REST 服务,我们可以正式实现这种“联姻”。

import (“net/http””github.com/gin-gonic/gin””github.com/golang/protobuf/jsonpb””google.golang.org/grpc”)// RestServer 为订单服务实现了一个 REST 服务type RestServer struct {server *http.ServerorderService OrderServiceServer // 与我们注入到 gRPC 服务的订单服务相同}// NewRestServer 是一个创建 RestServer 的便捷函数func NewRestServer(orderService OrderServiceServer, port string) RestServer {rs := RestServer{server: http.Server{Addr: “:” + port,Handler: router,},orderService: orderService,}// 注册 routesrouter.POST(“/order”, rs.create)router.GET(“/order/:id”, rs.retrieve)router.PUT(“/order”, rs.update)router.DELETE(“/order”, rs.delete)router.GET(“/order”, rs.list)return rs}// Start 启动服务器func (r RestServer) Start error {return r.server.ListenAndServe}// create 是一个处理函数,它根据订单请求创建订单 (JSON 主体)func (r RestServer) create(c *gin.Context) {var req CreateOrderRequest// unmarshal 订单请求err := jsonpb.Unmarshal(c.Request.Body, req)if err != nil {c.String(http.StatusInternalServerError, “error creating order request”)}// 根据请求,使用订单服务创建订单resp, err := r.orderService.Create(c.Request.Context, req)if err != nil {c.String(http.StatusInternalServerError, “error creating order”)}m := jsonpb.Marshaler{}if err := m.Marshal(c.Writer, resp); err != nil {c.String(http.StatusInternalServerError, “error sending order response”)}}func (r RestServer) retrieve(c *gin.Context) {c.String(http.StatusNotImplemented, “not implemented yet”)}func (r RestServer) update(c *gin.Context) {c.String(http.StatusNotImplemented, “not implemented yet”)}func (r RestServer) delete(c *gin.Context) {c.String(http.StatusNotImplemented, “not implemented yet”)}func (r RestServer) list(c *gin.Context) {c.String(http.StatusNotImplemented, “not implemented yet”)}

嵌入订单服务接口的 REST 服务示例

最后,更新main方法,将 REST + gRPC 结合起来。

import(“log””net””google.golang.org/grpc”)const (grpcPort = “50051”restPort = “8080”)func main {grpcServer := grpc.NewServerorderService := UnimplementedOrderServiceServer{}RegisterOrderServiceServer(grpCServer, orderService)lis, err := net.Listen(“tcp”, “:” + grpcPort)if err != nil {log.Fatalf(“failed to listen: %v”, err)}go func {

// Serve 是一个阻塞调用,因此需要将这个调用加入到 goroutine 中grpcServer.Serve(lis)}

restServer := NewRestServer(orderService, restPort)// Start 也在阻塞,但这是可以的,因为我们需要一个阻塞调用来防止 main 突然退// 出。我们很快就会重构这个逻辑!restServer.Start

}

使用服务接口统一 REST + gRPC 服务

现在,都使用相同的订单服务实现来启动并运行 gRPC 和 REST 服务了。请注意,我们可以对上面的代码片段进行一些优化,因为它涉及到了错误处理、并发、可读性等。稍后我们将解决这些问题。

如上所述,gRPC 框架提供了丰富的 protobuf 工具,可促进应用程序的快速开发,使开发人员能够生成客户端 / 服务端代码,包括可用于将 gRPC 与 REST 或其他 HTTP API 结合使用的服务接口。

2并发:Goroutines Channels

Goroutine 是与其他函数并发执行的函数。可以将它们视为不会阻塞当前执行线程的后台进程。在后台,这些轻量级的线程被多路复用到一个或多个(n:1)操作系统线程(OS threads)。这样一来,Go 程序可以处理数百万个 goroutine,而 Javafuture 可以处理的线程数量将会受到可用 OS 线程数的限制(因为 Java 线程与 OS 线程的比例是 1:1)。这种性能优势的注意事项是,Go 线程共享内存空间,并且必须同步访问该内存空间(这对于 Java 开发人员来说应该很熟悉)。这里 channel 可以从自由竞争状态和死锁的地狱中拯救我们。

Channel 是基本类型的管道(你可以把它们视为邮箱),它允许 goroutine 在没有互斥锁的情况下安全地来回共享数据。通道读 / 写 阻塞) 当前执行线程,直到发送方或接收方准备就绪为止。

下面是可能会使用 goroutine 的一些常见任务。

应用程序任务:运行 Web 服务端、DB 连接池、守护程序、API 轮询、数据处理队列 请求 / 事件任务:处理传入的 HTTP 请求,执行昂贵的子任务(例如多个网络调用)来完成请求,向 Kafka 发布新消息 即发即弃(Fire Forget)任务:日志记录、报警、度量指标

阻塞当前执行线程,直到服务端完成服务请求为止。如果你想了解 Go 的 HTTP 服务端是如何处理请求的,请签出源码(TL;DR,为每个传入的 HTTP 请求生成一个 goroutine)。

由于 grpcServer.Serve 和 restServer.Start 都是阻塞调用,因此在 main 执行线程中只能执行其中的一个调用。另一个必须在后台执行。REST 和 gRPC 服务的 start/serve 方法也会返回错误,我们需要优雅地处理这些错误。(关于此技巧的快速提示:将每个服务包装在一个暴露错误通道的结构体中。调用 goroutine 中的 start/serve 方法,将错误写入错误通道。这允许我们使用 select 来等待多个通道操作的执行完成)。

以下代码演示了如何优化 REST 和 gRPC 服务以进行后台处理和基于通道的错误传播。

import (“net/http””github.com/gin-gonic/gin””github.com/golang/protobuf/jsonpb””google.golang.org/grpc”)// RestServer 为订单服务实现了一个 REST 服务。type RestServer struct {server *http.ServerorderService OrderServiceServer // 与我们注入 gRPC 服务端的订单服务相同errCh chan error}// NewRestServer 是一个创建 RestServer 的便捷函数func NewRestServer(orderService OrderServiceServer, port string) RestServer {router := gin.Defaultrs := RestServer{server: http.Server{Addr: “:” + port,Handler: router,},orderService: orderService,errCh: make(chan error),}// 注册路由router.POST(“/order”, rs.create)router.GET(“/order/:id”, rs.retrieve)router.PUT(“/order”, rs.update)router.DELETE(“/order”, rs.delete)router.GET(“/order”, rs.list)return rs}// Start 在后台启动 REST 服务,将错误推入错误通道func (r RestServer) Start {go func {r.errCh – r.server.ListenAndServe}}// Stop 停止服务func (r RestServer) Stop error {return r.server.Close}// Error 返回服务端的错误通道func (r RestServer) Error chan error {return r.errCh}

重构 RestServer

import (“net””google.golang.org/grpc”)// GrpcServer 为订单服务实现 gRPC 服务type GrpcServer struct {server *grpc.ServererrCh chan errorlistener net.Listener}//NewGrpcServer 是一个创建 GrpcServer 的便捷函数func NewGrpcServer(service OrderServiceServer, port string) (GrpcServer, error) {lis, err := net.Listen(“tcp”, “:”+port)if err != nil {return GrpcServer{}, err}server := grpc.NewServerRegisterOrderServiceServer(server, service)return GrpcServer{server: server,listener: lis,errCh: make(chan error),}, nil}// Start 在后台启动服务,将任何错误传入错误通道func (g GrpcServer) Start {go func {g.errCh – g.server.Serve(g.listener)}}// Stop 停止 gRPC 服务func (g GrpcServer) Stop {g.server.GracefulStop}//Error 返回服务的错误通道func (g GrpcServer) Error chan error {return g.errCh}

GrpcServer

切记将 Go 应用视为实体。开发人员通常可以编写出可靠的服务级代码,然后使用大量条件log.Fatal语句和其他难以理解的逻辑来填充其main方法。

考虑为应用程序创建一个包含配置、服务端和其他应用程序级依赖的结构体。尽管 Go 提供了创建多个 init 函数的能力,但是应该尽量避免使用init。init函数有一些缺点,其中包括返回值为空。具体来说,Go 运行时(runtime) 将查找具有以下签名的包级函数

这意味着你不能从init函数中返回值。如果你试图初始化一个变量并且发生了错误,你可能会被迫 panic、退出应用程序或写入recover逻辑。初始化函数会使代码更难理解。相反,可以尝试创建自己的自定义构造函数,比如创建一个新应用程序、执行所有必要的应用程序初始化并返回应用程序的函数。如果在应用程序初始化过程中可能发生错误,只需更改函数的返回签名即可返回应用程序的实例和错误。

下面是main的优化版本,它为应用程序创建一个结构体,使用select来监听 REST 和 gRPC 服务的错误,并处理应用程序的启动 / 关闭(包括操作系统的终止信号)。

import (“log””os””os/signal””syscall”)const (grpcPort = “50051”restPort = “8080”)//app 是一个便捷的封装,用于启动和关闭订单微服务所需的所有东西type app struct {restServer RestServergrpcServer GrpcServer/* Listens for an application termination signalEx. (Ctrl X, Docker container shutdown, etc) */shutdownCh chan os.Signal}// start 在后台启动 REST 和 gRPC 服务func (a app) start {a.restServer.Start // non blocking nowa.grpcServer.Start // also non blocking :-)}// stop 关闭服务func (a app) shutdown error {a.grpcServer.Stopreturn a.restServer.Stop}// newApp 使用 REST 和 gRPC 服务创建一个新的应用程序// 这个函数执行所有与应用程序相关的初始化func newApp (app, error) {orderService := UnimplementedOrderServiceServer{}gs, err := NewGrpcServer(orderService, grpcPort) if err != nil {return app{}, err}quit := make(chan os.Signal, 1)signal.Notify(quit, os.Interrupt, syscall.SIGTERM)

return app{restServer: NewRestServer(orderService, restPort),grpcServer: gs,shutdownCh: quit,}, nil}// 运行启动应用程序,处理任何 REST 或 gRPC 服务的错误以及任何关机的信号func run error {app, err := newAppif err != nil {return err}app.startdefer app.shutdownselect {case restErr := -app.restServer.Error:return restErrcase grpcErr := -app.grpcServer.Error:return grpcErrcase -app.shutdownCh:return nil}}func main {if err := run; err != nil {log.Fatal(err)}}

重构 main

在创建或更新order之前,我们需要获取付款方式的预授权,并且我们应该确认要购买的商品是否有库存。假设这些子任务可能会出错(失败或超时),并且可以独立执行。处理请求级并发有几个选项。我们可以使用标准的 goroutine 和 channel,但也许还有更好的选择。

Waitgroups 允许我们启动一组 goroutine 并等待它们完成。waitGroup也可以工作,但它的职责是管理 waitGroup 计数器。ErrGroups 非常适合执行子任务集合。errGroup由一组执行子任务和处理错误传播的 goroutine 组成。errGroup等待(阻塞)直到所有子任务完成为止。

对传入和传出的服务请求使用 上下文(Context)。上下文允许跨客户端和服务端传播请求范围内的值、截止日期和取消信号。Context有一个Done通道,当Context被取消时,它可以通知 goroutine,允许它们提前退出并释放系统资源。当使用errgroup.WithContext时,如果第一次遇到子任务错误或第一次返回wait,则取消派生上下文。

在下面的示例中,validateOrder创建了一个errGroup,它派生出两个并发子任务,一个任务时preAuthorizePayment,另一个任务是checkInventory用于确认所有商品是否都有库存。在两个子任务中调用的函数都接受Context参数,并且在上下文取消(或请求超时)时能够提前返回。

import (“context””errors””time””golang.org/x/sync/errgroup”)var (ErrPreAuthorizationTimeout = errors.New(“pre-authorization request timeout”)ErrInventoryRequestTimeout = errors.New(“check inventory request timeout”)ErrItemOutOfStock = errors.New(“sorry one or more items in your order is out of stock”))// preAuthorizePayment 对支付方式进行预授权并返回错误。// 如果预先授权成功,则返回 nilfunc preAuthorizePayment(ctx context.Context, payment *PaymentMethod, orderAmount float32) error {// 在这里执行昂贵的授权逻辑——在这个例子中我们使用 sleep// 并返回 nil 来表示成功的授权timer := time.NewTimer(3 * time.Second)select {case -timer.C:return nilcase -ctx.Done:return ErrPreAuthorizationTimeout}}// checkInventory 返回一个布尔值和一个错误,表示是否所有商品是否都有库存//(true, nil) 表示所有商品都有库存并且没有遇到错误func checkInventory(ctx context.Context, items []*Item) (bool, error) {// 在这里执行昂贵的库存检查逻辑 – 在这个例子中我们使用 sleeptimer := time.NewTimer(2 * time.Second)select {case -timer.C:return true, nilcase -ctx.Done:return false, ErrInventoryRequestTimeout}}// getOrderTotal 计算订单总数func getOrderTotal(items []*Item) float32 {var total float32for _, item := range items {total += item.Price}return total}func validateOrder(ctx context.Context, items []*Item, payment *PaymentMethod) error {g, errCtx := errgroup.WithContext(ctx)g.Go(func error {return preAuthorizePayment(errCtx, payment, getOrderTotal(items))})g.Go(func error {itemsInStock, err := checkInventory(errCtx, items)if err != nil {return err}if !itemsInStock {return ErrItemOutOfStock}return nil})return g.Wait}

大多数仓库(和履约中心)都有订单管理系统,以实现高效、经济的订单履行。类似地,管理并发对于维持应用程序的质量至关重要。下面的示例使用waitgroup和channel来限制仓库一次可以处理的订单数量。

import (“fmt””sync””time”)// OrderDispatcher 是一个守护进程,它使用 sync 创建一个工作池。waitGroup 并发地// 处理和分发订单type OrderDispatcher struct {ordersCh chan *OrderorderLimit int // 并发处理的最大订单数}// NewOrderDispatcher 创建一个新的 OrderDispatcherfunc NewOrderDispatcher(orderLimit int, bufferSize int) OrderDispatcher {return OrderDispatcher{ordersCh: make(chan *Order, bufferSize), // initiliaze as a buffered channelorderLimit: orderLimit,}}// SubmitOrder 提交订单进行处理func (d OrderDispatcher) SubmitOrder(order *Order) {go func {d.ordersCh – order}}// Start 在后台启动调度程序func (d OrderDispatcher) Start {go d.processOrders}// Shutdown 通过关闭订单来关闭 OrderDispatcher// 注意:这个函数应该只在最后一个订单到达订单通道之后才执行。// 向一个封闭的通道提交命令会引起 panic。func (d OrderDispatcher) Shutdown {close(d.ordersCh)}// processOrders 使用“for range”和一个 sync.waitGroup 在后台处理所有传入的订单 func (d OrderDispatcher) processOrders {limiter := make(chan struct{}, d.orderLimit)var wg sync.WaitGroup// 连续地处理从订单通道接收到的订单// 当通道关闭时,此循环将终止for order := range d.ordersCh {limiter – struct{}{}wg.Add(1)go func(order *Order) {// TODO: 触发执行流程,将订单组装成一个包裹并发货,// 这里我们 sleep 并打印time.Sleep(50 * time.Millisecond)fmt.Printf(“Order (%v) has shipped \\n”, order) -limiterwg.Done}(order)}wg.Wait}func main {dispatcher := NewOrderDispatcher(3, 100)dispatcher.Startdefer dispatcher.Shutdowndispatcher.SubmitOrder( Order{Items: []*Item{{Deion: “iPhone Screen Protector”, Price: 9.99}}})dispatcher.SubmitOrder( Order{Items: []*Item{{Deion: “iPhone Case”, Price: 19.99}}})dispatcher.SubmitOrder( Order{Items: []*Item{{Deion: “Pixel Case”, Price: 14.99}}})dispatcher.SubmitOrder( Order{Items: []*Item{{Deion: “Bluetooth Speaker”, Price: 29.99}}})dispatcher.SubmitOrder( Order{Items: []*Item{{Deion: “4K Monitor”, Price: 159.99}}})dispatcher.SubmitOrder( Order{Items: []*Item{{Deion: “Inkjet Printer”, Price: 79.99}}})

time.Sleep(5 * time.Second) // 仅为了测试}

3有效的单元测试

在我早期的职业生涯(Java 时代),单元测试(unit testing) 让我想起了妈妈经常放在我餐盘里的蔬菜。小时候,我总是先吃好东西,然后偷偷地把蔬菜铲进垃圾桶里。换句话说,单元测试给我留下了不好的印象。这主要是因为它需要团队跟上新的 mock 框架的速度,这些框架通常很难理解,学习曲线很陡峭。更不用说,这些依赖于反射的嘲弄性框架了——正如 Rob Pike 曾经说过的那样,反射从来都不是清晰的。

然而,幸运的是,Go 改变了我对单元测试的看法。以下是我在测试过程中学到的一些技巧。

使用纯函数代替方法。纯函数是最容易测试的代码单元之一。纯函数是确定性的,不需要初始化就可以进行测试。方法是在类型(例如 struct)上定义的函数。为了测试一个方法,必须初始化它的父类型。参见下文。

// 要避免这种情况type OrderTotaler struct { items []*Item}// 这是一个方法。将它绑定到一个结构体上不会产生任何好处,// 因为在测试这个方法之前需要对结构体进行初始化func (t OrderTotaler) getOrderTotal float32 {var total float32for _, item := range t.items {total += item.Price}return total}// 这样做。这是一个纯函数func getOrderTotal(items []*Item) float32 {var total float32for _, item := range items {total += item.Price}return total}

方法 vs 纯函数(示例)

创建函数依赖。函数执行任务所需的任何外部依赖(DB、Web 服务调用、事件生成器等)都可以作为参数注入到函数中。具有嵌入式依赖的函数很难测试。开发人员通常通过使用能够在运行时(通过反射)更改(mock)外部依赖值的测试框架来绕过这种 代码味道。如果再看一下 validateOrder 函数(在上面的代码片段中),你可能会注意到它嵌入了外部依赖 preAuthorizePayment 和 verifyInventory。这个函数很难测试。因为 Go 支持一级函数——我们可以通过将 validateOrder 转换为 高阶函数 来解决这个问题。

var (ErrPreAuthorizationTimeout = errors.New(“pre-authorization request timeout”)ErrInventoryRequestTimeout = errors.New(“check inventory request timeout”)ErrItemOutOfStock = errors.New(“sorry one or more items in your order is out of stock”))// 为我们的外部依赖项创建别名type preAuthorizePaymentFunc func(context.Context, *PaymentMethod, float32) errortype checkInventoryFunc func (context.Context, []*Item) (bool, error)// 将依赖项作为参数传入到 validateOrder 中func validateOrder(ctx context.Context, items []*Item, payment *PaymentMethod,preAuthorizePayment preAuthorizePaymentFunc, checkInventory checkInventoryFunc) error {g, errCtx := errgroup.WithContext(ctx)g.Go(func error {return preAuthorizePayment(errCtx, payment, getOrderTotal(items))})g.Go(func error {itemsInStock, err := checkInventory(errCtx, items)if err != nil {return err}if !itemsInStock {return ErrItemOutOfStock}return nil})return g.Wait}

下面是将上述所有联系在一起的测试用例。

import (“context””errors””testing”)func TestVerifyOrder(t *testing.T) {ctx := context.BackgroundiphoneScreenProtector := Item{Deion: “iPhone Screen Protector”, Price: 9.99}iphoneCase := Item{Deion: “iPhone Case”, Price: 19.99}// function mock of external dependency #1preAuth := func(ctx context.Context, payment *PaymentMethod, amount float32) error {if amount = 0 || payment.PaymentType == PaymentMethod_UNDEFINED {return errors.New(“invalid pre authorization request”)}return nil}// function mock of external dependency #2checkInv := func(ctx context.Context, items []*Item) (bool, error) {if len(items) == 0 {return false, errors.New(“no items to check”)}if len(items) == 1 items[0] == iphoneScreenProtector {return true, nil}return false, nil}t.Run(“payment pre-authorization and inventory checks are successful”, func(t *testing.T) {visaPayment := PaymentMethod{PaymentType: PaymentMethod_VISA,PreAuthorizationToken: “fooBarToken”}// No mocking frameworks neededif err := validateOrder(ctx, []*Item{ iphoneScreenProtector}, visaPayment, preAuth, checkInv); err != nil {t.Error(“Expected nil, got “, err)}})t.Run(“error during payment pre-authorization”, func(t *testing.T) {invalidPayment := PaymentMethod{PaymentType: PaymentMethod_UNDEFINED,PreAuthorizationToken: “fooBarToken”}if err := validateOrder(ctx, []*Item{ iphoneScreenProtector}, invalidPayment, preAuth, checkInv); err == nil {t.Error(“Expected error, got nil”)}})t.Run(“item is out of stock”, func(t *testing.T) {visaPayment := PaymentMethod{PaymentType: PaymentMethod_VISA,PreAuthorizationToken: “fooBarToken”}if err := validateOrder(ctx, []*Item{ iphoneCase}, visaPayment, preAuth, checkInv); err == nil {t.Error(“Expected error, got nil”)}})// TODO determine what the other test cases are and write them :-)}

Mock 框架在用作工具而不是拐杖时非常有用。即使我们可以在没有第三方的情况下 mock 外部依赖,这些框架仍然能为单元测试繁琐地方(如执行测试断言)提供了价值。

对队友是友好的。正如 Rob Pike 所说的“清晰胜于聪明”,我总是鼓励开发人员在编写代码时要考虑到受众。清晰的代码易于编写,易于测试,并且应该易于开发人员(和非开发人员)理解。

原文链接:

https://levelup.gitconnected.com/the-golang-microservice-toolkit-7521516ee4b

赔偿9200万美元!TikTok与美国用户和解隐私诉讼

周鸿祎称微信靠摇妹子起家;华为东莞实验室爆炸损失3945万元;人社部回应延迟退休 | Q资讯

程序员的“黄金时代”,死去又重来?

放弃大厂高薪的程序员,涌进体制内

每周精要上线移动端,立刻订阅,你将获得

InfoQ 用户每周必看的精华内容集合:

资深技术编辑撰写或编译的 全球 IT 要闻;

一线技术专家撰写的 实操技术案例;

InfoQ 出品的 课程和 技术活动报名通道;

每周新鲜资讯

点个在看少个 bug👇

本文来自网络,不代表竞技新闻网立场,转载请注明出处:https://news.jingjims.com/hulianwang/2098.html

AAAI 2021闭幕,健壮、安全、高效的机器学习国际研讨会压轴举行

“蚂蚁呀嘿”爆红即凉!靠换脸上架、因安全下架

发表评论

您的电子邮箱地址不会被公开。

联系我们

联系我们

暂无

在线咨询: QQ交谈

邮箱: meiliao888@163.com

工作时间:周一至周五,10:00-19:00,节假日休息。
关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部