东边一条溪 發表於 2021-10-25 09:10:00

go-zero 实战之 blog 系统

<h2 id="go-zero-实战项目blog">go-zero 实战项目:blog</h2>
<p>本文以 <code>blog</code> 的网站后台为例,着重介绍一下如何使用 <code>go-zero</code> 开发 <code>blog</code> 的用户模块。</p>
<p>本文涉及的所有资料都已上传 github 仓库 <code>kougazhang/go-zero-demo</code>,感兴趣的同学可以自行下载。</p>
<p>用户模块是后台管理系统常见的模块,它的功能大家也非常熟悉。管理用户涉及到前端操作,用户信息持久化又离不开数据库。所以用户模块可谓是 "麻雀虽小五脏俱全"。本文将详细介绍一下如何使用 go-zero 完成用户模块功能,如:用户登录、添加用户、删除用户、修改用户、查询用户 等(完整的 api 文件请参考仓库代码)。</p>
<p><strong>blog 整体架构</strong></p>
<p><img src="https://pic2.zhimg.com/80/v2-2c362002879cf8d20909d0dcca666b0d_1440w.jpg" alt="blog 系统整体架构图" loading="lazy"></p>
<p>最上面是 api 网关层。go-zero 需要 api 网关层来代理请求,把 request 通过 gRPC 转发给对应的 rpc 服务去处理。这块把具体请求转发到对应的 rpc 服务的业务逻辑,需要手写。</p>
<p>接下来是 rpc 服务层。上图 rpc 服务中的 user 就是接下来向大家演示的模块。每个 rpc 服务可以单独部署。服务启动后会把相关信息注册到 ETCD,这样 api 网关层就可以通过 ECTD 发现具体服务的地址。rpc 服务处理具体请求的业务逻辑,需要手写。</p>
<p>最后是 <code>model</code> 层。model 层封装的是数据库操作的相关逻辑。如果是查询类的相关操作,会先查询 redis 中是否有对应的缓存。非查询类操作,则会直接操作 MySQL。goctl 能通过 sql 文件生成普通的 CRDU 代码。目前 goctl 这部分功能支持 MySQL、PostgreSQL、MongoDB。</p>
<p>下面演示如何使用 go-zero 开发一个 blog 系统的用户模块。</p>
<h2 id="api-网关层">api 网关层</h2>
<p><strong>编写 blog.api 文件</strong></p>
<ul>
<li><strong>生成 blog.api 文件</strong></li>
</ul>
<p>执行命令 <code>goctl api -o blog.api</code>,创建 blog.api 文件。</p>
<ul>
<li><strong>api 文件的作用</strong></li>
</ul>
<p>api 文件的详细语法请参阅文档 https://go-zero.dev/cn/api-grammar.html,本文按照个人理解谈一谈 api 文件的作用和基础语法。</p>
<p>api 文件是用来生成 api 网关层的相关代码的。</p>
<ul>
<li><strong>api 文件的语法</strong></li>
</ul>
<p>api 文件的语法和 Golang 语言非常类似,type 关键字用来定义结构体,service 部分用来定义 api 服务。</p>
<p>type 定义的结构体,主要是用来声明请求的入参和返回值的,即 request 和 response.</p>
<p>service 定义的 api 服务,则声明了路由,handler,request 和 response.</p>
<p>具体内容请结合下面的默认的生成的 api 文件进行理解。</p>
<pre><code class="language-text">// 声明版本,可忽略
syntax = "v1"

// 声明一些项目信息,可忽略
info(
   title: // TODO: add title
   desc: // TODO: add description
   author: "zhao.zhang"
   email: "zhao.zhang@upai.com"
)

// 重要配置
// request 是结构体的名称,可以使用 type 关键词定义新的结构体
type request {
   // TODO: add members here and delete this comment
   // 与 golang 语言一致,这里声明结构体的成员
}

// 语法同上,只是业务含义不同。response 一般用来声明返回值。
type response {
   // TODO: add members here and delete this comment
}

// 重要配置
// blog-api 是 service 的名称.
service blog-api {
   // GetUser 是处理请求的视图函数
   @handler GetUser // TODO: set handler name and delete this comment
   // get 声明了该请求使用 GET 方法
   // /users/id/:userId 是 url,:userId 表明是一个变量
   // request 就是上面 type 定义的那个 request, 是该请求的入参
   // response 就是上面 type 定义的那个 response, 是该请求的返回值。
   get /users/id/:userId(request) returns(response)

   @handler CreateUser // TODO: set handler name and delete this comment
   post /users/create(request)
}
</code></pre>
<ul>
<li><strong>编写 blog.api 文件</strong></li>
</ul>
<p>鉴于文章篇幅考虑完整的 blog.api 文件请参考 gitee 上的仓库。下面生成的代码是按照仓库上的 blog.api 文件生成的。</p>
<p><strong>api 相关代码</strong></p>
<ul>
<li><strong>生成相关的代码</strong></li>
</ul>
<p>执行命令 goctl api go -api blog.api -dir . ,生成 api 相关代码。</p>
<ul>
<li><strong>目录介绍</strong></li>
</ul>
<pre><code class="language-text">├── blog.api # api 文件
├── blog.go # 程序入口文件
├── etc
│   └── blog-api.yaml # api 网关层配置文件
├── go.mod
├── go.sum
└── internal
    ├── config
    │   └── config.go # 配置文件
    ├── handler # 视图函数层, handler 文件与下面的 logic 文件一一对应
    │   ├── adduserhandler.go
    │   ├── deleteuserhandler.go
    │   ├── getusershandler.go
    │   ├── loginhandler.go
    │   ├── routes.go
    │   └── updateuserhandler.go
    ├── logic # 需要手动填充代码的地方
    │   ├── adduserlogic.go
    │   ├── deleteuserlogic.go
    │   ├── getuserslogic.go
    │   ├── loginlogic.go
    │   └── updateuserlogic.go
    ├── svc # 封装 rpc 对象的地方,后面会将
    │   └── servicecontext.go
    └── types # 把 blog.api 中定义的结构体映射为真正的 golang 结构体
      └── types.go
</code></pre>
<ul>
<li><strong>文件间的调用关系</strong></li>
</ul>
<p><img src="https://pic2.zhimg.com/80/v2-f6e4c07789b4e05c5e9d6f3010b7e405_1440w.jpg" alt="img" loading="lazy"></p>
<p>因为到此时还没涉及到 rpc 服务,所以 api 内各模块的调用关系就是非常简单的单体应用间的调用关系。routers.go 是路由,根据 request Method 和 url 把请求分发到对应到的 handler 上,handler 内部会去调用对应的 logic. logic 文件内是我们注入代码逻辑的地方。</p>
<p><strong>小结</strong></p>
<p>Api 层相关命令:</p>
<ul>
<li>执行命令 <code>goctl api -o blog.api</code>, 创建 blog.api 文件。</li>
<li>执行命令 <code>goctl api go -api blog.api -dir .</code>,生成 api 相关代码。</li>
<li>加参数 goctl 也可以生成其他语言的 api 层的文件,比如 java、ts 等,这里就不展开了。</li>
</ul>
<h2 id="rpc-服务">rpc 服务</h2>
<p><strong>编写 proto 文件</strong></p>
<ul>
<li><strong>生成 user.proto 文件</strong></li>
</ul>
<p>使用命令 <code>goctl rpc template -o user.proto</code>, 生成 user.proto 文件</p>
<ul>
<li><strong>user.proto 文件的作用</strong></li>
</ul>
<p>user.proto 的作用是用来生成 rpc 服务的相关代码。</p>
<p>protobuf 的语法已经超出了 go-zero 的范畴了,这里就不详细展开了。</p>
<ul>
<li><strong>编写 user.proto 文件</strong></li>
</ul>
<p>鉴于文章篇幅考虑完整的 user.proto 文件请参考 gitee 上的仓库。</p>
<p><strong>生成 rpc 相关代码</strong></p>
<ul>
<li><strong>生成 user rpc 服务相关代码</strong></li>
</ul>
<p>使用命令 <code>goctl rpc proto -src user.proto -dir .</code> 生成 user rpc 服务的代码。</p>
<p><strong>小结</strong></p>
<p>rpc 服务相关命令:</p>
<ul>
<li>使用命令 <code>goctl rpc template -o user.proto</code>, 生成 user.proto 文件</li>
<li>使用命令 <code>goctl rpc proto -src user.proto -dir .</code> 生成 user rpc 服务的代码。</li>
</ul>
<h2 id="api-服务调用-rpc-服务">api 服务调用 rpc 服务</h2>
<p><strong>A:为什么本节要安排在 rpc 服务的后面?</strong></p>
<p>Q:因为 logic 部分的内容主体就是调用对应的 user rpc 服务,所以我们必须要在 user rpc 的代码已经生成后才能开始这部分的内容。</p>
<p><strong>A:api 网关层调用 rpc 服务的步骤</strong></p>
<p>Q:对这部分目录结构不清楚的,可以参考前文 “api 网关层-api 相关代码-目录介绍”。</p>
<ul>
<li><strong>编辑配置文件 etc/blog-api.yaml,配置 rpc 服务的相关信息。</strong></li>
</ul>
<pre><code class="language-yaml">Name: blog-api
Host: 0.0.0.0
Port: 8888
# 新增 user rpc 服务.
User:
Etcd:
    #Hosts 是 user.rpc 服务在 etcd 中的 value 值
    Hosts:
      - localhost:2379
    # Key 是 user.rpc 服务在 etcd 中的 key 值
    Key: user.rpc
</code></pre>
<ul>
<li><strong>编辑文件 config/config.go</strong></li>
</ul>
<pre><code class="language-go">type Config struct {
   rest.RestConf
   // 手动添加
   // RpcClientConf 是 rpc 客户端的配置, 用来解析在 blog-api.yaml 中的配置
   User zrpc.RpcClientConf
}
</code></pre>
<ul>
<li><strong>编辑文件 internal/svc/servicecontext.go</strong></li>
</ul>
<pre><code class="language-go">type ServiceContext struct {
   Config config.Config
   // 手动添加
   // users.Users 是 user rpc 服务对外暴露的接口
   User   users.Users
}

func NewServiceContext(c config.Config) *ServiceContext {
   return &amp;ServiceContext{
      Config: c,
      // 手动添加
      //zrpc.MustNewClient(c.User) 创建了一个 grpc 客户端
      User:   users.NewUsers(zrpc.MustNewClient(c.User)),
   }
}
</code></pre>
<ul>
<li><strong>编辑各个 logic 文件,这里以 internal/logic/loginlogic.go 为例</strong></li>
</ul>
<pre><code class="language-go">func (l *LoginLogic) Login(req types.ReqUser) (*types.RespLogin, error) {
   // 调用 user rpc 的 login 方法
   resp, err := l.svcCtx.User.Login(l.ctx, &amp;users.ReqUser{Username: req.Username, Password: req.Password})
   if err != nil {
      return nil, err
   }
   return &amp;types.RespLogin{Token: resp.Token}, nil
}
</code></pre>
<h2 id="model-层">model 层</h2>
<p><strong>编写 sql 文件</strong></p>
<p>编写创建表的 SQL 文件 user.sql, 并在数据库中执行。</p>
<pre><code class="language-sql">CREATE TABLE `user`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`username` varchar(255) NOT NULL UNIQUE COMMENT 'username',
`password` varchar(255) NOT NULL COMMENT 'password',
PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
</code></pre>
<p><strong>生成 model 相关代码</strong></p>
<p>运行命令 <code>goctl model mysql ddl -c -src user.sql -dir .</code>, 会生成操作数据库的 CRDU 的代码。</p>
<p>此时的 model 目录:</p>
<pre><code class="language-text">├── user.sql # 手写
├── usermodel.go # 自动生成
└── vars.go # 自动生成
</code></pre>
<p><strong>model 生成的代码注意点</strong></p>
<ul>
<li>model 这块代码使用的是拼接 SQL 语句,可能会存在 SQL 注入的风险。</li>
<li>生成 CRUD 的代码比较初级,需要我们手动编辑 usermodel.go 文件,自己拼接业务需要的 SQL。参见 usermdel.go 中的 FindByName 方法。</li>
</ul>
<h2 id="rpc-调用-model-层的代码">rpc 调用 model 层的代码</h2>
<p><strong>rpc 目录结构</strong></p>
<p>rpc 服务我们只需要关注下面加注释的文件或目录即可。</p>
<pre><code class="language-text">├── etc
│   └── user.yaml # 配置文件,数据库的配置写在这
├── internal
│   ├── config
│   │   └── config.go # config.go 是 yaml 对应的结构体
│   ├── logic # 填充业务逻辑的地方
│   │   ├── createlogic.go
│   │   ├── deletelogic.go
│   │   ├── getalllogic.go
│   │   ├── getlogic.go
│   │   ├── loginlogic.go
│   │   └── updatelogic.go
│   ├── server
│   │   └── usersserver.go
│   └── svc
│       └── servicecontext.go # 封装各种依赖
├── user
│   └── user.pb.go
├── user.go
├── user.proto
└── users
    └── users.go
</code></pre>
<p><strong>rpc 调用 model 层代码的步骤</strong></p>
<ul>
<li><strong>编辑 etc/user.yaml 文件</strong></li>
</ul>
<pre><code class="language-yaml">Name: user.rpc
ListenOn: 127.0.0.1:8080
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
# 以下为手动添加的配置
# mysql 配置
DataSource: root:1234@tcp(localhost:3306)/gozero
# 对应的表
Table: user
# redis 作为换存储
Cache:
- Host: localhost:6379
</code></pre>
<ul>
<li><strong>编辑 internal/config/config.go 文件</strong></li>
</ul>
<pre><code class="language-go">type Config struct {
   // zrpc.RpcServerConf 表明继承了 rpc 服务端的配置
   zrpc.RpcServerConf
   DataSource string          // 手动代码
   Cache      cache.CacheConf // 手动代码
}
</code></pre>
<ul>
<li><strong>编辑 internal/svc/servicecontext.go, 把 model 等依赖封装起来。</strong></li>
</ul>
<pre><code class="language-go">type ServiceContext struct {
   Config config.Config
   Modelmodel.UserModel // 手动代码
}

func NewServiceContext(c config.Config) *ServiceContext {
   return &amp;ServiceContext{
      Config: c,
      Model:model.NewUserModel(sqlx.NewMysql(c.DataSource), c.Cache), // 手动代码
   }
}
</code></pre>
<ul>
<li><strong>编辑对应的 logic 文件,这里以 internal/logic/loginlogic.go 为例:</strong></li>
</ul>
<pre><code class="language-go">func (l *LoginLogic) Login(in *user.ReqUser) (*user.RespLogin, error) {
   // todo: add your logic here and delete this line
   one, err := l.svcCtx.Model.FindByName(in.Username)
   if err != nil {
      return nil, errors.Wrapf(err, "FindUser %s", in.Username)
   }

   if one.Password != in.Password {
      return nil, fmt.Errorf("user or password is invalid")
   }

   token := GenTokenByHmac(one.Username, secretKey)
   return &amp;user.RespLogin{Token: token}, nil
}
</code></pre>
<h2 id="微服务运行演示">微服务运行演示</h2>
<p>我们是在单机环境下运行整个微服务,需要启动以下服务:</p>
<ul>
<li>Redis</li>
<li>Mysql</li>
<li>Etcd</li>
<li>go run blog.go -f etc/blog-api.yaml</li>
<li>go run user.go -f etc/user.yaml</li>
</ul>
<p>在上述服务中,rpc 服务要先启动,然后网关层再启动。</p>
<p>在仓库中我封装了 start.sh 和 stop.sh 脚本来分别在单机环境下运行和停止微服务。</p>
<p>好了,通过上述六个步骤,blog 用户模块的常见功能就完成了。</p>
<p>最后再帮大家强调下重点,除了 <code>goctl</code> 常用的命令需要熟练掌握,<code>go-zero</code> 文件命名也是有规律可循的。配置文件是放在 etc 目录下的 yaml 文件,该 yaml 文件对应的结构体在 <code>internal/config/config.go</code> 中。依赖管理一般会在 <code>internal/svc/servicecontext.go</code> 中进行封装。需要我们填充业务逻辑的地方是 <code>internal/logic</code> 目录下的文件。</p>
<h2 id="致谢">致谢</h2>
<p>感谢 <code>又拍云</code> 供稿!</p>
<h2 id="项目地址">项目地址</h2>
<p>https://github.com/zeromicro/go-zero</p>
<p>欢迎使用 <code>go-zero</code> 并 <strong>star</strong> 支持我们!</p><br><br>
来源:https://www.cnblogs.com/kevinwan/p/15456939.html
頁: [1]
查看完整版本: go-zero 实战之 blog 系统