Go大师课程(七): 深入protobuf

303 阅读16分钟

Go大师课程系列将学习

基础

准备

让我们开始创建一个新项目。首先,我将在main.go文件中创建一个简单的 hello-world 程序并运行它,只是为了确保 Go 正常运行。

package main

import "fmt"

func main() {
    fmt.Println("Hello world")
}

然后创建一个名为 的新文件夹protoc,并processor_message.proto在其下添加一个文件。

pcbook
├── proto
│   └── processor_message.proto
└── main.go

如何定义 protobuf 消息

现在回到我们的 proto 文件。此文件将包含笔记本电脑 CPU 的消息定义。

我们从 开始syntax = "proto3"

目前,Google 官方文档中提供了 2 个版本的 protocol buffer :proto2proto3。为简单起见,我们在本课程中仅使用proto3(较新的版本)。

语法非常简单,只需使用message关键字后跟消息名称即可。然后在消息块中,我们定义其所有字段,如下图所示:

image.png

注意消息名称应为UpperCamelCase,字段名称应为lower_snake_case。

我们可以使用许多内置的标量值数据类型,例如:stringboolbytefloatdouble和许多其他整数类型。我们还可以使用自己的数据类型,例如枚举或其他消息。

每个消息字段都应分配一个唯一的标签。标签比字段名称更重要,因为 protobuf 将使用它来序列化消息。

标签只是一个任意整数,最小值为 1,最大值为 2 29 - 1,但 19000 至 19999 之间的数字除外,因为它们是为内部协议缓冲区实现保留的。

请注意,标签 1 到 15 仅占用 1 个字节进行编码,而标签 16 到 2047 占用 2 个字节。因此,您应该明智地使用它们,例如:将标签 1 到 15 保存在出现频率非常高的字段中。

请记住,标签不需要按顺序(或连续)排列,但对于消息的同一级别字段,它们必须是唯一的。

定义 CPU 消息

现在让我们回到我们的 proto 文件并定义 CPU 消息。

syntax = "proto3";

message CPU {
  string brand = 1;
  string name = 2;
  uint32 number_cores = 3;
  uint32 number_threads = 4;
  double min_ghz = 5;
  double max_ghz = 6;
}

CPU 将具有类型的品牌string,例如“Intel”,并且名称也是类型的string,例如“Core i7-9850”。

我们需要跟踪 CPU 有多少个核心或线程。它们不能为负数,所以我们uint32在这里使用。

接下来,它有最小和最大频率,例如 2.4 Ghz 或类似的值。因此我们可以double在这里使用类型。

生成 Go 代码

现在我们已经完成了第一个 protobuf 消息。我们如何从中生成 Go 代码?

首先,我们需要安装 protocol buffer 编译器(或者protoc)。在 macOS 上,我们可以借助Homebrew轻松完成此操作。

您可以使用以下简单命令安装 Homebrew:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

安装 Homebrew 后,可以运行此命令来安装protoc

brew install protobuf

我们可以通过运行命令来检查它是否正常工作protoc

接下来我们会到grpc.io去复制并运行2个命令来安装2个库:golang grpc库和protoc-gen-go库。

go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go

现在一切就绪!我将创建一个名为的新文件夹pb来存储生成的 Go 代码。

pcbook
├── proto
│   └── processor_message.proto
├── pb
└── main.go

然后运行此命令来生成代码:

protoc --proto_path=proto proto/*.proto --go_out=plugins=grpc:pb

我们的 proto 文件位于proto文件夹内,所以我们告诉protoc在该文件夹中查找它。

通过go_out参数,我们告诉protoc使用grpc插件来生成Go代码,并将它们存储在pb我们之前创建的文件夹中。

现在如果我们在 vscode 中打开该文件夹,我们将看到一个新文件processor_message.pb.go

pcbook
├── proto
│   └── processor_message.proto
├── pb
│   └── processor_message.pb.go
└── main.go

查看内部,有一个 CPU 结构和所有字段,其数据类型正确,正如我们在协议缓冲区文件中定义的一样。

const _ = proto.ProtoPackageIsVersion3

type CPU struct {
    Brand                string   `protobuf:"bytes,1,opt,name=brand,proto3" json:"brand,omitempty"`
    Name                 string   `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
    NumberCores          uint32   `protobuf:"varint,3,opt,name=number_cores,json=numberCores,proto3" json:"number_cores,omitempty"`
    NumberThreads        uint32   `protobuf:"varint,4,opt,name=number_threads,json=numberThreads,proto3" json:"number_threads,omitempty"`
    MinGhz               float64  `protobuf:"fixed64,5,opt,name=min_ghz,json=minGhz,proto3" json:"min_ghz,omitempty"`
    MaxGhz               float64  `protobuf:"fixed64,6,opt,name=max_ghz,json=maxGhz,proto3" json:"max_ghz,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

func (m *CPU) Reset()         { *m = CPU{} }
func (m *CPU) String() string { return proto.CompactTextString(m) }
func (*CPU) ProtoMessage()    {}
func (*CPU) Descriptor() ([]byte, []int) {
    return fileDescriptor_466578cecc6db379, []int{0}
}

func (m *CPU) XXX_Unmarshal(b []byte) error {
    return xxx_messageInfo_CPU.Unmarshal(m, b)
}
func (m *CPU) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
    return xxx_messageInfo_CPU.Marshal(b, m, deterministic)
}
func (m *CPU) XXX_Merge(src proto.Message) {
    xxx_messageInfo_CPU.Merge(m, src)
}
func (m *CPU) XXX_Size() int {
    return xxx_messageInfo_CPU.Size(m)
}
func (m *CPU) XXX_DiscardUnknown() {
    xxx_messageInfo_CPU.DiscardUnknown(m)
}

var xxx_messageInfo_CPU proto.InternalMessageInfo

func (m *CPU) GetBrand() string {
    if m != nil {
        return m.Brand
    }
    return ""
}

func (m *CPU) GetName() string {
    if m != nil {
        return m.Name
    }
    return ""
}

func (m *CPU) GetNumberCores() uint32 {
    if m != nil {
        return m.NumberCores
    }
    return 0
}

func (m *CPU) GetNumberThreads() uint32 {
    if m != nil {
        return m.NumberThreads
    }
    return 0
}

func (m *CPU) GetMinGhz() float64 {
    if m != nil {
        return m.MinGhz
    }
    return 0
}

func (m *CPU) GetMaxGhz() float64 {
    if m != nil {
        return m.MaxGhz
    }
    return 0
}

gRPC 内部使用了一些特殊字段来序列化消息,但我们不需要关心它们。还生成了一些有用的 getter 函数。

编写 Makefile

我们用来生成代码的命令相当长,因此当我们更新 proto 文件并想要重新生成代码时,输​​入起来不太方便。因此,让我们创建一个 Makefile,其中包含一个简短的命令来执行此操作。

pcbook
├── proto
│   └── processor_message.proto
├── pb
│   └── processor_message.pb.go
├── main.go
└── Makefile

在这个 Makefile 中,我们添加了一个gen任务来运行代码生成命令,一个clean任务来随时删除所有生成的 go 文件,以及一个run任务来运行该main.go文件。

gen:
    protoc --proto_path=proto proto/*.proto --go_out=plugins=grpc:pb

clean:
    rm pb/*.go 

run:
    go run main.go

深入

  • 在protobuf消息字段中定义和使用自定义类型,例如枚举或其他消息。
  • 讨论何时使用嵌套类型以及何时不使用。
  • 将protobuf消息组织成多个文件,放入一个包中,然后将其导入到其他地方。
  • 探索一些 Google 已经定义的知名类型。
  • 了解重复字段、其中之一字段。
  • 使用选项告诉 protoc 使用我们想要的包名生成 Go 代码。

一个文件中有多条消息

让我们从processor_message.proto文件开始。我们可以在 1 个文件中定义多个消息,因此我将在此处添加一个 GPU 消息。这是有道理的,因为 GPU 也是一个处理器。

syntax = "proto3";

message CPU {
  string brand = 1;
  string name = 2;
  uint32 number_cores = 3;
  uint32 number_threads = 4;
  double min_ghz = 5;
  double max_ghz = 6;
}

message GPU {
  string brand = 1;
  string name = 2;
  double min_ghz = 3;
  double max_ghz = 4;
  // memory ?
}

它具有与 CPU 类似的一些字段,例如品牌、名称、最小和最大频率。唯一不同的是它有自己的内存。

自定义类型:消息和枚举

内存是一个非常流行的术语,可以在其他地方使用,例如 RAM 或存储(持久驱动器)。它有许多不同的测量单位,例如千字节、兆字节、千兆字节或太字节。所以我将在一个单独的memory_message.proto文件中将其定义为自定义类型,以便我们以后可以重复使用它。

pcbook
├── proto
│   ├── processor_message.proto
│   └── memory_message.proto
├── pb
│   └── processor_message.pb.go
├── main.go
└── Makefile

首先,我们需要定义测量单位。为此,我们将使用枚举。由于此单位应仅存在于内存上下文中,因此我们应该将其定义为内存消息中的嵌套类型。

syntax = "proto3";

message Memory {
  enum Unit {
    UNKNOWN = 0;
    BIT = 1;
    BYTE = 2;
    KILOBYTE = 3;
    MEGABYTE = 4;
    GIGABYTE = 5;
    TERABYTE = 6;
  }

  uint64 value = 1;
  Unit unit = 2;
}

惯例是,始终使用特殊值作为枚举的默认值,并为其分配标签 0。然后我们添加其他单位,从 BIT 到 TERABYTE。

内存消息将有2个字段:一个用于值,另一个用于单位。

导入 proto 文件

现在让我们回到processor_message.proto文件。我们必须导入文件memory_message.proto才能使用 Memory 类型。在 GPU 消息中,我们添加了一个 Memory 类型的新内存字段。

syntax = "proto3";

import "memory_message.proto";

message CPU {
  string brand = 1;
  string name = 2;
  uint32 number_cores = 3;
  uint32 number_threads = 4;
  double min_ghz = 5;
  double max_ghz = 6;
}

message GPU {
  string brand = 1;
  string name = 2;
  double min_ghz = 3;
  double max_ghz = 4;
  Memory memory = 5;
}

现在如果我们尝试生成 Go 代码,会出现错误“包名称不一致”

因为我们没有在proto文件中指定包名,所以protoc默认会使用文件名作为Go包。

protoc 在这里抛出错误的原因是,生成的 2 个 Go 文件将属于 2 个不同的包,但在 Go 中,我们不能将不同包的 2 个文件放在同一个文件夹中,在本例中就是 文件pb夹。

设置包名称

为了解决这个问题,我们必须通过使用此命令在我们的 proto 文件中指定 protoc 将生成的代码放在同一个包中package techschool.pcbook

文件memory_message.proto

syntax = "proto3";

package techschool.pcbook;

message Memory {
  enum Unit {
    UNKNOWN = 0;
    BIT = 1;
    BYTE = 2;
    KILOBYTE = 3;
    MEGABYTE = 4;
    GIGABYTE = 5;
    TERABYTE = 6;
  }

  uint64 value = 1;
  Unit unit = 2;
}

文件processor_message.proto

syntax = "proto3";

package techschool.pcbook;

import "memory_message.proto";

message CPU {
  string brand = 1;
  string name = 2;
  uint32 number_cores = 3;
  uint32 number_threads = 4;
  double min_ghz = 5;
  double max_ghz = 6;
}

message GPU {
  string brand = 1;
  string name = 2;
  double min_ghz = 3;
  double max_ghz = 4;
  Memory memory = 5;
}

现在如果我们make gen再次运行,它将起作用,并且生成的两个 Go 文件将属于同一个包techschool_pcbook。Protoc 在这里使用下划线,因为在 Go 中我们不能在包名中使用点。

文件memory_message.pb.go

package techschool_pcbook

import (
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

type Memory_Unit int32

const (
    Memory_UNKNOWN  Memory_Unit = 0
    Memory_BIT      Memory_Unit = 1
    Memory_BYTE     Memory_Unit = 2
    Memory_KILOBYTE Memory_Unit = 3
    Memory_MEGABYTE Memory_Unit = 4
    Memory_GIGABYTE Memory_Unit = 5
    Memory_TERABYTE Memory_Unit = 6
)
...

文件processor_message.pb.go

package techschool_pcbook

import (
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

type CPU struct {
    Brand                string   `protobuf:"bytes,1,opt,name=brand,proto3" json:"brand,omitempty"`
    Name                 string   `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
    NumberCores          uint32   `protobuf:"varint,3,opt,name=number_cores,json=numberCores,proto3" json:"number_cores,omitempty"`
    NumberThreads        uint32   `protobuf:"varint,4,opt,name=number_threads,json=numberThreads,proto3" json:"number_threads,omitempty"`
    MinGhz               float64  `protobuf:"fixed64,5,opt,name=min_ghz,json=minGhz,proto3" json:"min_ghz,omitempty"`
    MaxGhz               float64  `protobuf:"fixed64,6,opt,name=max_ghz,json=maxGhz,proto3" json:"max_ghz,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}
...

定义存储消息

让我们继续我们的项目。我将为storage_message.proto文件中的存储创建一条新消息。

pcbook
├── proto
│   ├── processor_message.proto
│   ├── memory_message.proto
│   └── storage_message.proto
├── pb
│   ├── processor_message.pb.go
│   └── memory_message.pb.go
├── main.go
└── Makefile

存储可以是硬盘驱动器或固态驱动器。所以我们应该Driver用这两个值定义一个枚举。

syntax = "proto3";

package techschool.pcbook;

import "memory_message.proto";

message Storage {
  enum Driver {
    UNKNOWN = 0;
    HDD = 1;
    SSD = 2;
  }

  Driver driver = 1;
  Memory memory = 2;
}

然后在存储消息中添加2个字段:驱动程序类型和内存大小。

使用选项为 Go 生成自定义包名称

protoc 为我们生成的Go 包名techschool_pcbook有点太长,并且与pb包含 Go 文件的文件夹名称不匹配。

所以我想告诉它用作pb包名,但仅适用于 Go,因为 Java 或其他语言将使用不同的包命名约定。

我们可以通过option go_package = "pb"在 proto 文件中进行设置来做到这一点。

文件storage_message.proto

syntax = "proto3";

package techschool.pcbook;

option go_package = "pb";

import "memory_message.proto";

message Storage {
  enum Driver {
    UNKNOWN = 0;
    HDD = 1;
    SSD = 2;
  }

  Driver driver = 1;
  Memory memory = 2;
}

文件memory_message.proto

syntax = "proto3";

package techschool.pcbook;

option go_package = "pb";

message Memory {
  enum Unit {
    UNKNOWN = 0;
    BIT = 1;
    BYTE = 2;
    KILOBYTE = 3;
    MEGABYTE = 4;
    GIGABYTE = 5;
    TERABYTE = 6;
  }

  uint64 value = 1;
  Unit unit = 2;
}

文件processor_message.proto

syntax = "proto3";

package techschool.pcbook;

option go_package = "pb";

import "memory_message.proto";

message CPU {
  string brand = 1;
  string name = 2;
  uint32 number_cores = 3;
  uint32 number_threads = 4;
  double min_ghz = 5;
  double max_ghz = 6;
}

message GPU {
  string brand = 1;
  string name = 2;
  double min_ghz = 3;
  double max_ghz = 4;
  Memory memory = 5;
}

现在如果我们运行make gen生成代码,所有生成的 Go 文件都将使用相同的pb包。

pcbook
├── proto
│   ├── processor_message.proto
│   ├── memory_message.proto
│   └── storage_message.proto
├── pb
│   ├── processor_message.pb.go
│   ├── memory_message.pb.go
│   └── storage_message.pb.go
├── main.go
└── Makefile

文件storage_message.pb.go

package pb

import (
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

type Storage_Driver int32

const (
    Storage_UNKNOWN Storage_Driver = 0
    Storage_HDD     Storage_Driver = 1
    Storage_SSD     Storage_Driver = 2
)

var Storage_Driver_name = map[int32]string{
    0: "UNKNOWN",
    1: "HDD",
    2: "SSD",
}

var Storage_Driver_value = map[string]int32{
    "UNKNOWN": 0,
    "HDD":     1,
    "SSD":     2,
}

func (x Storage_Driver) String() string {
    return proto.EnumName(Storage_Driver_name, int32(x))
}

func (Storage_Driver) EnumDescriptor() ([]byte, []int) {
    return fileDescriptor_170f09d838bd8a04, []int{0, 0}
}

type Storage struct {
    Driver               Storage_Driver `protobuf:"varint,1,opt,name=driver,proto3,enum=techschool.pcbook.Storage_Driver" json:"driver,omitempty"`
    Memory               *Memory        `protobuf:"bytes,2,opt,name=memory,proto3" json:"memory,omitempty"`
    XXX_NoUnkeyedLiteral struct{}       `json:"-"`
    XXX_unrecognized     []byte         `json:"-"`
    XXX_sizecache        int32          `json:"-"`
}
...

文件memory_message.pb.go

package pb

import (
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

type Memory_Unit int32

const (
    Memory_UNKNOWN  Memory_Unit = 0
    Memory_BIT      Memory_Unit = 1
    Memory_BYTE     Memory_Unit = 2
    Memory_KILOBYTE Memory_Unit = 3
    Memory_MEGABYTE Memory_Unit = 4
    Memory_GIGABYTE Memory_Unit = 5
    Memory_TERABYTE Memory_Unit = 6
)

var Memory_Unit_name = map[int32]string{
    0: "UNKNOWN",
    1: "BIT",
    2: "BYTE",
    3: "KILOBYTE",
    4: "MEGABYTE",
    5: "GIGABYTE",
    6: "TERABYTE",
}

var Memory_Unit_value = map[string]int32{
    "UNKNOWN":  0,
    "BIT":      1,
    "BYTE":     2,
    "KILOBYTE": 3,
    "MEGABYTE": 4,
    "GIGABYTE": 5,
    "TERABYTE": 6,
}

func (x Memory_Unit) String() string {
    return proto.EnumName(Memory_Unit_name, int32(x))
}

func (Memory_Unit) EnumDescriptor() ([]byte, []int) {
    return fileDescriptor_c0c7f919ccc765da, []int{0, 0}
}

type Memory struct {
    Value                uint64      `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"`
    Unit                 Memory_Unit `protobuf:"varint,2,opt,name=unit,proto3,enum=techschool.pcbook.Memory_Unit" json:"unit,omitempty"`
    XXX_NoUnkeyedLiteral struct{}    `json:"-"`
    XXX_unrecognized     []byte      `json:"-"`
    XXX_sizecache        int32       `json:"-"`
}
...

文件processor_message.pb.go

package pb

import (
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

type CPU struct {
    Brand                string   `protobuf:"bytes,1,opt,name=brand,proto3" json:"brand,omitempty"`
    Name                 string   `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
    NumberCores          uint32   `protobuf:"varint,3,opt,name=number_cores,json=numberCores,proto3" json:"number_cores,omitempty"`
    NumberThreads        uint32   `protobuf:"varint,4,opt,name=number_threads,json=numberThreads,proto3" json:"number_threads,omitempty"`
    MinGhz               float64  `protobuf:"fixed64,5,opt,name=min_ghz,json=minGhz,proto3" json:"min_ghz,omitempty"`
    MaxGhz               float64  `protobuf:"fixed64,6,opt,name=max_ghz,json=maxGhz,proto3" json:"max_ghz,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}
...

定义键盘消息

接下来,我们将定义键盘消息。它可以有 QWERTY、QWERTZ 或 AZERTY 布局。供您参考,QWERTZ 在德国广泛使用。而在法国,AZERTY 更受欢迎。

syntax = "proto3";

package techschool.pcbook;

option go_package = "pb";

message Keyboard {
  enum Layout {
    UNKNOWN = 0;
    QWERTY = 1;
    QWERTZ = 2;
    AZERTY = 3;
  }

  Layout layout = 1;
  bool backlit = 2;
}

键盘可以有背光,也可以没有背光,因此我们使用布尔字段。很简单,对吧?

定义屏幕消息

现在我们来写一个更复杂的消息:屏幕。它有一个嵌套的消息类型:Resolution。我们在这里使用嵌套类型的原因是:分辨率是一个与屏幕紧密相关的实体,单独存在时没有任何意义。

syntax = "proto3";

package techschool.pcbook;

option go_package = "pb";

message Screen {
  message Resolution {
    uint32 width = 1;
    uint32 height = 2;
  }

  enum Panel {
    UNKNOWN = 0;
    IPS = 1;
    OLED = 2;
  }

  float size_inch = 1;
  Resolution resolution = 2;
  Panel panel = 3;
  bool multitouch = 4;
}

类似地,我们有一个屏幕面板枚举,可以是 IPS 或 OLED。然后是屏幕尺寸(以英寸为单位)。最后是一个布尔字段,用于判断它是否是多点触控屏幕。

定义笔记本电脑消息

好了,我想我们基本上已经定义了笔记本电脑的所有必要组件。现在让我们定义笔记本电脑的消息。

syntax = "proto3";

package techschool.pcbook;

option go_package = "pb";

import "processor_message.proto";
import "memory_message.proto";

message Laptop {
    string id = 1;
    string brand = 2;
    string name = 3;
    CPU cpu = 4;
    Memory ram = 5;
}

它有一个字符串类型的唯一标识符。此 ID 将由服务器自动生成。它有一个品牌和一个名称。然后是 CPU 和 RAM。我们需要导入其他 proto 文件才能使用这些类型。

重复字段

一台笔记本电脑可以有多个 GPU,所以我们使用repeated关键字来告诉 protoc 这是一个 GPU 列表。

同样,笔记本电脑有多个存储器也是正常的,所以这个字段也应该重复。

syntax = "proto3";

package techschool.pcbook;

option go_package = "pb";

import "processor_message.proto";
import "memory_message.proto";
import "storage_message.proto";
import "screen_message.proto";
import "keyboard_message.proto";

message Laptop {
    string id = 1;
    string brand = 2;
    string name = 3;
    CPU cpu = 4;
    Memory ram = 5;
    repeated GPU gpus = 6;
    repeated Storage storages = 7;
    Screen screen = 8;
    Keyboard keyboard = 9;
}

接下来是 2 个常规字段:屏幕和键盘。这很简单。

Oneof 字段

那么笔记本电脑的重量呢?假设我们允许以公斤或磅为单位指定它。为了做到这一点,我们可以使用一个新关键字:oneof

syntax = "proto3";

package techschool.pcbook;

option go_package = "pb";

import "processor_message.proto";
import "memory_message.proto";
import "storage_message.proto";
import "screen_message.proto";
import "keyboard_message.proto";

message Laptop {
    string id = 1;
    string brand = 2;
    string name = 3;
    CPU cpu = 4;
    Memory ram = 5;
    repeated GPU gpus = 6;
    repeated Storage storages = 7;
    Screen screen = 8;
    Keyboard keyboard = 9;
    oneof weight {
        double weight_kg = 10;
        double weight_lb = 11;
    }
}

在此块中,我们定义了 2 个字段,一个用于千克,另一个用于磅。请记住,当您使用oneof字段组时,只有最后分配的字段才会保留其值。

知名类型

然后我们再添加 2 个字段:美元价格和笔记本电脑的发布年份。最后,我们需要一个时间戳字段来存储系统中记录的最后更新时间。

Timestamp 是 Google 已经定义的著名类型之一,因此我们只需要导入包并使用它。

syntax = "proto3";

package techschool.pcbook;

option go_package = "pb";

import "processor_message.proto";
import "memory_message.proto";
import "storage_message.proto";
import "screen_message.proto";
import "keyboard_message.proto";
import "google/protobuf/timestamp.proto";

message Laptop {
    string id = 1;
    string brand = 2;
    string name = 3;
    CPU cpu = 4;
    Memory ram = 5;
    repeated GPU gpus = 6;
    repeated Storage storages = 7;
    Screen screen = 8;
    Keyboard keyboard = 9;
    oneof weight {
        double weight_kg = 10;
        double weight_lb = 11;
    }
    double price_usd = 12;
    uint32 release_year = 13;
    google.protobuf.Timestamp updated_at = 14;
}

还有许多其他知名类型。请查看此链接以了解更多信息。

现在我们可以运行make gen来为所有消息生成 Go 代码。

pcbook
├── proto
│   ├── processor_message.proto
│   ├── memory_message.proto
│   ├── storage_message.proto
│   ├── keyboard_message.proto
│   ├── screen_message.proto
│   └── laptop_message.proto
├── pb
│   ├── processor_message.pb.go
│   ├── memory_message.pb.go
│   └── storage_message.pb.go
│   ├── keyboard_message.pb.go
│   ├── screen_message.pb.go
│   └── laptop_message.pb.go
├── main.go
└── Makefile