kratos 微服务框架商城实战初识 kratos
source link: https://xie.infoq.cn/article/64641cdf2e6dcd91c1c43971c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
kratos 微服务框架商城实战初识 kratos
看文章之前推荐看一下Kratos 官方文档 以便能更加流畅观看此文章。同时强烈建议阅读 Go 工程化-依赖注入、Project Layout 最佳实践 这两篇文章。
本机器这里已经安装好了go、kratos、proto、wire、make
等所需的工具。
初始化项目目录
进入自己电脑中存放 Go 项目的目录
新建 kratos-shop/service
目录,并进入到新建的目录中
执行 kratos new user
命令并进入 user
目录,执行命令 kratos proto add api/user/v1/user.proto
,
这时你在 /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
引入consul 服务
新建 user/configs/registry.yaml
文件,代码如下:
# 这里引入了 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 测试是否安装成功
修改 internal 服务目录
修改 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 输出查询自己的数据库,看看是否有插入的数据了。
Kratos-shop 源码 已经上传到 GitHub 上了,如果有疑问,请点击源码进行查看。也感谢您指出错误。
感谢您的耐心阅读,动动手指点个赞吧。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK