围绕grpc,打造工具库和代码生成工具
配置加载
app采用ini格式的配置,也考虑过使用yaml
,但yaml
的灵活度较高,代码生成比较难搞。将app的配置分为两种:
- 值配置:app中使用该值,支持string/int/int_array/string_array
- 对象配置:framework中加载该值,但不直接使用,支持redis/mysql/es,代码生成逻辑回识别这些
section
并生成对应的对象,对象的具体使用方法根据选择的sdk有所不同,这里不详细描述。
在迁移go之前,我做过几个简单的项目,类似http网关、http服务、对接kafka的消费者,采用的yaml
配置,每次新增项目,就需要将旧项目的配置加载部分抄过来,需要根据配置的构建对应的struct
,需要copy yaml的依赖包到新项目,很乏味,而且费事。所以搞了配置代码生成功能。
下面是代码生成工具的结果,注意Config
中的*.Val
和*.Obj
代表的是上述的两种配置配置。
package app
type autoBase struct {
Port int64
AdminPort int64
}
type autoMysql struct {
Addr string
Dbname string
User string
Passwd string
Timeout int64
ReadTimeout int64
WriteTimeout int64
}
type Config struct {
BaseVal autoBase
MysqlVal autoMysql
MysqlObj *DpMysql
}
工具库
工具库主要包含对redis/mysql/es的封装,以上服务是app主要使用的几种存储,github上已经存在比较成熟的sdk,直接拿过来用即可,再次封装的目的有两个:
- 抽离出通用服务的初始化,提升app的开发效率,新app只需要简单的在
ini
配置中设置下,就可以在代码中使用对应通用服务的client对象。 - 允许app依赖同一种通用服务的多个endpoint,配置文件中使用类似
redis_xxx
这种section
,framework
会自动生成和配置对应的client对象,app中可以使用app.RedisXxx
这种方式直接使用。
依赖上述配置加载,识别出通用服务的section
并调用提前写好的特定通用服务初始化工具方法和对象,生成初始化client对象的语句,并在app启动时进行加载。
package app
import (
"time"
"github.com/gomodule/redigo/redis"
)
// 包装redis.Pool不让上层用户直接使用第三方库中的redis,
// 因为会造成用户code中也import上面的github路径,这样就
// 不能控制app的使用方式。
type DpRedisPool struct {
redis.Pool
}
// redis库内部存在Options,这里根据最佳实践,筛选出需要
// app关注的,二次封装,虽然屏蔽细节,但经过打磨后应该会
// 节省app开发时间。
type redisOptions struct {
addr string
maxIdle int64
idleTimeout time.Duration
// 下面3个选项,在初始化时时放在Dial方法中用的,这里app不需要关注
dialConnTimeout time.Duration
dialReadTimeout time.Duration
dialWriteTimeout time.Duration
}
var defaultRedisOptions = redisOptions{
maxIdle: 50,
idleTimeout: 240 * time.Second,
dialConnTimeout: 300 * time.Millisecond,
dialReadTimeout: 1 * time.Second,
dialWriteTimeout: 1 * time.Second,
}
type redisOptionsFunc func(*redisOptions)
func redisAddr(s string) redisOptionsFunc {
return func(o *redisOptions) {
o.addr = s
}
}
func redisMaxIdle(s int64) redisOptionsFunc {
return func(o *redisOptions) {
o.maxIdle = s
}
}
func redisIdleTimeout(s int64) redisOptionsFunc {
return func(o *redisOptions) {
o.idleTimeout = time.Duration(s) * time.Millisecond
}
}
func redisDialConnTimeout(s int64) redisOptionsFunc {
return func(o *redisOptions) {
o.dialConnTimeout = time.Duration(s) * time.Millisecond
}
}
func redisDialReadTimeout(s int64) redisOptionsFunc {
return func(o *redisOptions) {
o.dialReadTimeout = time.Duration(s) * time.Millisecond
}
}
func redisDialWriteTimeout(s int64) redisOptionsFunc {
return func(o *redisOptions) {
o.dialWriteTimeout = time.Duration(s) * time.Millisecond
}
}
func initRedis(opt ...redisOptionsFunc) (*DpRedisPool, error) {
opts := defaultRedisOptions
for _, o := range opt {
o(&opts)
}
f := func() (redis.Conn, error) {
return redis.Dial(
"tcp",
opts.addr,
redis.DialConnectTimeout(opts.dialConnTimeout),
redis.DialReadTimeout(opts.dialReadTimeout),
redis.DialWriteTimeout(opts.dialWriteTimeout))
}
return &DpRedisPool{redis.Pool{
MaxIdle: int(opts.maxIdle),
IdleTimeout: opts.idleTimeout,
Dial: f}}, nil
}
逻辑本身比较简单,选择的是redigo
这个库,主要是对这个库的一些配置项和初始化方法进行封装。不用app做重复的事情了。
interceptor
soa服务的核心使用grpc,该工具库也是围绕grpc打造,在请求预处理这块,之前以为需要修改protoc-gen-go,在生成代码时对Service类进行二次封装,幸好grpc支持拦截器,这样就需要那个写一些比较难维护的代码生成逻辑。
初版,我仅增加两个拦截器:
- 统计接口耗时和qps的统计interceptor
- 从请求Header中读取Log-Id,没有就生成Log-Id的interceptor
package log
import (
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func createLogId() string {
t := time.Now().UnixNano()
r := rand.Intn(10000)
return fmt.Sprintf("%d%d", t, r)
}
func UnaryServerInterceptor(authFunc AuthFunc) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
var (
newCtx context.Context
logId string
)
md, ok := metadata.FromIncomingContext(ctx)
if ok {
v, ok := md["Log-Id"]
if ok {
logId = v[0]
}
}
if logId == "" {
logId = createLogId()
}
newCtx = context.WithValue(ctx, "Log-Id", logId)
return handler(newCtx, req)
}
}
func StreamServerInterceptor(authFunc AuthFunc) grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
var (
newCtx context.Context
logId string
)
md, ok := metadata.FromIncomingContext(ctx)
if ok {
v, ok := md["Log-Id"]
if ok {
logId = v[0]
}
}
if logId == "" {
logId = createLogId()
}
newCtx = context.WithValue(stream.Context(), "Log-Id", logId)
wrapped := grpc_middleware.WrapServerStream(stream)
wrapped.WrappedContext = newCtx
return handler(srv, wrapped)
}
}
在main.go中启动服务时,将上述方法注册到Server中即可。
依赖封装
框架依赖的代码库,已经拉下来并放到vendor
文件夹中,不需要app自己管理这部分代码。
handler和main.go生成
如果只使用grpc,参考gprc,利用protoc生成x.pb.go后,需要app自己实现proto文件中声明的service,我改造了protoc-gen-go,在生成pb文件的同时,会根据proto中service的定义,生成handler文件,用来给app做参考。
main.go这个文件是逻辑固定,下面给出demo:
package main
import (
"fmt"
"net"
"os"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"helloworld/framework/app"
"helloworld/framework/util"
"helloworld/handler"
pb "helloworld/helloworld"
)
func main() {
app.LoadConfig()
ctx := util.NewContextWithLogID(context.Background())
port := fmt.Sprintf(":%d", app.ConfigVal.BaseVal.Port)
lis, err := net.Listen("tcp", port)
if err != nil {
util.Errorf(ctx, fmt.Sprintf("failed to listen: %v", err))
os.Exit(3)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &handler.GreeterHandler{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
util.Errorf(ctx, fmt.Sprintf("failed to serve: %v", err))
os.Exit(3)
}
}
我这边和grpc官方例子的不同是,增加了配置加载以及替换掉之前的log库。
build.sh
该文件是项目初始化的发起点,主要工作就是发起代码生成,为app构建开发环境。设计思路直接看代码
#!/bin/bash
# caution!!!!! 项目名称==proto文件名称,app自己定义
project_name="helloworld"
path=`pwd`
gopath=$GOPATH/src
deadpool_path=$gopath/deadpool
cd $gopath
echo "Updating deadpool..."
# deadpool
if [ ! -d $deadpool_path ]
then
# deadpool封装工具库和代码生成工具
git clone http://gitlab.sftcwl.com/logistics/deadpool.git $deadpool_path
else
cd $deadpool_path
git pull
fi
echo "Generating config obj and init func..."
# 生成配置对象,以及配置加载代码
cd $deadpool_path
go run main.go
cp -r $deadpool_path/framework $path/
cp -r $deadpool_path/vendor $path/
cp -r $deadpool_path/deploy $path/
cp -r $deadpool_path/conf $path/
cp -r $deadpool_path/.gitignore $path/
cd $path
# proto文件放到和项目同名的子目录中
proto_path=$project_name.proto
if [ ! -d $project_name ]
then
mkdir $project_name
mv $proto_path $project_name
else
proto_path=$project_name/$proto_path
fi
echo "Generating main.go and service handler..."
# 生成handler文件(grpc service)以及main.go
echo `protoc -I $project_name/ $proto_path --go_out=plugins=grpc:$project_name`
# 存放用户的handler
if [ ! -d handler ]
then
mkdir handler
fi
echo "Project init completed"
echo "You can run 'cp -r template/* handler/' and then try 'go run main.go'"