Go-kratos 框架商城微服务实战之用户服务 (一) 初识 Kratos 框架
现在 kratos 框架有新版本发布,部分代码有出入,一切以官方文档为准,不过基本结构是差不多的。
kratos 微服务框架商城实战之用户服务(一)
推荐看一下Kratos 官方文档 更加流畅观看此文章,本机器这里已经安装好了
kratos、proto、wire、make等所需的命令工具
准备工作
初始化项目目录
进入自己电脑中存放 Go 项目的目录,
新建 kratos-shop/service 目录并进入到新建的目录中,
执行 kratos new user 命令并进入 user 目录,
执行命令 kratos proto add api/user/v1/user.proto ,
这时你在 kratos-shop/service/user/api/user/v1 目录下会看到新的 user.proto 文件已经创建好了,
接下来执行 kratos proto server api/user/v1/user.proto -t internal/service 命令生成对应的 service 文件。
删除不需要的 proto 文件 rm -rf api/helloworld/,
删除不需要的 service 文件 rm internal/service/greeter.go
完整的命令代码如下
mkdir -p kratos-shop/service cd kratos-shop/service kratos new user cd user kratos proto add api/user/v1/user.proto kratos proto server api/user/v1/user.proto -t internal/service rm -rf api/helloworld/ rm internal/service/greeter.go 修改 user.proto 文件,内容如下:
proto 基本的语法请自行学习,目前这里的只先提供了一个创建用户的 rpc 接口,后续会逐步添加其他 rpc 接口
syntax = "proto3"; package user.v1; option go_package = "user/api/user/v1;v1"; service User{ rpc CreateUser(CreateUserInfo) returns (UserInfoResponse); // 创建用户 } // 创建用户所需字段 message CreateUserInfo{ string nickName = 1; string password = 2; string mobile = 3; } // 返回用户信息 message UserInfoResponse{ int64 id = 1; string password = 2; string mobile = 3; string nickName = 4; int64 birthday = 5; string gender = 6; int32 role = 7; } 生成 user.proto 定义的接口信息
进入到 service/user 目录下,执行 make api 命令,这时可以看到 user/api/user/v1/ 目录下多出了 proto 创建的文件
cd user make api # 目录结构如下: ├── api │ └── user │ └── v1 │ ├── user.pb.go │ ├── user.proto │ └── user_grpc.pb.go 修改配置文件
修改 user/configs/config.yaml 文件,代码如下:
具体链接 mysql、redis 的参数填写自己本机的,本项目用到的是 gorm 。trace 是以后要用到的链路追踪的参数,先定义了。
server: http: addr: 0.0.0.0:8000 timeout: 1s grpc: addr: 0.0.0.0:50051 timeout: 1s data: database: driver: mysql source: root:root@tcp(127.0.0.1:3306)/shop_user?charset=utf8mb4&parseTime=True&loc=Local redis: addr: 127.0.0.1:6379 dial_timeout: 1s read_timeout: 0.2s write_timeout: 0.2s trace: endpoint: http://127.0.0.1:14268/api/traces 新建 user/configs/registry.yaml 文件,引入consul 服务,代码如下:
# 这里引入了 consul 的服务注册与发现,先把配置加入进去 consul: address: 127.0.0.1:8500 scheme: http 修改 user/internal/conf/conf.proto 配置文件
# 文件底部新增 consul 和 trace 的配置信息 message Trace { string endpoint = 1; } message Registry { message Consul { string address = 1; string scheme = 2; } Consul consul = 1; } 新生成 conf.pb.go 文件,执行 make config
# `service/user` 目录下,执行命令 make config 安装 consul 服务工具
# 这里使用的是 docker 工具进行创建的 docker run -d -p 8500:8500 -p 8300:8300 -p 8301:8301 -p 8302:8302 -p 8600:8600/udp consul consul agent -dev -client=0.0.0.0 # 浏览器访问 http://127.0.0.1:8500/ui/dc1/services 测试是否安装成功 修改服务代码
修改 user/internal/data/ 目录下的文件
修改 data.go添加如下内容:
package data import ( "github.com/go-kratos/kratos/v2/log" "github.com/go-redis/redis/extra/redisotel" "github.com/go-redis/redis/v8" "github.com/google/wire" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" "gorm.io/gorm/schema" slog "log" "os" "time" "user/internal/conf" ) // ProviderSet is data providers. var ProviderSet = wire.NewSet(NewData, NewDB, NewRedis, NewUserRepo) type Data struct { db *gorm.DB rdb *redis.Client } // NewData . func NewData(c *conf.Data, logger log.Logger, db *gorm.DB, rdb *redis.Client) (*Data, func(), error) { cleanup := func() { log.NewHelper(logger).Info("closing the data resources") } return &Data{db: db, rdb: rdb}, cleanup, nil } // NewDB . func NewDB(c *conf.Data) *gorm.DB { // 终端打印输入 sql 执行记录 newLogger := logger.New( slog.New(os.Stdout, "\r\n", slog.LstdFlags), // io writer logger.Config{ SlowThreshold: time.Second, // 慢查询 SQL 阈值 Colorful: true, // 禁用彩色打印 //IgnoreRecordNotFoundError: false, LogLevel: logger.Info, // Log lever }, ) db, err := gorm.Open(mysql.Open(c.Database.Source), &gorm.Config{ Logger: newLogger, DisableForeignKeyConstraintWhenMigrating: true, NamingStrategy: schema.NamingStrategy{ //SingularTable: true, // 表名是否加 s }, }) if err != nil { log.Errorf("failed opening connection to sqlite: %v", err) panic("failed to connect database") } return db } func NewRedis(c *conf.Data) *redis.Client { rdb := redis.NewClient(&redis.Options{ Addr: c.Redis.Addr, Password: c.Redis.Password, DB: int(c.Redis.Db), DialTimeout: c.Redis.DialTimeout.AsDuration(), WriteTimeout: c.Redis.WriteTimeout.AsDuration(), ReadTimeout: c.Redis.ReadTimeout.AsDuration(), }) rdb.AddHook(redisotel.TracingHook{}) if err := rdb.Close(); err != nil { log.Error(err) } return rdb } 这里的 wire 概念如果不熟悉的话,请参看Wire 依赖注入
修改 user/internal/service/ 目录下的文件
修改或者删除 user/internal/service/greeter.go 为 user.go, 添加代码如下:
package service import ( "context" "github.com/go-kratos/kratos/v2/log" v1 "user/api/user/v1" "user/internal/biz" ) type UserService struct { v1.UnimplementedUserServer uc *biz.UserUsecase log *log.Helper } // NewUserService new a greeter service. func NewUserService(uc *biz.UserUsecase, logger log.Logger) *UserService { return &UserService{uc: uc, log: log.NewHelper(logger)} } // CreateUser create a user func (u *UserService) CreateUser(ctx context.Context, req *v1.CreateUserInfo) (*v1.UserInfoResponse, error) { user, err := u.uc.Create(ctx, &biz.User{ Mobile: req.Mobile, Password: req.Password, NickName: req.NickName, }) if err != nil { return nil, err } userInfoRsp := v1.UserInfoResponse{ Id: user.ID, Mobile: user.Mobile, Password: user.Password, NickName: user.NickName, Gender: user.Gender, Role: int32(user.Role), Birthday: user.Birthday, } return &userInfoRsp, nil } 修改 ser/internal/service/service.go 文件, 代码如下:
package service import "github.com/google/wire" // ProviderSet is service providers. var ProviderSet = wire.NewSet(NewUserService) 修改或删除 user/internal/biz/greeter.go 为 user.go添加如下内容:
package biz import ( "context" "github.com/go-kratos/kratos/v2/log" ) // 定义返回数据结构体 type User struct { ID int64 Mobile string Password string NickName string Birthday int64 Gender string Role int } type UserRepo interface { CreateUser(context.Context, *User) (*User, error) } type UserUsecase struct { repo UserRepo log *log.Helper } func NewUserUsecase(repo UserRepo, logger log.Logger) *UserUsecase { return &UserUsecase{repo: repo, log: log.NewHelper(logger)} } func (uc *UserUsecase) Create(ctx context.Context, u *User) (*User, error) { return uc.repo.CreateUser(ctx, u) } 修改 user/internal/biz/biz.go 文件,内容如下:
package biz import "github.com/google/wire" // ProviderSet is biz providers. var ProviderSet = wire.NewSet(NewUserUsecase) 修改或删除 user/internal/data/greeter.go 为 user.go添加如下内容:
package data import ( "context" "crypto/sha512" "fmt" "github.com/anaskhan96/go-password-encoder" "github.com/go-kratos/kratos/v2/log" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "gorm.io/gorm" "time" "user/internal/biz" ) // 定义数据表结构体 type User struct { ID int64 `gorm:"primarykey"` Mobile string `gorm:"index:idx_mobile;unique;type:varchar(11) comment '手机号码,用户唯一标识';not null"` Password string `gorm:"type:varchar(100);not null "` // 用户密码的保存需要注意是否加密 NickName string `gorm:"type:varchar(25) comment '用户昵称'"` Birthday *time.Time `gorm:"type:datetime comment '出生日日期'"` Gender string `gorm:"column:gender;default:male;type:varchar(16) comment 'female:女,male:男'"` Role int `gorm:"column:role;default:1;type:int comment '1:普通用户,2:管理员'"` CreatedAt time.Time `gorm:"column:add_time"` UpdatedAt time.Time `gorm:"column:update_time"` DeletedAt gorm.DeletedAt IsDeletedAt bool } type userRepo struct { data *Data log *log.Helper } // NewUserRepo . 这里需要注意,上面 data 文件 wire 注入的是此方法,方法名不要写错了 func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo { return &userRepo{ data: data, log: log.NewHelper(logger), } } // CreateUser . func (r *userRepo) CreateUser(ctx context.Context, u *biz.User) (*biz.User, error) { var user User // 验证是否已经创建 result := r.data.db.Where(&biz.User{Mobile: u.Mobile}).First(&user) if result.RowsAffected == 1 { return nil, status.Errorf(codes.AlreadyExists, "用户已存在") } user.Mobile = u.Mobile user.NickName = u.NickName user.Password = encrypt(u.Password) // 密码加密 res := r.data.db.Create(&user) if res.Error != nil { return nil, status.Errorf(codes.Internal, res.Error.Error()) } return &biz.User{ ID: user.ID, Mobile: user.Mobile, Password: user.Password, NickName: user.NickName, Gender: user.Gender, Role: user.Role, }, nil } // Password encryption func encrypt(psd string) string { options := &password.Options{SaltLen: 16, Iterations: 10000, KeyLen: 32, HashFunction: sha512.New} salt, encodedPwd := password.Encode(psd, options) return fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd) } 修改 user/internal/server/ 目录下的文件
这里用不到 http 服务删除 http.go 文件,修改 grpc.go 文件内容如下:
package server import ( "github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/middleware/logging" "github.com/go-kratos/kratos/v2/middleware/recovery" "github.com/go-kratos/kratos/v2/transport/grpc" v1 "user/api/user/v1" "user/internal/conf" "user/internal/service" ) // NewGRPCServer new a gRPC server. func NewGRPCServer(c *conf.Server, greeter *service.UserService, logger log.Logger) *grpc.Server { var opts = []grpc.ServerOption{ grpc.Middleware( recovery.Recovery(), logging.Server(logger), ), } if c.Grpc.Network != "" { opts = append(opts, grpc.Network(c.Grpc.Network)) } if c.Grpc.Addr != "" { opts = append(opts, grpc.Address(c.Grpc.Addr)) } if c.Grpc.Timeout != nil { opts = append(opts, grpc.Timeout(c.Grpc.Timeout.AsDuration())) } srv := grpc.NewServer(opts...) v1.RegisterUserServer(srv, greeter) return srv } 修改 server.go 文件,这里加入了 consul 的服务,内容如下:
package server import ( "github.com/go-kratos/kratos/v2/registry" "github.com/google/wire" "user/internal/conf" consul "github.com/go-kratos/kratos/contrib/registry/consul/v2" consulAPI "github.com/hashicorp/consul/api" ) // ProviderSet is server providers. var ProviderSet = wire.NewSet(NewGRPCServer, NewRegistrar) // NewRegistrar 引入 consul func NewRegistrar(conf *conf.Registry) registry.Registrar { c := consulAPI.DefaultConfig() c.Address = conf.Consul.Address c.Scheme = conf.Consul.Scheme cli, err := consulAPI.NewClient(c) if err != nil { panic(err) } r := consul.New(cli, consul.WithHealthCheck(false)) return r } 修改启动程序
修改 user/cmd/wire.go文件
这里注入了consul需要的配置,需要添加进来
func initApp(*conf.Server, *conf.Data, *conf.Registry, log.Logger) (*kratos.App, func(), error) { panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp)) } 修改 user/cmd/user/main.go 文件
package main import ( "flag" "os" "github.com/go-kratos/kratos/v2" "github.com/go-kratos/kratos/v2/config" "github.com/go-kratos/kratos/v2/config/file" "github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/middleware/tracing" "github.com/go-kratos/kratos/v2/registry" "github.com/go-kratos/kratos/v2/transport/grpc" "user/internal/conf" ) // go build -ldflags "-X main.Version=x.y.z" var ( // Name is the name of the compiled software. Name = "shop.users.service" // Version is the version of the compiled software. Version = "v1" // flagconf is the config flag. flagconf string id, _ = os.Hostname() ) func init() { flag.StringVar(&flagconf, "conf", "../../configs", "config path, eg: -conf config.yaml") } func newApp(logger log.Logger, gs *grpc.Server, rr registry.Registrar) *kratos.App { return kratos.New( kratos.ID(id+"shop.user.service"), kratos.Name(Name), kratos.Version(Version), kratos.Metadata(map[string]string{}), kratos.Logger(logger), kratos.Server( gs, ), kratos.Registrar(rr), // consul 的引入 ) } func main() { flag.Parse() logger := log.With(log.NewStdLogger(os.Stdout), "ts", log.DefaultTimestamp, "caller", log.DefaultCaller, "service.id", id, "service.name", Name, "service.version", Version, "trace_id", tracing.TraceID(), "span_id", tracing.SpanID(), ) c := config.New( config.WithSource( file.NewSource(flagconf), ), ) defer c.Close() if err := c.Load(); err != nil { panic(err) } var bc conf.Bootstrap if err := c.Scan(&bc); err != nil { panic(err) } // consul 的引入 var rc conf.Registry if err := c.Scan(&rc); err != nil { panic(err) } app, cleanup, err := initApp(bc.Server, bc.Data, &rc, logger) if err != nil { panic(err) } defer cleanup() // start and wait for stop signal if err := app.Run(); err != nil { panic(err) } } 修改根目录 user/makefile 文件
在 go generate ./... 下面添加代码 wire: cd cmd/user/ && wire 根目录执行 make wire 命令
# service/user make wire 启动程序
别忘记根据 data 里面的 user struct 创建对应的数据库表,这里也可以写一个 gorm 创建表的文件进行创建。
启动程序 kratos run
根目录 service/user 执行命令 kratos run 简单测试
由于没写对外访问的 http 服务,这里还没有加入单元测试,所以先创建个文件链接启动过的 grpc 服务简单测试一下。
根目录新建 user/test/user.go 文件,添加如下内容:
package main import ( "context" "fmt" "google.golang.org/grpc" v1 "user/api/user/v1" ) var userClient v1.UserClient var conn *grpc.ClientConn func main() { Init() TestCreateUser() // 创建用户 conn.Close() } // Init 初始化 grpc 链接 注意这里链接的 端口 func Init() { var err error conn, err = grpc.Dial("127.0.0.1:50051", grpc.WithInsecure()) if err != nil { panic("grpc link err" + err.Error()) } userClient = v1.NewUserClient(conn) } func TestCreateUser() { rsp, err := userClient.CreateUser(context.Background(), &v1.CreateUserInfo{ Mobile: fmt.Sprintf("1388888888%d", 1), Password: "admin123", NickName: fmt.Sprintf("YWWW%d", 1), }) if err != nil { panic("grpc 创建用户失败" + err.Error()) } fmt.Println(rsp.Id) } 这里别忘记启动 kratos user 服务之后,再执行 test/user.go 文件,查询执行结果,是否有个ID输出 查询自己的数据库,看看是否有插入的数据了。
源码已经上传到 GitHub 上了,下一篇开始逐步完善用户服务的接口,

Reference
Go工程化-依赖注入 go-kratos.dev/blog/go-project-wire
Project Layout 最佳实践 go-kratos.dev/blog/go-layout-opera...
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
赞👍
make api 并不能生成 user_http.pb.go 你用的kratos版本是不是过低了
想问一下,在创建 proto 文件的时候, 如果有多个 message 有共同的字段, 可以把这些字段提出来吗? 而不是嵌套吗? 比如都有 id,name 字段,如果单独定义一个 message 包含,如:
但是想实现的的效果是:
如果这样:
呈现出来的效果是嵌套,也就是相当于多了一层 request,而不是希望的那样。 这种应该如何处理呢?
@Cliffs 😂 貌似不可以,你可以自己写写试试。嵌套复用的话,具体语法如下:
或者 🤔
但是这样子写的话,CreateRequest 就成了二维的啦。
难得看到这个系列文章
求問一下,有遇到這個問題的麼
非常棒
请问楼主用的kratos的那个版本啊!
make api 没有用,没有生成相应的文件
没有自动创建数据库表吗?一直没有找到
执行wire命令后,是不是应该将cmd/user/main的InitApp方法换成cmd/user/wirecmd/user/wire_gen里面的initApp方法然后注释wire.go的initApp方法呢,生成过后wire.go和wire_gen.go的方法就会重名了
在window下运行make api 报错
C:\ Is a Dir...使用bash来运行的,请教一下怎么解决。有完整的项目代码吗
redis配置中缺少 代码中需要的 password 和 db
很好的文章,kratos的官方文档作的很烂,有一个地方不能理解,CreateUser使用的是*biz.User这个返回值的参数来作where条件,如果我的需求只要返回id给前端的话,biz.User中如果没有mobile这个就无法运行了,感觉应该使用用户的输入参数更合理吧,proto中定义的.