实战案例——Hertz&Gorm快速实现一个Web服务 | 青训营

82 阅读3分钟

前言

实现一个web服务并不是想象的那么难,但我们需要清楚一些消息在后端是如何传递的。今天为大家讲解一个简单的注册服务后端是如何实现的。希望对大家有所帮助。话不多讲,开始表演。

认真听课.webp

且停下思考两分钟

注册服务日常可见,一个网站需要用户来支撑。想一想随便一个网站他们的注册界面是怎么样的。用户名,密码当然还有用于防止恶意程序的验证码。但最终后端注册服务需要的数据就是用户名(username)、密码(password)。对于我们的http中的报文,我们有http框架Hertz为我们处理http报文中的数据。而对于报文中的数据我们可以GORM将我们的数据映射到我们的数据库。思路基本清晰,开始实现。

IDL定义

参数的定义

message douyin_user_register_request {
   string username = 1[(api.form)="username", (api.body)="username", (api.vd)="len($)>0&&len($)<30"]; // 注册用户名,最长32个字符
   string password = 2[(api.form)="password", (api.body)="password", (api.vd)="len($)>0&&len($)<30"]; // 密码,最长32个字符
}

message douyin_user_register_response {
   int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
   int64 user_id = 3; // 用户id
   string token = 4; // 用户鉴权token
}

这里将定义的参数与http请求体里的参数进行绑定,可以省去重新调用方法PostForm手动获取参数的麻烦。

你可以参考 这里 了解更多关于注解的信息。

方法的定义

service UserHandler {
  rpc UserRegister(douyin_user_register_request) returns(douyin_user_register_response){
    option (api.post) = "douyin/user/register";
    //注册成功返回id 和token
  }
}

我们使用 POST 注解定义了 POST 方法和路由,hz 将根据定义的路由生成对应的路由组,Handler 框架以及中间件框架等。

hz 生成代码

生成

注example.com/m换成当前项目模块名,idl/user.proto是为当前相对文件路径

hz new -module example.com/m -I idl -idl idl/user.proto

整理 & 拉取依赖

go mod tidy

更新

hz update -idl idl/user.proto

生成文件结构如下

 hertz_gorm
 ├── biz
 |   ├── dal             // Logic code that interacts with the database
 │   ├── handler         // Main logical code that handles HTTP requests                  
 |   ├── model           // Go struct corresponding to the database table
 |   ├── router          // Middleware and mapping of routes to handlers
 ├── go.mod              // go.mod                 
 ├── idl                 // thift idl                  
 ├── main.go             // Initialize and start the server              
 ├── router.go           // Sample route registration
 ├── router_gen.go       // Route registration    
 ├── docker-compose.yml  // docker-compose.yml
 ├── Makefile            // Makefile

GORM操作数据库

配置GORM

要在数据库中使用 GORM,你首先需要使用驱动程序连接数据库并配置 GORM,如 biz/dal/mysql/init.go 所示。

 // biz/dal/mysql/init.go
 package mysql
 
 import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
 )
 
 var dsn = "root:123456@tcp(127.0.0.1:3306)/go_db"
 
 var DB *gorm.DB
 
 func Init() {
    var err error
    DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
       SkipDefaultTransaction: true,
       PrepareStmt:            true,
       Logger:                 logger.Default.LogMode(logger.Info),
    })
    if err != nil {
       panic(err)
    }
 }

这里我们通过 DSN 连接 MySQL 数据库,并维护一个全局数据库操作对象 DB

在GORM配置方面,由于本项目不涉及同时操作多张表,我们可以将 SkipDefaultTransaction 配置为 true 来跳过默认事务,并通过PrepareStmt 启用缓存以提高效率。

我们还使用了默认的 logger,以便我们可以清楚地看到 GORM 为我们生成的 SQL

操作MySQL

//biz/dal/mysql/user.go
package mysql

type User struct {
   ID              int64  `json:"id"`
   UserName        string `json:"user_name"`
   Password        string `json:"password"`
}

func (User) TableName() string {
   return "users"
}
func CreateUser(user *User) (int64, error) {
   err := DB.Create(&user).Error
   if err != nil {
      return 0, err
   }
   return user.ID, nil
}

先创建User结构体,用于接收传递的参数。这里结构体里的参数可以增加更多用户的属性,这里只为了简化实例。

CreateUser方法里使用DB对象的Create方法将user记录插入到数据库中。

HTTP请求处理

//biz/handler/usercode/user_handler.go
// UserRegister .
// @router douyin/user/register [POST]
func UserRegister(ctx context.Context, c *app.RequestContext) {
   var err error
   var user_id int64
   var req usercode.DouyinUserRegisterRequest
   err = c.BindAndValidate(&req)
   if err != nil {
      s := err.Error()
      c.String(consts.StatusBadRequest, err.Error())
      c.JSON(consts.StatusBadRequest, &usercode.DouyinUserRegisterResponse{
         StatusCode: consts.StatusBadRequest,
         StatusMsg:  &s,
      })
      return
   }
   user_id, err = mysql.CreateUser(&mysql.User{
   UserName:        req.Username,
   Password:        req.Password,
   })
   if err != nil {
      s := err.Error()
      c.JSON(consts.StatusOK, &usercode.DouyinUserRegisterResponse{
         StatusCode: consts.StatusBadRequest,
         StatusMsg:  &s,
      })
      return
   }
   resp := new(usercode.DouyinUserRegisterResponse)
   resp.UserId = user_id
   resp.StatusCode = 0
   statusMsg := "注册成功"
   resp.StatusMsg = &statusMsg
   c.JSON(consts.StatusOK, resp)
}

由于我们在 protobuf IDL中使用了api 注解,BindAndValidate 将为我们完成参数绑定和验证。所有有效的参数都会被注入到DouyinUserRegisterRequest中,这非常方便。

如果出现错误,我们可以使用 JSON 方法以 JSON 格式返回数据。无论是 DouyinUserRegisterResponse 还是业务代码,我们都可以直接使用hz生成的代码。

之后,我们可以通过在 dal层调用CreateUser来插入一个新用户到 MySQL 中,传入封装好的参数。

如果出现错误,就像一开始那样,返回包含错误代码和信息的JSON。否则,返回正确的服务代码,表示成功创建了用户。

运行

go build miniTikTok
./miniTikTok.exe

结果

请求

image.png

响应

响应.png

结言

hertz官方文档有许多有意思的案例,感兴趣的小伙伴可以去看一看。