Home

Awesome

Esim文档

    Esim专注解决业务问题:业务复杂度,如何测试,代码组织,扩展等问题。不会提供微服务整套技术体系,服务治理、服务注册、服务发现等都不是它专注解决的问题,这部分问题我们交给了Service Mesh。

    做为业务框架,Esim默认集成了gin和grpc两个技术框架,但吸收了业务和技术分离思想,所以为集成其他技术框架提供了接口,只要实现Transports接口,再把实例注册到App.trans就可以很轻松的集成其他技术框架。

type Transports interface {
	// start server
	Start()

	// graceful shutdown server
	GracefulShutDown()
}

func (app *App) RegisterTran(tran transports.Transports) {
	app.trans = append(app.trans, tran)
}

安装

环境 go 1.3 及以上

使用 module 包管理工具

go get github.com/jukylin/esim

cd github.com/jukylin/esim

go build -o esim ./tool && mv ./esim $GOPATH/bin

创建项目

esim new -s test

浏览器访问

启动服务

cd test

go run main.go

访问

http://localhost:8080

使用组件测试 [推荐]

cd  test/internal/transports/http/component-test/
go test

架构

    Esim的架构来源于《实现领域驱动设计》的六边形架构和阿里的COLA架构,这2个架构有一个共同点:业务与技术分离。这点很好的解决了由于微服务架构增加的网络通讯和事件导致开发效率越来越慢的问题。所以才决定由原来的三层架构转为四层架构。

此处输入图片的描述

分层

    Esim使用松散的分层架构,上层可以任意调用下层。使用松散的分层架构的原因,是因为我们在使用四层架构的过程中发现:一些简单的场景,app层的service只是增加了一层调用,并没有起到协调的作用,反而增加了一些不必要的工作,所以使用松散的架构,让controller直接调用domain的逻辑。

不能因为使用松散的架构,把原有app层的service职责都放到了controller里面!!!

此处输入图片的描述

各层职责

目录职责
controller负责显示信息和解析、校验请求,适配不同终端
application不包含业务规则,为下一层领域模型协调任务,分配工作
domain负责表达业务概念,业务状态信息和业务规则,是业务软件的核心
infrastructure为各层提供技术支持,持久化,领域事件等

编码规范

Jaeger
目录名小写/中横线
函数名驼峰
文件名下划线
变量驼峰
常量驼峰
包名当前目录名
请求地址*小写
请求参数小驼峰
返回参数小驼峰

v1 目录 + 文件名

目录定义文件接口
application/service应用层index_service.goIndexService
domain/service领域服务index_service.goIndexService
domain/entity实体index_entity.goIndexEntity
infra/event领域事件index_event.goPubIndexEventIndexEvent
infra/repo资源库index_repo.goDBIndexRepoIndexRepo
infra/dao数据访问对象index_dao.goIndexDao

v2 目录

目录定义文件接口
application/service应用层index.goIndexService
domain/service领域服务index.goIndexService
domain/entity实体index.goIndexEntity
infra/event领域事件index.goPubIndexEventIndexEvent
infra/repo资源库index.goDBIndexRepoIndexRepo
infra/dao数据访问对象index.goIndexDao

数据库设计规范小三样

`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`last_update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '删除标识',

特性

依赖注入

    Esim 使用wire实现编译时的依赖注入,它有以下优点:

    Esim将wire用于业务与基础设施之间。将基础设施的初始化从业务抽离出来,集中管理。

Esim使用wire示例

    基础设施的依赖和初始化都在 infra/infra.go 文件下。wire的使用主要分2步,以增加mysqlClient为例:

provide

before
type Infra struct {
	*container.Esim
}

var infraSet = wire.NewSet(
	wire.Struct(new(Infra), "*"),
	provideEsim,
)
after

type Infra struct {
	*container.Esim

	DB mysql.MysqlClient
}

var infraSet = wire.NewSet(
	wire.Struct(new(Infra), "*"),
	provideEsim,
	provideDb,
)

func provideDb(esim *container.Esim) mysql.MysqlClient {
    ......
	return mysqlClent
}

Inject

在当前目录下执行:wire命令,看到:

wire: projectPath/internal/infra: wrote projectPath/internal/infra/wire_gen.go

说明执行成功,就可以在项目中使用了。

infra.NewInfra().DB

依赖倒置

    依赖倒置和依赖注入一样,主要应用于业务与基础设施之间。主要的作用是让业务与技术实现分离。在实际的使用中我们把涉及io操作都放到了基础设施的资源库上。这样做的好处:

工具

前置条件:

  1. 在项目根目录下
  2. 配置环境变量
export ESIM_DB_HOST=127.0.0.1
export ESIM_DB_PORT=3306
export ESIM_DB_USER=root
export ESIM_DB_PASSWORD=123456

    由于DDD开发方式多了很多目录、文件,导致这部分工作变得很繁琐,所以db2entity 从mysql数据库的表开始,自动建立实体,生成简单的CRUD语句和资源库的接口与实现,并把生成的资源库注入到基础设施。一气呵成。

前置条件:

  1. 在模型目录下
  2. 开启 module, export GO111MODULE=on

    factory 命令可以自动对结构体进行初始化,内存对齐,生成临时对象池,reset和释放资源等操作,减少一些繁杂操作。

前置条件:

  1. 在接口目录下

    ifacer 命令根据接口的定义生成空实例

esim test

前置条件:

  1. 项目目录下
  2. 推荐使用gotests

    test 命令监听项目被修改文件,并在文件目录下执行go test 自动运行单元测试。当然为了减轻一些繁杂的工作,esim test还会执行wire, mockery等命令

配置

esim 默认为 dev 环境,esim主要由 dev 和 pro 环境

export ENV=pro

配置文件在项目的conf目录下

conf/conf.yaml

func provideConf(){
    options := config.ViperConfOptions{}

    file := []string{"conf/monitoring.yaml", "conf/conf.yaml"}
    conf := config.NewViperConfig(options.WithConfigType("yaml"),
    	options.WithConfFile(file))

    return conf
}
service_name := infra.NewInfra().Conf.GetString("appname")

日志

    日志会根据不同环境打印日志,开发和测试环境会把所有日志打印到终端,生产只会打印warn及以上的日志。     2套日志接口,一套没有上下文,一套有。使用上下文是为了把分布式环境下的日志通过tracer_id串起来。

func provideLogger(conf config.Config) log.Logger {
	var loggerOptions log.LoggerOptions

	logger := log.NewLogger(
		loggerOptions.WithDebug(conf.GetBool("debug")),
	)
	return logger}
infra.NewInfra().Logger.Infof("info %s", "test")

infra.NewInfra().Logger.Infoc(ctx, "info %s", "test")

HTTP

比官方接口多了(ctx)参数

func provideHttp(esim *container.Esim) *http.HttpClient {
	clientOptions := http.ClientOptions{}
	httpClent := http.NewHttpClient(
		clientOptions.WithTimeOut(esim.Conf.GetDuration("http_client_time_out")),
        clientOptions.WithProxy(
            func() interface {} {
                monitorProxyOptions := http.MonitorProxyOptions{}
                return http.NewMonitorProxy(
                    monitorProxyOptions.WithConf(esim.Conf),
                    monitorProxyOptions.WithLogger(esim.Logger))
            }),
	)

	return httpClent
}
resp, err := infra.NewInfra().Http.GetCtx(ctx, "http://www.baidu.com")
defer resp.Body.Close()

Mongodb

文档 https://github.com/mongodb/mongo-go-driver

func provideMongodb(esim *container.Esim) mongodb.MgoClient {
	options := mongodb.MgoClientOptions{}
	mongo := mongodb.NewMongo(
		mgoClientOptions.WithLogger(esim.logger),
		mgoClientOptions.WithConf(esim.conf),
		mgoClientOptions.WithMonitorEvent(
			func() MonitorEvent {
				monitorEventOptions := MonitorEventOptions{}
				return NewMonitorEvent(
					monitorEventOptions.WithConf(esim.conf),
					monitorEventOptions.WithLogger(esim.logger),
				)
			},
		)
	)

	return mongo
}

import "go.mongodb.org/mongo-driver/bson"

type Info struct{
	Title string
}


info := Info{}

coll := infra.NewInfra().Mgo.GetColl("database", "coll")
filter := bson.M{"phone": "123456"}
res := coll.FindOne(inf.Mgo.GetCtx(c.Request.Context()), filter).Decode(&info)

GRPC

文档 https://github.com/grpc/grpc-go

func provideGrpcClient(esim *container.Esim) *grpc.GrpcClient {

	clientOptional := grpc.ClientOptionals{}
	clientOptions := grpc.NewClientOptions(
		clientOptional.WithLogger(esim.Logger),
		clientOptional.WithConf(esim.Conf),
	)

	grpcClient := grpc.NewClient(clientOptions)

	return grpcClient
}
import (
    "pathto/protobuf/passport"
)

conn := infra.NewInfra().GrpcClient.DialContext(ctx, ":60080")
defer conn.Close()

client := passport.NewUserInfoClient(conn)

getUserByUserNameRequest := &passport.GetUserByUserNameRequest{}
getUserByUserNameRequest.Username = "123456"

replyData, err = client.GetUserByUserName(ctx, getUserByUserNameRequest)

Redis

文档 https://github.com/gomodule/redigo

func provideRedis(esim *container.Esim) *redis.RedisClient {
	redisClientOptions := redis.RedisClientOptions{}
	redisClent := redis.NewRedisClient(
		redisClientOptions.WithConf(esim.Conf),
		redisClientOptions.WithLogger(esim.Logger),
		redisClientOptions.WithProxy(
			func() interface{} {
				monitorProxyOptions := redis.MonitorProxyOptions{}
				return redis.NewMonitorProxy(
					monitorProxyOptions.WithConf(esim.Conf),
					monitorProxyOptions.WithLogger(esim.Logger),
					monitorProxyOptions.WithTracer(esim.Tracer),
				)
			},
		),
	)

	return redisClent
}

conn := infra.NewInfra().Redis.GetCtxRedisConn()
defer conn.Close()
key := "username:"+username
exists, err := redis.Bool(conn.Do(ctx, "exists", key))

Mysql

文档 https://gorm.io/docs/

func provideDb(esim *container.Esim) *mysql.MysqlClient {
    mysqlClientOptions := mysql.MysqlClientOptions{}
	mysqlClent := mysql.NewMysqlClient(
		mysqlClientOptions.WithConf(esim.Conf),
		mysqlClientOptions.WithLogger(esim.Logger),
		mysqlClientOptions.WithProxy(
			func() interface{} {
				monitorProxyOptions := mysql.MonitorProxyOptions{}
				return mysql.NewMonitorProxy(
					monitorProxyOptions.WithLogger(esim.Logger),
					monitorProxyOptions.WithConf(esim.Conf),
					monitorProxyOptions.WithTracer(esim.Tracer),
				)
			},
		),
	)

	return mysqlClent
}
var user model.User
infra.NewInfra().DB.GetDb(ctx, "db").Table("table").Where("username = ?", username).
	Select([]string{"id"}).First(&user)

Opentracing

     Esim使用Jaeger实现分布式追踪,默认为关闭状态。开启需要使用jaeger-client-go自带的环境变量