上篇我們介紹了 gRPC 的特性和運作原理,那今天就來介紹如何實際上手開發吧!
本操作皆是在 Mac 上執行
brew install protobuf
brew install protoc-gen-go-grpc protoc-gen-go
go mod init <your-module-name>
go get -u google.golang.org/grpc@latest
go get -u google.golang.org/protobuf@latest
這是我們本篇會需要撰寫的所有檔案,(深吸一口氣)那我們就準備開始吧!
./
├── api
│ ├── controller # 控制器
│ │ └── greeter_controller.go
├── bootstrap
│ ├── app.go # 核心應用
├── cmd
│ └── main.go # 啟動命令
├── domain # 領域層
│ └── greeter.go
├── proto # Protocol Buffers 相關
│ ├── demo.proto # .proto 定義文件
│ ├── demo.pb.go # 生成的 Protobuf 代碼
│ └── demo_grpc.pb.go # 生成的 gRPC 代碼
├── repository
│ └── greeter_repo.go
├── server
│ └── server.go # gRPC 服務端實作
├── client
│ └── client.go # gRPC 客戶端實作
├── usecase
│ └── greeter_usecase.go
├── go.mod
├── go.sum
└── Makefile # Makefile 文件
總共:10 個目錄,14 個檔案
*/proto/
目錄下撰寫 demo.proto
來去定義 Greeter 服務:syntax = "proto3";
package chat;
option go_package = "proto/chat";
// Request
message HelloRequest {
string name = 1;
}
// Response
message HelloReply {
string message = 1;
}
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
syntax = "proto3";
:是 Protocol Buffers 的第三版,簡化了定義資料結構的方式。proto3 是目前最常用的版本,提供更現代的語法特性。(如果想了解詳情可以查看底下的額外資料。)package chat;
:這行指定了這個.proto
檔案所屬的 package,在 gRPC 生成的 Go 程式碼(xxx.pb.go、xxx_grpc.pb.go)中,這個 package 名稱會用來組織生成的類別
或函式
。option go_package = "proto/chat";
:在生成 Go 程式碼時,gRPC 和 Protobuf 相關的程式碼應該放在哪個 Go package 中。確保這個專案能夠被正確引用。message HelloRequest {}
:這個定義了一個資料結構(類似於 Go 中的 struct)用於傳遞資料,名稱為HelloRequest
。在這個資料結構中,有一個屬性:
string name = 1;
:定義了一個字串類型的屬性name
,用來表示客戶端傳遞過來的名字。這裡的1
是屬性的字段編號,這個編號在 Protobuf 的序列化和反序列化過程中非常重要,如果要增加更多參數的話,就依序遞增編號就行嘍。service Greeter {}
:定義了一個 gRPC 服務 Greeter。在 gRPC 中,服務就是一個遠端呼叫的接口(類似於 API 的 endpoint)。這裡的 Greeter 服務提供了SayHello
方法。
- 輸入:接受一個名為
HelloRequest
的資料結構作為輸入。- 輸出:回傳一個名為
HelloReply
的資料結構作為回應。
Makefile
是一個用來定義專案的編譯規則的檔案,主要應用在自動化編譯和建置流程。它的核心概念來自於 Unix 的 make
工具,可以根據定義的指令(目標)來執行特定的操作,比如編譯程式、生成文件、清理舊的編譯結果等。Makefile
通常用來簡化並自動化重複性工作。
# 定義預設目標
all: server client
# 定義 Protocol Buffer 文件相關變數
PROTO_DIR=proto
PROTO_FILES=$(PROTO_DIR)/*.proto
# 系統架構,amd64 或 arm64
ARCH ?= arm64
# 根據 ARCH 定義不同的 proto-gen 目標
ifeq ($(ARCH), arm64)
proto-gen:
/opt/homebrew/bin/protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
$(PROTO_FILES)
else
proto-gen:
/usr/local/bin/protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
$(PROTO_FILES)
endif
#===================================================
# 構建服務器
server:
go build -o bin/server ./cmd
# 構建客戶端
client:
go build -o bin/client ./cmd
# 運行服務器,依賴 proto-gen 和 server
run-server: proto-gen server
./bin/server -mode=server
# 運行客戶端,依賴 proto-gen 和 client
run-client: proto-gen client
./bin/client -mode=client -name=BeeChat
# 定義虛擬目標
.PHONY: all proto-gen server client run-server run-client clean
# 清理構建產物
clean:
rm -rf bin/
我把它拆成2段這樣會比較好說明,那第一段的部分就是我們要讓寫好的
.proto
檔案能夠去生成以下兩個gRPC 的程式碼,並確保他們能放在*/proto/
目錄下:
chat.pb.go
:由 Protobuf 生成的序列化和反序列化相關的程式碼。chat_grpc.pb.go
:由 gRPC 生成的服務端和客戶端相關的程式碼。第二段的部分如果有看過第22天最後介紹的 Go Native 工具的話,應該猜得到他在幹嘛
go build -o bin/server ./cmd
就是用來編譯伺服器端的 Go 程式碼,並生成一個可執行檔放置到 bin/ 目錄,命名為 server。
./bin/server -mode=server
會運行編譯後的伺服器可執行檔,並傳遞-mode=server
參數。
all: server client
:當我執行make all
時,就會自動去執行server
跟client
指令的內容。那之所以放在最上方是因為當我直接執行make
時,他會自動去抓取第一個寫好的指令來做執行,所以有這樣的好處在。
.PHONY: proto-gen
:proto-gen
會被標記為虛擬目標,這意味著每次你執行 make proto-gen 時,Makefile 都會執行定義的指令,而不會去檢查檔案系統中是否已經存在名為 proto-gen 的檔案或目錄。
# Apple Silicon 指令
make proto-gen
# Intel 指令 (make 後面的順序不影響)
make proto-gen ARCH=amd64
小提示:因為我是在 Mac 上做生成,我電腦上的 cpu 是屬於
arm64
架構,所以前面有先宣告ARCH ?= arm64
,但如果是 Windows/Linux 可能就需要使用amd64
架構。那我要來去做運行時,可以直接使用make proto-gen
但如果是 Windows/Linux 的話則可以使用make proto-gen ARCH=amd64
來去做生成。
domain/greeter.go
中定義 Greeter 領域模型:package domain
type Greeter interface {
SayHello(name string) (string, error)
}
領域層是應用程式的核心,負責定義業務邏輯和業務規則。在這裡,
Greeter
是一個接口(interface),它定義了應用程式的核心功能,即SayHello
方法。
領域層不依賴於具體的實現細節,使得業務邏輯與技術細節分離。
repository/greeter_repo.go
中實現 GreeterRepository:package repository
import (
"fmt"
"grpcDemo/domain"
)
type GreeterRepository struct{}
func NewGreeterRepository() domain.Greeter {
return &GreeterRepository{}
}
func (repo *GreeterRepository) SayHello(name string) (string, error) {
return fmt.Sprintf("Hello, %s!", name), nil
}
Repository 層負責具體的數據訪問和操作。在這個例子中,
GreeterRepository
是domain.Greeter
接口的具體實現。
func SayHello() {}
: 實現了 domain.Greeter 接口的方法,返回格式化的問候語。func NewGreeterRepository() {}
:提供一個創建 GreeterRepository 實例的方式,並將其作為domain.Greeter
接口返回。這種做法使得上層不需要知道具體的實現。
usecase/greeter_usecase.go
中實現 GreeterUsecase:package usecase
import (
"grpcDemo/domain"
)
type GreeterUsecase struct {
repo domain.Greeter
}
func NewGreeterUsecase(repo domain.Greeter) *GreeterUsecase {
return &GreeterUsecase{repo: repo}
}
func (uc *GreeterUsecase) SayHello(name string) (string, error) {
return uc.repo.SayHello(name)
}
Use Case 層負責應用程式的具體業務用例(use cases),協調領域層和其他層之間的交互。在這裡,
GreeterUsecase
使用domain.Greeter
接口來執行具體的業務邏輯。
Use Case 層作為不同層之間的橋樑,協調它們來完成具體的業務需求。
func NewGreeterUsecase() {}
:創建並返回一個GreeterUsecase
的實例,注入所需的domain.Greeter
實現。
api/controller/greeter_controller.go
中實現 gRPC 服務的控制器:package controller
import (
"context"
pb "grpcDemo/proto"
"grpcDemo/usecase"
)
type GreeterController struct {
usecase *usecase.GreeterUsecase
pb.UnimplementedGreeterServer
}
func NewGreeterController(uc *usecase.GreeterUsecase) *GreeterController {
return &GreeterController{usecase: uc}
}
func (ctl *GreeterController) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
message, err := ctl.usecase.SayHello(req.Name)
if err != nil {
return nil, err
}
return &pb.HelloReply{Message: message}, nil
}
Controller 層負責處理來自外部的請求(如 gRPC 請求),並將這些請求轉發到 Use Case 層進行處理。它是應用程式的接口層,負責協調來自用戶端的輸入和應用邏輯的執行。
Controller 只負責處理請求和回應,不涉及具體的業務邏輯。
GreeterController struct {}
:
- 欄位
usecase
: 持有一個usecase.GreeterUsecase
的實例,用於調用業務邏輯。- 內嵌
pb.UnimplementedGreeterServer
: 這是 gRPC 生成的未實現服務器結構,確保未實現的方法會返回錯誤,並允許後續擴展。func NewGreeterController() {}
:創建並返回一個GreeterController
的實例,注入所需的 GreeterUsecase 實現。這樣做促進了依賴注入,使得控制器與具體的 Use Case 實現解耦。func SayHello() {}
:
- 簽名:
SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error)
- 作用: 實現 gRPC 定義的
SayHello
方法。接收來自客戶端的HelloRequest
,調用 Use Case 的SayHello
方法,並將結果封裝成HelloReply
返回給客戶端。
server/server.go
中啟動 gRPC 服務:package server
import (
"log"
"net"
"google.golang.org/grpc"
"grpcDemo/api/controller"
pb "grpcDemo/proto"
"grpcDemo/repository"
"grpcDemo/usecase"
)
type Server struct {
grpcServer *grpc.Server
}
func NewServer() *Server {
return &Server{
grpcServer: grpc.NewServer(),
}
}
func (sv *Server) Start(port string) error {
lis, err := net.Listen("tcp", port)
if err != nil {
return err
}
// 初始化 Repository, Usecase, Controller
repo := repository.NewGreeterRepository()
greeterUsecase := usecase.NewGreeterUsecase(repo)
greeterController := controller.NewGreeterController(greeterUsecase)
// 註冊 Greeter 服務
pb.RegisterGreeterServer(sv.grpcServer, greeterController)
log.Printf("gRPC server listening on %s", port)
return sv.grpcServer.Serve(lis)
}
func (sv *Server) Stop() {
sv.grpcServer.GracefulStop()
}
Server 層負責啟動和管理 gRPC 服務器,註冊服務器實現,並處理來自客戶端的連接請求。
Server struct {}
:grpcServer 持有一個 grpc.Server 的實例,負責處理 gRPC 請求。func NewServer() {}
:創建並返回一個 Server 的實例,初始化 grpcServer。func Start() {}
:
port string
:指定服務器監聽的端口。net.Listen
:開始監聽指定的 TCP 端口。pb.RegisterGreeterServer
:將GreeterController
註冊到 gRPC 服務器。Serve
:啟動服務器開始處理進來的請求。func Stop() {}
:允許正在處理的請求完成後,停止 gRPC 服務器。
client/client.go
中實現 gRPC 客戶端:package client
import (
"context"
"google.golang.org/grpc/credentials/insecure"
"log"
"time"
"google.golang.org/grpc"
pb "grpcDemo/proto"
)
func RunClient(address string, name string) {
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer func(conn *grpc.ClientConn) {
err := conn.Close()
if err != nil {
log.Fatalf("failed to close connection: %v", err)
}
}(conn)
c := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
req, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", req.GetMessage())
}
客戶端層負責與 gRPC 服務器進行通信,發送請求並接收回應。在這個例子中,客戶端將發送 SayHello 請求到服務器,並接收問候語回應。
在 bootstrap/app.go
用於應用程式的啟動和初始化邏輯
package bootstrap
import (
"log"
"grpcDemo/server"
)
func RunServer() {
srv := server.NewServer()
if err := srv.Start(":50051"); err != nil {
log.Fatalf("failed to start server: %v", err)
}
}
Bootstrap 層負責初始化和啟動應用程式的主要組件
func RunServer() {}
:負責啟動 gRPC 服務器,這是應用程式的核心部分。
server.NewServer()
:創建一個新的Server
實例。srv.Start(":50051")
:啟動服務器,監聽在端口 50051 上的連接請求。
cmd/main.go
中啟動服務端或客戶端:package main
import (
"flag"
"log"
"grpcDemo/bootstrap"
"grpcDemo/client"
)
func main() {
mode := flag.String("mode", "server", "啟動模式: server 或 client")
name := flag.String("name", "World", "客戶端請求的名稱")
flag.Parse()
if *mode == "server" {
bootstrap.RunServer()
} else if *mode == "client" {
client.RunClient("localhost:50051", *name)
} else {
log.Fatalf("未知的模式: %s", *mode)
}
}
main.go 是應用程式的入口點,負責解析命令行參數並根據指定的模式啟動服務器或客戶端。
今天我們學會了怎麼開發 gRPC 程式,還順便認識了 Makefile 文件,那明天再來教大家怎麼執行測試寫好的 api 吧!