Go工程師體系課 003【學習筆記】

grpc

  • grpc
  • grpc-go

grpc 無縫整合了 protobuf

protobuf

  • 習慣用 Json、XML 資料儲存格式的你們,相信大多都沒聽過 Protocol Buffer。
  • Protocol Buffer 其實是 Google 出品的一種輕量 & 高效的結構化資料儲存格式,效能比 Json、XML 真的強!太!多!
  • protobuf 經歷了 protobuf2 和 protobuf3,pb3 比 pb2 簡化了很多,目前主流的版本是 pb3。

protoc 的下載

下載 protoc 和go get github.com/golang/protobuf/protoc-gen-go

syntax = "proto3";

option go_package = "."; //這名不寫現在編譯不了

message HelloRequest {
  string name = 1; //1 是編號不是值
}
protoc --proto_path=. \
  --go_out=. \
  --go-grpc_out=. \
  ./helloworld.proto

會在目錄中產生一個helloworld.pb.go的檔案

壓縮比的對比

package main

import (
 "RpcLearn/protobuf"
 "encoding/json"
 "fmt"
 "github.com/golang/protobuf/proto"
)

// TIP <p>To run your code, right-click the code and select <b>Run</b>.</p> <p>Alternatively, click
// the <icon src="AllIcons.Actions.Execute"/> icon in the gutter and select the <b>Run</b> menu item from here.</p>
type Hello struct {
 Name string `json:"name"`
}

func main() {
 //TIP <p>Press <shortcut actionId="ShowIntentionActions"/> when your caret is at the underlined text
 // to see how GoLand suggests fixing the warning.</p><p>Alternatively, if available, click the lightbulb to view possible fixes.</p>
 //s := "gopher"
 //fmt.Printf("Hello and welcome, %s!\n", s)
 //
 //for i := 1; i <= 5; i++ {
 // //TIP <p>To start your debugging session, right-click your code in the editor and select the Debug option.</p> <p>We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
 // // for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.</p>
 // fmt.Println("i =", 100/i)
 //}
 jsonStruct := Hello{
  Name: "gopher",
 }
 jsonReq, _ := json.Marshal(jsonStruct)
 fmt.Println(string(jsonReq))
 fmt.Println(len(jsonReq))
 req := __.HelloRequest{
  Name: "gopher",
 }
 rsp, _ := proto.Marshal(&req)
 fmt.Println(rsp)
 fmt.Println(len(rsp))

}

差不多兩倍的差異

Go 語言中使用 Protobuf 的變化說明

教學內容 目前實際
以前只有一個 .pb.go 檔案 現在產生兩個檔案:一個 .pb.go(資料結構),一個 _grpc.pb.go(gRPC 介面)
外掛程式不分離 外掛程式已拆分為 protoc-gen-goprotoc-gen-go-grpc,需要分別安裝和呼叫

📌 目前行為說明

從 Go 的 Protobuf 外掛程式 v1.20+ 開始,gRPC 支援被拆分為獨立的外掛程式 protoc-gen-go-grpc,用於產生服務介面相關程式碼。這種方式更模組化,職責更清晰。

因此,執行以下指令:

protoc --proto_path=. \
  --go_out=. \
  --go-grpc_out=. \
  ./your_file.proto

將會產生:

  • your_file.pb.go: 包含訊息結構(如 Request、Response)
  • your_file_grpc.pb.go: 包含服務介面定義(如 YourServiceServer)

建議保留並同步這兩個檔案。

簡單梳理一下 grpc 開啟的流程

  1. 建立存根檔案及方法定義,然後編譯
// proto/helloworld.proto
syntax = "proto3";
option go_package = ".;proto";

service Greeter{
    rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
    string name = 1;
}
message HelloReply {
    string message = 1;
}
  1. server 端實作定義方法的呼叫,注意 server 實作(鴨子型別)
package main

import (
 "RpcLearn/grpc_test/proto"
 "context"
 "google.golang.org/grpc"
 "log"
 "net"
)

type Server struct {
 proto.UnimplementedGreeterServer // 👈 重點:嵌入這個型別
}

func (s *Server) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
 reply := &proto.HelloReply{
  Message: "Hello " + req.Name,
 }
 return reply, nil
}

func main() {
 g := grpc.NewServer()                     // 建立一個新的gRPC伺服器
 proto.RegisterGreeterServer(g, &Server{}) // 註冊服務
 listener, err := net.Listen("tcp", ":9090")
 if err != nil {
  log.Fatalf("failed to listen: %v", err)
 }
 err = g.Serve(listener)
 if err != nil {
  log.Fatalf("failed to serve: %v", err)
 }
}
  1. 用戶端呼叫實作
package main

import (
 "RpcLearn/grpc_test/proto"
 "context"
 "fmt"
 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials/insecure"
)

func main() {
 // Create a new gRPC server
 //conn, err := grpc.Dial("localhost:9090", grpc.WithInsecure())
 // 使用安全連線
 //creds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
 conn, err := grpc.NewClient("localhost:9090",
  //grpc.WithInsecure()
  grpc.WithTransportCredentials(insecure.NewCredentials()), // ✅ 新方式
 )
 if err != nil {
  panic(err)
 }
 defer conn.Close()
 c := proto.NewGreeterClient(conn)
 r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "world"})
 if err != nil {
  panic(err)
 }
 fmt.Println(r.Message)

}

grpc 的 stream 方式(串流模式)

  1. 簡單模式(simple RPC)
  2. 伺服器端的資料流
  3. 用戶端的資料流
  4. 雙向的

集中學習 grpc

Protobuf 型別 Go 型別
double float64
float float32
int32 int32
int64 int64
uint32 uint32
uint64 uint64
sint32 int32
sint64 int64
fixed32 uint32
fixed64 uint64

預設值

預設值說明

在 Protobuf 中,不同欄位型別有以下預設值:

  • 字串型別(string):預設值是一個空字串。
  • 位元組型別(bytes):預設值是一個空的位元組序列。
  • 布林型別(bool):預設值是 false
  • 數值型別(數字型別):預設值為 0
  • 列舉型別:預設值是第一個定義的列舉值,且必須是 0
  • 訊息型別(message):預設值是未設定(即 null)。

注意事項

  1. 對於純量訊息欄位:一旦訊息被解析後,無法區分欄位是否被設定為預設值(例如布林值是否為 false)還是欄位根本未被設定。
  2. 布林值的使用建議:避免定義布林值的預設值為 false,因為可能會導致觸發無意的行為。
  3. 純量訊息的序列化:如果一個純量訊息欄位被設定為預設值,該值不會被序列化傳輸。

可以透過參考 Generated Code Guide 來更詳細地瞭解 Protobuf 預設值的處理方式。


列舉型別的定義

在需要為訊息型別的某個欄位指定一組「預定義值序列」時,可以使用列舉型別。每個列舉值都對應一個整數值。

範例

下面是一個列舉的定義範例:

message SearchRequest {
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 1;
}
  • 伺服器端資料流(取得即時資料)
  • 用戶端資料流(上報資料)
  • 雙向資料流
 syntax = "proto3";

option go_package = ".;proto";

service Greeter {
    rpc GetStream (StreamReqData) returns (stream StreamResData); // 伺服器端串流模式
    rpc PutStream (stream StreamReqData) returns ( StreamResData); // 用戶端串流模式
    rpc AllStream (stream StreamReqData) returns (stream StreamResData); // 雙向串流模式
}

message StreamReqData {
    string data = 1;
}

message StreamResData {
    string message = 1;
}

編譯產生 Go 的 protobuf

protoc --proto_path=. \
  --go_out=. \
  --go-grpc_out=. \
  ./helloworld.proto

伺服器端程式碼

package main

import (
 "RpcLearn/stream_grpc_test/proto"
 "fmt"
 "google.golang.org/grpc"
 "log"
 "net"
 "sync"
 "time"
)

const PORT = ":50052"

type server struct {
 proto.UnimplementedGreeterServer // 👈 重點:嵌入這個型別
}

func (s server) GetStream(data *proto.StreamReqData, res proto.Greeter_GetStreamServer) error {
 //TODO implement me
 //panic("implement me")
 i := 0
 for {
  i++
  _ = res.Send(&proto.StreamResData{
   Message: fmt.Sprintf("%v", time.Now().Unix()),
  })
  time.Sleep(time.Second)
  if i >= 10 {
   break
  }
 }
 return nil
}

func (s server) PutStream(cliStr proto.Greeter_PutStreamServer) error {
 //TODO implement me
 //panic("implement me")//
 for {
  if a, err := cliStr.Recv(); err != nil {
   fmt.Println(err)
   break
  } else {
   fmt.Println(a)
  }

 }
 return nil
}

func (s server) AllStream(allStr proto.Greeter_AllStreamServer) error {
 //TODO implement me
 //panic("implement me")
 wg := sync.WaitGroup{}
 wg.Add(2)
 go func() {
  defer wg.Done()
  for {
   if a, err := allStr.Recv(); err != nil {
    fmt.Println(err)
    break
   } else {
    fmt.Println("用戶端傳送的訊息:", a)
   }
  }

 }()
 go func() {
  defer wg.Done()
  for {
   allStr.Send(&proto.StreamResData{
    Message: fmt.Sprintf("我是伺服器端資料:%v\n", time.Now().Unix()),
   })
   time.Sleep(time.Second)
  }

 }()
 wg.Wait()
 return nil
}

func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if (!ok) {
        // handle missing metadata
    }
    // do something with metadata
    return &pb.SomeResponse{}, nil
}

func main() {
 // 建立一個新的gRPC伺服器
 g := grpc.NewServer()
 proto.RegisterGreeterServer(g, &server{}) // 註冊服務
 listener, err := net.Listen("tcp", PORT)
 if err != nil {
  log.Fatalf("failed to listen: %v", err)
 }
 err = g.Serve(listener)
 if err != nil {
  log.Fatalf("failed to serve: %v", err)
 }
}

用戶端程式碼

package main

import (
 "RpcLearn/stream_grpc_test/proto"
 "context"
 "fmt"
 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials/insecure"
 "sync"
 "time"
)

func main() {
 // Create a new gRPC server
 conn, err := grpc.("localhost:50052",
  //grpc.WithInsecure()
  grpc.WithTransportCredentials(insecure.NewCredentials()), // ✅ 新方式
 )
 if err != nil {
  panic(err)
 }
 defer conn.Close()
 c := proto.NewGreeterClient(conn)
 //res, _ := c.GetStream(context.Background(), &proto.StreamReqData{Data: "慕課網"})
 //for {
 // r, err := res.Recv()
 // if err != nil {
 //  break
 // }
 // fmt.Println(r)
 //}
 //// 用戶端串流模式
 //putS, _ := c.PutStream(context.Background())
 //i := 0
 //for {
 // i++
 // putS.Send(&proto.StreamReqData{Data: fmt.Sprintf("慕課網%v", i)})
 // time.Sleep(time.Second)
 // if i >= 10 {
 //  break
 // }
 //}

 //雙向串流模式
 allStr, _ := c.AllStream(context.Background())
 wg := sync.WaitGroup{}
 wg.Add(2)
 go func() {
  defer wg.Done()
  for {
   if a, err := allStr.Recv(); err != nil {
    fmt.Println(err)
    break
   } else {
    fmt.Println("我是用戶端:%v", a)
   }
  }
 }()
 go func() {
  defer wg.Done()
  i := 0
  for {
   i++
   allStr.Send(&proto.StreamReqData{Data: fmt.Sprintf("雙向的慕課網:%v", i)})
   time.Sleep(time.Second)
   if i >= 10 {
    break
   }
  }
 }()
 wg.Wait()
}

Protobuf 型別與 Go 型別對應關係(含編碼建議)

.proto Type 描述說明 Go Type
double 浮點型別 float64
float 浮點型別 float32
int32 使用變長編碼,對負值效率低;如有可能為負數,建議用 sint32 替代 int32
int64 使用變長編碼 int64
uint32 使用變長編碼 uint32
uint64 使用變長編碼 uint64
sint32 使用變長編碼,對負值更高效(比 uint32 更好) int32
sint64 使用變長編碼,對負值更高效 int64
fixed32 始終佔 4 位元組,適用於總是比 2²²⁸ 大的值,比 uint32 更高效 uint32
fixed64 始終佔 8 位元組,適用於總是比 2²⁵⁶ 大的值,比 uint64 更高效 uint64
sfixed32 始終佔 4 位元組 int32
sfixed64 始終佔 8 位元組 int64
bool 布林值 bool
string 必須是 UTF-8 或 ASCII 編碼的文字 string
bytes 可包含任意順序的位元組資料 []byte

option go_package

編譯產生目錄的指定

如何在一個 proto 中引入其他 proto

import "base.proto"

import "google/protobuf/empty.proto"

巢狀的 message

列舉型別

enum Gender{
  MALE = 0;
  FEMALE = 1;
}

map 型別 map<string,string> mp = 4

如何使用時間戳

message HelloRequest {
  string name = 1; // 姓名 相當於文件
  string url = 2;
  Gender g = 3;
  map<string, string> mp = 4;
  google.protobuf.Timestamp addTime = 5;
}

grpc 的更多功能

gRPC 讓我們可以像本地呼叫一樣實現遠端呼叫,對於每一次的 RPC 呼叫中,都可能會有一些有用的資料,而這些資料就可以透過 metadata 來傳遞。metadata 是以 key-value 的形式儲存資料的,其中 key 是 string 型別,而 value 是[]string,即一個字串陣列型別。metadata 使得 client 和 server 能夠為對方提供關於本次呼叫的一些資訊,就像一次 http 請求的 RequestHeader 和 ResponseHeader 一樣。http 中 header 的生命週期是一 http 請求,那麼 metadata 的生命週期就是一次 RPC 呼叫。

metadata

不是直接侵入到業務程式碼中的

// 新建
type MD mp[string][]string
// 建立
// 第一種方式
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})

// 第二種方式 key 不區分大小寫,會被統一轉成小寫。
md := metadata.Pairs(
    "key1", "val1",
    "key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
    "key2", "val2",
)

傳送 metadata

md := metadata.Pairs("key", "val")

// 新建一個有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)

// 單向 RPC
response, err := client.SomeRPC(ctx, someRequest)

接收 metadata

func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        // handle missing metadata
    }
    // do something with metadata
    return &pb.SomeResponse{}, nil
}

grpc 的攔截器

伺服器端和用戶端的攔截器

package main

import (
 "RpcLearn/grpc_test/proto"
 "context"
 "google.golang.org/grpc"
 "log"
 "net"
)

type Server struct {
 proto.UnimplementedGreeterServer // 👈 重點:嵌入這個型別
}

func (s *Server) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
 reply := &proto.HelloReply{
  Message: "Hello " + req.Name,
 }
 return reply, nil
}

func main() {
 interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
  log.Println("Request:接收到一個新的請求", req)
  resp, err = handler(ctx, req)
  if err != nil {
   log.Println("Error:", err)
  }
  log.Println("Response:處理這個新請求完成", resp)
  return resp, err
 }
 opt := grpc.UnaryInterceptor(interceptor)
 g := grpc.NewServer(opt)                  // 建立一個新的gRPC伺服器
 proto.RegisterGreeterServer(g, &Server{}) // 註冊服務
 listener, err := net.Listen("tcp", ":9090")
 if err != nil {
  log.Fatalf("failed to listen: %v", err)
 }
 err = g.Serve(listener)
 if err != nil {
  log.Fatalf("failed to serve: %v", err)
 }
}
package main

import (
 "RpcLearn/grpc_test/proto"
 "context"
 "fmt"
 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials/insecure"
 "time"
)

func main() {
 // Create a new gRPC server
 //conn, err := grpc.Dial("localhost:9090", grpc.WithInsecure())
 // 使用安全連線
 //creds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
 // 添加攔截器
 clientInterceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
  start := time.Now()
  err := invoker(ctx, method, req, reply, cc, opts...)
  fmt.Println("耗時", time.Since(start), err)
  return err
 }
 opt := grpc.WithUnaryInterceptor(clientInterceptor)
 conn, err := grpc.NewClient("localhost:9090",
  //grpc.WithInsecure()
  grpc.WithTransportCredentials(insecure.NewCredentials()), // ✅ 新方式
  opt,
 )
 if err != nil {
  panic(err)
 }
 defer conn.Close()
 c := proto.NewGreeterClient(conn)
 r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "world"})
 if err != nil {
  panic(err)
 }
 fmt.Println(r.Message)

}

主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/4772

(0)
Walker的頭像Walker
上一篇 2025年11月25日 03:00
下一篇 2025年11月25日 01:00

相關推薦

簡體中文 繁體中文 English
歡迎🌹 Coding never stops, keep learning! 💡💻 光臨🌹