从事api后端接口开发也有五六年时间了,都没有好好的整理下api项目架构模板以及如何从零开始设计。
抽空写个文章记录下,顺便检查下自己对这块的理解,如有不正确的地方,欢迎底下友好交流。
本文的目的是检查自己对架构设计的理解,思考架构设计的意义和常用的设计思想
按照软件工程流程。我们需要需求分析(之前的步骤忽略)、概要设计、详细设计、编码和测试。
本文主要谈概要设计这块。以最基础的博客网站为例,参考煎鱼大佬编写的go语言编程之旅。
go语言编程之旅源文地址:前言
一、项目结构目录设计
我使用的web框架是gin
go get http://github.com/gin-gonic/gin v1.9.0
使用上面命令下载并将gin添加到go.mod文件中
go_api_framework
├── configs
├── docs
├── global
├── internal
│ ├── dao
│ ├── middleware
│ ├── model
│ ├── routers
│ └── service
├── pkg
├── tmp
├── scripts
├── tests
└── third_party
- configs:配置文件。
- docs:文档集合。
- global:全局变量。
- internal:内部模块。
- dao:数据访问层(Database Access Object),所有与数据相关的操作都会在 dao 层进行,例如 MySQL、ElasticSearch 等。
- middleware:HTTP 中间件。
- model:模型层,用于存放 model 对象。
- routers:路由相关逻辑处理。
- service:项目核心业务逻辑。
- pkg:项目相关的模块包。
- tmp:项目生成的临时文件。
- scripts:各类构建,安装,分析等操作的脚本。
- tests:各种测试相关文件,包括性能测试,压力测试等等
- third_party:第三方的资源工具,例如 Swagger UI。
这里我将storage改名为tmp,临时文件我更习惯用tmp命名
下面简单说明下外层目录的创建逻辑
1、除了业务逻辑相关的代码,为了实现系统的高可扩展性,我们需要对系统进行部分配置,如启用或禁用某些组件。所以需要编写配置相关的代码。于是就有了configs目录,存放配置相关的文件,可以是json、yaml、xml等任何可识别的格式。并且可以根据不同的运行环境编写多个配置文件
2、为了配合前后端开发,我们需要定义接口,编写接口文档。于是就有了docs目录。用来存放编写的接口文档,推荐使用swagger自动生成。(因为很多开发都不喜欢写文档。。。)
3、我们编写业务的时候会用到一些全局变量,这些变量作用于整个系统。于是就有了global目录,存放全局变量文件。如日志logger、setting配置等等
4、编写业务逻辑,这些是整个系统的核心。于是有了internal目录,存放业务核心内容
5、编写系统功能过程中会使用到一些模块包,于是有了pkg目录(package)和一些第三方开源软件,于是有了third_party。以及会生成一些临时文件,于是有了tmp目录。
6、系统设计和编码完成后,需要对系统进行构建和部署,于是有了scripts目录。
7、部署构建完成之后,需要对系统进行测试,测试是否能够实现需求描述的功能,于是有了tests目录。
上述简单描述了创建外层目录
下面描述下internal内部目录的创建逻辑
可以从自上而下去理解,也可以自下而上去理解。我采用自下而上的方式来说明
在model中编写sql等数据库相关语句
在dao中对这些数据库语句进行封装
在service中编写业务逻辑调用dao中封装的数据库操作
在routers中对入参和出参进行校验以及日志打印等并调用svc处理业务逻辑。
编写过程中可能使用到一些中间件,创建middleware目录存放这些中间件
二、添加常用功能
1、添加配置
配置我使用viper来解析
go get http://github.com/spf13/viper v1.15.0
1.1、创建config.yaml文件来保存具体配置
在configs目录下添加配置文件,内容如下,可以根据需要修改
Server: 服务相关配置 RunMode: debug 运行模式 HttpPort: 8000 监听端口 ReadTimeout: 60 读超时时间,秒为单位 WriteTimeout: 60 写超时时间,秒为单位 App: 接口相关配置 DefaultPageSize: 10 默认每页大小 MaxPageSize: 100 最大的页数 LogSavePath: tmp/logs 日志存放路径 LogFileName: app 日志文件名称 LogFileExt: .log 日志文件后缀 Database: 数据库相关配置 DBType: mysql 数据库类型 Username: 数据库账号 Password: 数据库密码 Host: 数据库ip和端口号 DBName: 数据库名称 TablePrefix: blog_ 数据库表前缀 Charset: utf8 数据库编码 ParseTime: True 是否解析时间 MaxIdleConns: 10 最大的idle连接数 MaxOpenConns: 30 最大的数据库连接数
1.2、创建配置结构体实现解析
添加完配置,在pkg目录下新建setting目录,创建setting文件
该文件自定义一个结构体,嵌套了viper。实例化的时候添加配置路径、文件名和后缀。实例化viper解析配置文件。并实现ReadSection解析不同部分的配置
package setting
import (
"github.com/spf13/viper"
)
type Setting struct {
viper *viper.Viper
}
func NewSetting() (*Setting, error) {
vp := viper.New()
vp.SetConfigName("config")
vp.AddConfigPath("configs/")
vp.SetConfigType("yaml")
err := vp.ReadInConfig()
if err != nil {
return nil, err
}
return &Setting{vp}, nil
}
func (s *Setting) ReadSection(k string, v interface{}) error {
err := s.viper.UnmarshalKey(k, v)
if err != nil {
return err
}
return nil
}
我们的配置分为三部分,创建一个section文件来解析不同部分的配置
package setting
import "time"
/*
在section中定义要使用的配置结构体,通过setting进行解析。
*/
type ServerSetting struct {
HttpPort string
RunMode string
ReadTimeout time.Duration
WriteTimeout time.Duration
}
type AppSetting struct {
DefaultPageSize int
MaxPageSize int
LogSavePath string
LogFileName string
LogFileExt string
}
type DataBaseSetting struct {
ParseTime bool
MaxIdleConns int
MaxOpenConns int
DBType string
UserName string
Password string
Host string
DBName string
TablePrefix string
Charset string
}
1.3、实例化全局配置
在global目录下创建setting文件,实例化全局配置
var (
ServerSetting *setting.ServerSetting
AppSetting *setting.AppSetting
DatabaseSetting *setting.DataBaseSetting
)
1.4、项目启动时初始化配置
在main文件中创建初始化配置函数,在init中调用该函数,完成配置初始化
package main
func init() {
err := setupSettings()
if err != nil {
log.Fatalf("init.setupSetting err:%v", err)
}
}
func setupSettings() error {
s, err := setting.NewSetting()
if err != nil {
return err
}
err = s.ReadSection("Server", &global.ServerSetting)
if err != nil {
return err
}
err = s.ReadSection("App", &global.AppSetting)
if err != nil {
return err
}
err = s.ReadSection("Database", &global.DatabaseSetting)
if err != nil {
return err
}
global.ServerSetting.ReadTimeout *= time.Second
global.ServerSetting.WriteTimeout *= time.Second
return nil
}
这么做的原因是为了提高可扩展性,将不同部分的配置分开,当我们需要添加新的配置时,只需要在config.yaml文件新增配置,然后将section对应结构体上添加配置即可
2、添加日志
日志采用go自带的log,做了下封装
采用lumberjack实现日志滚动压缩
go get "http://gopkg.in/natefinch/lumberjack.v2"
2.1、在pkg目录下创建logger文件,内容如下
package logger
import (
"context"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"io"
"log"
"runtime"
"time"
)
type Level int8
type Fields map[string]interface{}
const (
LevelDebug Level = iota
LevelInfo
LevelWarn
LevelError
LevelFatal
LevelPanic
)
func (l Level) String() string {
switch l {
case LevelDebug:
return "debug"
case LevelInfo:
return "info"
case LevelWarn:
return "warn"
case LevelError:
return "error"
case LevelFatal:
return "fatal"
case LevelPanic:
return "panic"
}
return ""
}
type Logger struct {
newLogger *log.Logger
ctx context.Context
fields Fields
callers []string
}
func NewLogger(w io.Writer, prefix string, flag int) *Logger {
l := log.New(w, prefix, flag)
return &Logger{newLogger: l}
}
func (l *Logger) clone() *Logger {
nl := *l
return &nl
}
func (l *Logger) WithFields(f Fields) *Logger {
ll := l.clone()
if ll.fields == nil {
ll.fields = make(Fields)
}
for k, v := range f {
ll.fields[k] = v
}
return ll
}
func (l *Logger) WithContext(ctx context.Context) *Logger {
ll := l.clone()
ll.ctx = ctx
return ll
}
func (l *Logger) WithCaller(skip int) *Logger {
ll := l.clone()
pc, file, line, ok := runtime.Caller(skip)
if ok {
f := runtime.FuncForPC(pc)
ll.callers = []string{fmt.Sprintf("%s: %d %s", file, line, f.Name())}
}
return ll
}
func (l *Logger) WithCallersFrames() *Logger {
maxCallerDepth := 25
minCallerDepth := 1
callers := []string{}
pcs := make([]uintptr, maxCallerDepth)
depth := runtime.Callers(minCallerDepth, pcs)
frames := runtime.CallersFrames(pcs[:depth])
for frame, more := frames.Next(); more; frame, more = frames.Next() {
callers = append(callers, fmt.Sprintf("%s: %d %s", frame.File, frame.Line, frame.Function))
if !more {
break
}
}
ll := l.clone()
ll.callers = callers
return ll
}
func (l *Logger) WithTrace() *Logger {
ginCtx, ok := l.ctx.(*gin.Context)
if ok {
return l.WithFields(Fields{
"trace_id": ginCtx.MustGet("X-Trace-Id"),
"span_id": ginCtx.MustGet("X-Span-Id"),
})
}
return l
}
func (l *Logger) JSONFormat(level Level, message string) map[string]interface{} {
data := make(Fields, len(l.fields)+4)
data["level"] = level.String()
data["time"] = time.Now().Local().UnixNano()
data["message"] = message
data["callers"] = l.callers
if len(l.fields) > 0 {
for k, v := range l.fields {
if _, ok := data[k]; !ok {
data[k] = v
}
}
}
return data
}
func (l *Logger) Output(level Level, message string) {
body, _ := json.Marshal(l.JSONFormat(level, message))
content := string(body)
switch level {
case LevelDebug, LevelInfo, LevelWarn, LevelError:
l.newLogger.Print(content)
case LevelFatal:
l.newLogger.Fatal(content)
case LevelPanic:
l.newLogger.Panic(content)
}
}
func (l *Logger) log(level Level, ctx context.Context, v ...interface{}) {
l.WithContext(ctx).WithTrace().Output(level, fmt.Sprint(v...))
}
func (l *Logger) logf(level Level, ctx context.Context, format string, v ...interface{}) {
l.WithContext(ctx).WithTrace().Output(level, fmt.Sprintf(format, v...))
}
func (l *Logger) Debug(ctx context.Context, v ...interface{}) {
l.log(LevelDebug, ctx, v)
}
func (l *Logger) Debugf(ctx context.Context, format string, v ...interface{}) {
l.logf(LevelDebug, ctx, format, v)
}
func (l *Logger) Info(ctx context.Context, v ...interface{}) {
l.log(LevelInfo, ctx, v)
}
func (l *Logger) Infof(ctx context.Context, format string, v ...interface{}) {
l.logf(LevelInfo, ctx, format, v)
}
func (l *Logger) Warn(ctx context.Context, v ...interface{}) {
l.log(LevelWarn, ctx, v)
}
func (l *Logger) Warnf(ctx context.Context, format string, v ...interface{}) {
l.logf(LevelWarn, ctx, format, v)
}
func (l *Logger) Error(ctx context.Context, v ...interface{}) {
l.log(LevelError, ctx, v)
}
func (l *Logger) Errorf(ctx context.Context, format string, v ...interface{}) {
l.logf(LevelError, ctx, format, v)
}
func (l *Logger) Fatal(ctx context.Context, v ...interface{}) {
l.log(LevelFatal, ctx, v)
}
func (l *Logger) Fatalf(ctx context.Context, format string, v ...interface{}) {
l.logf(LevelFatal, ctx, format, v)
}
func (l *Logger) Panic(ctx context.Context, v ...interface{}) {
l.log(LevelPanic, ctx, v)
}
func (l *Logger) Panicf(ctx context.Context, format string, v ...interface{}) {
l.logf(LevelPanic, ctx, format, v)
}
自定义了各种错误等级以及一个logger结构体,嵌套了go自带的log,该结构体实现各种错误等级的打印。
并且实现了多个With函数,这些函数可以当作中间件看待。其实扩展性还是不够强,应该可以自由选择要使用的With函数,而不是直接在log和logf中写死。
2.2、新增全局logger变量
修改之间的global文件下的setting文件,新增logger
Logger *logger.Logger
2.3、项目启动时初始化配置
在main文件中i添加setupLogger函数
func setupLogger() error {
global.Logger = logger.NewLogger(&lumberjack.Logger{
Filename: global.AppSetting.LogSavePath + "/" + global.AppSetting.LogFileName + global.AppSetting.LogFileExt,
MaxSize: 600,
MaxAge: 10,
LocalTime: true,
}, "", log.LstdFlags).WithCaller(2)
return nil
}
并在init函数新增setupLogger函数,在启动时初始化Logger
err = setupLogger()
if err != nil {
log.Fatalf("init.setupLogger err: %v", err)
}
3、添加数据库
我采用mysql作为数据库,使用gorm做orm
go get "http://github.com/jinzhu/gorm"
3.1、封装数据库基础数据
在internal目录的model目录下创建model文件,内容如下
const (
STATE_OPEN = 1
STATE_CLOSE = 0
)
type Model struct {
Id uint32 `gorm:"primary_key" json:"id"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
CreatedOn uint32 `json:"created_on"`
ModifiedOn uint32 `json:"modified_on"`
DeletedOn uint32 `json:"deleted_on"`
IsDel uint8 `json:"is_del"`
}
func NewDBEngine(databaseSetting *setting.DataBaseSetting) (*gorm.DB, error) {
s := "%s:%s@tcp(%s)/%s?charset=%s&parseTime=%t&loc=Local"
db, err := gorm.Open(databaseSetting.DBType, fmt.Sprintf(s,
databaseSetting.UserName,
databaseSetting.Password,
databaseSetting.Host,
databaseSetting.DBName,
databaseSetting.Charset,
databaseSetting.ParseTime,
))
if err != nil {
return nil, err
}
if global.ServerSetting.RunMode == "debug" {
db.LogMode(true)
}
db.SingularTable(true)
db.Callback().Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback)
db.Callback().Update().Replace("gorm:update_time_stamp", updateTimeStampForUpdateCallback)
db.Callback().Delete().Replace("gorm:delete", deleteCallback)
db.DB().SetMaxIdleConns(databaseSetting.MaxIdleConns)
db.DB().SetMaxOpenConns(databaseSetting.MaxOpenConns)
return db, nil
}
func updateTimeStampForCreateCallback(scope *gorm.Scope) {
if !scope.HasError() {
nowTime := time.Now().Unix()
if createTimeField, ok := scope.FieldByName("CreatedOn"); ok {
if createTimeField.IsBlank {
_ = createTimeField.Set(nowTime)
}
}
if modifyTimeField, ok := scope.FieldByName("ModifiedOn"); ok {
if modifyTimeField.IsBlank {
_ = modifyTimeField.Set(nowTime)
}
}
}
}
func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
if _, ok := scope.Get("gorm:update_column"); !ok {
_ = scope.SetColumn("ModifiedOn", time.Now().Unix())
}
}
func deleteCallback(scope *gorm.Scope) {
if !scope.HasError() {
var extraOption string
if str, ok := scope.Get("gorm:delete_option"); ok {
extraOption = fmt.Sprint(str)
}
deletedOnField, hasDeletedOnField := scope.FieldByName("DeletedOn")
isDelField, hasIsDelField := scope.FieldByName("IsDel")
if !scope.Search.Unscoped && hasDeletedOnField && hasIsDelField {
now := time.Now().Unix()
scope.Raw(fmt.Sprintf(
"UPDATE %v SET %v=%v,%v=%v%v%v",
scope.QuotedTableName(),
scope.Quote(deletedOnField.DBName),
scope.AddToVars(now),
scope.Quote(isDelField.DBName),
scope.AddToVars(1),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
} else {
scope.Raw(fmt.Sprintf(
"DELETE FROM %v%v%v",
scope.QuotedTableName(),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
}
}
}
func addExtraSpaceIfExist(str string) string {
if str != "" {
return " " + str
}
return ""
}
创建了一个Model结构体,内部定义了数据库常用字段。
实现NewDBEngine实例化数据库引擎。
并实现了多个callback函数,在执行某些特定操作时新增或更新基础字段
3.3、初始化全局数据库引擎
在global目录下创建db文件,新增
var (
DBEngine *gorm.DB
)
3.4、项目启动时初始化数据库
在main文件下新增setupDBEngine函数
func setupDBEngine() error {
var err error
global.DBEngine, err = model.NewDBEngine(global.DatabaseSetting)
if err != nil {
return err
}
return nil
}
在init函数中调用
err = setupDBEngine()
if err != nil {
log.Fatalf("init.setupDBEngine err: %v", err)
}
后续内容下次再补充
标签:return,浅谈,err,ctx,api,func,设计,Logger,string From: https://www.cnblogs.com/lgh344902118/p/17321400.html