go-zero微服务框架使用教程 - 走进微服务

2,470 阅读9分钟

安装go-zero

go-zero是现在比较流行的golang微服务框架。是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。

go-zero 安装 | go-zero Documentation

安装Etcd

Etcd是一个高可用的分布式键值存储系统,相当于加强版redis(可靠性更强)。主要用于共享配置信息和服务发现。它采用Rft一致性算法来保证数据的强致性,并且支持对数进行监视和更新

etcd主要用于微服务的配置中心、服务发现。存储服务对应的地址。传统配置文件模式,ip地址改变了就得重新修改配置和重启,etcd的使用解决了这个问题。

安装地址:Releases · etcd-io/etcd (github.com)

基础语法

写一个视频微服务,提供一个htp接口,用户查询一个视频的信息,并且把关联用户id的用户名也查出来那么用户微服务就要提供一个方法,根据用户id返回用户信息。

rpc

  1. 在user目录下创建rpc目录,并编写user.proto

     syntax="proto3";
     package user;
     option go_package ="./user";
     message IdRequest {
       string id =1;
     }
     message UserResponse {
       //用户id
       string id = 1;
       //用户名称
       string name = 2;
       //用户性别
       bool gender = 3;
     }
     ​
     service User {
       rpc getUser(IdRequest) returns(UserResponse);
     }
     ​
    
  2. 在rpc目录下用命令使用goctl工具创建rpc代码

     goctl rpc protoc user.proto --go_out=types --go-grpc_out=types --zrpc_out=.
    
  3. 在internal.logic目录下能看到刚生成的代码,编写完善GetUser方法

    image-20240323143302446

image-20240323143339134

  1. 在etc目录下user.yaml文件中,可以看到配置文件

image-20240323143456573

  1. 我们可以验证一下成果了,使用go mod tidy更新依赖后go run user.go跑一下代码

  2. 用apifox试试能不能正常服务

    1. 创建rpc项目,然后根据指引导入proto文件

    image-20240323143841132

    1. 在接口那里就可以找到proto文件中对应的key了,调用后即可获取刚才编写的值

    image-20240323144300140.png

api

  1. 创建video.api目录,在其中创建video.api文件

     type (
         VideoReq {
             Id string `path:"id"`
         }
         VideoRes {
             Id   string `json:"id"`
             Name string `json:"name"`
         }
     )
     ​
     service video {
         @handler getVideo
         get /api/videos/:id (VideoReq) returns (VideoRes)
     }
    
  2. 使用命令创建goctl api go -api video/api/video.api -dir video/api

  3. 在config中加入user rpc的地址

    image-20240323145238514

  4. 配置etcd的地址,以及对应的key,框架会自动通过etcd来获取user.rpc的地址

    image-20240323150021931

  5. 完善服务的依赖

    image-20240323145421781

  6. 现在可以在logic.getvideologic.go中编写逻辑了

     func (l *GetVideoLogic) GetVideo(req *types.VideoReq) (resp *types.VideoRes, err error) {
         //现在可以像调用本地方法一样调用rpc方法
         getUser, err := l.svcCtx.UserRpc.GetUser(context.Background(), &user.IdRequest{Id: "1"})
         if err != nil {
             return nil, err
         }
         return &types.VideoRes{
             Id:   req.Id,
             Name: getUser.Name,
         }, nil
     }
    
  7. 启动!go run video.go

  8. 试试能不能用

    image-20240323150402362.png image-20240323150501920

总结

  1. 编写用户微服务的rpc服务的proto文件
  2. 生成代码
  3. 添加自己的逻辑
  4. 编写视频微服务的api服务的api文件
  5. 生成代码
  6. 完善依赖,配置
  7. 添加自己的逻辑
image-20240323150729979.png

go-zero省了很多代码,让我们更专注于业务的开发

api怎么写

先写一个小示例

  1. 创建user.api

image-20240323153946992

前缀:设置好的前缀会自动加到后缀的前面

image-20240323164333910.png

代码:

 type LoginRequest{
     Username string `json:"username"`
     Pwd string `json:"password"`
 }
 ​
 type LoginResponse{
     Code int `json:code`
     Data string `json:data`
     Msg string `json:msg`
 }
 ​
 type UserInfo{
     UserId uint `json:"user_id"`
     Username string `json:"username"`
 }
 ​
 type UserInfoResponse{
     Code int `json:code`
     Data UserInfo `json:data`
     Msg string `json:msg`
 }
 ​
 service users{
     @handler login
     post /api/usrs/login (LoginRequest) returns(LoginResponse)
 ​
     @handler userInfo
     get /api/usrs/info returns(UserInfoResponse)
 }

2. 通过user.api创建api

 goctl api go -api user.api -dir .
 #解释:使用goctl api 转成 go -api ,读取 user.api 转到 -dir 路径 .

如果在user.api里新增接口,则重新运行该命令以生成最新的代码

  1. 写完逻辑试验一下
      func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
       fmt.Println(req.Username, req.Pwd)
       return &types.LoginResponse{
       Code: 0,
       Data: "xxx.xxx.xxx",
       Msg:  "successful",
       }, nil
      }

go run users.go

image-20240323155258354

对Response进行封装

封装response

每次都写Response很麻烦,因为他们内容差不多,所以咱们封装一下

  1. 我们将user.api里的response先给删了

    image-20240323161725401转存失败,建议直接上传图片文件

  2. 在根目录下建一个common.response目录,创建一个文件用于封装response

     type ResponseBody struct {
         Code int    `json:"code"`
         Data any    `json:"data"`
         Msg  string `json:"msg"`
     }
     ​
     func Response(r *http.Request, w http.ResponseWriter, res any, err error) {
         if err != nil {
             //可以根据不同错误码返回不同的错误信息
             body := ResponseBody{
                 Code: 1,
                 Data: nil,
                 Msg:  "ERR",
             }
             httpx.WriteJson(w, http.StatusOK, body)
             return
         }
         body := ResponseBody{
             Code: 0,
             Data: res,
             Msg:  "successful",
         }
         httpx.WriteJson(w, http.StatusOK, body)
     }
     ​
    
  3. 修改handler,用封装好的替代原来的好几个判断语句

    image-20240323162038629

模板定制化

由于response的封装go-zero是不知道的,所以每次生成handler都需要咱们手动去改。我们可以使用官方的模板定制化工具去修改

  1. 打开handler.tpl文件。

    1. 全局搜一下有没有这个文件,有的话直接改
    2. 没有的话使用这个命令生成goctl template init
  2. 修改其中的代码

    image-20240323163025828.png

     package {{.PkgName}}
     ​
     import (
         "net/http"
         "github.com/zeromicro/go-zero/rest/httpx"
         {{.ImportPackages}}
         "gozeroStudy/common/response"
     )
     ​
     func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
         return func(w http.ResponseWriter, r *http.Request) {
             {{if .HasRequest}}var req types.{{.RequestType}}
             if err := httpx.Parse(r, &req); err != nil {
                 httpx.ErrorCtx(r.Context(), w, err)
                 return
             }
             {{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
             {{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}})
             {{if .HasResp}}response.Response(r,w,resp,err) {{else}}response.Response(r,w,nil,err){{end}}
         }
     }
     ​
    
  3. 再次转化就可以发现,handler变成了我们想要的样子。

api校验

什么是JWT

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

用于什么地方:

  • Authorization (授权) : 用户登录后每个请求都包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
  • Information Exchange (信息交换) : 安全的在各方之间传输信息。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

配置JWT

在.api中可以增加校验

image-20240323165117008.png

重新生成会发现internal.handler.routes.go中多了一行校验

image-20240323165207203

点进这个serverCtx.Config.Auth中我们会看到Config里有这个Auth

image-20240323165309506

在配置文件中我们可以配置jwt的密钥和过期时间

image-20240323194431237.png

签发JWT

在登录的时候需要签发JWT,后续认证的时候会自动认证

  1. 在common下创建jwts文件夹,然后复制一份公共代码进去

     package jwts
     ​
     import (
       "errors"
       "github.com/golang-jwt/jwt/v4"
       "time"
     )
     ​
     // JwtPayLoad jwt中payload数据
     type JwtPayLoad struct {
       UserID   uint   `json:"user_id"`
       Username string `json:"username"` // 用户名
       Role     int    `json:"role"`     // 权限  1 普通用户  2 管理员
     }
     ​
     type CustomClaims struct {
       JwtPayLoad
       jwt.RegisteredClaims
     }
     ​
     // GenToken 创建 Token
     func GenToken(user JwtPayLoad, accessSecret string, expires int64) (string, error) {
       claim := CustomClaims{
         JwtPayLoad: user,
         RegisteredClaims: jwt.RegisteredClaims{
           ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(expires))),
         },
       }
     ​
       token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
       return token.SignedString([]byte(accessSecret))
     }
     ​
     // ParseToken 解析 token
     func ParseToken(tokenStr string, accessSecret string, expires int64) (*CustomClaims, error) {
     ​
       token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
         return []byte(accessSecret), nil
       })
       if err != nil {
         return nil, err
       }
       if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
         return claims, nil
       }
       return nil, errors.New("invalid token")
     }
    
  2. 在logic中写签发逻辑

    image-20240323171922979

    image-20240323171957909.png

  • 即jwts里定义的类

    image-20240323172035891.png

  1. 试试能不能用

    image-20240323195103914

    image-20240323195221683.png

JWT验证失败

jwt验证失败默认是发的401,这对前端非常不友好,那么我们就需要自定义一个验证失败的回调

在user.go里写一个,server创建时传进去就行

image-20240323195644970

结果:

image-20240323195820616.png

操作数据库

操作mysql

  1. 使用goctl的model工具可以通过sql文件生成golang代码
  goctl model mysql ddl --src user.sql dir .
  #使用goctl model mysql ddl 打开--src user.sql 生成 dir 到 . ("."是当前文件夹的意思)

image-20240323203004202

  1. 修改api的代码

写数据库地址

 账户(root):密码@tcp(127.0.0.1:3306)/数据库名?charset=utf8mb4&parseTime=True

image-20240323203945584

在config中配置刚才写的数据库地址

image-20240323204002262

在servicecontext.go中将自己的结构体注入到服务中

image-20240323204052904.png

  1. 然后我们就可以通过刚刚写的UsersModel操作数据库了

image-20240323204213578

  1. 试一下能不能用(此处是随便往数据库里存了个值)

image-20240323204723088

image-20240323204629215

  1. 我们可以在通过goctl生成的代码里修改具体数据库交互逻辑

    image-20240323205129687

    image-20240323205324348

操作gorm

用gorm开发更快一点,但是得手写

  1. 创建一个model

    image-20240323212036795

  2. 和直接操作mysql一样,修改配置项

    image.png
  3. 在common里写一个init函数用于连接数据库

    image-20240323213254503

  4. 服务注入

image-20240323211947491.png

  1. 试试能不能用

    image-20240323215129087.png

    image-20240323215139063.png

RPC怎么写

示例

用proto生成

首先当然要编写一个proto文件

 syntax = "proto3";
 ​
 package user;
 ​
 option go_package="./user";
 ​
 message UserInfoRequest {
     uint32 user_id = 1;//1是指序列化之后的位置
 }
 ​
 message UserInfoResponse {
   uint32 user_id = 1;
   string username = 2;
 }
 ​
 message UserCreateRequest{
   string username = 1;
   string password = 2;
 }
 ​
 message UserCreateResponse{
    string err = 1;
 }
 ​
 service user{
   rpc UserInfo(UserInfoRequest)returns(UserInfoResponse);
   rpc UserCreate(UserCreateRequest)returns(UserCreateResponse);
 }
 ​

然后进入目录,也是用goctl工具生成rpc代码

 goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.

可以看到types里已经生成了对应的grpc代码

image-20240324095657242.png

internal.server中已经自动生成了服务端代码,服务端自动调用logic的函数,那么我们就只需要完善logic里的逻辑部分即可,省了很多事。

image-20240324100131356.png

同时,客户端的代码也生成好了,非常方便

image-20240324100524426

服务拆分

现实我们服务多得很,怎么拆分呢?只需要生成的时候在语句最后加个-m即可

例如,在proto中我们将服务随便拆成两份。

image-20240324103042295.png

生成的时候语句加个-m

 goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. -m

可以看到果然拆成了两份

image-20240324103035395

测试一下(需要重新导入proto)

image-20240324103512532.png

操作gorm

和api中的接入是差不多的——创建model、修改配置项、服务注入

image-20240324104258862.png

image-20240324104446016

image-20240324105006096

然后修改logic以访问mysql即可

image-20240324105616254

image-20240324105708681转存失败,建议直接上传图片文件

连接api

我们知道,一个微服务要有rpc服务和api服务,我们得把他们接起来

生成rpc

像前文一样创建proto,生成rpc以及连接gorm

 syntax = "proto3";
 ​
 package user;
 ​
 option go_package="./user";
 ​
 message UserInfoRequest {
   uint32 user_id = 1;//1是指序列化之后的位置
 }
 ​
 message UserInfoResponse {
   uint32 user_id = 1;
   string username = 2;
 }
 ​
 message UserCreateRequest{
   string username = 1;
   string password = 2;
 }
 ​
 message UserCreateResponse{
   string err = 1;
 }
 ​
 service user{
   rpc UserCreate(UserCreateRequest)returns(UserCreateResponse);
   rpc UserInfo(UserInfoRequest)returns(UserInfoResponse);
 }
 ​
 ​
 // goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. -m

生成api

来复习一下

创建api文件夹以及user.api文件,编写api文件

 type UserCreateRequest {
     Username string `json:"username"`
     Pwd      string `json:"password"`
 }
 ​
 type UserInfoRequest {
     UserId uint `path:"id"`
 }
 ​
 type UserInfoResponse {
     UserId   uint   `json:"user_id"`
     Username string `json:"username"`
 }
 ​
 @server (
     prefix: /api/users
 )
 service users {
     @handler create
     post / (UserCreateRequest) returns (string)
 ​
     @handler userInfo
     get /:id (UserInfoRequest) returns (UserInfoResponse)
 }
 ​

进入目录使用goctl生成代码

 goctl api go -api user.api -dir .

连接Etcd

image-20240324111955789

配置rpc

image-20240324112053493

服务注入

image-20240324113442949

写一下逻辑

image-20240324140433546

image-20240324140410581

试试能不能用

image-20240324140451749.png

success!api到rpc到数据库成功连接

在b站学的,大佬讲的真好 ——— 课程视频地址