溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么用Golang構(gòu)建gRPC服務(wù)

發(fā)布時(shí)間:2021-12-15 09:41:52 來源:億速云 閱讀:163 作者:小新 欄目:云計(jì)算

這篇文章主要介紹怎么用Golang構(gòu)建gRPC服務(wù),文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們一定要看完!

為什么使用gRPC

我們的示例是一個(gè)簡單的路線圖應(yīng)用,客戶端可以獲取路線特征信息、創(chuàng)建他們的路線摘要,還可以與服務(wù)器或者其他客戶端交換比如交通狀態(tài)更新這樣的路線信息。

借助gRPC,我們可以在.proto文件中定義我們的服務(wù),并以gRPC支持的任何語言來實(shí)現(xiàn)客戶端和服務(wù)器,客戶端和服務(wù)器又可以在從服務(wù)器到你自己的平板電腦的各種環(huán)境中運(yùn)行-gRPC還會為你解決所有不同語言和環(huán)境之間通信的復(fù)雜性。我們還獲得了使用protocol buffer的所有優(yōu)點(diǎn),包括有效的序列化(速度和體積兩方面都比JSON更有效率),簡單的IDL(接口定義語言)和輕松的接口更新。

安裝

安裝grpc包

首先需要安裝gRPC golang版本的軟件包,同時(shí)官方軟件包的examples目錄里就包含了教程中示例路線圖應(yīng)用的代碼。

$ go get google.golang.org/grpc

然后切換到grpc-go/examples/route_guide:目錄:

$ cd $GOPATH/src/google.golang.org/grpc/examples/route_guide

安裝相關(guān)工具和插件

  • 安裝protocol buffer編譯器 安裝編譯器最簡單的方式是去https://github.com/protocolbuffers/protobuf/releases 下載預(yù)編譯好的protoc二進(jìn)制文件,倉庫中可以找到每個(gè)平臺對應(yīng)的編譯器二進(jìn)制文件。這里我們以Mac Os為例,從https://github.com/protocolbuffers/protobuf/releases/download/v3.6.0/protoc-3.6.0-osx-x86_64.zip 下載并解壓文件。 更新PATH系統(tǒng)變量,或者確保protoc放在了PATH包含的目錄中了。

  • 安裝protoc編譯器插件

$ go get -u github.com/golang/protobuf/protoc-gen-go

編譯器插件protoc-gen-go將安裝在$GOBIN中,默認(rèn)位于$GOPATH/bin。編譯器protoc必須在$PATH中能找到它:

$ export PATH=$PATH:$GOPATH/bin

定義服務(wù)

首先第一步是使用protocol buffer定義gRPC服務(wù)還有方法的請求和響應(yīng)類型,你可以在下載的示例代碼examples/route_guide/routeguide/route_guide.proto中看到完整的.proto文件。

要定義服務(wù),你需要在.proto文件中指定一個(gè)具名的service

service RouteGuide {
   ...
}

然后在服務(wù)定義中再來定義rpc方法,指定他們的請求和響應(yīng)類型。gRPC允許定義四種類型的服務(wù)方法,這四種服務(wù)方法都會應(yīng)用到我們的RouteGuide服務(wù)中。

  • 一個(gè)簡單的RPC,客戶端使用存根將請求發(fā)送到服務(wù)器,然后等待響應(yīng)返回,就像普通的函數(shù)調(diào)用一樣。

// 獲得給定位置的特征
rpc GetFeature(Point) returns (Feature) {}
  • 服務(wù)器端流式RPC,客戶端向服務(wù)器發(fā)送請求,并獲取流以讀取回一系列消息??蛻舳藦姆祷氐牧髦凶x取,直到?jīng)]有更多消息為止。如我們的示例所示,可以通過將stream關(guān)鍵字放在響應(yīng)類型之前來指定服務(wù)器端流方法。

//獲得給定Rectangle中可用的特征。結(jié)果是
//流式傳輸而不是立即返回
//因?yàn)榫匦慰赡軙采w較大的區(qū)域并包含大量特征。
rpc ListFeatures(Rectangle) returns (stream Feature) {}
  • 客戶端流式RPC,其中客戶端使用gRPC提供的流寫入一系列消息并將其發(fā)送到服務(wù)器。客戶端寫完消息后,它將等待服務(wù)器讀取所有消息并返回其響應(yīng)。通過將stream關(guān)鍵字放在請求類型之前,可以指定客戶端流方法。

// 接收路線上被穿過的一系列點(diǎn)位, 當(dāng)行程結(jié)束時(shí)
// 服務(wù)端會返回一個(gè)RouteSummary類型的消息.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
  • 雙向流式RPC,雙方都使用讀寫流發(fā)送一系列消息。這兩個(gè)流是獨(dú)立運(yùn)行的,因此客戶端和服務(wù)器可以按照自己喜歡的順序進(jìn)行讀寫:例如,服務(wù)器可以在寫響應(yīng)之前等待接收所有客戶端消息,或者可以先讀取消息再寫入消息,或其他一些讀寫組合。每個(gè)流中的消息順序都會保留。您可以通過在請求和響應(yīng)之前都放置stream關(guān)鍵字來指定這種類型的方法。

//接收路線行進(jìn)中發(fā)送過來的一系列RouteNotes類型的消息,同時(shí)也接收其他RouteNotes(例如:來自其他用戶)
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

我們的.proto文件中也需要所有請求和響應(yīng)類型的protocol buffer消息類型定義。比如說下面的Point消息類型:

// Points被表示為E7表示形式中的經(jīng)度-緯度對。
//(度數(shù)乘以10 ** 7并四舍五入為最接近的整數(shù))。
// 緯度應(yīng)在+/- 90度范圍內(nèi),而經(jīng)度應(yīng)在
// 范圍+/- 180度(含)
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

生成客戶端和服務(wù)端代碼

接下來要從我們的.proto服務(wù)定義生成gRPC客戶端和服務(wù)端的接口。我們使用protoc編譯器和上面安裝的編譯器插件來完成這些工作:

在示例route_guide的目錄下運(yùn)行:

protoc -I routeguide/ routeguide/route_guide.proto --go_out=plugins=grpc:routeguide

運(yùn)行命令后會在示例route_guide目錄的routeguide目錄下生成route_guide.pb.go文件。

pb.go文件里面包含:

  • 用于填充、序列化和檢索我們定義的請求和響應(yīng)消息類型的所有protocol buffer代碼。

  • 一個(gè)客戶端存根用來讓客戶端調(diào)用RouteGuide服務(wù)中定義的方法。

  • 一個(gè)需要服務(wù)端實(shí)現(xiàn)的接口類型RouteGuideServer,接口類型中包含了RouteGuide服務(wù)中定義的所有方法。

創(chuàng)建gRPC服務(wù)端

首先讓我們看一下怎么創(chuàng)建RouteGuide服務(wù)器。有兩種方法來讓我們的RouteGuide服務(wù)工作:

  • 實(shí)現(xiàn)我們從服務(wù)定義生成的服務(wù)接口:做服務(wù)實(shí)際要做的事情。

  • 運(yùn)行一個(gè)gRPC服務(wù)器監(jiān)聽客戶端的請求然后把請求派發(fā)給正確的服務(wù)實(shí)現(xiàn)。 你可以在剛才安裝的gPRC包的grpc-go/examples/route_guide/server/server.go找到我們示例中RouteGuide`服務(wù)的實(shí)現(xiàn)代碼。下面讓我們看看他是怎么工作的。

實(shí)現(xiàn)RouteGuide

如你所見,實(shí)現(xiàn)代碼中有一個(gè)routeGuideServer結(jié)構(gòu)體類型,它實(shí)現(xiàn)了protoc編譯器生成的pb.go文件中定義的RouteGuideServer接口。

type routeGuideServer struct {
        ...
}
...

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
        ...
}
...

func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
        ...
}
...

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
        ...
}
...

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
        ...
}
...

普通PRC

routeGuideServer實(shí)現(xiàn)我們所有的服務(wù)方法。首先,讓我們看一下最簡單的類型GetFeature,它只是從客戶端獲取一個(gè)Point,并從其Feature數(shù)據(jù)庫中返回相應(yīng)的Feature信息。

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
    for _, feature := range s.savedFeatures {
        if proto.Equal(feature.Location, point) {
            return feature, nil
        }
    }
    // No feature was found, return an unnamed feature
    return &pb.Feature{"", point}, nil
}

這個(gè)方法傳遞了RPC上下文對象和客戶端的Point protocol buffer請求消息,它在響應(yīng)信息中返回一個(gè)Feature類型的protocol buffer消息和錯(cuò)誤。在該方法中,我們使用適當(dāng)?shù)男畔⑻畛?code>Feature,然后將其返回并返回nil錯(cuò)誤,以告知gRPC我們已經(jīng)完成了RPC的處理,并且可以將Feature返回給客戶端。

服務(wù)端流式RPC

現(xiàn)在,讓我們看一下服務(wù)方法中的一個(gè)流式RPC。 ListFeatures是服務(wù)器端流式RPC,因此我們需要將多個(gè)Feature發(fā)送回客戶端。

func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
    for _, feature := range s.savedFeatures {
        if inRange(feature.Location, rect) {
            if err := stream.Send(feature); err != nil {
                return err
            }
        }
    }
    return nil
}

如你所見,這次我們沒有獲得簡單的請求和響應(yīng)對象,而是獲得了一個(gè)請求對象(客戶端要在其中查找FeatureRectangle)和一個(gè)特殊的RouteGuide_ListFeaturesServer對象來寫入響應(yīng)。

在該方法中,我們填充了需要返回的所有Feature對象,并使用Send()方法將它們寫入RouteGuide_ListFeaturesServer。最后,就像在簡單的RPC中一樣,我們返回nil錯(cuò)誤來告訴gRPC我們已經(jīng)完成了響應(yīng)的寫入。如果此調(diào)用中發(fā)生任何錯(cuò)誤,我們將返回非nil錯(cuò)誤; gRPC層會將其轉(zhuǎn)換為適當(dāng)?shù)腞PC狀態(tài),以在線上發(fā)送。

客戶端流式RPC

現(xiàn)在,讓我們看一些更復(fù)雜的事情:客戶端流方法RecordRoute,從客戶端獲取點(diǎn)流,并返回一個(gè)包含行程信息的RouteSummary。如你所見,這一次該方法根本沒有request參數(shù)。相反,它獲得一個(gè)RouteGuide_RecordRouteServer流,服務(wù)器可以使用該流來讀取和寫入消息-它可以使用Recv()方法接收客戶端消息,并使用SendAndClose()方法返回其單個(gè)響應(yīng)。

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
    var pointCount, featureCount, distance int32
    var lastPoint *pb.Point
    startTime := time.Now()
    for {
        point, err := stream.Recv()
        if err == io.EOF {
            endTime := time.Now()
            return stream.SendAndClose(&pb.RouteSummary{
                PointCount:   pointCount,
                FeatureCount: featureCount,
                Distance:     distance,
                ElapsedTime:  int32(endTime.Sub(startTime).Seconds()),
            })
        }
        if err != nil {
            return err
        }
        pointCount++
        for _, feature := range s.savedFeatures {
            if proto.Equal(feature.Location, point) {
                featureCount++
            }
        }
        if lastPoint != nil {
            distance += calcDistance(lastPoint, point)
        }
        lastPoint = point
    }
}

在方法主體中,我們使用RouteGuide_RecordRouteServerRecv()方法不停地讀取客戶端的請求到一個(gè)請求對象中(在本例中為Point),直到?jīng)]有更多消息為止:服務(wù)器需要要在每次調(diào)用后檢查從Recv()返回的錯(cuò)誤。如果為nil,則流仍然良好,并且可以繼續(xù)讀??;如果是io.EOF,則表示消息流已結(jié)束,服務(wù)器可以返回其RouteSummary。如果錯(cuò)誤為其他值,我們將返回錯(cuò)誤“原樣”,以便gRPC層將其轉(zhuǎn)換為RPC狀態(tài)。

雙向流式RPC

最后讓我們看一下雙向流式RPC方法RouteChat()

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
    for {
        in, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        key := serialize(in.Location)

        s.mu.Lock()
        s.routeNotes[key] = append(s.routeNotes[key], in)
        // Note: this copy prevents blocking other clients while serving this one.
        // We don't need to do a deep copy, because elements in the slice are
        // insert-only and never modified.
        rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
        copy(rn, s.routeNotes[key])
        s.mu.Unlock()

        for _, note := range rn {
            if err := stream.Send(note); err != nil {
                return err
            }
        }
    }
}

這次,我們得到一個(gè)RouteGuide_RouteChatServer流,就像在客戶端流示例中一樣,該流可用于讀取和寫入消息。但是,這次,當(dāng)客戶端仍在向其消息流中寫入消息時(shí),我們會向流中寫入要返回的消息。

此處的讀寫語法與我們的客戶端流式傳輸方法非常相似,不同之處在于服務(wù)器使用流的Send()方法而不是SendAndClose(),因?yàn)榉?wù)器會寫入多個(gè)響應(yīng)。盡管雙方總是會按照對方的寫入順序來獲取對方的消息,但是客戶端和服務(wù)器都可以以任意順序進(jìn)行讀取和寫入-流完全獨(dú)立地運(yùn)行(意思是服務(wù)器可以接受完請求后再寫流,也可以接收一條請求寫一條響應(yīng)。同樣的客戶端可以寫完請求了再讀響應(yīng),也可以發(fā)一條請求讀一條響應(yīng))

啟動服務(wù)器

一旦實(shí)現(xiàn)了所有方法,我們還需要啟動gRPC服務(wù)器,以便客戶端可以實(shí)際使用我們的服務(wù)。以下代碼段顯示了如何啟動RouteGuide服務(wù)。

flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
        log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{})
... // determine whether to use TLS
grpcServer.Serve(lis)

為了構(gòu)建和啟動服務(wù)器我們需要:

  • 指定要監(jiān)聽客戶端請求的接口lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))。

  • 使用grpc.NewServer()創(chuàng)建一個(gè)gRPC server的實(shí)例。

  • 使用gRPC server注冊我們的服務(wù)實(shí)現(xiàn)。

  • 使用我們的端口詳細(xì)信息在服務(wù)器上調(diào)用Serve()進(jìn)行阻塞等待,直到進(jìn)程被殺死或調(diào)用Stop()為止。

創(chuàng)建客戶端

在這一部分中我們將為RouteGuide服務(wù)創(chuàng)建Go客戶端,你可以在grpc-go/examples/route_guide/client/client.go 看到完整的客戶端代碼。

創(chuàng)建客戶端存根

要調(diào)用服務(wù)的方法,我們首先需要?jiǎng)?chuàng)建一個(gè)gRPC通道與服務(wù)器通信。我們通過把服務(wù)器地址和端口號傳遞給grpc.Dial()來創(chuàng)建通道,像下面這樣:

conn, err := grpc.Dial(*serverAddr)
if err != nil {
    ...
}
defer conn.Close()

如果你請求的服務(wù)需要認(rèn)證,你可以在grpc.Dial中使用DialOptions設(shè)置認(rèn)證憑證(比如:TLS,GCE憑證,JWT憑證)--不過我們的RouteGuide服務(wù)不需要這些。

設(shè)置gRPC通道后,我們需要一個(gè)客戶端存根來執(zhí)行RPC。我們使用從.proto生成的pb包中提供的NewRouteGuideClient方法獲取客戶端存根。

client := pb.NewRouteGuideClient(conn)

生成的pb.go文件定義了客戶端接口類型RouteGuideClient并用客戶端存根的結(jié)構(gòu)體類型實(shí)現(xiàn)了接口中的方法,所以通過上面獲取到的客戶端存根client可以直接調(diào)用下面接口類型中列出的方法。

type RouteGuideClient interface {
    GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error)

    ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error)

    RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error)
    RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error)
}

每個(gè)實(shí)現(xiàn)方法會再去請求gRPC服務(wù)端相對應(yīng)的方法獲取服務(wù)端的響應(yīng),比如:

func (c *routeGuideClient) GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) {
    out := new(Feature)
    err := c.cc.Invoke(ctx, "/routeguide.RouteGuide/GetFeature", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

RouteGuideClient接口的完整實(shí)現(xiàn)可以在生成的pb.go文件里找到。

調(diào)用服務(wù)的方法

現(xiàn)在讓我們看看如何調(diào)用服務(wù)的方法。注意在gRPC-Go中,PRC是在阻塞/同步模式下的運(yùn)行的,也就是說RPC調(diào)用會等待服務(wù)端響應(yīng),服務(wù)端將返回響應(yīng)或者是錯(cuò)誤。

普通RPC

調(diào)用普通RPC方法GetFeature如同直接調(diào)用本地的方法。

feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})
if err != nil {
        ...
}

如你所見,我們在之前獲得的存根上調(diào)用該方法。在我們的方法參數(shù)中,我們創(chuàng)建并填充一個(gè)protocol buffer對象(在本例中為Point對象)。我們還會傳遞一個(gè)context.Context對象,該對象可讓我們在必要時(shí)更改RPC的行為,例如超時(shí)/取消正在調(diào)用的RPC(cancel an RPC in flight)。如果調(diào)用沒有返回錯(cuò)誤,則我們可以從第一個(gè)返回值中讀取服務(wù)器的響應(yīng)信息。

服務(wù)端流式RPC

這里我們會調(diào)用服務(wù)端流式方法ListFeatures,方法返回的流中包含了地理特征信息。如果你讀過上面的創(chuàng)建客戶端的章節(jié),這里有些東西看起來會很熟悉--流式RPC在兩端實(shí)現(xiàn)的方式很類似。

rect := &pb.Rectangle{ ... }  // initialize a pb.Rectangle
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
    ...
}
for {
    feature, err := stream.Recv()
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatalf("%v.ListFeatures(_) = _, %v", client, err)
    }
    log.Println(feature)
}

和簡單RPC調(diào)用一樣,調(diào)用時(shí)傳遞了一個(gè)方法的上下文和一個(gè)請求。但是我們?nèi)』氐氖且粋€(gè)RouteGuide_ListFeaturesClient實(shí)例而不是一個(gè)響應(yīng)對象??蛻舳丝梢允褂?code>RouteGuide_ListFeaturesClient流讀取服務(wù)器的響應(yīng)。

我們使用RouteGuide_ListFeaturesClientRecv()方法不停地將服務(wù)器的響應(yīng)讀入到一個(gè)protocol buffer響應(yīng)對象中(本例中的Feature對象),直到?jīng)]有更多消息為止:客戶端需要在每次調(diào)用后檢查從Recv()返回的錯(cuò)誤err。如果為nil,則流仍然良好,并且可以繼續(xù)讀取;如果是io.EOF,則消息流已結(jié)束;否則就是一定RPC錯(cuò)誤,該錯(cuò)誤會通過err傳遞給調(diào)用程序。

客戶端流式RPC

客戶端流方法RecordRoute與服務(wù)器端方法相似,不同之處在于,我們僅向該方法傳遞一個(gè)上下文并獲得一個(gè)RouteGuide_RecordRouteClient流,該流可用于寫入和讀取消息。

// 隨機(jī)的創(chuàng)建一些Points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
    points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
stream, err := client.RecordRoute(context.Background())// 調(diào)用服務(wù)中定義的客戶端流式RPC方法
if err != nil {
    log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)
}
for _, point := range points {
    if err := stream.Send(point); err != nil {// 向流中寫入多個(gè)請求消息
        if err == io.EOF {
            break
        }
        log.Fatalf("%v.Send(%v) = %v", stream, point, err)
    }
}
reply, err := stream.CloseAndRecv()// 從流中取回服務(wù)器的響應(yīng)
if err != nil {
    log.Fatalf("%v.CloseAndRecv() got error %v, want %v", stream, err, nil)
}
log.Printf("Route summary: %v", reply)

RouteGuide_RecordRouteClient有一個(gè)Send()。我們可以使用它發(fā)送請求給服務(wù)端。一旦我們使用Send()寫入流完成后,我們需要在流上調(diào)用CloseAndRecv()方法讓gRPC知道我們已經(jīng)完成了請求的寫入并且期望得到一個(gè)響應(yīng)。我們從CloseAndRecv()方法返回的err中可以獲得RPC狀態(tài)。如果狀態(tài)是nil,CloseAndRecv()的第一個(gè)返回值就是一個(gè)有效的服務(wù)器響應(yīng)。

雙向流式RPC

最后,讓我們看一下雙向流式RPC RouteChat()。與RecordRoute一樣,我們只向方法傳遞一個(gè)上下文對象,然后獲取一個(gè)可用于寫入和讀取消息的流。但是,這一次我們在服務(wù)器仍將消息寫入消息流的同時(shí),通過方法的流返回值。

stream, err := client.RouteChat(context.Background())
waitc := make(chan struct{})
go func() {
    for {
        in, err := stream.Recv()
        if err == io.EOF {
            // read done.
            close(waitc)
            return
        }
        if err != nil {
            log.Fatalf("Failed to receive a note : %v", err)
        }
        log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
    }
}()
for _, note := range notes {
    if err := stream.Send(note); err != nil {
        log.Fatalf("Failed to send a note: %v", err)
    }
}
stream.CloseSend()
<-waitc

除了在完成調(diào)用后使用流的CloseSend()方法外,此處的讀寫語法與我們的客戶端流方法非常相似。盡管雙方總是會按照對方的寫入順序來獲取對方的消息,但是客戶端和服務(wù)器都可以以任意順序進(jìn)行讀取和寫入-兩端的流完全獨(dú)立地運(yùn)行。

啟動應(yīng)用

要編譯和運(yùn)行服務(wù)器,假設(shè)你位于$GOPATH/src/google.golang.org/grpc/examples/route_guide文件夾中,只需:

$ go run server/server.go

同樣,運(yùn)行客戶端:

$ go run client/client.go

你將看到如下輸出:

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>
Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 >
name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 >
...
name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 >
Traversing 56 points.
Route summary: point_count:56 distance:497013163
Got message First message at point(0, 1)
Got message Second message at point(0, 2)
Got message Third message at point(0, 3)
Got message First message at point(0, 1)
Got message Fourth message at point(0, 1)
Got message Second message at point(0, 2)
Got message Fifth message at point(0, 2)
Got message Third message at point(0, 3)
Got message Sixth message at point(0, 3)

以上是“怎么用Golang構(gòu)建gRPC服務(wù)”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對大家有幫助,更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI