然后编写服务端:
package main import ( "context" "fmt" "google.golang.org/grpc" "matsuri/yoyoyo" "net" ) type Server struct { } func (s *Server) SayHello (ctx context.Context, request *yoyoyo.HelloRequest) (*yoyoyo.HelloResponse, error) { res := new(yoyoyo.HelloResponse) res.Age = 38 // Gender 是一个 int32 类型的变量,至于为什么,后面会说 res.Gender = 1 // 然后这里的类型是 *yoyoyo.Response 的切片,但是注意:不是 HelloResponse_Response ,而是 Response // 因为我们在定义 Response 的时候将它写在了外面,不是嵌套在 HelloResponse 里面的,所以这导致了两个名字不一样,这一点要注意 res.Hobby = []*yoyoyo.Response{{Hobby: "草莓牛奶"}, {Hobby: "纳豆"}, {Hobby: "money"}, {Hobby: "阿宅"}} return res, nil } func main() { // 直接进行 grpc 服务端创建、注册等逻辑没有变化 server := grpc.NewServer() yoyoyo.RegisterHelloServer(server, &Server{}) listener, err := net.Listen("tcp", ":33333") if err != nil { fmt.Println(err) return } if err = server.Serve(listener); err != nil { fmt.Println(err) return } }客户端进行调用:
package main import ( "context" "fmt" "google.golang.org/grpc" "matsuri/yoyoyo" ) func main() { conn, err := grpc.Dial("127.0.0.1:33333", grpc.WithInsecure()) if err != nil { fmt.Println(err) return } defer func() { _ = conn.Close() }() client := yoyoyo.NewHelloClient(conn) // 不需要参数,所以 HelloRequest 传递一个空的结构体 response, _ := client.SayHello(context.Background(), &yoyoyo.HelloRequest{}) fmt.Println(response.Age) // 38 fmt.Println(response.Gender) // FEMALE fmt.Println(response.Hobby) // [hobby:"草莓牛奶" hobby:"纳豆" hobby:"money" hobby:"阿宅"] for _, v := range response.Hobby { fmt.Print(v.Hobby, " ") } // 草莓牛奶 纳豆 money 阿宅 }完全没有任何问题,结果和我们预期是一致的,但我们注意一下里面的 Gender 属性。我们在 proto 文件中是这么做的,MALE = 0,FEMALE = 1,然后当我们返回 1 的时候,客户端调用会得到字符串 "FEMALE"。至于为什么能实现这种结果,我们还是要到 protoc 生成的 Go 文件中看一下,也就是 hello.pb.go。
// 定义一个枚举,也可以放在 HelloResponse 里面实现 type Gender int32 const ( Gender_MALE Gender = 0 Gender_FEMALE Gender = 1 ) // Enum value maps for Gender. var ( Gender_name = map[int32]string{ 0: "MALE", 1: "FEMALE", } Gender_value = map[string]int32{ "MALE": 0, "FEMALE": 1, } )以上包含注释的所有内容,都是 protoc 帮我们自动生成的,我们看到 Gender 是一个 int32 的别名,然后定义了两个变量 Gender_MALE 和 Gender_FEMALE,所以除了手动指定 0 或者 1,还可以直接使用 Gender_MALE 和 Gender_FEMALE。当然这不是重点,重点是下面它做了一个映射,所以才能返回字符串。
然后再来看看 Python 调用会怎么样,记得一定要先根据 proto 文件生成对应的 Python 文件,否则是没法调用的。
import grpc import hello_pb2_grpc as pb2_grpc import hello_pb2 as pb2 # 连接到 Go 的服务端,它监听 33333 端口 channel = grpc.insecure_channel("127.0.0.1:33333") client = pb2_grpc.HelloStub(channel=channel) response = client.SayHello( # 传递一个 Empty(), 也可以传递 HelloRequest 里面什么都不写 pb2.HelloRequest() ) print(response.age) # 38 print(response.gender) # 1 print(response.hobby) """ [hobby: "\350\215\211\350\216\223\347\211\233\345\245\266" , hobby: "\347\272\263\350\261\206" , hobby: "money" , hobby: "\351\230\277\345\256\205" ] """ # 上面的值是使用八进制数表示的,比如:8 进制的 97 是 141,那么 "\141\141\141" 就等同于 "aaa" # 同理 16 进制的 97 是 61,那么 "\x61\x61\x61" 也表示 "aaa" print([item.hobby for item in response.hobby]) # [\'草莓牛奶\', \'纳豆\', \'money\', \'阿宅\']比较尴尬的是,Python 调用 gender 返回的是 1,所以我们还需要通过 proto 文件确定相关的映射关系。
所以 Python 和 Go 之间通过 gRPC 是很容易实现交互的,主要是两者必须要根据同一份 proto 文件生成对应的源文件才可以。里面的服务(service)、方法、参数和返回值的载体应该要大写,至于载体里面的成员是否大写就没有要求了。如果是小写,那么 Go 会自动变成大写,但是 Python 通过小写也能够进行调用。
protobuf 中的 map 类型这个 map 类型用起来应该是非常开心的,这种数据结构非常的方便,那么还是来介绍一下该如何定义。
map 在定义的时候,是需要指定 key、value 的类型的,声明方式类似于 C++。
syntax = "proto3"; option go_package = ".;yoyoyo"; // 对 Python 无影响 message HelloRequest { // 里面没有任何成员 } message Response { string hobby = 1; } message HelloResponse { // 我们只返回一个 info,信息啥的都在里面 map<string, string> info = 1; } service Hello { rpc SayHello(HelloRequest) returns (HelloResponse) {} }