【二】golang实战之用户服务

如果采用go module模式,以下内容可在任何位置进行开发

搭建用户服务

首先开始开发用户服务,建立user-service服务目录结构,构建目录结构之前,可以先看看 Go 应用程序项目的基本布局。github上比较受认可的一个golang项目目录结构。我将仿照该目录结构来构建我的项目结构。初步文件目录结构如下

│  .gitignore
│  LICENSE
│  README.md
│  go.mod
│  go.sum
│
├─cmd
│  └─user-service
│          main.go
│
├─configs
├─docs
├─init
└─internal
    ├─dao
    ├─handler
    ├─models
    └─service

配置go.mod

在根目录创建的go.mod中新增如下内容

module imooc/user-service

go 1.16

连接数据库

/internal/models下新建databse.go,定义连接数库的配置结构体。go中多变量一起出现时可定义成一个结构体。

package models

type DBConfig struct {
	Username          string
	Password          string
	Host              string
	Port              int
	Dbname            string
	DefaultStringSize int
	MaxIdleConn       int
	MaxOpenConn       int
}

编写连接函数,在/internal/dao下新建config.go文件,定义创建数据库连接池函数

package dao

import (
	"fmt"
	"imooc/user-service/internal/models"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"

)

func CreateDatabasePool(config *models.DBConfig) (*gorm.DB, error) {
	dsn := fmt.Sprintf(
		"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=UTC",
		config.Username, config.Password,
		config.Host, config.Port, config.Dbname,
	)
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN:                       dsn,
		DefaultStringSize:         uint(config.DefaultStringSize),
		DisableDatetimePrecision:  true,
		DontSupportRenameIndex:    true,
		DontSupportRenameColumn:   true,
		SkipInitializeWithVersion: false,
	}), &gorm.Config{SkipDefaultTransaction: true})
	if err != nil {
		return nil, err
	}
	sqlDB, _ := db.DB()
	sqlDB.SetMaxIdleConns(config.MaxIdleConn)
	sqlDB.SetMaxOpenConns(config.MaxOpenConn)
	sqlDB.SetConnMaxLifetime(time.Hour)
	return db, nil
}

完成后执行go mod tidy即可自动下载所需要的库

编写用户model

/internal/models下新建user.go文件,编写以下内容

package models

import (
	"time"
)

type BaseModel struct {
	Id          int64     `gorm:"primarykey" json:"id"`
	CreatedTime time.Time `gorm:"default:current_timestamp" json:"createdTime"`
	UpdatedTime time.Time `gorm:"default:current_timestamp on update current_timestamp" json:"updatedTime"`
}

type User struct {
	BaseModel
	Name        string      `gorm:"default null" json:"name"`        // 用户名称
	NickName    string      `gorm:"default null" json:"nickName"`    // 用户昵称
	Password    string      `gorm:"not null" json:"-"`               // 用户密码
	Mobile      string      `gorm:"not null" json:"mobile"`          // 用户手机号
	HeadUrl     string      `gorm:"default null" json:"headUrl"`     // 用户头像
	Gender      string      `gorm:"default 1" json:"gender"`         // 用户性别
	Birthday    time.Time   `gorm:"default null" json:"birthday"`    // 用户生日
	Address     string      `gotm:"default null" json:"address"`     // 用户地址
	Role        int32       `gorm:"default 1" json:"role"`           // 用户角色
	Description string      `gorm:"default null" json:"description"` // 用户描述
}

func (u *User) TableName() string {
	return "user"
}

编写protobuf

我的protobuf采用的是独立项目,这样的好处是不需要每个项目都复制一份protobuf文件,而且还可以通用一些message,减小代码量。新建mxshop-api文件夹,然后在里面新建如下文件

image-20211125224247136

其中,README.md为文档说明,Makefile为编译脚本,LICENSE为许可,.gitignore为git提交时忽略的文件。proto存放在api下面,并初步命令为v0

Makefile文件内容

gen-go:
	user
user:
	protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:.  api/user/v0/*.proto

.PHONY: setup

LICENSE

Apache License 
Version 2.0, January 2004 
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.

"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.

"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.

"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).

"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.

"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:

(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and

(b) You must cause any modified files to carry prominent notices stating that You changed the files; and

(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and

(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.

You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.

Copyright [2021] [fiecato]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

[apache-licenses](http://www.apache.org/licenses/LICENSE-2.0)

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

.gitignore

# ---> Go
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test
*.prof

# ---> VisualStudioCode
.settings

user protobuf

编写user的protobuf文件

syntax = "proto3";
package proto;

import "google/protobuf/timestamp.proto";
import "api/common/v0/common.proto";

option go_package = "imooc/mxshop-api/api/user/v0;v0";

service UserService {
    rpc RegusterUser(RegisterUserRequest) returns (RegisterUserResponse);
    rpc GetUser(UserDetailRequest) returns (UserDetailResponse);
    rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
    rpc ListUser(ListUserRequest) returns (ListUserResponse);
}

enum Gender {
    Unkown = 0;
    Male = 1;
    Female = 2;
}

message RegisterUserRequest {
    string username = 1;
    string password = 2;
    string mobile = 3;
}

message RegisterUserResponse {
    UserDetailTO user = 1;
}

message UserDetailTO {
    int64 id = 1;
    string username = 2;
    string password = 3;
    string nickName = 4;
    string mobile = 5;
    string gender = 6;
    string head_url = 7;
    uint64 birthday = 8;
    string address = 9;
    string desc = 10;
    string role = 11;
    google.protobuf.Timestamp createdTime = 12;
    google.protobuf.Timestamp updatedTime = 13;
}


message UserDetailRequest {
    int64 id = 1;
    string mobile = 2;
}

message UserDetailResponse {
    UserDetailTO user = 1;
}

message UpdateUserRequest {
    int64 id = 1;
    string username = 2;
    string nickName = 3;
    string mobile = 4;
    string gender = 5;
    string head_url = 6;
    uint64 birthday = 7;
    string address = 8;
    string desc = 9;
    string role = 10;
}

message UpdateUserResponse {
    UserDetailTO user = 1;
}

message ListUserRequest {
    PageModel page = 1;
}

message ListUserResponse {
    PageModel page = 1;
    repeated UserDetailTO users = 2;
}

其中,用到了PageMode通用message

syntax = "proto3";
package proto;

option go_package = "imooc/mxshop-api/api/common/v0;v0";

message PageModel {
    int64 page = 1;
    int64 pageSize = 2;
    int64 total = 3;
}

Makefile新增

common:
	protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:.  api/common/v0/*.proto

然后执行命令编译proto文件

make common

make user

成功之后就会生成go对应的protobuf文件了,但是这个时候生成的protbuf文件会提示红色报错。另外,如果只想生成单个protobuf文件,将

protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:.  api/user/v0/*.proto

修改为

protoc --go_out=plugins=grpc:. --go-grpc_out=paths=source_relative:.  api/user/v0/*.proto

即可

image-20211127012200866

这是因为我的项目中没有go.mod,go的包管理文件,在根目录新建一个go.mod,增加以下内容

module imooc/mxshop-api

go 1.16

require (
	google.golang.org/grpc v1.40.0
	google.golang.org/protobuf v1.27.1
)

增加完毕之后会提示报错

image-20211127012355341

此时,在该目录下执行如下命令进行包同步即可

go mod tidy

同步完成会生成一个go.sum文件,该文件记录了当前所有使用和依赖的包及其版本。同时,生成的protobuf文件也不再有红色提示。

image-20211127012514992

在用户服务的go.mod中增加mxshop-api目录的引用

replace imooc/mxshop-api => ../mxshop-api

将user的model中的性别修改为如下

	Gender      user.Gender `gorm:"default 1" json:"gender"`         // 用户性别

在根目录执行同步最新库,go.mod会变成如下

module imooc/user-service

go 1.16

require (
	gorm.io/driver/mysql v1.2.0
	gorm.io/gorm v1.22.3
	imooc/mxshop-api v0.0.0-00010101000000-000000000000
)

replace imooc/mxshop-api => ../mxshop-api

用户列表

dao层(database access object)

DAO层主要是做数据持久层的工作,负责与数据库进行联络的一些任务都封装在此,主要根据传递条件进行数据库数据增删改查,将数据存放到model或者数据库。

创建dao/user.go文件,用于实现user的增删改查接口实现。定义一个接口,然后将用户dao定义一个结构体,实现接口方法后,即可在外面直接使用该接口提供的方法。

package dao

import (
	"imooc/user-service/internal/models"

	"gorm.io/gorm"

)

type UserMgrDao interface {
	Tx() UserMgrDao
	Rollback()
	Commit()
    List() (list []models.User, err error)
}

type UserMgr struct {
	db *gorm.DB
}


func UserMgrInstance(db *gorm.DB) UserMgrDao {
	return &UserMgr{db: db}
}

func (u UserMgr) Tx() UserMgrDao {
	u.db = u.db.Begin()
	return UserMgrDao(&u)
}

func (u UserMgr) Rollback() {
	u.db.Rollback()
}

func (u UserMgr) Commit() {
	u.db.Commit()
}

func (u *UserMgr) List() (list []models.User, err error) {
	list = make([]models.User, 0)
	err = u.db.Debug().Find(&list).Error
	return
}

service层(业务层)

internal/service下新建文件user.go用来实现用户列表的具体业务实现和rpc与model之间的转化工作

package service

import (
	"context"
	user "imooc/mxshop-api/api/user/v0"
	"imooc/user-service/internal/dao"

	"github.com/golang/protobuf/ptypes/timestamp"
	"go.uber.org/zap"
)

type UserService interface {
}

type UserServiceImp struct {
	UserDao dao.UserMgrDao
}

func (slf *UserServiceImp) ListUser(ctx context.Context, in *user.ListUserRequest) (out *user.ListUserResponse, err error) {
	users, err := slf.UserDao.List()
	if err != nil {
		zap.S().Errorf("UserDao-List failed:%v", err)
		return
	}
	out = &user.ListUserResponse{
		Page:  in.GetPage(),
		Users: make([]*user.UserDetailTO, 0),
	}
	for _, user := range users {
		out.Users = append(out.Users, &user.UserDetailTO{
			Id:          user.Id,
			Username:    user.Username,
			NickName:    user.NickName,
			Mobile:      user.Mobile,
			Gender:      user.Gender,
			HeadUrl:     user.HeadUrl,
			Birthday:    user.Birthday,
			Address:     user.Address,
			Desc:        user.Description,
			Role:        user.Role,
			CreatedTime: &timestamp.Timestamp{Seconds: v.CreatedTime.Unix()},
			UpdatedTime: &timestamp.Timestamp{Seconds: v.UpdatedTime.Unix()},
		})
	}
	return
}

修改user.proto文件,将用户性别改为我们定义的enum Gender,同时,增加用户角色枚举值

// 用户角色枚举值
enum Role {
    UnknownRole = 0;
    Admin = 1;
    Normal = 2;
}

修改性别,将

    string gender = 6;
    
    string role = 11;

修改为

    Gender gender = 6;
    
    Role role = 11;

然后将rpc函数命名拼写错误修改下

rpc RegusterUser(RegisterUserRequest) returns (RegisterUserResponse);

改为

rpc RegisterUser(RegisterUserRequest) returns (RegisterUserResponse);

然后执行make user即可重新生成proto文件

修改models/user.go中User的Role

	Role        int32       `gorm:"default 1" json:"role"`           // 用户角色

更改为

	Role        user.Role       `gorm:"default 1" json:"role"`           // 用户角色

日志

可以看到在service层用到了zap,这是一个日志库,可以直接使用,但是想要将日志信息记录到文件和按个人需求格式化日志信息则需要做一些初始化配置。

首先将init文件夹改下名,不改也没关系(个人习惯),重命名为initializer,然后新建log.go文件。定义日志配置struct,方便接收参数。在internal/models/onfig.go文件增加以下代码

type LogConfig struct {
	LogPath    string
	MaxSize    int
	MaxBackups int
	MaxAge     int
	Level      string
}

initializer下创建log.go文件,进行log初始化的代码编写

package initializer

import (
	"imooc/user-service/internal/models"
	"os"

	"go.uber.org/zap"
	"gopkg.in/natefinch/lumberjack.v2"

	"go.uber.org/zap/zapcore"
)

func GetLevel(lvl string) zapcore.Level {
	switch lvl {
	case "debug", "DEBUG":
		return zapcore.DebugLevel
	case "info", "INFO", "": // make the zero value useful
		return zapcore.InfoLevel
	case "warn", "WARN":
		return zapcore.WarnLevel
	case "error", "ERROR":
		return zapcore.ErrorLevel
	case "dpanic", "DPANIC":
		return zapcore.DPanicLevel
	case "panic", "PANIC":
		return zapcore.PanicLevel
	case "fatal", "FATAL":
		return zapcore.FatalLevel
	default:
		return zapcore.InfoLevel
	}
}

func InitLogger(config *models.LogConfig) {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder

	lumberJackLogger := &lumberjack.Logger{
		Filename:   config.LogPath,
		MaxSize:    config.MaxSize,
		MaxBackups: config.MaxBackups,
		MaxAge:     config.MaxAge,
		Compress:   false,
	}
	core := zapcore.NewCore(
		zapcore.NewConsoleEncoder(encoderConfig),
		zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(lumberJackLogger)),
		GetLevel(config.Level),
	)
	zap.ReplaceGlobals(zap.New(core, zap.AddCaller()))
}

以后想使用时就可以直接用zap.s()来调用Debug或者Info等之类函数了。

handler层

handler意思为处理者,在java中主要负责通知主线程按接收到的子线程顺序进行处理。而在grpc中,该层主要负责rpc接口的接收和调用,即负责接收其他rpc服务或者api服务的调用接口,然后向下调用真正的处理函数或者返回。

在handler文件夹下创建user.go文件,在里面实现我们在grpc中定义的ListUser函数。

package service

import (
	"context"
	user "imooc/mxshop-api/api/user/v0"
	"imooc/user-service/internal/service"
)

type UserService struct {
	user.UserServiceServer
}

func (u *UserService) ListUser(ctx context.Context, in *user.ListUserRequest) (out *user.ListUserResponse, err error) {
	operator := service.UserServiceInstance()
	out, err = operator.ListUser(ctx, in)
	return
}

需要在service/user.go中增加UserServiceInstance函数

func UserServiceInstance() UserService {
	return &UserServiceImp{UserDao: dao.UserMgrInstance()}
}

需要修改dao\user.go文件的UserMgrInstance,将传参db更改为全局变量global.DB

修改前

func UserMgrInstance(db *gorm.db) UserMgrDao {
	return &UserMgr{db: db}
}

修改后

func UserMgrInstance() UserMgrDao {
	return &UserMgr{db: global.DB}
}

需要在根目录创建global\globa.go文件

package global

import "gorm.io/gorm"

var (
	DB *gorm.DB
)

至此,我们的第一个rpc接口就已经写好了,接下来写main.go,实现调用吧

main

要想运行,必然需要获取一些配置参数,一般开发初期我们可以直接写死,当然,写入文件进行读取也是比较好的,所以,接下来先实现配置读取。

models\config.go中增加服务配置读取,同时将mysql的也修改了

// 修改
type DBConfig struct {
	Username          string `mapstructure:"username"`
	Password          string `mapstructure:"password"`
	Host              string `mapstructure:"host"`
	Port              int    `mapstructure:"port"`
	Dbname            string `mapstructure:"dbname"`
	DefaultStringSize int    `mapstructure:"defaultStringSize"`
	MaxIdleConn       int    `mapstructure:"maxIdleConn"`
	MaxOpenConn       int    `mapstructure:"maxOpenConn"`
}


type LogConfig struct {
	LogPath    string `mapstructure:"logPath"`
	MaxSize    int    `mapstructure:"maxSize"`
	MaxBackups int    `mapstructure:"maxBackups"`
	MaxAge     int    `mapstructure:"maxAge"`
	Level      string `mapstructure:"level"`
}

// 新增
type ServiceConfig struct {
	Host string `mapstructure:"host"`
	Port int    `mapstructure:"port"`
}

global/global.go文件中新增

var (
	DBConfig      *models.DBConfig
	LogConfig     *models.LogConfig
	ServiceConfig *models.ServiceConfig
)

func init() {
	DBConfig = &models.DBConfig{}
	LogConfig = &models.LogConfig{}
	ServiceConfig = &models.ServiceConfig{}
}

initializer/config.go中新增如下内容

func InitConfig() {
	vp := viper.New()
	vp.AddConfigPath("configs/")
	vp.SetConfigFile(".yml")
	err := vp.ReadInConfig()
	if err != nil {
		panic(fmt.Sprintf("Read config failed:%v", err.Error()))
	}
	err = vp.UnmarshalKey("db", &global.DBConfig)
	if err != nil {
		panic(fmt.Sprintf("Read mysql config failed:%v", err))
	}
	err = vp.UnmarshalKey("log", &global.LogConfig)
	if err != nil {
		panic(fmt.Sprintf("Read log config failed:%v", err))
	}
	err = vp.UnmarshalKey("service", &global.ServiceConfig)
	if err != nil {
		panic(fmt.Sprintf("Read service config failed:%v", err))
	}
}

同时修改initializer/log.go中的InitLogger

func InitLogger() {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder

	lumberJackLogger := &lumberjack.Logger{
		Filename:   global.LogConfig.LogPath,
		MaxSize:    global.LogConfig.MaxSize,
		MaxBackups: global.LogConfig.MaxBackups,
		MaxAge:     global.LogConfig.MaxAge,
		Compress:   false,
	}
	core := zapcore.NewCore(
		zapcore.NewConsoleEncoder(encoderConfig),
		zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(lumberJackLogger)),
		GetLevel(global.LogConfig.Level),
	)
	zap.ReplaceGlobals(zap.New(core, zap.AddCaller()))
}

同时,将dao/database.go文件的内容移动到initializer文件夹init.go中,并增建初始化函数

func InitDatabase() {
	var err error
	zap.S().Infof("Inita database at [%s:%d:%s]", global.DBConfig.Host, global.DBConfig.Port, global.DBConfig.Dbname)
	global.DB, err = CreateDatabasePool(global.DBConfig)
	if err != nil {
		panic(err.Error())
	}
}

为方便统一,将另外两个文件的内容也移动到init.go文件中来,此时文件结构为

│  .gitignore
│  go.mod
│  go.sum
│  LICENSE
│  README.md
│
├─.idea
│      .gitignore
│      modules.xml
│      user-service.iml
│      vcs.xml
│      watcherTasks.xml
│      workspace.xml
│
├─cmd
│  └─user-service
│          main.go
│
├─configs
├─docs
│      user-database.md
│
├─global
│      global.go
│
├─initializer
│      init.go
│
└─internal
    ├─dao
    │      database.go
    │      user.go
    │
    ├─handler
    │      user.go
    │
    ├─models
    │      config.go
    │      user.go
    │
    └─service
            user.go

然后在main.go中进行初始化、调用和注册grpc

func main() {
	initializer.InitConfig()

	initializer.InitLogger()

	initializer.InitDatabase()

	addr := fmt.Sprintf("%s:%d", global.ServiceConfig.Host, global.ServiceConfig.Port)
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		zap.S().Errorf("failed to listen [%s], err: %v", addr, err)
		return
	}
	grpcServer := grpc.NewServer()
	user.RegisterUserServiceServer(grpcServer, &handler.UserService{})
	zap.S().Infof("User-service start at %s 。。。", addr)
	if err := grpcServer.Serve(listener); err != nil {
		return
	}
}

调试

image-20211213001534049

将下面红框中路径修改为main.go文件所在路径

image-20211213001614501

点击引用和ok之后,点击运行即可

image-20211213001631642

提示没有安装grpc,按要求安装一下

image-20211213001657254

再次错误,还没有创建配置文件,在configs下面创建config.yml文件,然后修改下InitConfig

// 增加
	vp.SetConfigName("config")

// 修改
	vp.SetConfigFile(".yml")
// 更改为
	vp.SetConfigType("yml")

image-20211213001824330

按如下格式填写即可

db:
  host: xx.xx.xx.xx
  port: xxxx
  username: xxx
  password: xxxxxxx
  dbname: xxxxx
  defaultStringSize: xx
  maxIdleConn: xx
  maxOpenConn: xxx

log:
  logPath: xxx/xxx/xxx.xx
  maxSize: xx
  maxBackups: xx
  maxAge: xx
  level: xx

service:
  host: xx.xx.xx.xx
  port: xxxx

新建数据库

image-20211213002519047

之后重新启动如下

image-20211213002900285

安装rpc请求工具bloomrpc

在github直接搜索即可,可直接下载releases版本。该工具类似于postman,可以模拟发送rpc请求,方便我们的调试。

整个界面很简洁

image-20211214000530265

添加protobuf文件根目录路径

image-20211214000731726

然后打开想要调试的protobuf文件

image-20211214000811257

确认之后,左边就会出现function列表,鼠标选中想要调试的function,然后右边就会出现请求参数,在env右侧填入rpc地址即可访问。

image-20211214000959498

image-20211214001119518

提示我数据库还没有user表,所以我先去创建一张表,然后添加一点数据。

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名称',
  `nick_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户昵称',
  `password` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户密码',
  `mobile` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户手机号',
  `head_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户头像',
  `gender` int NOT NULL DEFAULT 1 COMMENT '用户性别',
  `birthday` date NULL DEFAULT NULL COMMENT '用户生日',
  `address` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户地址',
  `role` int NOT NULL DEFAULT 1 COMMENT '用户角色',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户描述',
  `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

添加数据

image-20211214001857331

然后再次请求列表,可以看到已经成功返回了。至此列表接口在rpc层就已经调通了。

image-20211214001914590

用户查询

用户查询的grpc方法在前面我们就已经定义好了,所以接下只需要实现即可。

dao/user.go中查询接口。

type UserMgrDao interface {
	GetUserById(id int64) (value models.User, err error)
	GetUserByMobile(mobile string) (value models.User, err error)
}

// 按用户ID进行查询
func (u *UserMgr) GetUserById(id int64) (value models.User, err error) {
	err = u.db.Debug().Where(&models.User{BaseModel: models.BaseModel{Id: id}}).First(&value).Error
	return
}

// 按用户手机号进行查询
func (u *UserMgr) GetUserByMobile(mobile string) (value models.User, err error) {
	err = u.db.Debug().Where(&models.User{Mobile: mobile}).First(&value).Error
	return
}

service\user.go中实现查询方法。记得注册到interface

type UserService interface {
	ListUser(ctx context.Context, in *user.ListUserRequest) (out *user.ListUserResponse, err error)
	GetUserByIdOrMobile(ctx context.Context, in *user.UserDetailRequest) (out *user.UserDetailResponse, err error)
}

func (slf *UserServiceImp) GetUserByIdOrMobile(ctx context.Context, in *user.UserDetailRequest) (out *user.UserDetailResponse, err error) {
	out = &user.UserDetailResponse{}
	value := models.User{}
	if in.GetId() != 0 {
		value, err = slf.UserDao.GetUserById(in.GetId())
		if errors.Is(err, gorm.ErrRecordNotFound) {
			zap.S().Errorf("GetUserById [%d] not exist", in.Id)
			return
		} else if err != nil {
			zap.S().Errorf("GetUserById [%d] failed: %v", in.Id, err)
			return
		}
	} else if in.GetMobile() != "" {
		value, err = slf.UserDao.GetUserByMobile(in.Mobile)
		if errors.Is(err, gorm.ErrRecordNotFound) {
			zap.S().Errorf("GetUserByMobile [%s] not exist", in.Mobile)
			return
		} else if err != nil {
			zap.S().Errorf("GetUserByMobile [%s] failed: %v", in.Mobile, err)
			return
		}
	}
	out.User = &user.UserDetailTO{
		Id:          value.Id,
		NickName:    value.NickName,
		Username:    value.Name,
		Mobile:      value.Mobile,
		Password:    value.Password,
		Gender:      value.Gender,
		HeadUrl:     value.HeadUrl,
		Birthday:    uint64(value.Birthday.Unix()),
		Address:     value.Address,
		Desc:        value.Description,
		Role:        value.Role,
		CreatedTime: &timestamp.Timestamp{Seconds: value.CreatedTime.Unix()},
		UpdatedTime: &timestamp.Timestamp{Seconds: value.UpdatedTime.Unix()},
	}
	return
}

然后再handler/user.go中调用具体实现,从而实现grpc方法

func (u *UserService) GetUser(ctx context.Context, in *user.UserDetailRequest) (out *user.UserDetailResponse, err error) {
	operator := service.UserServiceInstance()
	out, err = operator.GetUserByIdOrMobile(ctx, in)
	return
}

根据列表返回的ID或者手机号来查询用户

用户Id查询

image-20211215235223561

手机号查询

image-20211215235254550

可以看出两个查询都没有问题,接下来实现注册用户

注册用户

dao/user.go中实现创建用户。传递指针是因为想在上层直接取到数据保存时的其余值以及主键ID。

type UserMgrDao interface {
	Tx() UserMgrDao
	Rollback()
	Commit()
	List() (list []models.User, err error)
	GetUserById(id int64) (value models.User, err error)
	GetUserByMobile(mobile string) (value models.User, err error)
	Create(value *models.User) (err error)
}


func (u *UserMgr) Create(value *models.User) (err error) {
	err = u.db.Create(value).Error
	return
}

serice/user.go实现创建的具体实现

type UserService interface {
	ListUser(ctx context.Context, in *user.ListUserRequest) (out *user.ListUserResponse, err error)
	GetUserByIdOrMobile(ctx context.Context, in *user.UserDetailRequest) (out *user.UserDetailResponse, err error)
	CreateUser(ctx context.Context, in *user.RegisterUserRequest) (out *user.RegisterUserResponse, err error)
}

func (slf *UserServiceImp) CreateUser(ctx context.Context, in *user.RegisterUserRequest) (out *user.RegisterUserResponse, err error) {
	out = &user.RegisterUserResponse{}
	value := models.User{
		Mobile:   in.GetMobile(),
		Name:     in.GetUsername(),
		Password: in.GetPassword(),
	}
	tx := slf.UserDao.Tx()
	err = tx.Create(&value)
	if err != nil {
		zap.S().Errorf("CreateUser failed: %v", err)
		tx.Rollback()
		return
	}
	tx.Commit()
	out.User = &user.UserDetailTO{
		Id:          value.Id,
		NickName:    value.NickName,
		Username:    value.Name,
		Mobile:      value.Mobile,
		Password:    value.Password,
		Gender:      value.Gender,
		HeadUrl:     value.HeadUrl,
		Birthday:    uint64(value.Birthday.Unix()),
		Address:     value.Address,
		Desc:        value.Description,
		Role:        value.Role,
		CreatedTime: &timestamp.Timestamp{Seconds: value.CreatedTime.Unix()},
		UpdatedTime: &timestamp.Timestamp{Seconds: value.UpdatedTime.Unix()},
	}
	return
}

handler/user.go实现grpc

func (u *UserService) RegisterUser(ctx context.Context, in *user.RegisterUserRequest) (out *user.RegisterUserResponse, err error) {
	operator := service.UserServiceInstance()
	out, err = operator.CreateUser(ctx, in)
	return
}

重启服务进行测试

image-20211216001535017

该问题是因为创建时间的问题,经过检查发现是model定义问题

	CreatedTime time.Time `gorm:"defulat:current_timestamp" json:"createdTime"`
	UpdatedTime time.Time `gorm:"defulat:current_timestamp on update current_timestamp" json:"updatedTime"`

修改为

	CreatedTime time.Time `gorm:"default:current_timestamp" json:"createdTime"`
	UpdatedTime time.Time `gorm:"default:current_timestamp on update current_timestamp" json:"updatedTime"`

	Address     string      `gotm:"default null" json:"address"`     // 用户地址

修改为

	Address     string      `gorm:"default null" json:"address"`     // 用户地址

另外,默认值应该为default:null这种格式,user model中需要将默认值赋值更改一下,然后重新启动

image-20211216002746319

更新用户

实现dao/user.go的更新。因为gorm采用struct进行更新时会自动忽略0, nil, “”, false等golang空值,所以,如果想要将其更改到这些值就只有通过interface来进行更新。

type UserMgrDao interface {
	Tx() UserMgrDao
	Rollback()
	Commit()
	List() (list []models.User, err error)
	GetUserById(id int64) (value models.User, err error)
	GetUserByMobile(mobile string) (value models.User, err error)
	Create(value *models.User) (err error)
	Update(id int64, value map[string]interface{}) (err error)
}

func (u *UserMgr) Update(id int64, value map[string]interface{}) (err error) {
	err = u.db.Debug().Model(&models.User{BaseModel: models.BaseModel{Id: id}}).Updates(value).Error
	return
}

实现业务层,service/user.go

type UserService interface {
	ListUser(ctx context.Context, in *user.ListUserRequest) (out *user.ListUserResponse, err error)
	GetUserByIdOrMobile(ctx context.Context, in *user.UserDetailRequest) (out *user.UserDetailResponse, err error)
	CreateUser(ctx context.Context, in *user.RegisterUserRequest) (out *user.RegisterUserResponse, err error)
	UpdateUser(ctx context.Context, in *user.UpdateUserRequest) (out *user.UpdateUserResponse, err error)
}

func (slf *UserServiceImp) transformUserToRpc(value models.User) *user.UserDetailTO {
	return &user.UserDetailTO{
		Id:          value.Id,
		Username:    value.Name,
		NickName:    value.NickName,
		Mobile:      value.Mobile,
		Gender:      value.Gender,
		HeadUrl:     value.HeadUrl,
		Birthday:    uint64(value.Birthday.Unix()),
		Address:     value.Address,
		Desc:        value.Description,
		Role:        value.Role,
		CreatedTime: &timestamp.Timestamp{Seconds: value.CreatedTime.Unix()},
		UpdatedTime: &timestamp.Timestamp{Seconds: value.UpdatedTime.Unix()},
	}
}

func (slf *UserServiceImp) UpdateUser(ctx context.Context, in *user.UpdateUserRequest) (out *user.UpdateUserResponse, err error) {
	out = &user.UpdateUserResponse{}
	value := map[string]interface{}{}
	if in.Role != user.Role_UnknownRole {
		value["role"] = in.Role
	}
	if in.Birthday != 0 {
		timestampBirthday := timestamp.Timestamp{Seconds: int64(in.Birthday)}
		value["birthday"] = timestampBirthday.AsTime()
	}
	if in.Gender != user.Gender_Unknown {
		value["gender"] = in.Gender
	}
	if in.NickName != "" {
		value["nick_name"] = in.NickName
	}
	if in.Mobile != "" {
		value["mobile"] = in.Mobile
	}
	if in.Address != "" {
		value["address"] = in.Address
	}
	if in.Desc != "" {
		value["description"] = in.Desc
	}
	if in.HeadUrl != "" {
		value["head_url"] = in.HeadUrl
	}
	if in.Username != "" {
		value["name"] = in.Username
	}
	tx := slf.UserDao.Tx()
	err = tx.Update(in.Id, value)
	if err != nil {
		zap.S().Errorf("Update user faild: %v", err)
		tx.Rollback()
		return
	}
	tx.Commit()
	userInfo, _ := slf.UserDao.GetUserById(in.GetId())

	out.User = slf.transformUserToRpc(userInfo)
	return
}

将用户转化为rpc的封装成一个函数。实现handler层

func (u *UserService) UpdateUser(ctx context.Context, in *user.UpdateUserRequest) (out *user.UpdateUserResponse, err error) {
	operator := service.UserServiceInstance()
	out, err = operator.UpdateUser(ctx, in)
	return
}

调试

image-20211216011210517

至此,user-service的全部接口已经完成,下一步将开发user-web

posted @ 2022-03-06 19:40  丶吃鱼的猫  阅读(389)  评论(0编辑  收藏  举报