go grpc高級用法
錯誤處理
gRPC 一般不在 message 中定義錯誤。畢竟每個 gRPC 服務(wù)本身就帶一個 error 的返回值,這是用來傳輸錯誤的專用通道。gRPC 中所有的錯誤返回都應(yīng)該是 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è)務(wù)中遇到的錯誤場景。
gRPC 提供了在錯誤中補充信息的機制:status.WithDetails 方法
Client 通過將 error 重新轉(zhuǎn)換位 status.Status ,就可以通過 status.Details 方法直接獲取其中的內(nèi)容。
status.Detials 返回的是個slice, 是interface{}的slice,然而go已經(jīng)自動做了類型轉(zhuǎn)換,可以通過斷言直接使用。
服務(wù)端示例
- 生成一個 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)了這個機制。
多路復(fù)用
同一臺服務(wù)器上的多個RPC服務(wù)的多路復(fù)用,比如同時保存一個訂單的存根、一個歡迎的存根因為多個RPC服務(wù)運行在一個服務(wù)端上,所以客戶端的多個存根之間是可以共享gRPC連接的
服務(wù)端代碼
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
// 注冊進訂單服務(wù)
ordermgt_pb.RegisterOrderManagementServer(grpcServer, &orderMgtServer{})
// 注冊進歡迎服務(wù)
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()
// 訂單服務(wù)建立實例連接
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)
// 歡迎服務(wù)建立實例連接
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ù)
在多個微服務(wù)的調(diào)用當中,信息交換常常是使用方法之間的參數(shù)傳遞的方式,但是在有些場景下,一些信息可能和 RPC 方法的業(yè)務(wù)參數(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è)置的元數(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()
服務(wù)端收發(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")
// 設(shè)置trailer
grpc.SetTrailer(ctx,trailer)
負載均衡
負載均衡器代理
也就是說后端的結(jié)構(gòu)對gRPC客戶端是不透明的,客戶端只需要知道均衡器的斷點就可以了,比如NGINX代理、Envoy代理
客戶端負載均衡
func main(){
roundrobinConn, err := grpc.Dial(
address,
grpc.WithBalancerName("round_robin"), // 指定負載均衡的算法
// 默認是"pick_first",也就是從服務(wù)器列表中第一個服務(wù)端開始嘗試發(fā)送請求,成功則后續(xù)所有RPC都發(fā)往這個服務(wù)器
// "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)度任務(wù)
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ù)
在服務(wù)端會對已注冊的壓縮器自動解碼,響應(yīng)時自動編碼
始終從客戶端獲取指定的壓縮方法,如果沒被注冊就會返回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ā)代理服務(wù)的代碼實現(xiàn)
這篇文章主要介紹了基于go手動寫個轉(zhuǎn)發(fā)代理服務(wù)的代碼實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友們下面隨著小編來一起學(xué)習學(xué)習吧2019-02-02
Golang并發(fā)讀取文件數(shù)據(jù)并寫入數(shù)據(jù)庫的項目實踐
本文主要介紹了Golang并發(fā)讀取文件數(shù)據(jù)并寫入數(shù)據(jù)庫的項目實踐,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友們下面隨著小編來一起學(xué)習學(xué)習吧2022-06-06

