go grpc高級用法
錯誤處理
gRPC 一般不在 message 中定義錯誤。畢竟每個 gRPC 服務本身就帶一個 error 的返回值,這是用來傳輸錯誤的專用通道。gRPC 中所有的錯誤返回都應該是 nil 或者 由 status.Status 產(chǎn)生的一個error。這樣error可以直接被調(diào)用方Client識別。
常規(guī)用法
當遇到一個go錯誤的時候,直接返回是無法被下游client識別的。
恰當?shù)淖龇ㄊ?/strong>:
調(diào)用 status.New 方法,并傳入一個適當?shù)腻e誤碼,生成一個 status.Status 對象
調(diào)用該 status.Err 方法生成一個能被調(diào)用方識別的error,然后返回
st := status.New(codes.NotFound, “some description”)
err := st.Err()
傳入的錯誤碼是 codes.Code 類型。
此外還有更便捷的辦法:使用 status.Error。它避免了手動轉(zhuǎn)換的操作。
err := status.Error(codes.NotFound, "some description")
進階用法
上面的錯誤有個問題,就是 code.Code 定義的錯誤碼只有固定的幾種,無法詳盡地表達業(yè)務中遇到的錯誤場景。
gRPC 提供了在錯誤中補充信息的機制:status.WithDetails 方法
Client 通過將 error 重新轉(zhuǎn)換位 status.Status ,就可以通過 status.Details 方法直接獲取其中的內(nèi)容。
status.Detials 返回的是個slice, 是interface{}的slice,然而go已經(jīng)自動做了類型轉(zhuǎn)換,可以通過斷言直接使用。
服務端示例
- 生成一個 status.Status 對象
- 填充錯誤的補充信息
// 生成一個 status.Status st := status.New(codes.ResourceExhausted, "Request limit exceeded.") // 填充錯誤的補充信息 WithDetails ds, err := st.WithDetails( &epb.QuotaFailure{ Violations: []*epb.QuotaFailure_Violation{{ Subject: fmt.Sprintf("name:%s", in.Name), Description: "Limit one greeting per person", }}, }, ) if err != nil { return nil, st.Err() } return nil, ds.Err()
客戶端的示例
- 調(diào)用RPC錯誤后,解析錯誤信息
- 通過斷言直接獲取錯誤詳情
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"}) // 調(diào)用 RPC 如果遇到錯誤就對錯誤處理 if err != nil { // 轉(zhuǎn)換錯誤 s := status.Convert(err) // 解析錯誤信息 for _, d := range s.Details() { // 通過斷言直接使用 switch info := d.(type) { case *epb.QuotaFailure: log.Printf("Quota failure: %s", info) default: log.Printf("Unexpected type: %s", info) } } }
原理
這個錯誤是如何傳遞給調(diào)用方Client的呢?
是放到 metadata中的,而metadata是放到HTTP的header中的。
metadata是key:value格式的數(shù)據(jù)。錯誤的傳遞中,key是個固定值:grpc-status-details-bin。
而value,是被proto編碼過的,是二進制安全的。
目前大多數(shù)語言都實現(xiàn)了這個機制。
多路復用
同一臺服務器上的多個RPC服務的多路復用,比如同時保存一個訂單的存根、一個歡迎的存根因為多個RPC服務運行在一個服務端上,所以客戶端的多個存根之間是可以共享gRPC連接的
服務端代碼
func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } grpcServer := grpc.NewServer() // 注冊進訂單服務 ordermgt_pb.RegisterOrderManagementServer(grpcServer, &orderMgtServer{}) // 注冊進歡迎服務 hello_pb.RegisterGreeterServer(grpcServer, &helloServer{}) }
客戶端代碼
func main() { conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() // 訂單服務建立實例連接 orderManagementClient := pb.NewOrderManagementClient(conn) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() order1 := pb.Order{Id: "101", Items:[]string{"iPhone XS", "Mac Book Pro"}, Destination:"San Jose, CA", Price:2300.00} res, addErr := orderManagementClient.AddOrder(ctx, &order1) // 歡迎服務建立實例連接 helloClient := hwpb.NewGreeterClient(conn) hwcCtx, hwcCancel := context.WithTimeout(context.Background(), time.Second) defer hwcCancel() helloResponse, err := helloClient.SayHello(hwcCtx, &hwpb.HelloRequest{Name: "gRPC Up and Running!"}) fmt.Println("Greeting: ", helloResponse.Message) }
元數(shù)據(jù)
在多個微服務的調(diào)用當中,信息交換常常是使用方法之間的參數(shù)傳遞的方式,但是在有些場景下,一些信息可能和 RPC 方法的業(yè)務參數(shù)沒有直接的關(guān)聯(lián),所以不能作為參數(shù)的一部分,在 gRPC 中,可以使用元數(shù)據(jù)來存儲這類信息。
元數(shù)據(jù)創(chuàng)建
// 方法1 md := metadata.Pairs( "1", "v1", "1", "v2", // 方法1會把相同的鍵的字段合并,[ ]string{"v1","v2"} "2", "v3", ) // 方法2 md := metadata.New(map[string]string{"1":"v1","2":"v2"})
客戶端收發(fā)
在context中設置的元數(shù)據(jù)會轉(zhuǎn)換成線路層的gRPC頭信息和 trailer
客戶端發(fā)送這些頭信息,收件方會以頭信息的形式接收他們
// 創(chuàng)建元數(shù)據(jù) md := metadata.Pairs( "timestamp", time.Now().Format(time.StampNano), "kn", "vn", ) // 創(chuàng)建新元數(shù)據(jù)的上下文,這種方法會替換掉已有的上下文 mdCtx := metadata.NewOutgoingContext(context.Background(), md) // 這種方法是將元數(shù)據(jù)附加到已有的上下文 ctxA := metadata.AppendToOutgoingContext(mdCtx, "k1", "v1", "k1", "v2", "k2", "v3") // 定義頭信息和 trailer,可以用來接收元數(shù)據(jù) var header, trailer metadata.MD order1 := pb.Order{Id: "101", Items: []string{"iPhone XS", "Mac Book Pro"}, Destination: "San Jose, CA", Price: 2300.00} res, _ := client.AddOrder(ctxA, &order1, grpc.Header(&header), grpc.Trailer(&trailer)) log.Print("AddOrder Response -> ", res.Value) // 獲取頭信息 head, err := res.Header() // 獲取trailer trail, err := res.Trailer()
服務端收發(fā)
// 從上下文中獲取元數(shù)據(jù)列表 md, metadataAvailable := metadata.FromIncomingContext(ctx) if !metadataAvailable { return nil, status.Errorf(codes.DataLoss, "UnaryEcho: failed to get metadata") } // 操作元數(shù)據(jù)邏輯 if t, ok := md["timestamp"]; ok { fmt.Printf("timestamp from metadata:\n") for i, e := range t { fmt.Printf("====> Metadata %d. %s\n", i, e) } } // 創(chuàng)建元數(shù)據(jù) header := metadata.New(map[string]string{"location": "San Jose", "timestamp": time.Now().Format(time.StampNano)}) // 發(fā)送頭信息 grpc.SendHeader(ctx, header) trailer := metadata.Pairs("status","ok") // 設置trailer grpc.SetTrailer(ctx,trailer)
負載均衡
負載均衡器代理
也就是說后端的結(jié)構(gòu)對gRPC客戶端是不透明的,客戶端只需要知道均衡器的斷點就可以了,比如NGINX代理、Envoy代理
客戶端負載均衡
func main(){ roundrobinConn, err := grpc.Dial( address, grpc.WithBalancerName("round_robin"), // 指定負載均衡的算法 // 默認是"pick_first",也就是從服務器列表中第一個服務端開始嘗試發(fā)送請求,成功則后續(xù)所有RPC都發(fā)往這個服務器 // "round_robin"輪詢調(diào)度算法,連接所有地址,每次向后端發(fā)送一個RPC grpc.WithInsecure(), ) if err != nil { log.Fatalf("did not connect: %v", err) } defer roundrobinConn.Close() // 起10個RPC調(diào)度任務 makeRPCs(roundrobinConn, 10) } func makeRPCs(cc *grpc.ClientConn, n int) { hwc := ecpb.NewEchoClient(cc) for i := 0; i < n; i++ { callUnary(hwc) } } func callUnary(c ecpb.EchoClient) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() }
壓縮數(shù)據(jù)
在服務端會對已注冊的壓縮器自動解碼,響應時自動編碼
始終從客戶端獲取指定的壓縮方法,如果沒被注冊就會返回Unimplemented
func main() { conn, err := grpc.Dial(address, grpc.WithInsecure()) defer conn.Close() client := pb.NewOrderManagementClient(conn) ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5) defer cancel() order1 := pb.Order{Id: "101", Items:[]string{"iPhone XS", "Mac Book Pro"}, Destination:"San Jose, CA", Price:2300.00} // 通過 grpc.UseCompressor(gzip.Name) 就可以輕松壓縮數(shù)據(jù) res, _ := client.AddOrder(ctx, &order1, grpc.UseCompressor(gzip.Name)) }
到此這篇關(guān)于go grpc高級用法的文章就介紹到這了,更多相關(guān)go grpc內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于go手動寫個轉(zhuǎn)發(fā)代理服務的代碼實現(xiàn)
這篇文章主要介紹了基于go手動寫個轉(zhuǎn)發(fā)代理服務的代碼實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-02-02Golang并發(fā)讀取文件數(shù)據(jù)并寫入數(shù)據(jù)庫的項目實踐
本文主要介紹了Golang并發(fā)讀取文件數(shù)據(jù)并寫入數(shù)據(jù)庫的項目實踐,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-06-06