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.gouser.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.gouser.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.gouser.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 协议》,转载必须注明作者和本文链接
微信搜索:上帝喜爱笨人
本帖由 zhaocrazy 于 3年前 加精
讨论数量: 32

make api 并不能生成 user_http.pb.go 你用的 kratos 版本是不是过低了

3年前 评论
Aliliin (楼主) 3年前

想问一下,在创建 proto 文件的时候, 如果有多个 message 有共同的字段, 可以把这些字段提出来吗? 而不是嵌套吗? 比如都有 id,name 字段,如果单独定义一个 message 包含,如:

message Request {
  int32 id = 1;
  string name = 2;
}

但是想实现的的效果是:

message CreateRequest {
  int32 id = 1;
  string name = 2;
  string info = 3;
}

如果这样:

message CreateRequest {
  Request request = 1;
  string info = 3;
}

呈现出来的效果是嵌套,也就是相当于多了一层 request,而不是希望的那样。 这种应该如何处理呢?

3年前 评论
Aliliin (楼主) 3年前

@Cliffs 😂 貌似不可以,你可以自己写写试试。嵌套复用的话,具体语法如下:

message Request {
  int32 id = 1;
  string name = 2;
}

message CreateRequest {
  Request.id = 1;
  Request.name = 2;
  string info = 3;
}

或者 🤔

message Request {
  int32 id = 1;
  string name = 2;
}

message CreateRequest {
  repeated  Request request = 1;
  string info = 3;
}

但是这样子写的话,CreateRequest 就成了二维的啦。

3年前 评论
blankqwq 2年前

难得看到这个系列文章

2年前 评论
Aliliin (楼主) 2年前
inject initApp: no provider found for *github.com/go-kratos/kratos/v2/log.Helper
        needed by *user/internal/service.UserService in provider set "ProviderSet" (/home/soda/kratos-shop/service/user/internal/service/service.go:6:19)
        needed by *github.com/go-kratos/kratos/v2/transport/grpc.Server in provider set "ProviderSet" (/home/soda/kratos-shop/service/user/internal/server/server.go:13:19)
        needed by *github.com/go-kratos/kratos/v2.App in provider "newApp" (/home/soda/kratos-shop/service/user/cmd/user/main.go:30:6)
wire: user/cmd/user: generate failed
wire: at least one generate failure
make: *** [Makefile:46:wire] 错误 1

求問一下,有遇到這個問題的麼

2年前 评论
Aliliin (楼主) 2年前
X_L11 (作者) 2年前
[root@localhost user]# go run test/user.go
2

非常棒

2年前 评论
Alexliu51617 2年前

请问楼主用的 kratos 的那个版本啊!

2年前 评论
Aliliin (楼主) 2年前

make api 没有用,没有生成相应的文件

2年前 评论
Aliliin (楼主) 2年前

没有自动创建数据库表吗?一直没有找到

2年前 评论
Aliliin (楼主) 2年前

执行 wire 命令后,是不是应该将 cmd/user/main 的 InitApp 方法换成 cmd/user/wirecmd/user/wire_gen 里面的 initApp 方法然后注释 wire.go 的 initApp 方法呢,生成过后 wire.go 和 wire_gen.go 的方法就会重名了

2年前 评论
Aliliin (楼主) 2年前

在 window 下运行 make api 报错 C:\ Is a Dir... 使用 bash 来运行的,请教一下怎么解决。

$ make api
makefile:62: *** 遗漏分隔符 。 停止。
/usr/bin/sh: C:/: Is a directory
protoc --proto_path=./api \
               --proto_path=./third_party \
               --go_out=paths=source_relative:./api \
               --go-http_out=paths=source_relative:./api \
               --go-grpc_out=paths=source_relative:./api \
               --openapi_out=fq_schema_naming=true,default_response=false:. \

Missing input file.
make: *** [api] 错误 1
2年前 评论
Aliliin (楼主) 2年前
Asuna 1年前

有完整的项目代码吗

2年前 评论
Aliliin (楼主) 2年前

redis 配置中缺少 代码中需要的 password 和 db

1年前 评论
Aliliin (楼主) 1年前

很好的文章,kratos 的官方文档作的很烂,有一个地方不能理解,CreateUser 使用的是 * biz.User 这个返回值的参数来作 where 条件,如果我的需求只要返回 id 给前端的话,biz.User 中如果没有 mobile 这个就无法运行了,感觉应该使用用户的输入参数更合理吧,proto 中定义的.

11个月前 评论
Aliliin (楼主) 11个月前
xilin (作者) 11个月前