Go微服务精讲:Go-Zero全流程实战即时通讯

315 阅读6分钟

Go微服务精讲:Go-Zero全流程实战即时通讯

核心代码,注释必读

// download:3w ukoou com

开发三原则

Clarity(清晰)

作者引用了Hal Abelson and Gerald Sussman的一句话:

Programs must be written for people to read, and only incidentally for machines to execute

程序是什么,程序必须是为了开发人员阅读而编写的,只是偶尔给机器去执行,99%的时间程序代码面向的是开发人员,而只有1%的时间可能是机器在执行,这里比例不是重点,从中我们可以看出,清晰的代码是多么的重要,因为所有程序,不仅是Go语言,都是由开发人员编写,供其他人阅读和维护。

Simplicity(简单)

Simplicity is prerequisite for reliability

Edsger W. Dijkstra认为:可靠的前提条件就是简单,我们在实际开发中都遇到过,这段代码在写什么,想要完成什么事情,开发人员不理解这段代码,因此也不知道如何去维护,这就带来了复杂性,程序越是复杂就越难维护,越难维护就会是程序变得越来越复杂,因此,遇到程序变复杂时首先应该想到的是——重构,重构会重新设计程序,让程序变得简单。

Productivity(生产力)

在go-zero团队中,一直在强调这个话题,开发人员成产力的多少,并不是你写了多少行代码,完成了多少个模块开发,而是我们需要利用各种有效的途径来利用有限的时间完成开发效率最大化,而Goctl的诞生正是为了提高生产力, 因此这个开发原则我是非常认同的。

Go微服务精讲:Go-Zero全流程实战即时通讯 - go-zero中集成gorm

打开/etc/user.yaml,删除其中的3-7行,并增加一行Mode: dev

yaml
Name: user.rpc  
ListenOn: 0.0.0.0:8080
Mode: dev

接着打开internal/login/pingLogic.go,定位到文件末尾。这个接口是模板自带的存活检查,但是没有具体实现,现在会返回一个空json。增加一个Pong: "pong"键值来补全响应。

go
func (l *PingLogic) Ping(in *user.Request) (*user.Response, error) {  
    return &user.Response{  
        Pong: "pong",  
    }, nil  
}

现在运行go run user.go,已经能够正常编译和启动。当看到以下输出,说明微服务启动成功。

shell
Starting rpc server at 0.0.0.0:8080...

我想要验证一下这个接口是否符合预期,但是rpc服务不像http服务一样可以直接打开浏览器访问,这里就需要使用面向rpc的请求工具:

grpcurl

github.com/fullstoryde…

github提供了多种安装方法,因为我们本地已经搭建了go开发环境,因此可以直接使用go install命令安装

shell
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest

现在命令行使用grpcurl请求我们的微服务:

shell
复制代码
grpcurl -plaintext 127.0.0.1:8080 user.User/Ping

其中userUser分别对应user.proto中定义的package nameservice name,而Ping是具体的方法。

你应该会得到如下的输出,代表服务正常工作。

目前这个微服务只能实现最基础的存活探测功能。要给微服务添加更多功能,首先需要在proto文件中定义对应的接口。

proto文件现有的RequestResponse数据结构是针对Ping方法的,这里先将其重命名成PingRequestPingResponse

本文中仅演示简单的用户创建和用户信息获取功能,不设置额外的鉴权和token发放,相关内容我会在后面的文章补全。因此,新设两个接口CreateQueryById

protobuf
syntax = "proto3";  
  
package user;  
option go_package = "./user";  
  
message PingRequest {  
    string ping = 1;  
}  
  
message PingResponse {  
    string pong = 1;  
}  
  
message CreateRequest {  
    string username = 1;  
    string password = 2;  
}  
  
message CreateResponse {  
    int64 user_id = 1;  
}  
  
message QueryByIdRequest {  
    int64 user_id = 1;  
}  
  
message QueryResponse {  
    int64 user_id = 1;  
    string username = 2;  
    bytes password = 12;  
    int64 created_at = 15;  
    int64 updated_at = 16;  
}  
  
service User {  
    rpc Ping(PingRequest) returns(PingResponse);  
    rpc Create(CreateRequest) returns(CreateResponse);  
    rpc QueryById(QueryByIdRequest) returns(QueryResponse);  
}

Go微服务精讲:Go-Zero全流程实战即时通讯 - 服务发现机制

对于服务端服务发现来说,服务调用方无需关注服务发现的具体细节,只需要知道服务的DNS域名即可,支持不同语言的接入,对基础设施来说,需要专门支持负载均衡器,对于请求链路来说多了一次网络跳转,可能会有性能损耗。也可以把咱们比较熟悉的 nginx 反向代理理解为服务端服务发现。

客户端服务发现

对于客户端服务发现来说,由于客户端和服务端采用了直连的方式,比服务端服务发现少了一次网络跳转,对于服务调用方来说需要内置负载均衡器,不同的语言需要各自实现。

对于微服务架构来说,我们期望的是去中心化依赖,中心化的依赖会让架构变得复杂,当出现问题的时候也会让整个排查链路变得繁琐,所以在 go-zero 中采用的是客户端服务发现的模式。

gRPC的服务发现

gRPC提供了自定义Resolver的能力来实现服务发现,通过 Register方法来进行注册自定义的Resolver,自定义的Resolver需要实现Builder接口,定义如下:

grpc-go/resolver/resolver.go:261

type Builder interface {
    Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
    Scheme() string
}

先说下 Scheme() 方法的作用,该方法返回一个stirng。注册的 Resolver 会被保存在一个全局的变量m中,m是一个map,这个map的key即为 Scheme() 方法返回的字符串。也就是多个Resolver是通过Scheme来进行区分的,所以我们定义 Resolver 的时候 Scheme 不要重复,否则 Resolver 就会被覆盖。

grpc-go/resolver/resolver.go:49

func Register(b Builder) {
    m[b.Scheme()] = b
}

再来看下Build方法,Build方法有三个参数,还有Resolver返回值,乍一看不知道这些参数是干嘛的,遇到这种情况该怎么办呢?其实也很简单,去源码里看一下Build方法在哪里被调用的,就知道传入的参数是哪里来的,是什么含义了。

使用gRPC进行服务调用前,需要先创建一个 ClientConn 对象,最终发起调用的时候,其实是调用了 ClientConn 的 Invoke 方法,可以看下如下代码,其中 ClientConn 是通过调用 NewGreeterClient 传入的,NewGreeterClient 为 protoc 自动生成的代码,并赋值给 cc 属性,示例代码中创建 ClientConn 调用的是 Dial 方法,底层也会调用 DialContext