一. Kratos 簡介
Kratos 是 B 站基于 Golang 實(shí)現(xiàn)的一個(gè)開源的面向微服務(wù)的框架. 使用 Kratos 可以很方便地構(gòu)建一個(gè)規(guī)范的服務(wù).
開源 GitHub 地址: https://github.com/go-kratos/kratos
文檔地址: https://go-kratos.dev/docs/
二. Kratos 特點(diǎn)
- 簡單易用
- 高效
- 穩(wěn)定
- 高性能
- 可擴(kuò)展
- 容錯(cuò)性好
- 豐富的工具鏈
以上特點(diǎn), 還都是從 Readme 文檔翻譯出來的, 后面將逐一驗(yàn)證和體驗(yàn).
三. Kratos 實(shí)現(xiàn)一個(gè) Helloworld
安裝
go get github.com/go-kratos/kratos/cmd/kratos/v2@latest
根據(jù)文檔
# create project template
kratos new helloworld
cd helloworld
# download modules
go mod download
# generate Proto template
kratos proto add api/helloworld/helloworld.proto
# generate Proto source code
kratos proto client api/helloworld/helloworld.proto
# generate server template
kratos proto server api/helloworld/helloworld.proto -t internal/service
# generate all proto source code, wire, etc.
go generate ./...
編譯并執(zhí)行
mkdir bin
# compile
go build -o ./bin/ ./...
# run
./bin/helloworld -conf ./configs
就以上這么一頓操作, 就一起完成一個(gè)服務(wù)的構(gòu)建.
四. 目錄結(jié)構(gòu)分析
tree -d
查看目錄結(jié)構(gòu)
.
├── api
│ └── helloworld
│ └── v1
├── cmd
│ └── helloworld
├── configs
└── internal
├── biz
├── conf
├── data
├── server
└── service
1. /api
這個(gè)文件夾下面, 包含 proto 文件和編譯 proto 文件生成的 go 文件.
之所以把目錄命名為 api, 是因?yàn)榭梢园?proto 當(dāng)作 api 文檔, 生成的 go 文件就相當(dāng)于 api.
2. /cmd
這個(gè)文件夾下的代碼, 主要負(fù)責(zé)程序的啟動记罚、關(guān)閉钩杰、配置初始化等.
cmd 大概率是 command 的縮寫, 下面的文件夾是一個(gè) helloworld 應(yīng)用并且包含 main.go文件.
在執(zhí)行 go build
的時(shí)候, 就會以文件夾的名稱來命名應(yīng)用.
一般不建議在 /cmd
這個(gè)文件夾下放置過多的代碼和文件夾.
如果代碼不是可重用的, 或者你不希望其他人重用它, 請將該代碼放到 /internal
目錄中.
3. /configs
放置配置文件模板, 或默認(rèn)配置, 一般是 yaml 文件.
比如注冊的服務(wù)的地址, 各種數(shù)據(jù)庫的連接配置等.
4. /internal
這個(gè)文件夾下面, 一般都放置不希望作為第三包給他人使用的, 即是當(dāng)作私有庫.
-
/biz
目錄: 業(yè)務(wù)邏輯的組裝層, 類似于 DDD (領(lǐng)域驅(qū)動設(shè)計(jì)) 的 domain 層. -
/conf
目錄: 這里會有 proto 文件和編譯 proto 得到的 go 代碼, 而且 proto 會與上面的/configs
下的 yaml 文件對應(yīng). -
/data
目錄: 業(yè)務(wù)數(shù)據(jù)訪問, 包含 cache傻唾、db 等封裝, 實(shí)現(xiàn)了 biz 的 repo 接口. 類似于 DDD 的 repo. -
/server
目錄: gRPC 和 HTTP 服務(wù)的注冊. -
/service
目錄: 這里是實(shí)現(xiàn)上層的/api
的 interface. 類似于 DDD 的 application 層.
更多參考: https://github.com/golang-standards/project-layout/blob/master/README_zh.md
五. 關(guān)鍵代碼分析
helloworld/cmd/helloworld/main.go
func main() {
flag.Parse()
logger := log.NewStdLogger(os.Stdout)
c := config.New(
config.WithSource(
file.NewSource(flagconf),
),
config.WithDecoder(func(kv *config.KeyValue, v map[string]interface{}) error {
return yaml.Unmarshal(kv.Value, v)
}),
)
if err := c.Load(); err != nil {
panic(err)
}
var bc conf.Bootstrap
if err := c.Scan(&bc); err != nil {
panic(err)
}
app, cleanup, err := initApp(bc.Server, bc.Data, logger)
if err != nil {
panic(err)
}
defer cleanup()
// start and wait for stop signal
if err := app.Run(); err != nil {
panic(err)
}
}
這里使用 "github.com/go-kratos/kratos/v2/config"
這個(gè)包, 它的作用是將解析 /configs/config.yaml
文件解析為 protobuf, 內(nèi)部隱藏的操作是, 將 yaml 文件轉(zhuǎn)為 json, 然后再轉(zhuǎn)為 protobuf.
六. 結(jié)合 Kratos 實(shí)現(xiàn) gRPC 客戶端
想法: 前面學(xué)習(xí)了 gRPC, 這里學(xué)習(xí)了 Kratos, 而 Kratos 里也有 gRPC, 已經(jīng)實(shí)現(xiàn)了 gRPC 的服務(wù)端. 所以, 不如結(jié)合 Kratos 實(shí)現(xiàn)一個(gè) gRPC 客戶端, 以作 Kratos 實(shí)戰(zhàn).
根據(jù)上面的目錄結(jié)構(gòu)分析, 客戶端的實(shí)現(xiàn)邏輯代碼, 應(yīng)當(dāng)放在 /internal/client
文件夾下.
另外 main.go
文件, 應(yīng)該放在 /cmd/helloworld_client
文件夾下.
如果先前沒有的文件夾則創(chuàng)建.
以下主要結(jié)合了 Kratos 的 log 和 config 兩個(gè)工具, 以及 wire (github.com/google/wire
).
1./internal/client/grpc.go
為了使用 wire
, 在 NewGrpcClient
函數(shù)的返回值的第二個(gè)參數(shù), 必須是 func()
類型, 用于 cleanup.
NewGrpcClient
函數(shù)主要有兩步, 第一步是建立 gRPC 服務(wù)連接并連接 greeter
RPC, 第二步是 cleanup 處理.
GrpcClient
結(jié)構(gòu)體主要是實(shí)現(xiàn) RPC 的通信.
package client
import (
"context"
"time"
"helloworld/internal/conf"
"google.golang.org/grpc"
"github.com/go-kratos/kratos/v2/log"
v1 "helloworld/api/helloworld/v1"
"github.com/pkg/errors"
)
type GrpcClient struct {
logger *log.Helper
cli v1.GreeterClient
}
func (gc *GrpcClient) DoSay(name string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
reply, err := gc.cli.SayHello(ctx, &v1.HelloRequest{Name: name})
if err != nil {
return errors.Wrap(err, "could not greet")
}
gc.logger.Infof("Greeting: %s", reply.GetMessage())
return nil
}
func NewGrpcClient(confServer *conf.Server, logger log.Logger) (*GrpcClient, func(), error) {
addr := confServer.Grpc.Addr
conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
return nil, nil, errors.Wrap(err, "did not dail gRPC server")
}
gc := &GrpcClient{
logger: log.NewHelper("client/greeter", logger),
cli: v1.NewGreeterClient(conn),
}
cleanup := func() {
if err := conn.Close(); err != nil {
gc.logger.Errorf("hanppend a little wrongs when close gRPC server:\n %v", err)
}
}
return gc, cleanup, nil
}
2./internal/client/client.go
wire.NewSet
可以放多個(gè)函數(shù), 不分順序.
package client
import "github.com/google/wire"
var ProviderSet = wire.NewSet(NewGrpcClient)
3./cmd/helloworld_client/wire.go
這個(gè)文件主要是通過 wire 命令來生成 wire_gen.go
, 關(guān)鍵函數(shù)是 wire.Build
, 同時(shí)不要?jiǎng)h掉這行代碼 //+build wireinject
.
//+build wireinject
package main
import (
"helloworld/internal/client"
"helloworld/internal/conf"
"github.com/go-kratos/kratos/v2/log"
"github.com/google/wire"
)
func initClient(confServer *conf.Server, logger log.Logger) (*client.GrpcClient, func(), error) {
panic(wire.Build(client.ProviderSet))
}
4./cmd/helloworld_client/main.go
這里也主要學(xué)習(xí)了 helloworld 應(yīng)用中的 conf 的使用, 以及發(fā)了 100 次的 SayHello.
package main
import (
"flag"
"os"
"strconv"
"helloworld/internal/conf"
"github.com/go-kratos/kratos/v2/config"
"github.com/go-kratos/kratos/v2/config/file"
"github.com/go-kratos/kratos/v2/log"
"gopkg.in/yaml.v2"
)
var (
flagconf string
logger log.Logger
)
func init() {
flag.StringVar(&flagconf, "conf", "../../configs", "config path, eg: -conf config.yaml")
logger = log.NewStdLogger(os.Stdout)
}
func main() {
flag.Parse()
// yaml 先轉(zhuǎn) json,
// json 再轉(zhuǎn) protobuf
cfg := config.New(
config.WithSource(
file.NewSource(flagconf),
),
config.WithDecoder(func(kv *config.KeyValue, v map[string]interface{}) error {
return yaml.Unmarshal(kv.Value, v)
}),
)
if err := cfg.Load(); err != nil {
panic(err)
}
var bc conf.Bootstrap
if err := cfg.Scan(&bc); err != nil {
panic(err)
}
gcli, cleanup, err := initClient(bc.Server, logger)
if err != nil {
panic(err)
}
defer cleanup()
lg := log.NewHelper("client/main", logger)
for i := 0; i < 100; i++ {
if err := gcli.DoSay("fango" + strconv.Itoa(i+1)); err != nil {
lg.Error(err)
}
}
lg.Info("finished")
}
5.編譯 helloworld_client
go build -o ./bin/ ./cmd/helloworld_client