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
完整的命令代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 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/g reeter.go
修改 user.proto
文件,内容如下:
proto 基本的语法请自行学习,目前这里的只先提供了一个创建用户的 rpc 接口,后续会逐步添加其他 rpc 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 创建的文件
1 2 3 4 5 6 7 8 9 10 11 cd user make api ├── api │ └── user │ └── v1 │ ├── user.pb.go │ ├── user.proto │ └── user_grpc.pb.go
修改配置文件 修改 user/configs/config.yaml
文件,代码如下:
具体链接 mysql、redis 的参数填写自己本机的,本项目用到的是 gorm 。trace 是以后要用到的链路追踪的参数,先定义了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 server: http: addr: 0.0 .0 .0 :8000 timeout: 1 s grpc: addr: 0.0 .0 .0 :50051 timeout: 1 sdata: 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: 1 s read_timeout: 0.2 s write_timeout: 0.2 strace: endpoint: http:
新建 user/configs/registry.yaml
文件,引入consul 服务,代码如下: 1 2 3 4 # 这里引入了 consul 的服务注册与发现,先把配置加入进去 consul: address: 127.0 .0 .1 :8500 scheme: http
修改 user/internal/conf/conf.proto
配置文件 1 2 3 4 5 6 7 8 9 10 11 12 # 文件底部新增 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
1 2 # `service/user` 目录下,执行命令 make config
安装 consul 服务工具 1 2 3 4 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
修改服务代码 修改 user/internal/data/
目录下的文件 修改 user/internal/data/greeter.go
为 user.go
添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 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" )var ProviderSet = wire.NewSet(NewData, NewDB, NewRedis, NewUserRepo)type Data struct { db *gorm.DB rdb *redis.Client } 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 } func NewDB(c *conf .Data) *gorm.DB { newLogger := logger.New( slog.New(os.Stdout, "\r\n" , slog.LstdFlags), logger.Config{ SlowThreshold: time.Second, Colorful: true, LogLevel: logger.Info, }, ) db , err := gorm.Open (mysql.Open (c.Database.Source), &gorm.Config{ Logger: newLogger, DisableForeignKeyConstraintWhenMigrating: true, NamingStrategy: schema.NamingStrategy{ }, }) 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
, 添加代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package serviceimport ( "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 }func NewUserService (uc *biz.UserUsecase, logger log.Logger) *UserService { return &UserService{uc: uc, log: log.NewHelper(logger)} }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
文件, 代码如下:
1 2 3 4 5 6 package serviceimport "github.com/google/wire" var ProviderSet = wire .NewSet (NewUserService)
修改或删除 user/internal/biz/greeter.go
为 user.go
添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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
文件,内容如下:
1 2 3 4 5 6 package bizimport "github.com/google/wire" var ProviderSet = wire .NewSet (NewUserUsecase)
修改或删除 user/internal/data/greeter.go
为 user.go
添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 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 } func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo { return &userRepo{ data: data, log: log.NewHelper (logger), } } 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 } 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
文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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" ) 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 的服务,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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" )var ProviderSet = wire.NewSet (NewGRPCServer, NewRegistrar) 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 需要的配置,需要添加进来
1 2 3 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
文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 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" )var ( Name = "shop.users.service" Version = "v1" 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), ) } 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 ) } 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() if err := app .Run (); err != nil { panic(err ) } }
修改根目录 user/makefile
文件 1 2 3 4 在 go generate ./... 下面添加代码wire : cd cmd/user/ && wire
根目录执行 make wire
命令 1 2 # service/user make wire
启动程序
别忘记根据 data 里面的 user struct 创建对应的数据库表,这里也可以写一个 gorm 创建表的文件进行创建。
启动程序 kratos run 1 2 根目录 service/user 执行命令 kratos run
简单测试 由于没写对外访问的 http 服务,这里还没有加入单元测试,所以先创建个文件链接启动过的 grpc 服务简单测试一下。
根目录新建 user/test/user.go
文件,添加如下内容: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 () } 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 上了,下一篇开始逐步完善用户服务的接口, 参考 Go 工程化-依赖注入 https://go-kratos.dev/blog/go-project-wire
Project Layout 最佳实践 https://go-kratos.dev/blog/go-layout-operation-process 感谢您的耐心阅读,动动手指点个 star 吧。
关注我获取更新