本文主要对go语言笔记服务项目这个开源项目所实现的功能和主要的实现流程包括运用的技术点做出一些解释。
项目原地址:简易笔记项目。
笔记服务项目功能描述
该项目总共分为三个服务模块,分别是demoapi(API服务),demouser(用户数据管理),demonote(笔记数据管理),API模块采用了Kitex框架和Hertz框架,用来发现其他模块注册的服务并处理,并且通过不同的通信协议去分别操作demouser和demonote两个服务,展示处理请求的结果;demouser服务模块用来进行用户数据管理,分别使用了Gorm和Kitex两个框架支持protobuf协议,实现了用户数据的校验(账号密码校验),创建用户数据,获取用户数据三种功能,并且通过Gorm连接数据库,进行数据的存取拷贝;demonote服务模块用来进行用户笔记数据管理,同样是使用Gorm和Kitex两个框架支持的是thrift协议,能够实现笔记数据的创建,数据的删除,数据的查询,数据的更新,笔记的获取返回等五个功能。具体的功能图如下所示:
笔记服务项目具体实现
首先根据业务去创建IDL,分别完成了note和user(笔记管理和用户管理)的IDL,笔记使用thrift协议,用户使用protobuf协议,创建IDL包含一些状态码和信息,对外提供了RPC接口,其中,note需要完成创建,更新,删除,查询笔记,user需要完成创建,查询校验用户的功能。项目中的所有服务都是用的是Kitex框架来进行服务间的RPC通讯,Kitex框架可以自动根据IDL生成代码,之后只用完善不同的业务功能即可,关于各个框架的介绍可以看这里,下图所示为笔记服务的IDL:
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 {
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)
}
然后在编写服务函数之前先完成各种错误码的定义包括用户不存在/已经存在等,常量的设置,一些中间件的配置等,具体代码如下所示:
package errno
import (
"errors"
"fmt"
)
const (
SuccessCode = 0
ServiceErrCode = 10001
ParamErrCode = 10002
UserAlreadyExistErrCode = 10003
AuthorizationFailedErrCode = 10004
)
type ErrNo struct {
ErrCode int64
ErrMsg string
}
func (e ErrNo) Error() string {
return fmt.Sprintf("err_code=%d, err_msg=%s", e.ErrCode, e.ErrMsg)
}
func NewErrNo(code int64, msg string) ErrNo {
return ErrNo{code, msg}
}
func (e ErrNo) WithMessage(msg string) ErrNo {
e.ErrMsg = msg
return e
}
var (
Success = NewErrNo(SuccessCode, "Success")
ServiceErr = NewErrNo(ServiceErrCode, "Service is unable to start successfully")
ParamErr = NewErrNo(ParamErrCode, "Wrong Parameter has been given")
UserAlreadyExistErr = NewErrNo(UserAlreadyExistErrCode, "User already exists")
AuthorizationFailedErr = NewErrNo(AuthorizationFailedErrCode, "Authorization failed")
)
// ConvertErr convert error to Errno
func ConvertErr(err error) ErrNo {
Err := ErrNo{}
if errors.As(err, &Err) {
return Err
}
s := ServiceErr
s.ErrMsg = err.Error()
return s
}
这里是完成错误状态的设置,主要包括五种状态码,登陆成功,服务器打开失败,登陆信息错误,用户已经存在,校验失败等,其中自定义错误码要去实现一个接口Error()。
package constants
const (
NoteTableName = "note"
UserTableName = "user"
SecretKey = "secret key"
IdentityKey = "id"
Total = "total"
Notes = "notes"
NoteID = "note_id"
ApiServiceName = "demoapi"
NoteServiceName = "demonote"
UserServiceName = "demouser"
MySQLDefaultDSN = "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local"
EtcdAddress = "127.0.0.1:2379"
CPURateLimit float64 = 80.0
DefaultLimit = 10
)
在常量文件中设置包括gorm查询用到的参数,用户名,笔记名,,jwt插件要用到的变量,etcd地址,cpu的限制,三个服务的名称等。
package middleware
import (
"context"
"github.com/cloudwego/kitex/pkg/endpoint"
"github.com/cloudwego/kitex/pkg/klog"
"github.com/cloudwego/kitex/pkg/rpcinfo"
)
var _ endpoint.Middleware = CommonMiddleware
// CommonMiddleware common middleware print some rpc info、real request and real response
func CommonMiddleware(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req, resp interface{}) (err error) {
ri := rpcinfo.GetRPCInfo(ctx)
// get real request
klog.Infof("real request: %+v\n", req)
// get remote service information
klog.Infof("remote service name: %s, remote method: %s\n", ri.To().ServiceName(), ri.To().Method())
if err = next(ctx, req, resp); err != nil {
return err
}
// get real response
klog.Infof("real response: %+v\n", resp)
return nil
}
}
再编写项目中用到的中间件,这些中间件都只允许在初始化的时候设置,不允许动态修改,client.go中间件打印服务端的地址,rpc超时,连接超时,server.go的中间件打印客户端的地址,common.go打印真实请求和相应信息,通过日志打印出来(三个都是通过kitex框架下rpc连接函数实现的),使用transport_pipeline bound扩展去实现连接级别和请求级别的限流,只要完成onread去获得cpu使用率大于80%就返回错误信息;tracer中完成了jaeger的初始化,保证opentraceing能够把数据上传到jaeger中去,jaeger通过环境变量去初始化,环境变量在api中导入这里展示的中间件是common.go中实现的过程。注,关于opentraceing,jaeger的介绍可以看这篇文章。
完成了上述步骤就可以正常编写业务代码了,先是在demoapi服务中的handlers文件夹中完成不同的功能函数主要包括用户创建,用户校验,笔记创建,笔记删除,笔记查询,笔记更新几个功能,在rpc文件夹中封装了调用其他rpc服务函数的逻辑,rpc中使用etcd进行服务发现完成node和user客户端的代码,下面是rpc中note的部分代码实现:
var noteClient noteservice.Client
func initNoteRpc() {
r, err := etcd.NewEtcdResolver([]string{constants.EtcdAddress})
if err != nil {
panic(err)
}
c, err := noteservice.NewClient(
constants.NoteServiceName,
client.WithMiddleware(middleware.CommonMiddleware),
client.WithInstanceMW(middleware.ClientMiddleware),
client.WithMuxConnection(1), // mux
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 {
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
}
然后在api的main函数中先是对链路追踪和rpc都进行了初始化(初始化中已经完成了对于端口的配置和绑定),然后再设置了服务端端口和JWT认证(HTTP Status Message Func包含用于设置 jwt 校验流程发生错误时响应所包含的错误信息;Login Response用于设置登录的响应函数;Unauthorized用于设置 jwt 验证流程失败的响应函数;Authenticator用于设置登录时认证用户信息的函数(必要配置))再设置监听事件从另外两个node和user中去查看或是申请其他请求,具体实现逻辑如下:
func Init() {
tracer.InitJaeger(constants.ApiServiceName)
rpc.InitRPC()
}
func main() {
Init()
r := server.New(
server.WithHostPorts("127.0.0.1:8080"),
server.WithHandleMethodNotAllowed(true),
)
authMiddleware, _ := jwt.New(&jwt.HertzJWTMiddleware{
Key: []byte(constants.SecretKey),
Timeout: time.Hour,
MaxRefresh: time.Hour,
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(int64); ok {
return jwt.MapClaims{
constants.IdentityKey: v,
}
}
return jwt.MapClaims{}
},
HTTPStatusMessageFunc: func(e error, ctx context.Context, c *app.RequestContext) string {
switch e.(type) {
case errno.ErrNo:
return e.(errno.ErrNo).ErrMsg
default:
return e.Error()
}
},
LoginResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
c.JSON(consts.StatusOK, map[string]interface{}{
"code": errno.SuccessCode,
"token": token,
"expire": expire.Format(time.RFC3339),
})
},
Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
c.JSON(code, map[string]interface{}{
"code": errno.AuthorizationFailedErrCode,
"message": message,
})
},
Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
var loginVar handlers.UserParam
if err := c.Bind(&loginVar); err != nil {
return "", jwt.ErrMissingLoginValues
}
if len(loginVar.UserName) == 0 || len(loginVar.PassWord) == 0 {
return "", jwt.ErrMissingLoginValues
}
return rpc.CheckUser(context.Background(), &userdemo.CheckUserRequest{UserName: loginVar.UserName, Password: loginVar.PassWord})
},
TokenLookup: "header: Authorization, query: token, cookie: jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
})
r.Use(recovery.Recovery(recovery.WithRecoveryHandler(
func(ctx context.Context, c *app.RequestContext, err interface{}, stack []byte) {
hlog.SystemLogger().CtxErrorf(ctx, "[Recovery] err=%v\nstack=%s", err, stack)
c.JSON(consts.StatusInternalServerError, map[string]interface{}{
"code": errno.ServiceErrCode,
"message": fmt.Sprintf("[Recovery] err=%v\nstack=%s", err, stack),
})
})))
v1 := r.Group("/v1")
user1 := v1.Group("/user")
user1.POST("/login", authMiddleware.LoginHandler)
user1.POST("/register", handlers.Register)
note1 := v1.Group("/note")
note1.Use(authMiddleware.MiddlewareFunc())
note1.GET("/query", handlers.QueryNote)
note1.POST("", handlers.CreateNote)
note1.PUT("/:note_id", handlers.UpdateNote)
note1.DELETE("/:note_id", handlers.DeleteNote)
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "no route")
})
r.NoMethod(func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "no method")
})
r.Spin()
}
最后再完成另外两个服务demouser和demonote,在demonote中先要根据生成的IDL在handler中完成业务逻辑包括笔记的创建,查询,更新,获取,删除;然后在dal文件夹中使用gorm框架连接数据库,根据不同的逻辑去对数据库进行不同的操作:
import (
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/constants"
"gorm.io/driver/mysql"
"gorm.io/gorm"
gormopentracing "gorm.io/plugin/opentracing"
)
var DB *gorm.DB
// Init init DB
func Init() {
var err error
DB, err = gorm.Open(mysql.Open(constants.MySQLDefaultDSN),
&gorm.Config{
PrepareStmt: true,
SkipDefaultTransaction: true,
},
)
if err != nil {
panic(err)
}
if err = DB.Use(gormopentracing.New()); err != nil {
panic(err)
}
}
type Note struct {
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
}
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
}
在rpc文件中用etcd进行服务注册:
var userClient userservice.Client
func initUserRpc() {
r, err := etcd.NewEtcdResolver([]string{constants.EtcdAddress})
if err != nil {
panic(err)
}
c, err := userservice.NewClient(
constants.UserServiceName,
client.WithMiddleware(middleware.CommonMiddleware),
client.WithInstanceMW(middleware.ClientMiddleware),
client.WithMuxConnection(1), // mux
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)
}
userClient = c
}
// MGetUser multiple get list of user info
func MGetUser(ctx context.Context, req *userdemo.MGetUserRequest) (map[int64]*userdemo.User, error) {
resp, err := userClient.MGetUser(ctx, req)
if err != nil {
return nil, err
}
if resp.BaseResp.StatusCode != 0 {
return nil, errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
res := make(map[int64]*userdemo.User)
for _, u := range resp.Users {
res[u.UserId] = u
}
return res, nil
}
最后在main中设置链路追踪,数据库连接和rpc的初始化,进行服务注册和端口绑定:
func Init() {
tracer2.InitJaeger(constants.NoteServiceName)
rpc.InitRPC()
dal.Init()
}
func main() {
r, err := etcd.NewEtcdRegistry([]string{constants.EtcdAddress}) // r should not be reused.
if err != nil {
panic(err)
}
addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8888")
if err != nil {
panic(err)
}
Init()
svr := note.NewServer(new(NoteServiceImpl),
server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: constants.NoteServiceName}), // server name
server.WithMiddleware(middleware.CommonMiddleware), // middleWare
server.WithMiddleware(middleware.ServerMiddleware),
server.WithServiceAddr(addr), // address
server.WithLimit(&limit.Option{MaxConnections: 1000, MaxQPS: 100}), // limit
server.WithMuxTransport(), // Multiplex
server.WithSuite(trace.NewDefaultServerSuite()), // tracer
server.WithBoundHandler(bound.NewCpuLimitHandler()), // BoundHandler
server.WithRegistry(r), // registry
)
err = svr.Run()
if err != nil {
klog.Fatal(err)
}
}
之后再完成demouser即可完成本项目的全部工作,demouser的实现过程包括业务逻辑都和demonote类似。