【二】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文件夹,然后在里面新建如下文件
其中,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
即可
这是因为我的项目中没有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
)
增加完毕之后会提示报错
此时,在该目录下执行如下命令进行包同步即可
go mod tidy
同步完成会生成一个go.sum
文件,该文件记录了当前所有使用和依赖的包及其版本。同时,生成的protobuf文件也不再有红色提示。
在用户服务的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: ×tamp.Timestamp{Seconds: v.CreatedTime.Unix()},
UpdatedTime: ×tamp.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
}
}
调试
将下面红框中路径修改为main.go
文件所在路径
点击引用和ok之后,点击运行即可
提示没有安装grpc,按要求安装一下
再次错误,还没有创建配置文件,在configs
下面创建config.yml
文件,然后修改下InitConfig
// 增加
vp.SetConfigName("config")
// 修改
vp.SetConfigFile(".yml")
// 更改为
vp.SetConfigType("yml")
按如下格式填写即可
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
新建数据库
之后重新启动如下
安装rpc请求工具bloomrpc
在github直接搜索即可,可直接下载releases版本。该工具类似于postman,可以模拟发送rpc请求,方便我们的调试。
整个界面很简洁
添加protobuf文件根目录路径
然后打开想要调试的protobuf文件
确认之后,左边就会出现function列表,鼠标选中想要调试的function,然后右边就会出现请求参数,在env右侧填入rpc地址即可访问。
提示我数据库还没有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;
添加数据
然后再次请求列表,可以看到已经成功返回了。至此列表接口在rpc层就已经调通了。
用户查询
用户查询的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: ×tamp.Timestamp{Seconds: value.CreatedTime.Unix()},
UpdatedTime: ×tamp.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查询
手机号查询
可以看出两个查询都没有问题,接下来实现注册用户
注册用户
在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: ×tamp.Timestamp{Seconds: value.CreatedTime.Unix()},
UpdatedTime: ×tamp.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
}
重启服务进行测试
该问题是因为创建时间的问题,经过检查发现是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中需要将默认值赋值更改一下,然后重新启动
更新用户
实现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: ×tamp.Timestamp{Seconds: value.CreatedTime.Unix()},
UpdatedTime: ×tamp.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
}
调试
至此,user-service的全部接口已经完成,下一步将开发user-web