首页 > 其他分享 >浅谈-api项目设计(上)

浅谈-api项目设计(上)

时间:2023-04-15 17:03:47浏览次数:50  
标签:return 浅谈 err ctx api func 设计 Logger string

从事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

相关文章

  • Vue3组合API自动引入插件
    插件名:unplugin-auto-importurl:https://github.com/antfu/unplugin-auto-import安装1、下载插件npmiunplugin-auto-import-D2、配置vite.config.tsimportvuefrom'@vitejs/plugin-vue'import{defineConfig}from'vite'//引入插件,因为我使用的vite+ts,所以这里引......
  • 虾皮API接口根据关键词取商品列表(商品详情,库存,排序,价格...)返回值及说明
    参数说明通用参数说明version:API版本key:调用key,测试key:test_api_keyapi_name:API类型[item_search,item_get]cache:[yes,no]默认yes,将调用缓存的数据,速度比较快result_type:[json,xml,serialize,var_export]返回数据格式,默认为jsonlang:[cn,en,ru]翻译语言,默认cn简体中......
  • 虾皮API接口根据关键词取商品列表(商品详情,库存,排序,价格...)返回值及说明
    参数说明通用参数说明version:API版本key:调用key,测试key:test_api_keyapi_name:API类型[item_search,item_get]cache:[yes,no]默认yes,将调用缓存的数据,速度比较快result_type:[json,xml,serialize,var_export]返回数据格式,默认为jsonlang:[cn,en,ru]翻译语言,默认cn简体中文API:i......
  • 标准的WebApi应该有哪些元素
    提问标准的WebApi应该有哪些元素回答声明完整的响应码200,404,401,400添加Operation添加Tag聚合业务申明请求和响应类型标注参数来源FromHeader使用IActionResult代替ActionResult[Tag("查询类服务")][HttpGet,Route("mytoute",Name=nameof(GetSomething)......
  • 第8章_索引的创建与设计原则
    1.索引的声明与使用1.1索引的分类MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。从功能逻辑上说,索引主要有4种,分别是普通索引、唯一索引、主键索引、全文索引。按照物理实现方式,索引可以分为2种:聚簇索引和非聚簇索引。按照作用......
  • 在Node.JS中,调用JShaman的Web API接口,加密JS代码。
    在Node.JS中,调用JShaman的WebAPI接口,加密JS代码。源码varjs_code=` functionNewObject(prefix) { varcount=0; this.SayHello=function(msg) { count++; alert(prefix+msg); } this.GetCount=function() { returncount; } } varobj=newNewO......
  • 手撕商城体系之支付系统设计与实现
    继续接前文手撕商城系统架构设计与实现支付系统是商城体系里面另一个关键核心系统,所有商城线上交易行为最终转化收入业绩重要支撑。支付最主要目标是保证系统稳定、高可靠,承载高并发支付结算场景。广大企业是没有支付牌照的,全国有支付牌照的公司就那么20几家,所以众多公司都是接入......
  • 如何在WPF中调用Windows 10/11 API(UWP/WinRT)
    最近在github上看到一个音乐播放器项目,dopamine(项目地址:https://github.com/digimezzo/dopamine-windows.git)在编译时,提示有一个库找不到  找了好一会,才发现这是调用了UWP的库。在最初Windows8出来时,这一套新的运行时叫WindowsRT,后面到Windows10时,改成了UWP。因为......
  • Apifox中更新token的两种方式(手动、自动)
    Apifox关于token的使用方式前言,关于token的使用,仅做了简单的demo测试token效果。1.手动登录获取token顾名思义,因为只有登录之后才有token的信息,所以在调用其他接口前需要拥有token才能访问。操作步骤1)添加全局变量、参数在右上角环境中配置详细信息:全局参数填写参数名以及默认......
  • 数据库设计类软件
    PDManer元数建模PDManer元数建模,是一款多操作系统开源免费的桌面版关系数据库模型建模工具,相对于PowerDesigner,他具备界面简洁美观,操作简单,上手容易等特点。支持Windows,Mac,Linux等操作系统,也能够支持国产操作系统,能够支持的数据库如下:MySQL,PostgreSQL,Oracle,SQLServer等常见......