Skip to content

Protobuf

เว็บไซต์ทางการ: Protocol Buffers | Google Developers

บทนำ

บทช่วยสอนทางการ: Protocol Buffer Basics: Go | Protocol Buffers | Google Developers

Protocol Buffers เป็นกลไกการซีเรียลไลเซชันข้อมูลที่มีโครงสร้างแบบ可扩展 ที่ไม่ขึ้นกับภาษาและไม่ขึ้นกับโปรโตคอล ซึ่งเปิดแหล่งโดยกูเกิลในปี 2008 ใช้ในการแพ็กเก็ตและอันแพ็กเก็ตได้รวดเร็ว มักใช้ในด้านการสื่อสาร RPC สามารถกำหนดวิธีการจัดโครงสร้างข้อมูล จากนั้นสามารถใช้ซอร์สโค้ดที่สร้างขึ้นเป็นพิเศษเพื่อเขียนและอ่านข้อมูลที่มีโครงสร้างจากสตรีมข้อมูลต่างๆ ได้อย่างง่ายดาย และใช้กับภาษาต่างๆ เกี่ยวกับ Protocol Buffers ต่อไปนี้จะเรียกว่า protobuf

protobuf ค่อนข้างเป็นที่นิยม โดยเฉพาะในส่วนของ go gRPC ใช้เป็นกลไกการซีเรียลไลเซชันสำหรับการส่งโปรโตคอล

ไวยากรณ์

ก่อนอื่นจากตัวอย่างหนึ่งเพื่อดูว่าไฟล์ protobuf มีลักษณะอย่างไร โดยรวมแล้วไวยากรณ์ของมันง่ายมาก ใช้เวลาสิบกว่านาทีก็สามารถใช้งานได้ ด้านล่างนี้เป็นตัวอย่างของไฟล์ชื่อ search.proto นามสกุลไฟล์ของ protobuf คือ .proto

protobuf
syntax = "proto3";

message SearchRequest {
  string query = 1;
  string number = 2;
}

message SearchResult {
  string data = 1;
}

service SearchService {
  rpc Search(SearchRequest) returns(SearchResult);
}
  • บรรทัดแรก syntax = "proto3"; หมายถึงใช้ไวยากรณ์ proto3 โดยค่าเริ่มต้นใช้ไวยากรณ์ proto3
  • การประกาศ message คล้ายกับโครงสร้าง เป็นโครงสร้างพื้นฐานของ proto
  • SearchRequest กำหนดสามฟิลด์ แต่ละฟิลด์มีชื่อและประเภท
  • service กำหนดบริการหนึ่งบริการ หนึ่งบริการประกอบด้วยหนึ่งหรือมากกว่าหนึ่ง rpc interface
  • rpc interface ต้องมีพารามิเตอร์และค่าส่งคืนหนึ่งและเพียงหนึ่งเท่านั้น ประเภทของมันต้องเป็น message ไม่สามารถเป็นประเภทพื้นฐาน

อีกสิ่งที่ควรสังเกตคือ ทุกบรรทัดในไฟล์ proto ต้องมีเครื่องหมายเซมิโคลอนต่อท้าย

คำอธิบาย

สไตล์คำอธิบายเหมือนกับ go ทุกประการ

protobuf
syntax = "proto3";

/* คำอธิบาย
 * คำอธิบาย */
message SearchRequest {
  string query = 1; //คำอธิบาย
  string number = 2;
}

ประเภท

ตัวปรับประเภทสามารถปรากฏได้เฉพาะใน message เท่านั้น ไม่สามารถปรากฏเดี่ยวๆ

ประเภทพื้นฐาน

proto TypeGo Type
doublefloat64
floatfloat32
int32int32
int64int64
uint32uint32
uint64uint64
sint32int32
sint64int64
fixed32uint32
fixed64uint64
sfixed32int32
sfixed64int64
boolbool
stringstring
bytes[]byte

อาร์เรย์

เพิ่มตัวปรับ repeated หน้าประเภทพื้นฐานเพื่อแสดงว่าเป็นประเภทอาร์เรย์ ตรงกับสไลซ์ใน go

protobuf
message Company {
  repeated string employee = 1;
}

map

การกำหนดประเภท map ใน protobuf มีรูปแบบดังนี้

map<key_type, value_type> map_field = N;

key_type ต้องเป็นตัวเลขหรือสตริง value_type ไม่มีข้อจำกัดประเภท ดูตัวอย่าง

protobuf
message Person {
  map<string, int64> cards = 1;
}

ฟิลด์

จริงๆ แล้ว proto ไม่ใช่ประเภทคีย์-ค่าแบบดั้งเดิม ในการประกาศไฟล์ proto จะไม่ปรากฏข้อมูลเฉพาะ หลังจากแต่ละฟิลด์ = ตามด้วยควรเป็นหมายเลขเฉพาะใน message ปัจจุบัน หมายเลขเหล่านี้ใช้สำหรับระบุและกำหนดฟิลด์เหล่านี้ในข้อความไบนารี หมายเลขเริ่มต้นจาก 1 หมายเลข 1-15 จะใช้ 1 ไบต์ หมายเลข 16-2047 จะใช้สองไบต์ ดังนั้นพยายามให้ฟิลด์ที่ปรากฏบ่อยมีหมายเลข 1-15 เพื่อประหยัดพื้นที่ และควรเว้นที่ว่างบางส่วนสำหรับฟิลด์ที่อาจปรากฏบ่อยในอนาคต

ฟิลด์ใน message ควรเป็นไปตามกฎต่อไปนี้

  • singular: โดยค่าเริ่มต้นเป็นฟิลด์ประเภทนี้ ใน message ที่มีโครงสร้างดี มีและเพียง 0 หรือ 1 ฟิลด์นี้เท่านั้น นั่นคือไม่สามารถมีฟิลด์เดียวกันซ้ำได้ การประกาศดังนี้จะรายงานข้อผิดพลาด

    protobuf
    syntax = "proto3";
    
    message SearchRequest {
      string query = 1;
      string number = 2;
      string number = 3;//ฟิลด์ซ้ำ
    }
  • optional: คล้ายกับ singular เพียงแต่สามารถตรวจสอบค่าฟิลด์ว่าถูกตั้งค่าหรือไม่ อาจมีสองสถานการณ์ต่อไปนี้

    • set: จะถูกซีเรียลไลซ์
    • unset: จะไม่ถูกซีเรียลไลซ์
  • repeated: ฟิลด์ประเภทนี้สามารถปรากฏ 0 ครั้งหรือมากกว่า จะเก็บค่าซ้ำตามลำดับ (พูดง่ายๆ ก็คืออาร์เรย์ สามารถอนุญาตให้ค่าประเภทเดียวกันปรากฏหลายครั้งและเก็บตามลำดับที่ปรากฏ ก็คือดัชนี)

  • map: ฟิลด์ประเภทคีย์-ค่า วิธีการประกาศดังนี้

    protobuf
    map<string,int32> config = 3;

ฟิลด์ที่สงวนไว้

คำสำคัญ reserve สามารถประกาศฟิลด์ที่สงวนไว้ หลังจากประกาศหมายเลขฟิลด์ที่สงวนไว้แล้ว จะไม่สามารถใช้เป็นหมายเลขฟิลด์และชื่อของฟิลด์อื่นได้อีก และจะเกิดข้อผิดพลาดเมื่อคอมไพล์ คำตอบทางการจากกูเกิลคือ: หากไฟล์ proto ลบหมายเลขบางตัวในเวอร์ชันใหม่ ในอนาคตผู้ใช้รายอื่นอาจใช้หมายเลขที่ถูกลบเหล่านี้ซ้ำ แต่ถ้าเปลี่ยนกลับเป็นหมายเลขเวอร์ชันเก่าจะทำให้หมายเลขที่ฟิลด์ตรงกันไม่สอดคล้องกันและเกิดข้อผิดพลาด ฟิลด์ที่สงวนไว้สามารถทำหน้าที่เตือนในช่วงคอมไพล์ เตือนว่าคุณไม่สามารถใช้ฟิลด์ที่สงวนไว้นี้ได้ มิฉะนั้นการคอมไพล์จะไม่ผ่าน

protobuf
syntax = "proto3";

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  reserved "a"; //ประกาศฟิลด์ที่มีชื่อเฉพาะเป็นฟิลด์ที่สงวนไว้
  reserved 1 to 2; //ประกาศลำดับหมายเลขเป็นฟิลด์ที่สงวนไว้
  reserved 3,4; //ประกาศ
}

ดังนั้นไฟล์นี้จะไม่ผ่านการคอมไพล์

ฟิลด์ที่เลิกใช้

หากฟิลด์ถูกเลิกใช้ สามารถเขียนดังนี้

protobuf
message Body {
  string name = 1 [deprecated = true];
}

การนับ

สามารถประกาศค่าคงที่การนับและใช้เป็นประเภทฟิลด์ได้ ควรสังเกตว่าองค์ประกอบแรกของการนับต้องเป็นศูนย์ เพราะค่าเริ่มต้นของการนับคือองค์ประกอบแรก

protobuf
syntax = "proto3";

enum Type {
  GET = 0;
  POST = 1;
  PUT = 2;
  DELETE = 3;
}

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  Type type = 5;
}

เมื่อมีการนับที่มีค่าเดียวกันในการนับ สามารถใช้นามสกุลการนับ

protobuf
syntax = "proto3";

enum Type {
  option allow_alias = true; //ต้องเปิดการตั้งค่าที่อนุญาตให้ใช้นามสกุล
  GET = 0;
  GET_ALIAS = 0; //นามสกุลของฟิลด์ GET
  POST = 1;
  PUT = 2;
  DELETE = 3;
}

message SearchRequest {
  string query = 1;
  string number = 2;
  map<string, int32> config = 3;
  repeated string a = 4;
  Type type = 5;
}

ข้อความซ้อนกัน

protobuf
message Outer {                  // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

message สามารถประกาศ message ซ้อนกันได้ เหมือนกับโครงสร้างซ้อนกัน

Package

คุณสามารถเพิ่มตัวปรับแพ็กเกจเสริมให้กับไฟล์ protobuf เพื่อป้องกันการชนกันของชื่อระหว่างประเภทข้อความโปรโตคอล

protobuf
package foo.bar;
message Open { ... }

จากนั้นคุณสามารถใช้ชื่อแพ็กเกจเมื่อประกาศฟิลด์ในประเภทข้อความ:

protobuf
message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

Import

การนำเข้าสามารถให้ไฟล์ protobuf หลายไฟล์แชร์คำจำกัดความ ไวยากรณ์มีดังนี้ เมื่อไม่สามารถละเลยนามสกุลไฟล์ได้

protobuf
import "a/b/c.proto";

เมื่อการนำเข้าทั้งหมดใช้พาธสัมพัทธ์ พาธสัมพัทธ์นี้ไม่ใช่พาธสัมพัทธ์ระหว่างไฟล์นำเข้าและไฟล์ที่นำเข้า แต่ขึ้นอยู่กับพาธการสแกนที่ระบุเมื่อตัวคอมไพเลอร์ protoc สร้างโค้ด สมมติว่ามีโครงสร้างไฟล์ดังนี้

pb_learn
│  common.proto

├─monster
│      monster.proto

└─player
        health.proto
        player.proto

หากเราต้องการสร้างโค้ดส่วนของไดเรกทอรี player เท่านั้น และระบุเฉพาะไดเรกทอรี player เมื่อสแกนพาธ การนำเข้าระหว่าง health.proto กับ player.proto สามารถเขียนชื่อไฟล์เดียวได้ เช่น player.proto นำเข้า health.proto

protobuf
import "health.proto";

หากในเวลานี้ player.proto นำเข้าไฟล์ในไดเรกทอรี common.proto หรือ monster จะล้มเหลวในการคอมไพล์ ดังนั้นการเขียนแบบนี้จึงผิดอย่างสมบูรณ์ เพราะตัวคอมไพเลอร์ไม่สามารถหาไฟล์เหล่านี้ได้

go
import "../common.proto"; // การเขียนที่ผิด

TIP

พูดเสริมว่า สัญลักษณ์ .., . เหล่านี้ไม่อนุญาตให้ปรากฏในพาธการนำเข้า

สมมติว่าระบุ pb_learn เป็นพาธการสแกนเมื่อคอมไพล์ จากนั้นสามารถนำเข้าไฟล์ในไดเรกทอรีอื่นผ่านพาธสัมพัทธ์ พาธการนำเข้าจริงคือที่อยู่สัมบูรณ์ของไฟล์สัมพัทธ์กับ pb_learn ดูตัวอย่างการนำเข้าไฟล์อื่นของ player.proto ด้านล่าง

protobuf
import "common.proto";
imrpot "monster/monster.proto";
import "player/health.proto";

แม้ว่า health.proto จะอยู่ในไดเรกทอรีเดียวกัน ตอนนี้ก็ต้องใช้พาธสัมพัทธ์ ดังนั้นในโครงการหนึ่ง โดยทั่วไปเราจะสร้างโฟลเดอร์แยกต่างหากเพื่อเก็บไฟล์ protobuf ทั้งหมด และระบุเป็นพาธการสแกนเมื่อคอมไพล์ และการดำเนินการนำเข้าทั้งหมดในไดเรกทอรีนี้ก็ขึ้นอยู่กับพาธสัมพัทธ์

TIP

หากคุณใช้ตัวแก้ไข goland สำหรับไดเรกทอรี protobuf ที่คุณสร้างขึ้นเอง โดยค่าเริ่มต้นจะไม่สามารถวิเคราะห์ได้ ซึ่งจะทำให้เกิดสถานการณ์สีแดง หากต้องการให้ goland ระบุได้ก็ต้องตั้งค่าพาธการสแกนด้วยตนเอง หลักการเหมือนกับที่กล่าวมาข้างต้น วิธีการตั้งค่ามีดังนี้ เปิดการตั้งค่าดังนี้

File | Settings | Languages & Frameworks | Protocol Buffers

เพิ่มพาธการสแกนด้วยตนเองใน Import Paths พาธการสแกนนี้ควรเหมือนกับพาธที่คุณระบุเมื่อคอมไพล์

Any

ประเภท Any อนุญาตให้คุณใช้ข้อความเป็นประเภทฝังตัวโดยไม่ต้องมีคำจำกัดความ proto ของพวกมัน เราสามารถนำเข้าประเภทที่กูเกิลกำหนดไว้โดยตรง มันมีมาให้ ไม่ต้องเขียนด้วยตนเอง

protobuf
import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

กูเกิลยังกำหนดประเภทอื่นๆ ไว้มากมาย ไปที่ protobuf/ptypes at master · golang/protobuf (github.com) เพื่อดูเพิ่มเติม มีหลักๆ รวมถึง

  • การห่อหุ้มประเภทพื้นฐาน
  • ประเภทเวลา
  • ประเภท Duration

เกี่ยวกับคำจำกัดความ protobuf ของพวกมันควรอยู่ในไดเรกทอรี inlucde ของตัวคอมไพเลอร์ protoc

OneOf

คำอธิบายที่เอกสารทางการให้ไว้ซับซ้อนเกินไป พูดภาษามนุษย์其实就是แสดงว่าฟิลด์จะมีประเภทที่เป็นไปได้หลายประเภทเมื่อส่งข้อมูล แต่สุดท้ายจะมีเพียงประเภทเดียวที่ถูกใช้ ภายในไม่อนุญาตให้มีฟิลด์ที่ปรับด้วย repeated นี่ก็เหมือนกับ union ในภาษา c

protobuf
message Stock {
    // Stock-specific data
}

message Currency {
    // Currency-specific data
}

message ChangeNotification {
  int32 id = 1;
  oneof instrument {
    Stock stock = 2;
    Currency currency = 3;
  }
}

Service

คำสำคัญ service สามารถกำหนดบริการ RPC หนึ่งบริการ RPC หนึ่งบริการประกอบด้วย rpc interface หลายอัน interface แบ่งเป็น one-way interface และ stream interface

protobuf
message Body {
  string name = 1;
}

service ExampleService {
  rpc DoSomething(Body) returns(Body);
}

และ stream interface ยังแบ่งเป็น one-way stream และ two-way stream มักใช้คำสำคัญ stream เพื่อปรับ ดูตัวอย่างด้านล่าง

protobuf
message Body {
  string name = 1;
}

service ExampleService {
  // ไคลเอนต์สตรีม
  rpc DoSomething(stream Body) returns(Body);
  // เซิร์ฟเวอร์สตรีม
  rpc DoSomething1(Body) returns(stream Body);
  // ทวิทิศทางสตรีม
  rpc DoSomething2(stream Body) returns(stream Body);
}

สิ่งที่เรียกว่าสตรีมคือการส่งข้อมูลซึ่งกันและกันในระยะยาวในการเชื่อมต่อหนึ่งครั้ง ไม่เหมือน one-way interface ที่ถามตอบง่ายๆ

Empty

empty จริงๆ แล้วเป็น message ว่าง ตรงกับโครงสร้างว่างใน go มันใช้ปรับฟิลด์น้อยมาก ส่วนใหญ่ใช้แสดงว่า rpc interface บางอย่างไม่มีพารามิเตอร์หรือไม่มีค่าส่งคืน

protobuf
syntax = "proto3";

import "google/protobuf/empty.proto";

service EmptyService {
  rpc Do(google.protobuf.Empty) returns(google.protobuf.Empty);
}

Option

option มักใช้ควบคุมพฤติกรรมบางอย่างของ protobuf เช่น ควบคุมแพ็กเกจที่สร้างซอร์สโค้ดภาษา go สามารถประกาศดังนี้

protobuf
option go_package = "github/jack/sample/pb_learn;pb_learn"

ด้านหน้าเซมิโคลอนคือพาธการนำเข้าของไฟล์ซอร์สอื่นหลังจากสร้างโค้ด ด้านหลังเซมิโคลอนคือชื่อแพ็กเกจของไฟล์ที่สร้าง

มันสามารถทำการปรับ优化ได้ มีค่าที่ใช้ได้หลายค่าต่อไปนี้ ไม่สามารถประกาศซ้ำได้

  • SPEED ระดับการปรับ优化สูงสุด ปริมาตรโค้ดที่สร้างใหญ่ที่สุด โดยค่าเริ่มต้นเป็นนี้
  • CODE_SIZE จะลดปริมาตรโค้ดที่สร้าง แต่จะพึ่งพาการสะท้อนเพื่อซีเรียลไลเซชัน
  • LIFE_RUNEIMTE ปริมาตรโค้ดเล็กที่สุด แต่จะขาดคุณสมบัติบางอย่าง

ด้านล่างนี้เป็นตัวอย่างการใช้

protobuf
option optimize_for = SPEED;

นอกจากนี้ option ยังสามารถเพิ่มข้อมูลเมตาบางอย่างให้กับ message และ enum ได้ ใช้การสะท้อนสามารถรับข้อมูลเหล่านี้ ซึ่งมีประโยชน์เป็นพิเศษเมื่อทำการตรวจสอบพารามิเตอร์

การคอมไพล์

การคอมไพล์ก็คือการสร้างโค้ด ด้านบนเพียงกำหนดไฟล์ protobuf เมื่อใช้งานจริงต้องแปลงเป็นซอร์สโค้ดภาษาใดภาษาหนึ่งจึงจะสามารถใช้งานได้ เราทำสิ่งนี้ผ่านตัวคอมไพเลอร์ protoc มันรองรับหลายภาษา

การติดตั้ง

การดาวน์โหลดตัวคอมไพเลอร์ไปที่ protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format (github.com) เพื่อดาวน์โหลด Release เวอร์ชันล่าสุด โดยทั่วไปเป็นไฟล์บีบอัด

protoc-25.1-win64
│  readme.txt

├─bin
│      protoc.exe

└─include
    └─google
        └─protobuf
            │  any.proto
            │  api.proto
            │  descriptor.proto
            │  duration.proto
            │  empty.proto
            │  field_mask.proto
            │  source_context.proto
            │  struct.proto
            │  timestamp.proto
            │  type.proto
            │  wrappers.proto

            └─compiler
                    plugin.proto

หลังจากดาวน์โหลดเสร็จต้องเพิ่มไดเรกทอรี bin ลงในตัวแปรสภาพแวดล้อม เพื่อให้สามารถใช้คำสั่ง protoc ได้ หลังจากเสร็จแล้วดูเวอร์ชัน หากสามารถเอาต์พุตได้ตามปกติแสดงว่าติดตั้งสำเร็จ

bash
$ protoc --version
libprotoc 25.1

ตัวคอมไพเลอร์ที่ดาวน์โหลดมาโดยค่าเริ่มต้นไม่รองรับภาษา go เพราะการสร้างโค้ดภาษา go เป็นไฟล์ที่รันได้แยกต่างหาก ภาษาอื่นรวมกันไว้ ดังนั้นติดตั้งปลั๊กอินภาษา go เพิ่มเติม ใช้สำหรับแปลคำจำกัดความ protocbuf เป็นซอร์สโค้ดภาษา go

bash
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

หากต้องการสร้างโค้ดบริการ gRPC ด้วย ติดตั้งปลั๊กอินดังนี้

bash
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

หลังจากติดตั้งแล้วดูเวอร์ชัน

bash
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.3.0

$ protoc-gen-go --version
protoc-gen-go.exe v1.31.0

ปลั๊กอินเหล่านี้เป็นไฟล์ไบนารีแยกต่างหาก แต่สามารถเรียกใช้ได้ผ่าน protoc เท่านั้น ไม่สามารถรันเดี่ยวๆ

(this program should be run by protoc, not directly)

นอกจากนี้ยังมีปลั๊กอินอื่นๆ อีกมากมาย เช่น ปลั๊กอินสร้างเอกสาร interface openapi เป็นต้น หากสนใจสามารถค้นหาด้วยตนเอง

การสร้าง

ยังคงใช้ตัวอย่างก่อนหน้า来讲 โครงสร้างมีดังนี้

pb_learn
│  common.proto

├─monster
│      monster.proto

└─player
        health.proto
        player.proto

สำหรับการสร้างโค้ด ต้องระบุพารามิเตอร์สามตัวทั้งหมด

  1. พาธการสแกน บอกตัวคอมไพเลอร์ว่าค้นหาไฟล์ protobuf จากที่ไหนและวิเคราะห์พาธการนำเข้าอย่างไร
  2. พาธการสร้าง ไฟล์ที่คอมไพล์เสร็จแล้ววางไว้ที่ไหน
  3. ไฟล์เป้าหมาย ระบุไฟล์เป้าหมายใดบ้างที่จะถูกคอมไพล์

ก่อนเริ่มต้นต้องแน่ใจว่าตั้งค่า go_package ในไฟล์ protobuf ถูกต้อง ดูพารามิเตอร์ที่รองรับผ่าน protoc -h ที่ใช้บ่อยที่สุดคือ -I หรือ --proto_path สามารถใช้หลายครั้งเพื่อระบุพาธการสแกนหลายพาธ เช่น

bash
$ protoc --proto_path=./pb_learn --proto_path=./third_party

เพียงระบุพาธการสแกนไม่เพียงพอ ต้องระบุพาธการสร้างและไฟล์ protobuf เป้าหมาย ที่นี่สร้างไฟล์ go ดังนั้นใช้พารามิเตอร์ --go_out รองรับโดยปลั๊กอิน protoc-gen-go ที่ดาวน์โหลดก่อนหน้า

bash
$ cd pb_learn

$ protoc --proto_path=. --go_out=. common.proto

$ ls
common.pb.go  common.proto  monster/  player/

พารามิเตอร์ --go_out คือระบุพาธการสร้าง . หมายถึงพาธปัจจุบัน common.proto คือระบุไฟล์ที่จะคอมไพล์ หากต้องการสร้างโค้ด grpc (ต้องมีปลั๊กอิน grpc ก่อน) สามารถเพิ่มพารามิเตอร์ --go-grpc_out (หากไฟล์ protobuf ไม่ได้กำหนด service จะไม่สร้างไฟล์ที่สอดคล้องกัน)

bash
$ protoc --proto_path=. --go_out=. --go-grpc_out=. common.proto

$ ls
common.pb.go  common.proto  common_grpc.pb.go  monster/  player/

common.pb.go คือคำจำกัดความประเภท protobuf ที่สร้าง common_grpc.pb.go คือโค้ด gRPC ที่สร้าง มันขึ้นอยู่กับ前者 หากไม่มีคำจำกัดความของภาษาที่สอดคล้องกัน ก็ไม่สามารถสร้างโค้ด gRPC ได้

หากต้องการคอมไพล์ไฟล์ protobuf ทั้งหมดในไดเรกทอรีนี้ สามารถใช้ wildcard * เช่น

bash
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.proto

หากต้องการรวมไฟล์ทั้งหมด สามารถใช้ wildcard ** เช่น ./**/*.proto

bash
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.proto

แต่ วิธีนี้ใช้ได้เฉพาะกับ shell ที่รองรับ wildcard ประเภทนี้ เช่น ภายใต้ windows cmd หรือ powershell ไม่รองรับการเขียนแบบนี้

powershell
D> protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.proto
Invalid file name pattern or missing input file "./**/*.proto"

โชคดีที่ gitbash รองรับคำสั่ง linux หลายคำสั่ง สามารถทำให้ windows รองรับไวยากรณ์นี้ได้ เพื่อหลีกเลี่ยงการเขียนคำสั่งซ้ำทุกครั้ง สามารถใส่ไว้ใน makefile

makefile
.PHONY: all

proto_gen:
  protoc --proto_path=. \
       --go_out=paths=source_relative:. \
       --go-grpc_out=paths=source_relative:. \
       ./**/*.proto ./*.proto

จะสังเกตว่ามี paths=source_relative:. เพิ่มขึ้นมา นี่คือการตั้งค่าโหมดพาธการสร้างไฟล์ มีตัวเลือกที่ใช้ได้ดังนี้

  • paths=import โดยค่าเริ่มต้นเป็นนี้ ไฟล์จะสร้างในไดเรกทอรีที่ระบุโดย import มันสามารถเป็นพาธโมดูลได้ เช่น มีไฟล์ protos/buzz.proto ระบุ paths=example.com/project/protos/fizz สุดท้ายจะสร้าง example.com/project/protos/fizz/buzz.pb.go
  • module=$PREFIX เมื่อสร้าง จะลบคำนำหน้าพาธออก ในตัวอย่างข้างต้น หากระบุคำนำหน้า example.com/project สุดท้ายจะสร้าง protos/fizz/buzz.pb.go โหมดนี้ส่วนใหญ่ใช้เพื่อสร้างในโมดูลโดยตรง (รู้สึกว่า好像ไม่มีความแตกต่าง)
  • paths=source_relative ไฟล์ที่สร้างจะรักษาโครงสร้างสัมพัทธ์เดียวกับไฟล์ protobuf ในไดเรกทอรีที่ระบุ

หลังเครื่องหมายเซมิโคลอน : คือพาธการสร้างที่ระบุ

|  common.proto
|  common.pb.go

├─monster
│      monster.pb.go
│      monster.proto

└─player
        health.pb.go
        health.proto
        health_grpc.pb.go
        player.pb.go
        player.proto

การสะท้อน

สามารถขยาย enum และ message ผ่าน options ก่อนนำเข้า "google/protobuf/descriptor.proto"

protobuf
import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  optional string string_name = 123456789;
}

enum Integer {
  INT64 = 0[
    (string_name) = "int_64"
  ];
}

นี่เท่ากับการเพิ่มข้อมูลเมตาให้กับค่าการนับนี้ สำหรับ message ก็เช่นเดียวกัน ดังนี้

protobuf
import "google/protobuf/descriptor.proto";

extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}

message MyMessage {
  option (my_option) = "Hello world!";
}

นี่เท่ากับการมี reflection เกี่ยวกับ protobuf หลังจากสร้างโค้ดสามารถเข้าถึงผ่าน Descriptor ได้ ดังนี้

go
func main() {
  message := pb_learn.MyMessage{}
  message.ProtoReflect().Descriptor().Options().ProtoReflect().Range(func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool {
    fmt.Println(descriptor.FullName(), ":", value)
    return true
  })
}

เอาต์พุต

my_option:"Hello world!"

วิธีนี้สามารถเปรียบเทียบกับการเพิ่ม tag ให้กับโครงสร้างใน go รู้สึกไม่ต่างกันมาก ตามวิธีนี้ยังสามารถ实现ฟังก์ชันการตรวจสอบพารามิเตอร์ได้ เพียงแค่เขียนกฎใน options ตรวจสอบผ่าน Descriptor

Golang by www.golangdev.cn edit