Go三件套之笔记项目|青训营笔记

165 阅读6分钟

这是我参与「第五届青训营 」笔记创作活动的第6天

一、本堂课重点内容

本节课主要讲解一个笔记项目,主要包括项目介绍,功能介绍,关键代码讲解,由于项目代码庞大,所以在关键代码讲解部分,主要介绍creat API来串起三个框架。 项目地址:

其中优化版使用了统一协议,并且使用了更多的扩展。

项目介绍

笔记项目是一个使用Hertz、Kitex、Grom搭建出来的具备一定业务逻辑的后端API项目。

服务名称服务介绍传输协议主要技术栈
demoapiAPI服务HTTPKitex/Hertz
demouser用户数据管理Protobuf(pb)Gorm/Kitex
demonote笔记数据管理ThriftGorm/Kitex

表格中三个服务分别使用了不同的传输协议(普通版),仅是为了进行差别演示,实际开发中也可以使用同种协议。

功能介绍

  • api服务要对外提供 api 接口,通过协议供前端或后端调用,完成相应功能。
  • user服务是一个 RPC 服务,主要提供查询、创建、校验用户的接口。
  • note是提供笔记的 CRUD 的基础能力,也是一个RPC服务

image.png

调用关系

  • 首先user服务和note服务在启动时会通过服务注册把自己的信息注册到etcd中,并且只由这两个服务访问数据库。
  • 调用方向 api 服务发送HTTP请求,api 服务通过服务发现调用相应的服务,当需要用户服务时便调用user服务,需要笔记服务时便调用note服务,然后在api服务中对获取的数据做组装。
  • api服务不直接访问MySQL,这样设计的一个好处是,当需要再创建admin服务时,可以复用一些代码接口。

image.png

IDL介绍

idl定义结束后,使用Kitex进行代码生成。

user IDL介绍

user的idl使用proto3(pb)来定义。以下为user.proto文件内容。

syntax = "proto3";
package user;
option go_package = "userdemo";

message BaseResp {
    int64 status_code = 1;
    string status_message = 2;
    int64 service_time = 3;
}

message User {
    int64 user_id = 1;
    string user_name = 2;
    string avatar = 3;
}

message CreateUserRequest {
    string user_name = 1;
    string password = 2;
}

message CreateUserResponse {
    BaseResp base_resp = 1;
}

message MGetUserRequest {  //批量获取请求
    repeated int64 user_ids = 1;
}

message MGetUserResponse {   //批量获取相应
    repeated User users = 1;
    BaseResp base_resp = 2;
}

message CheckUserRequest{   //校验用户信息请求
    string user_name = 1;
    string password = 2;
}

message CheckUserResponse{
    int64 user_id = 1;
    BaseResp base_resp = 2;
}

service UserService {  //对外提供RPC接口
    rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) {}
    rpc MGetUser (MGetUserRequest) returns (MGetUserResponse) {}
    rpc CheckUser (CheckUserRequest) returns (CheckUserResponse) {}
}

note IDL介绍

note idl使用thrift定义。以下为note.thrift文件内容。

namespace go notedemo

struct BaseResp {
    1:i64 status_code
    2:string status_message
    3:i64 service_time
}

struct Note {
    1:i64 note_id
    2:i64 user_id
    3:string user_name
    4:string user_avatar
    5:string title
    6:string content
    7:i64 create_time
}

struct CreateNoteRequest {
    1:string title
    2:string content
    3:i64 user_id
}

struct CreateNoteResponse {
    1:BaseResp base_resp
}

struct DeleteNoteRequest {
    1:i64 note_id
    2:i64 user_id
}

struct DeleteNoteResponse {
    1:BaseResp base_resp
}

struct UpdateNoteRequest {
    1:i64 note_id
    2:i64 user_id
    3:optional string title
    4:optional string content
}

struct UpdateNoteResponse {
    1:BaseResp base_resp
}

struct MGetNoteRequest {  //批量查找请求
    1:list<i64> note_ids
}

struct MGetNoteResponse {  //批量查找响应
    1:list<Note> notes
    2:BaseResp base_resp
}

struct QueryNoteRequest {
    1:i64 user_id
    2:optional string search_key
    3:i64 offset
    4:i64 limit
}

struct QueryNoteResponse {
    1:list<Note> notes
    2:i64 total
    3:BaseResp base_resp
}

service NoteService {  //对外提供RPC接口
    CreateNoteResponse CreateNote(1:CreateNoteRequest req)
    MGetNoteResponse MGetNote(1:MGetNoteRequest req)
    DeleteNoteResponse DeleteNote(1:DeleteNoteRequest req)
    QueryNoteResponse QueryNote(1:QueryNoteRequest req)
    UpdateNoteResponse UpdateNote(1:UpdateNoteRequest req)
}

项目技术栈

image.png

关键代码介绍

关键代码讲解部分以普通版代码为例。当用户创建新笔记时,会产生一系列的函数调用,以下将按照调用次序介绍各个函数。Hertz 调用 Kitex Client(rpc.CreateNote)创建笔记,Client调用下游的Kitex Server(noteClient.CreateNote)创建笔记,Server通过调用Grom(db.CreateNote)创建笔记,Grom对数据库操作(DB.WithContext(ctx).Create(notes).Error)写入笔记。

Hertz关键代码

对Hertz生成的代码进行修改。HTTP框架Hertz主要做的是对接口或数据进行聚合(比如聚合note和user数据),然后对外提供API服务。文件路径:easy_note/cmd/api/handlers/create_note.go

package handlers

import (
	"context"

	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/cmd/api/rpc"
	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo"
	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/constants"
	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/errno"
	"github.com/hertz-contrib/jwt"
)

// CreateNote create note info
func CreateNote(ctx context.Context, c *app.RequestContext) {
	var noteVar NoteParam   //声明结构体
	if err := c.Bind(&noteVar); err != nil {    //绑定结构体
		SendResponse(c, errno.ConvertErr(err), nil)   //反馈给前端绑定失败
		return
	}

	if len(noteVar.Title) == 0 || len(noteVar.Content) == 0 {  //参数校验
		SendResponse(c, errno.ParamErr, nil)   //标题和内容非法返回err
		return
	}

	claims := jwt.ExtractClaims(ctx, c)   //使用 jwt做用户数据处理
	userID := int64(claims[constants.IdentityKey].(float64))
	err := rpc.CreateNote(context.Background(), &notedemo.CreateNoteRequest{
		UserId:  userID,         //调用api创建新笔记
		Content: noteVar.Content, Title: noteVar.Title,
	})
	if err != nil {
		SendResponse(c, errno.ConvertErr(err), nil)   //创建失败
		return
	}
	SendResponse(c, errno.Success, nil)  //创建成功
}

Kitex Client关键代码

使用RPC框架Kitex,创建Client,调用Server服务。文件路径:easy_note/cmd/api/rpc/note.go

package rpc

import (
	"context"
	"time"

	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo"
	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo/noteservice"
	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/constants"
	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/errno"
	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/middleware"
	"github.com/cloudwego/kitex/client"
	"github.com/cloudwego/kitex/pkg/retry"
	etcd "github.com/kitex-contrib/registry-etcd"
	trace "github.com/kitex-contrib/tracer-opentracing"
)

var noteClient noteservice.Client

func initNoteRpc() {  //初始化Client
	r, err := etcd.NewEtcdResolver([]string{constants.EtcdAddress})   //生成用于Client服务发现对象
	if err != nil {
		panic(err)
	}

	c, err := noteservice.NewClient(     //封装 Client,定义服务名
		constants.NoteServiceName,
		client.WithMiddleware(middleware.CommonMiddleware),  //注入中间件
		client.WithInstanceMW(middleware.ClientMiddleware),
		client.WithMuxConnection(1),                       // mux 用于I/O多路复用
		client.WithRPCTimeout(3*time.Second),              // rpc timeout
		client.WithConnectTimeout(50*time.Millisecond),    // conn timeout
		client.WithFailureRetry(retry.NewFailurePolicy()), // retry
		client.WithSuite(trace.NewDefaultClientSuite()),   // tracer (分布式路径跟踪)
		client.WithResolver(r),                            // resolver
	)
	if err != nil {
		panic(err)
	}
	noteClient = c  //赋值给全局对象
}

// CreateNote create note info
func CreateNote(ctx context.Context, req *notedemo.CreateNoteRequest) error {
	resp, err := noteClient.CreateNote(ctx, req)  //请求服务创建笔记
	if err != nil {
		return err   //出现网络异常错误
	}
	if resp.BaseResp.StatusCode != 0 {   //不等于0时 出现业务错误
		return errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
	}
	return nil
}

// QueryNotes query list of note info
func QueryNotes(ctx context.Context, req *notedemo.QueryNoteRequest) ([]*notedemo.Note, int64, error) {
	resp, err := noteClient.QueryNote(ctx, req)
	if err != nil {
		return nil, 0, err
	}
	if resp.BaseResp.StatusCode != 0 {
		return nil, 0, errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
	}
	return resp.Notes, resp.Total, nil
}

// UpdateNote update note info
func UpdateNote(ctx context.Context, req *notedemo.UpdateNoteRequest) error {
	resp, err := noteClient.UpdateNote(ctx, req)
	if err != nil {
		return err
	}
	if resp.BaseResp.StatusCode != 0 {
		return errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
	}
	return nil
}

// DeleteNote delete note info
func DeleteNote(ctx context.Context, req *notedemo.DeleteNoteRequest) error {
	resp, err := noteClient.DeleteNote(ctx, req)
	if err != nil {
		return err
	}
	if resp.BaseResp.StatusCode != 0 {
		return errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
	}
	return nil
}

Kitex Server关键代码

Server调用Grom创建笔记。文件路径:easy_note/cmd/note/service/create_note.go

package service

import (
	"context"

	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo"

	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/cmd/note/dal/db"
)

type CreateNoteService struct {
	ctx context.Context
}

// NewCreateNoteService new CreateNoteService
func NewCreateNoteService(ctx context.Context) *CreateNoteService {
	return &CreateNoteService{ctx: ctx}
}

// CreateNote create note info
func (s *CreateNoteService) CreateNote(req *notedemo.CreateNoteRequest) error {
	noteModel := &db.Note{   //调用grom方法,kitex结构体与grom结构体可能不一致,所以需要转换
		UserID:  req.UserId,
		Title:   req.Title,
		Content: req.Content,
	}
	return db.CreateNote(s.ctx, []*db.Note{noteModel}) //调用grom代码创建note
}

Grom关键代码

ORM框架对数据库操作。文件路径:easy_note/cmd/note/dal/db/note.go

package db

import (
	"context"

	"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/constants"

	"gorm.io/gorm"
)

type Note struct {   //grom结构体
	gorm.Model
	UserID  int64  `json:"user_id"`
	Title   string `json:"title"`
	Content string `json:"content"`
}

func (n *Note) TableName() string {  //表名
	return constants.NoteTableName
}

// CreateNote create note info
func CreateNote(ctx context.Context, notes []*Note) error {
	if err := DB.WithContext(ctx).Create(notes).Error; err != nil {
		return err  //使用Grom写入数据库,WithContext用来传递上下文,用于链路追踪等
	}
	return nil
}

// MGetNotes multiple get list of note info
func MGetNotes(ctx context.Context, noteIDs []int64) ([]*Note, error) {
	var res []*Note
	if len(noteIDs) == 0 {
		return res, nil
	}

	if err := DB.WithContext(ctx).Where("id in ?", noteIDs).Find(&res).Error; err != nil {
		return res, err
	}
	return res, nil
}

// UpdateNote update note info
func UpdateNote(ctx context.Context, noteID, userID int64, title, content *string) error {
	params := map[string]interface{}{}
	if title != nil {
		params["title"] = *title
	}
	if content != nil {
		params["content"] = *content
	}
	return DB.WithContext(ctx).Model(&Note{}).Where("id = ? and user_id = ?", noteID, userID).
		Updates(params).Error
}

// DeleteNote delete note info  软删
func DeleteNote(ctx context.Context, noteID, userID int64) error {
	return DB.WithContext(ctx).Where("id = ? and user_id = ? ", noteID, userID).Delete(&Note{}).Error
}

// QueryNote query list of note info
func QueryNote(ctx context.Context, userID int64, searchKey *string, limit, offset int) ([]*Note, int64, error) {
	var total int64
	var res []*Note
	conn := DB.WithContext(ctx).Model(&Note{}).Where("user_id = ?", userID)

	if searchKey != nil {
		conn = conn.Where("title like ?", "%"+*searchKey+"%")
	}

	if err := conn.Count(&total).Error; err != nil {
		return res, total, err
	}

	if err := conn.Limit(limit).Offset(offset).Find(&res).Error; err != nil {
		return res, total, err
	}

	return res, total, nil
}

总结

easy_note 是使用 go语言写的一个微服务项目。并不是所有的服务都需要写成大单体服务,比如 HTTP框架 + 操作数据库,随着微服务的普及,我们可以把一些基础服务抽象出来,比如easy_note服务抽象出来note服务和user服务,这样做的好处是其他API也可以进行复用其中的微服务。