这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天
这两个都是用来定义接口的。用的地方比较多。
ProtoBuf
首先还是安装protobuf。我们只需要protoc就可以了。从github的release中可以下载到对应的版本。解压即可用。
随后安装protoc-gen-go。注意路径是google.golang.org/protobuf/cmd/protoc-gen-go@latest,
不是github.com/golang/protobuf/protoc-gen-go@latest。
之后我们在使用protoc的时候加上--go_out参数即可导出go代码了。
下面是一个简单的protobuf的代码:
syntax = "proto3";
option go_package=".";
message Student {
string name = 1;
bool male = 2;
repeated int32 scores = 3;
}
第一行我们标定使用的是proto3。如果你用的是proto2,这个字段就写proto2。
由于我们这个文件不从属于任何一个go package,所以我们需要指定其当前目录输出,否则我们生成代码的时候会报错。
下面就是我们的一个消息结构了。以message开头, Student为这个消息的名称。其中有三个参数,参数的定义和c语言模式很像。注意后面的等于号与标号,这个是必须的。
repeated和singular是一对关键字。通常singular可以不行,默认添加,表示单个。repeated其实就是表示数组,在go里面就是切片。
proto可以写注释,格式和c格式一致。一个proto文件可以写多个message(也就是多个结构体,如果需要表示复合类型的,那么多个message就是必须的,否则无法表示复合类型)。这里只要将这个message看成sturct或者class其实就行了。proto通过给定的字段来生成类,添加上proto的方法就是我们的输出了。
使用以下命令导出代码:
protoc --go_out=. student.proto
可以看到导出了一个student.pb.go的文件。
type Student struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Male bool `protobuf:"varint,2,opt,name=male,proto3" json:"male,omitempty"`
Scores []int32 `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"`
}
导出的结构体和我们提供的一致,这里为了要导出所以首字母都变大写了。go这种按照名称是否大写开头来判断是否导出还是挺有意思的。
为了让结构更加清晰,我们将go_package设为了"./student"也就是新建了一个student包来放我们的代码。test是我的项目。
package main
import (
"fmt"
"google.golang.org/protobuf/proto"
"test/student"
)
func main() {
test := &student.Student{
Name: "test",
Male: true,
Scores: []int32{98, 85, 88},
}
data, _ := proto.Marshal(test)
newTest := &student.Student{}
proto.Unmarshal(data, newTest)
// Now test and newTest contain the same data.
if test.GetName() != newTest.GetName() {
fmt.Printf("data mismatch %q != %q", test.GetName(), newTest.GetName())
}
}
就已经可以使用了。
下面是嵌套的情况:
syntax = "proto3";
option go_package = "./student";
message Address {
string locate = 1;
int32 room = 2;
}
message Student {
string name = 1;
bool male = 2;
repeated int32 scores = 3;
Address locate = 4;
}
编译出来为:
type Address struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Locate string `protobuf:"bytes,1,opt,name=locate,proto3" json:"locate,omitempty"`
Room int32 `protobuf:"varint,2,opt,name=room,proto3" json:"room,omitempty"`
}
...
type Student struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Male bool `protobuf:"varint,2,opt,name=male,proto3" json:"male,omitempty"`
Scores []int32 `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"`
Locate *Address `protobuf:"bytes,4,opt,name=locate,proto3" json:"locate,omitempty"`
}
protobuf是支持rpc的。
syntax = "proto3";
option go_package = "./student";
message Address {
string locate = 1;
int32 room = 2;
}
message Student {
string name = 1;
bool male = 2;
repeated int32 scores = 3;
}
service GetAddressService {
rpc GetAddress(Student) returns(Address);
}
使用service关键字创建service,这里我们创建一个GetAddress服务,入参Student返回值Address。但是protogo没有实现service所以无法导出interface,想要用就需要使用插件。这里我们略过。
Thrift
Thrift和protobuf类似。
又是艰难的安装过程。
[Apache Thrift - Download]下载源文件。
进入根目录执行:
./configure
make
sudo make install
go要使用的话,还需要go get github.com/apache/thrift/lib/go/thrift
Thrift有以下类型:
基本类型:
- bool: 布尔值
- byte: 8位有符号整数
- i16: 16位有符号整数
- i32: 32位有符号整数
- i64: 64位有符号整数
- double: 64位浮点数
- string: UTF-8编码的字符串
- binary: 二进制串
结构体类型: struct: 定义的结构体对象
容器类型:
- list: 有序元素列表
- set: 无序无重复元素集合
- map: 有序的key/value集合
异常类型: exception: 异常类型
服务类型: service: 具体对应服务的类
从官方给的样例来看吧。
官方给出了两个文件,一个是需要用到的部分共享定义:
/**
* This Thrift file can be included by other Thrift files that want to share
* these definitions.
*/
namespace cl shared
namespace cpp shared
namespace d share // "shared" would collide with the eponymous D keyword.
namespace dart shared
namespace java shared
namespace perl shared
namespace php shared
namespace haxe shared
namespace netstd shared
struct SharedStruct {
1: i32 key
2: string value
}
service SharedService {
SharedStruct getStruct(1: i32 key)
}
另一个是:
/**
* Thrift files can reference other Thrift files to include common struct
* and service definitions. These are found using the current path, or by
* searching relative to any paths specified with the -I compiler flag.
*
* Included objects are accessed using the name of the .thrift file as a
* prefix. i.e. shared.SharedObject
*/
include "shared.thrift"
/**
* Thrift files can namespace, package, or prefix their output in various
* target languages.
*/
namespace cl tutorial
namespace cpp tutorial
namespace d tutorial
namespace dart tutorial
namespace java tutorial
namespace php tutorial
namespace perl tutorial
namespace haxe tutorial
namespace netstd tutorial
/**
* Thrift lets you do typedefs to get pretty names for your types. Standard
* C style here.
*/
typedef i32 MyInteger
/**
* Thrift also lets you define constants for use across languages. Complex
* types and structs are specified using JSON notation.
*/
const i32 INT32CONSTANT = 9853
const map<string,string> MAPCONSTANT = {'hello':'world', 'goodnight':'moon'}
/**
* You can define enums, which are just 32 bit integers. Values are optional
* and start at 1 if not supplied, C style again.
*/
enum Operation {
ADD = 1,
SUBTRACT = 2,
MULTIPLY = 3,
DIVIDE = 4
}
/**
* Structs are the basic complex data structures. They are comprised of fields
* which each have an integer identifier, a type, a symbolic name, and an
* optional default value.
*
* Fields can be declared "optional", which ensures they will not be included
* in the serialized output if they aren't set. Note that this requires some
* manual management in some languages.
*/
struct Work {
1: i32 num1 = 0,
2: i32 num2,
3: Operation op,
4: optional string comment,
}
/**
* Structs can also be exceptions, if they are nasty.
*/
exception InvalidOperation {
1: i32 whatOp,
2: string why
}
/**
* Ahh, now onto the cool part, defining a service. Services just need a name
* and can optionally inherit from another service using the extends keyword.
*/
service Calculator extends shared.SharedService {
/**
* A method definition looks like C code. It has a return type, arguments,
* and optionally a list of exceptions that it may throw. Note that argument
* lists and exception lists are specified using the exact same syntax as
* field lists in struct or exception definitions.
*/
void ping(),
i32 add(1:i32 num1, 2:i32 num2),
i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),
/**
* This method has a oneway modifier. That means the client only makes
* a request and does not listen for any response at all. Oneway methods
* must be void.
*/
oneway void zip()
}
原文档中的注释已经足够的详细。
第一句include,我们可以像c语言一样包含另一个thrift文件。两个文件中包含了名称一样的namespace,但他们所属于的包不同一个是属于shared一个属于tutorial,所以不会有碰撞的问题。
namespace标明了我们这个thrift在相应的语言中被翻译成什么,对go来说就是package,对cpp来说就是class。
namespace go mypackage // 生成go代码的话生成代码就在mypackage这个包。
后面我们看到typedef这个关键字,我们可以像cpp一样来定义我们的别名。
thrift支持常量。可以看到给出的常量定义和cpp语法一致。你要是学过cpp的话,对thrift肯定能立马上手。
struct Work {
1: i32 num1 = 0,
2: i32 num2,
3: Operation op,
4: optional string comment,
}
结构体中的结构需要有标号,也就是前面的1:这个玩意,可以给字段附上初始值。optional关键字可以让字段不进入序列化。exception的定义和结构体一致。
service可以继承。使用extends关键字即可。
service Calculator extends shared.SharedService {
void ping(),
i32 add(1:i32 num1, 2:i32 num2),
i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),
oneway void zip()
}
我们的Calculator服务继承了shared中的SharedService服务。服务中包含四个方法(继承的没算)。
定义方法的格式和cpp的函数定义类似,不过我们的参数列表格式则是和上方的struct的字段定义一致。
方法可以有异常,使用throws告知这个方法会存在异常,并在throws的列表中添加这个参数。
oneway表示这个方法只能被请求,但是不会有返回,oneway方法必须为void。
我没有找到thrift的详细文档,我只能通过他人的实例我学习thrift在代码中的使用,导致有很多地方我也不明白。(自己用起来感觉没有protobuf好用)
我们编写一个简单的示例:
namespace go user
struct User {
1: i32 ID,
2: string Name,
3: optional string Address
}
service GetUserService {
User GetUserByName(1: string name)
User GetUserByID(1: i32 ID)
}
生成代码。生成代码中,包依赖可能需要自己手动修改一下。
服务端:
package main
import (
"context"
"demo/gen-go/user"
"fmt"
thrift "github.com/apache/thrift/lib/go/thrift"
)
// define a service
type GetUser struct{}
// 下面来实现thrift提供的接口
func (service *GetUser) GetUserByName(ctx context.Context, name string) (_r *user.User, _err error) {
return &user.User{ID: 12, Name: name}, nil
}
func (service *GetUser) GetUserByID(ctx context.Context, ID int32) (_r *user.User, _err error) {
fmt.Println("get a call")
return &user.User{ID: ID, Name: "lihua"}, nil
}
func main() {
// 创建handle,为了使用其实现的函数。
handler := &GetUser{}
// 别人的代码里面很多的方法都弃用了。这边就是得到两个类:
// 一个是transport的工厂类,一个是protocol的工厂类。
transportFactory := thrift.NewTFramedTransportFactoryConf(thrift.NewTTransportFactory(), &thrift.TConfiguration{})
protocolFactory := thrift.NewTBinaryProtocolFactoryConf(&thrift.TConfiguration{})
var transport thrift.TServerTransport
const address = "127.0.0.1:8888"
var err error
// 创建套接字
transport, err = thrift.NewTServerSocket(address)
if err != nil {
fmt.Println("Server runing error.")
panic(err)
}
fmt.Printf("Service start on %s\n", address)
// 定义工作
processor := user.NewGetUserServiceProcessor(handler)
// 创建服务, 这里使用的simpleserver一般只有在演示的时候才用,毕竟是单线程阻塞模式。
server := thrift.NewTSimpleServer4(processor, transport, transportFactory, protocolFactory)
// 开启服务
err = server.Serve()
if err != nil {
fmt.Println("Server running error.")
panic(err)
}
}
随后我们实现client。部分设置保持一致:
package main
import (
"client/gen-go/user"
"context"
"fmt"
thrift "github.com/apache/thrift/lib/go/thrift"
)
const remote = "127.0.0.1:8888"
func main() {
transportFactory := thrift.NewTFramedTransportFactoryConf(thrift.NewTTransportFactory(), &thrift.TConfiguration{})
protocolFactory := thrift.NewTBinaryProtocolFactoryConf(&thrift.TConfiguration{})
// 老代码里使用的是NewTSocket,但是被弃用了。
transport := thrift.NewTSocketConf(remote, &thrift.TConfiguration{})
clientTransport, _ := transportFactory.GetTransport(transport)
// 创建client,通过transport和protocol保存的设置来生成client。
client := user.NewGetUserServiceClientFactory(clientTransport, protocolFactory)
// 注意这里是transport open不是client open。
if err := transport.Open(); err != nil {
fmt.Println("Start transport error.")
panic(err)
}
defer transport.Close()
// thrift0.17版本自动添加了context参数,使用时需要传入context。
ctx := context.Background()
fmt.Println(client.GetUserByID(ctx, 31))
}
运行:
server:
Service start on 127.0.0.1:8888
get a call
client:
User({ID:31 Name:lihua Address:<nil>}) <nil>