+1

[Góc System Design] Bản chất của google.api.http: Phép thuật biến gRPC thành REST API trong hệ sinh thái Golang

Hãy tưởng tượng bạn vừa đập đi xây lại hệ thống Backend. Bạn dùng Go, chọn gRPC và Protocol Buffers làm chuẩn giao tiếp nội bộ giữa các Microservices vì tốc độ siêu việt và khả năng truyền tải nhị phân (binary) tối ưu của nó. Mọi thứ trong backend chạy mượt mà như một cỗ máy F1.

Nhưng ngay ngày hôm sau, team Frontend (React/Vue) và team Mobile gõ cửa: "Bọn em không gọi được gRPC trực tiếp từ Browser dễ dàng đâu. Trả lại API RESTful trả về JSON cho bọn em đi!"

Lúc này, bạn đứng trước hai lựa chọn:

  1. Lựa chọn đau khổ: Viết thêm một HTTP Server (dùng Gin, Fiber hoặc Echo), tạo các routes, hứng request JSON từ Client, parse JSON ra struct, sau đó tạo một gRPC Client gọi xuống backend, lấy kết quả, rồi lại parse thành JSON trả về. Bạn vừa phải bảo trì code gRPC, vừa phải bảo trì code HTTP Controller.

  2. Lựa chọn của chuyên gia: Sử dụng google.api.http.

Dưới góc độ thực chiến trong Golang, google.api.http chính là "chiếc đũa phép" giúp bạn không bao giờ phải viết lại một dòng code HTTP Controller nào nữa.

1. Bản chất: Tấm bản đồ phiên dịch (Mapping Metadata)

Bản thân google.api.http không phải là code thực thi, nó là một Custom Option (tùy chọn mở rộng) được Google định nghĩa sẵn trong file google/api/annotations.proto.

Vai trò duy nhất của nó là cung cấp Metadata (Siêu dữ liệu). Nó giống như một tấm bản đồ chỉ dẫn cho các công cụ sinh code (code generators) biết cách ánh xạ (map) một hàm gRPC thuần túy thành một endpoint HTTP/RESTful.

Khi bạn thêm option này vào file .proto:

Protocol Buffers

syntax = "proto3";

import "google/api/annotations.proto";

service UserService {
  rpc CreateUser (CreateUserRequest) returns (UserResponse) {
    // Tấm bản đồ phiên dịch nằm ở đây:
    option (google.api.http) = {
      post: "/api/v1/users"
      body: "*"
    };
  }
}

message CreateUserRequest {
  string name = 1;
  int32 age = 2;
}

Bạn đang tuyên bố một bản hợp đồng kép: "Hàm CreateUser này có thể gọi bằng gRPC, nhưng nếu ai đó gửi một HTTP POST request vào route /api/v1/users, hãy bọc toàn bộ body JSON lại và ném vào hàm này cho tôi".

2. Trái tim của hệ sinh thái Go: gRPC-Gateway

Tấm bản đồ google.api.http sẽ trở nên vô dụng nếu không có người đọc nó. Và trong hệ sinh thái Golang, "người đọc" xuất sắc nhất chính là thư viện grpc-gateway.

Khi bạn chạy lệnh protoc kết hợp với plugin protoc-gen-grpc-gateway, plugin này sẽ đọc các option google.api.http và tự động sinh ra một file code Go (thường có đuôi là .pb.gw.go).

File code này chứa một Reverse Proxy (Proxy ngược) viết hoàn toàn bằng Go thuần. Quy trình hoạt động thực tế như sau:

  1. Client (Browser/Postman): Gửi một HTTP POST /api/v1/users với payload JSON {"name": "Hieu", "age": 25}.

  2. Reverse Proxy (gRPC-Gateway): Nhận HTTP Request. Nó kiểm tra route và thấy khớp với khai báo google.api.http.

  3. Dịch thuật (Translation): Proxy tự động unmarshal (giải mã) chuỗi JSON kia thành struct CreateUserRequest của Go.

  4. Gọi nội bộ: Proxy đóng vai trò như một gRPC Client, dùng chuẩn HTTP/2 nhị phân bắn struct đó gọi thẳng vào hàm gRPC CreateUser ở backend.

  5. Trả về: Backend xử lý xong trả về gRPC Response. Proxy nhận được, tự động marshal (mã hóa) ngược lại thành JSON và trả về cho HTTP Client kèm HTTP Status Code chuẩn (như 200 OK, 404 Not Found...).

3. Nghệ thuật Mapping (Ánh xạ) Dữ Liệu

Sức mạnh của google.api.http nằm ở việc nó cho phép bạn bóc tách và map dữ liệu từ HTTP request vào gRPC message cực kỳ chi tiết, không chỉ giới hạn ở JSON body:

  • Map qua Path Variable (URL Parameter):

    Protocol Buffers

    rpc GetUser (GetUserRequest) returns (UserResponse) {
      option (google.api.http) = {
        get: "/api/v1/users/{user_id}" // Lấy user_id từ URL gán vào trường user_id của Request
      };
    }
    message GetUserRequest { string user_id = 1; }
    
    
  • Map qua Query String: Nếu bạn gọi GET /api/v1/users?name=hieu&age=25, Gateway sẽ tự động gom các query param này nhét vào các trường tương ứng trong struct gRPC Request.

  • Tách biệt Body và URL:

    Protocol Buffers

    rpc UpdateUser (UpdateUserRequest) returns (UserResponse) {
      option (google.api.http) = {
        patch: "/api/v1/users/{user_id}"
        body: "user" // Chỉ lấy trường 'user' trong payload JSON, ID thì lấy từ URL
      };
    }
    
    

4. Giá trị cốt lõi mang lại cho Backend Developer

Khi triển khai kiến trúc này bằng Golang, bạn sẽ nhận được 3 lợi ích khổng lồ:

  1. Single Source of Truth (Một nguồn chân lý duy nhất): File .proto trở thành trái tim của toàn bộ hệ thống API. Bạn định nghĩa API một lần duy nhất. Không có chuyện tài liệu REST ghi một đằng, code gRPC chạy một nẻo.

  2. Giải phóng sức lao động: Bỏ qua hoàn toàn tầng API Controller/Routing truyền thống (như việc setup Gin/Echo routes rối rắm). Bạn chỉ tập trung viết business logic cho hàm gRPC, framework sẽ tự lo việc hứng HTTP, parse JSON, và xử lý lỗi (biến các mã gRPC Codes như NOT_FOUND thành HTTP 404).

  3. Tự động hóa Swagger/OpenAPI: Từ các option google.api.http này, bạn có thể chạy thêm plugin protoc-gen-openapiv2 để tự động sinh ra file swagger.json. Frontend sẽ ngay lập tức có giao diện Postman/Swagger UI để test API mà bạn không cần gõ thêm một dòng mô tả nào.

Vai trò của google.api.http không đơn thuần là một cú pháp thêm thắt cho vui. Nó đại diện cho triết lý API-First Design, nơi cấu trúc giao tiếp được định nghĩa rõ ràng ngay từ đầu, và biến Golang trở thành một cỗ máy sinh code hoàn hảo phục vụ cùng lúc cả hai thế giới: Tốc độ của gRPC nội bộ và tính phổ biến của RESTful API bên ngoài.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí