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 協議》,轉載必須註明作者和本文連結