這是 Go 語言從零到 Web 應用系列的第十六篇。前面我們用 REST API 建構服務,現在來學 Go 的另一個強項——gRPC,了解為什麼它在微服務架構中如此受歡迎。

gRPC vs REST

面向 REST gRPC
協議 HTTP/1.1(通常) HTTP/2
資料格式 JSON(文字) Protocol Buffers(二進位)
型別安全 弱(需要手動解析) 強(自動產生型別)
串流 不原生支持 原生支持雙向串流
效能 較慢(JSON 解析) 快 3-10 倍
瀏覽器支持 原生支持 需要 gRPC-Web
適用場景 公開 API、前端 微服務間通訊

什麼時候用 gRPC:

  • 微服務之間的內部通訊
  • 需要高效能和低延遲
  • 需要串流(streaming)
  • 強型別的服務契約

Protocol Buffers

安裝

# 安裝 protoc 編譯器
brew install protobuf  # macOS

# 安裝 Go 的 protoc plugin
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

確保 $GOPATH/bin 在你的 PATH 中。

定義 .proto 檔案

// proto/book.proto
syntax = "proto3";

package bookstore;

option go_package = "github.com/yourname/bookstore/pb";

// 訊息定義
message Book {
  int64 id = 1;
  string title = 2;
  string author = 3;
  int32 year = 4;
}

message CreateBookRequest {
  string title = 1;
  string author = 2;
  int32 year = 3;
}

message GetBookRequest {
  int64 id = 1;
}

message ListBooksRequest {}

message ListBooksResponse {
  repeated Book books = 1;
}

// 服務定義
service BookService {
  // Unary RPC
  rpc GetBook(GetBookRequest) returns (Book);
  rpc CreateBook(CreateBookRequest) returns (Book);
  rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);

  // Server Streaming RPC
  rpc WatchBooks(ListBooksRequest) returns (stream Book);
}

產生 Go 程式碼

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       proto/book.proto

這會產生兩個檔案:

  • proto/book.pb.go:訊息的 Go 結構體
  • proto/book_grpc.pb.go:gRPC 服務的介面和 client

Unary RPC

最基本的 RPC 模式:client 發一個 request,server 回一個 response。

Server 端

package main

import (
    "context"
    "log"
    "net"
    "sync"

    pb "github.com/yourname/bookstore/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

type bookServer struct {
    pb.UnimplementedBookServiceServer
    mu     sync.RWMutex
    books  map[int64]*pb.Book
    nextID int64
}

func newBookServer() *bookServer {
    return &bookServer{
        books:  make(map[int64]*pb.Book),
        nextID: 1,
    }
}

func (s *bookServer) CreateBook(ctx context.Context, req *pb.CreateBookRequest) (*pb.Book, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    book := &pb.Book{
        Id:     s.nextID,
        Title:  req.Title,
        Author: req.Author,
        Year:   req.Year,
    }
    s.books[s.nextID] = book
    s.nextID++

    return book, nil
}

func (s *bookServer) GetBook(ctx context.Context, req *pb.GetBookRequest) (*pb.Book, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    book, ok := s.books[req.Id]
    if !ok {
        return nil, status.Errorf(codes.NotFound, "book %d not found", req.Id)
    }
    return book, nil
}

func (s *bookServer) ListBooks(ctx context.Context, req *pb.ListBooksRequest) (*pb.ListBooksResponse, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    books := make([]*pb.Book, 0, len(s.books))
    for _, b := range s.books {
        books = append(books, b)
    }
    return &pb.ListBooksResponse{Books: books}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    grpcServer := grpc.NewServer()
    pb.RegisterBookServiceServer(grpcServer, newBookServer())

    log.Println("gRPC server listening on :50051")
    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

Client 端

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    pb "github.com/yourname/bookstore/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    // 建立連線
    conn, err := grpc.NewClient("localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        log.Fatalf("failed to connect: %v", err)
    }
    defer conn.Close()

    client := pb.NewBookServiceClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // 建立書籍
    book, err := client.CreateBook(ctx, &pb.CreateBookRequest{
        Title:  "The Go Programming Language",
        Author: "Donovan & Kernighan",
        Year:   2015,
    })
    if err != nil {
        log.Fatalf("CreateBook failed: %v", err)
    }
    fmt.Printf("Created: %+v\n", book)

    // 取得書籍
    got, err := client.GetBook(ctx, &pb.GetBookRequest{Id: book.Id})
    if err != nil {
        log.Fatalf("GetBook failed: %v", err)
    }
    fmt.Printf("Got: %+v\n", got)

    // 列出所有書籍
    resp, err := client.ListBooks(ctx, &pb.ListBooksRequest{})
    if err != nil {
        log.Fatalf("ListBooks failed: %v", err)
    }
    for _, b := range resp.Books {
        fmt.Printf("  - %s by %s (%d)\n", b.Title, b.Author, b.Year)
    }
}

Streaming RPC

Server Streaming

Server 一次回傳多個 response(例如即時推送新增的書籍):

// Server 端
func (s *bookServer) WatchBooks(req *pb.ListBooksRequest, stream pb.BookService_WatchBooksServer) error {
    // 先送出現有的書籍
    s.mu.RLock()
    for _, book := range s.books {
        if err := stream.Send(book); err != nil {
            s.mu.RUnlock()
            return err
        }
    }
    s.mu.RUnlock()

    // 持續監聽新書(簡化範例用 ticker 模擬)
    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()

    for i := 0; i < 5; i++ {
        select {
        case <-ticker.C:
            book := &pb.Book{
                Id:    int64(100 + i),
                Title: fmt.Sprintf("New Book %d", i),
            }
            if err := stream.Send(book); err != nil {
                return err
            }
        case <-stream.Context().Done():
            return stream.Context().Err()
        }
    }
    return nil
}
// Client 端
stream, err := client.WatchBooks(ctx, &pb.ListBooksRequest{})
if err != nil {
    log.Fatal(err)
}

for {
    book, err := stream.Recv()
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Received: %s\n", book.Title)
}

gRPC 攔截器(Interceptor)

攔截器是 gRPC 版本的中間件,可以在 RPC 呼叫前後加入邏輯。

Unary Interceptor

// Logging Interceptor
func loggingInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    start := time.Now()

    // 呼叫實際的 handler
    resp, err := handler(ctx, req)

    // 記錄日誌
    log.Printf("method=%s duration=%s error=%v",
        info.FullMethod,
        time.Since(start),
        err,
    )

    return resp, err
}

// 認證 Interceptor
func authInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
    }

    tokens := md.Get("authorization")
    if len(tokens) == 0 {
        return nil, status.Errorf(codes.Unauthenticated, "missing token")
    }

    // 驗證 token...
    return handler(ctx, req)
}

// 註冊攔截器
grpcServer := grpc.NewServer(
    grpc.ChainUnaryInterceptor(
        loggingInterceptor,
        authInterceptor,
    ),
)

Stream Interceptor

func streamLoggingInterceptor(
    srv interface{},
    ss grpc.ServerStream,
    info *grpc.StreamServerInfo,
    handler grpc.StreamHandler,
) error {
    start := time.Now()
    err := handler(srv, ss)
    log.Printf("stream method=%s duration=%s error=%v",
        info.FullMethod,
        time.Since(start),
        err,
    )
    return err
}

grpcServer := grpc.NewServer(
    grpc.ChainUnaryInterceptor(loggingInterceptor),
    grpc.ChainStreamInterceptor(streamLoggingInterceptor),
)

錯誤處理

gRPC 有自己的錯誤碼系統:

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// Server 端回傳錯誤
return nil, status.Errorf(codes.NotFound, "book %d not found", id)
return nil, status.Errorf(codes.InvalidArgument, "title is required")
return nil, status.Errorf(codes.Internal, "database error: %v", err)

// Client 端處理錯誤
book, err := client.GetBook(ctx, req)
if err != nil {
    st, ok := status.FromError(err)
    if ok {
        switch st.Code() {
        case codes.NotFound:
            fmt.Println("Book not found")
        case codes.InvalidArgument:
            fmt.Println("Invalid request:", st.Message())
        default:
            fmt.Println("Error:", st.Message())
        }
    }
}

常用的 gRPC 狀態碼:

說明 對應 HTTP
OK 成功 200
NotFound 資源不存在 404
InvalidArgument 參數錯誤 400
Unauthenticated 未認證 401
PermissionDenied 無權限 403
Internal 內部錯誤 500

下一步

學會了 gRPC,我們有了微服務之間通訊的利器。下一篇來學微服務架構的核心概念——如何拆分服務、服務間如何溝通、以及必須知道的設計模式。