mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-24 20:52:22 +00:00
ch4-02: 完善细节
This commit is contained in:
parent
e57aafd85a
commit
ffecb1ddda
@ -4,7 +4,7 @@ Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据
|
|||||||
|
|
||||||
## Protobuf入门
|
## Protobuf入门
|
||||||
|
|
||||||
对于没有用过Protobuf的读者,建议先从官网了解下基本用法。这里我们尝试如何将Protobuf和RPC结合在一起使用,通过Protobuf来最终保证RPC的接口规范和安全。Protobuf中最基本的数据单元是message,是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。
|
对于没有用过Protobuf的读者,建议先从官网了解下基本用法。这里我们尝试将Protobuf和RPC结合在一起使用,通过Protobuf来最终保证RPC的接口规范和安全。Protobuf中最基本的数据单元是message,是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。
|
||||||
|
|
||||||
首先创建hello.proto文件,其中包装HelloService服务中用到的字符串类型:
|
首先创建hello.proto文件,其中包装HelloService服务中用到的字符串类型:
|
||||||
|
|
||||||
@ -18,11 +18,11 @@ message String {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
开头的syntax语句表示采用Protobuf第三版本的语法。第三版的Protobuf对语言进行了提炼简化,所有成员均采用类似Go语言中的零值初始化(不再支持自定义默认值),同时消息成员也不再支持required特性。然后package指令指明当前是main包(这样可以和Go的包名保持一致),当然用户也可以针对不同的语言定制对应的包路径和名称。最后message关键字定义一个新的String类型,在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员,该成员的Protobuf编码时的成员编号为1。
|
开头的syntax语句表示采用proto3的语法。第三版的Protobuf对语言进行了提炼简化,所有成员均采用类似Go语言中的零值初始化(不再支持自定义默认值),因此消息成员也不再需要支持required特性。然后package指令指明当前是main包(这样可以和Go的包名保持一致,简化例子代码),当然用户也可以针对不同的语言定制对应的包路径和名称。最后message关键字定义一个新的String类型,在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员,该成员编码时用1编号代替名字。
|
||||||
|
|
||||||
在XML或JSON等数据描述语言中,一般通过成员的名字来绑定对应的数据。但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,但是也非常不便于人类查阅。我们目前并不关注Protobuf的编码技术,最终生成的Go结构体可以自由采用JSON或gob等编码格式,因此大家可以暂时忽略Protobuf的成员编码部分。
|
在XML或JSON等数据描述语言中,一般通过成员的名字来绑定对应的数据。但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,但是也非常不便于人类查阅。我们目前并不关注Protobuf的编码技术,最终生成的Go结构体可以自由采用JSON或gob等编码格式,因此大家可以暂时忽略Protobuf的成员编码部分。
|
||||||
|
|
||||||
Protobuf核心的工具集是C++语言开发的,在官方的protoc编译器中并不支持Go语言。要想基于上面的hello.proto文件生成相应的Go代码,需要安装相应的工具。首先是安装官方的protoc工具,可以从 https://github.com/google/protobuf/releases 下载。然后是安装针对Go语言的代码生成插件,可以通过`go get github.com/golang/protobuf/protoc-gen-go`命令安装。
|
Protobuf核心的工具集是C++语言开发的,在官方的protoc编译器中并不支持Go语言。要想基于上面的hello.proto文件生成相应的Go代码,需要安装相应的插件。首先是安装官方的protoc工具,可以从 https://github.com/google/protobuf/releases 下载。然后是安装针对Go语言的代码生成插件,可以通过`go get github.com/golang/protobuf/protoc-gen-go`命令安装。
|
||||||
|
|
||||||
然后通过以下命令生成相应的Go代码:
|
然后通过以下命令生成相应的Go代码:
|
||||||
|
|
||||||
@ -37,9 +37,6 @@ $ protoc --go_out=. hello.proto
|
|||||||
```go
|
```go
|
||||||
type String struct {
|
type String struct {
|
||||||
Value string `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"`
|
Value string `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"`
|
||||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
|
||||||
XXX_unrecognized []byte `json:"-"`
|
|
||||||
XXX_sizecache int32 `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *String) Reset() { *m = String{} }
|
func (m *String) Reset() { *m = String{} }
|
||||||
@ -57,9 +54,9 @@ func (m *String) GetValue() string {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
生成的结构体中有一些以`XXX_`为名字前缀的成员,目前可以忽略这些成员。同时String类型还自动生成了一组方法,其中ProtoMessage方法表示这是一个实现了proto.Message接口的方法。此外Protobuf还为每个成员生成了一个Get方法,Get方法不仅可以处理空指针类型,而且可以和Protobuf第二版的方法保持一致(第二版的自定义默认值特性依赖这类方法)。
|
生成的结构体中还会包含一些以`XXX_`为名字前缀的成员,我们已经隐藏了这些成员。同时String类型还自动生成了一组方法,其中ProtoMessage方法表示这是一个实现了proto.Message接口的方法。此外Protobuf还为每个成员生成了一个Get方法,Get方法不仅可以处理空指针类型,而且可以和Protobuf第二版的方法保持一致(第二版的自定义默认值特性依赖这类方法)。
|
||||||
|
|
||||||
基于新的String类型,我们可以重新实现HelloService:
|
基于新的String类型,我们可以重新实现HelloService服务:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type HelloService struct{}
|
type HelloService struct{}
|
||||||
@ -94,7 +91,7 @@ $ protoc --go_out=plugins=grpc:. hello.proto
|
|||||||
|
|
||||||
在生成的代码中多了一些类似HelloServiceServer、HelloServiceClient的新类型。这些类型是为grpc服务的,并不符合我们的RPC要求。
|
在生成的代码中多了一些类似HelloServiceServer、HelloServiceClient的新类型。这些类型是为grpc服务的,并不符合我们的RPC要求。
|
||||||
|
|
||||||
grpc插件为我们提供了改进思路,下面我们将探索如何为我们的RPC生成安全的代码。
|
不过grpc插件为我们提供了改进的思路,下面我们将探索如何为我们的RPC生成安全的代码。
|
||||||
|
|
||||||
|
|
||||||
## 定制代码生成插件
|
## 定制代码生成插件
|
||||||
@ -104,8 +101,8 @@ Protobuf的protoc编译器是通过插件机制实现对不同语言的支持。
|
|||||||
参考grpc插件的代码,可以发现generator.RegisterPlugin函数可以用来注册插件。插件是一个generator.Plugin接口:
|
参考grpc插件的代码,可以发现generator.RegisterPlugin函数可以用来注册插件。插件是一个generator.Plugin接口:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// A Plugin provides functionality to add to the output during Go code generation,
|
// A Plugin provides functionality to add to the output during
|
||||||
// such as to produce RPC stubs.
|
// Go code generation, such as to produce RPC stubs.
|
||||||
type Plugin interface {
|
type Plugin interface {
|
||||||
// Name identifies the plugin.
|
// Name identifies the plugin.
|
||||||
Name() string
|
Name() string
|
||||||
@ -113,7 +110,8 @@ type Plugin interface {
|
|||||||
// code generation begins.
|
// code generation begins.
|
||||||
Init(g *Generator)
|
Init(g *Generator)
|
||||||
// Generate produces the code generated by the plugin for this file,
|
// Generate produces the code generated by the plugin for this file,
|
||||||
// except for the imports, by calling the generator's methods P, In, and Out.
|
// except for the imports, by calling the generator's methods P, In,
|
||||||
|
// and Out.
|
||||||
Generate(file *FileDescriptor)
|
Generate(file *FileDescriptor)
|
||||||
// GenerateImports produces the import declarations for this file.
|
// GenerateImports produces the import declarations for this file.
|
||||||
// It is called after Generate.
|
// It is called after Generate.
|
||||||
@ -162,7 +160,7 @@ func (p *netrpcPlugin) genServiceCode(svc *descriptor.ServiceDescriptorProto) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
要使用该插件需要先通过generator.RegisterPlugin函数注册插件,可以在init函数完成:
|
要使用该插件需要先通过generator.RegisterPlugin函数注册插件,可以在init函数中完成:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func init() {
|
func init() {
|
||||||
@ -170,11 +168,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
因为Go语言的包只能静态导入,我们无法向已经安装的protoc-gen-go添加我们新编写的插件。我们可以完整克隆protoc-gen-go对应main函数:
|
因为Go语言的包只能静态导入,我们无法向已经安装的protoc-gen-go添加我们新编写的插件。我们将重新克隆protoc-gen-go对应main函数:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// copy from https://github.com/golang/protobuf/blob/master/protoc-gen-go/main.go
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -186,9 +182,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Begin by allocating a generator. The request and response structures are stored there
|
|
||||||
// so we can do error handling easily - the response structure contains the field to
|
|
||||||
// report failure.
|
|
||||||
g := generator.New()
|
g := generator.New()
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(os.Stdin)
|
data, err := ioutil.ReadAll(os.Stdin)
|
||||||
@ -233,13 +226,13 @@ func main() {
|
|||||||
$ protoc --go-netrpc_out=plugins=netrpc:. hello.proto
|
$ protoc --go-netrpc_out=plugins=netrpc:. hello.proto
|
||||||
```
|
```
|
||||||
|
|
||||||
其中`--go-netrpc_out`参数告知protoc编译器加载名为protoc-gen-go-netrpc的插件,插件中的`plugins=netrpc`指示启用内部名为netrpc的netrpcPlugin插件。
|
其中`--go-netrpc_out`参数告知protoc编译器加载名为protoc-gen-go-netrpc的插件,插件中的`plugins=netrpc`指示启用内部唯一的名为netrpc的netrpcPlugin插件。在新生成的hello.pb.go文件中将包含增加的注释代码。
|
||||||
|
|
||||||
在新生成的hello.pb.go文件中将包含增加的注释代码。至此,手工定制的Protobuf代码生成插件终于可以工作了。
|
至此,手工定制的Protobuf代码生成插件终于可以工作了。
|
||||||
|
|
||||||
## 自动生成完整的RPC代码
|
## 自动生成完整的RPC代码
|
||||||
|
|
||||||
在前面的例子中我们已经构件了最小化的netrpcPlugin插件,并且通过克隆protoc-gen-go的主程序创建了新的protoc-gen-go-netrpc的插件程序。我们现在开始继续完善netrpcPlugin插件,最终目标是生成RPC安全接口。
|
在前面的例子中我们已经构件了最小化的netrpcPlugin插件,并且通过克隆protoc-gen-go的主程序创建了新的protoc-gen-go-netrpc的插件程序。现在开始继续完善netrpcPlugin插件,最终目标是生成RPC安全接口。
|
||||||
|
|
||||||
首先是自定义的genImportCode方法中生成导入包的代码:
|
首先是自定义的genImportCode方法中生成导入包的代码:
|
||||||
|
|
||||||
@ -266,7 +259,7 @@ type ServiceMethodSpec struct {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
然后我们新建一个buildServiceSpec方法用来构造每个服务的ServiceSpec元信息:
|
然后我们新建一个buildServiceSpec方法用来解析每个服务的ServiceSpec元信息:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (p *netrpcPlugin) buildServiceSpec(svc *descriptor.ServiceDescriptorProto) *ServiceSpec {
|
func (p *netrpcPlugin) buildServiceSpec(svc *descriptor.ServiceDescriptorProto) *ServiceSpec {
|
||||||
@ -286,6 +279,8 @@ func (p *netrpcPlugin) buildServiceSpec(svc *descriptor.ServiceDescriptorProto)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
其中输入参数是`*descriptor.ServiceDescriptorProto`类型,完整描述了一个服务的所有信息。然后通过`svc.GetName()`就可以获取Protobuf文件中定义对服务对名字。Protobuf文件中的名字转为Go语言对名字后,需要通过`generator.CamelCase`函数进行一次转换。类似的,在for循环中我们通过`m.GetName()`获取方法的名字,然后再转为Go语言中对应的名字。比较复杂的是对输入和输出参数名字的解析:首先需要通过`m.GetInputType()`获取输入参数的类型,然后通过`p.ObjectNamed`类型对应的类对象信息,最后获取类对象的名字。
|
||||||
|
|
||||||
然后我们就可以基于buildServiceSpec方法构造的服务的元信息生成服务的代码:
|
然后我们就可以基于buildServiceSpec方法构造的服务的元信息生成服务的代码:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -350,11 +345,13 @@ const tmplService = `
|
|||||||
|
|
||||||
type {{.ServiceName}}Interface interface {
|
type {{.ServiceName}}Interface interface {
|
||||||
{{- range $_, $m := .MethodList}}
|
{{- range $_, $m := .MethodList}}
|
||||||
{{$m.MethodName}}(in *{{$m.InputTypeName}}, out *{{$m.OutputTypeName}}) error
|
{{$m.MethodName}}(*{{$m.InputTypeName}}, *{{$m.OutputTypeName}}) error
|
||||||
{{- end}}
|
{{- end}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Register{{.ServiceName}}(srv *rpc.Server, x {{.ServiceName}}Interface) error {
|
func Register{{.ServiceName}}(
|
||||||
|
srv *rpc.Server, x {{.ServiceName}}Interface,
|
||||||
|
) error {
|
||||||
if err := srv.RegisterName("{{.ServiceName}}", x); err != nil {
|
if err := srv.RegisterName("{{.ServiceName}}", x); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -367,7 +364,9 @@ type {{.ServiceName}}Client struct {
|
|||||||
|
|
||||||
var _ {{.ServiceName}}Interface = (*{{.ServiceName}}Client)(nil)
|
var _ {{.ServiceName}}Interface = (*{{.ServiceName}}Client)(nil)
|
||||||
|
|
||||||
func Dial{{.ServiceName}}(network, address string) (*{{.ServiceName}}Client, error) {
|
func Dial{{.ServiceName}}(network, address string) (
|
||||||
|
*{{.ServiceName}}Client, error,
|
||||||
|
) {
|
||||||
c, err := rpc.Dial(network, address)
|
c, err := rpc.Dial(network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -376,14 +375,14 @@ func Dial{{.ServiceName}}(network, address string) (*{{.ServiceName}}Client, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
{{range $_, $m := .MethodList}}
|
{{range $_, $m := .MethodList}}
|
||||||
func (p *{{$root.ServiceName}}Client) {{$m.MethodName}}(in *{{$m.InputTypeName}}, out *{{$m.OutputTypeName}}) error {
|
func (p *{{$root.ServiceName}}Client) {{$m.MethodName}}(
|
||||||
|
in *{{$m.InputTypeName}}, out *{{$m.OutputTypeName}},
|
||||||
|
) error {
|
||||||
return p.Client.Call("{{$root.ServiceName}}.{{$m.MethodName}}", in, out)
|
return p.Client.Call("{{$root.ServiceName}}.{{$m.MethodName}}", in, out)
|
||||||
}
|
}
|
||||||
{{end}}
|
{{end}}
|
||||||
`
|
`
|
||||||
```
|
```
|
||||||
|
|
||||||
当Protobuf的插件定制工作完成后,每次hello.proto文件中RPC服务的变化都可以自动生成代码。同时,采用类似的技术也可以为其它语言编写代码生成插件。
|
当Protobuf的插件定制工作完成后,每次hello.proto文件中RPC服务的变化都可以自动生成代码。也可以通过更新插件的模板,调整或增加生成代码的内容。在掌握了定制Protobuf插件技术后,你将彻底拥有这个技术。
|
||||||
|
|
||||||
在掌握了定制Protobuf插件技术后,你将彻底拥有这个技术。
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user