go从0到1项目实战体系三十五:三层架构所有代码

发布时间:2023年12月25日

1. 前言:

①. 网站数百万的商品表、商品类别表,完成对应API的开发.

②. 三层架构模式解耦.

③. 目的:
   a. 在有限的资源内完成尽可能的优化系统性能和科学节约资源消耗.
   b. 尽可能的提高QPS.
   c. 经常CPU资源100%,可能需要程序优化、系统优化、硬件扩容优化.

2. 初始化文件:

(1). config配置文件(BsyErp\Contract\Init\Config.go):

package Init
const (
	HTTP_METHOD_GET  = "GET"
	HTTP_METHOD_POST = "POST"
	SERVER_ADDRESS   = ":8080"
)

(2). DB初始化文件(BsyErp\Contract\Init\DBInit.go):

package Init
import (
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"log"
)
var db *gorm.DB
func init() {
	var err error
	db, err = gorm.Open("mysql",
		"root:@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=True&loc=Local")
	if err != nil {
		log.Fatal(err)
	}
	db.SingularTable(true)
	db.DB().SetMaxIdleConns(10)
	db.DB().SetMaxOpenConns(50)
}
func  GetDB() *gorm.DB {
	return db
}

3. 三层架构:

①. 三层架构模式,并不是MVC,在go-kit中也使用了.

②. 考虑到可扩展性,整个业务代码写完后,框架都变化了.使用其它的框架.需要快速修改和切换.
   a. 需要有一定的模式.
   b. 有了一定的架构后,代码基本上不用变.只要改相关的接口部分.
   c. gin web框架的代码,如果换成微服务框架grpc,使用proto文件生成对应的文件,移植起来比较方便.

3.1 业务解耦:

v1.Handle("GET", "/prods", func(context *gin.Context) {
    // result := []*Models.Books{}     // 另一种写法:如果加了*指针,下面就不用加&
    result := Models.BookList{}
    Init.GetDB().Limit(10).Order("book_id desc").Find(&result)
    context.JSON(http.StatusOK, result)
}):. Handle的第三个参数,这个函数最终是要返回一个func,只要有context *gin.Context这个参数就可以.
   => 关键是要返回一个func(context *gin.Context). 如果有参数,怎么获取?
   => 参数包括url形式的、form形式的、json形式的.. result := Models.BookList{}
   Init.GetDB().Limit(10).Order("book_id desc").Find(&result)
   => 这两句代码用面向对象来讲就是属于哪一个类的方法,属于哪个struct.. result获取出来了,一定是用json返回吗?是否还有其它方式,并不仅仅是输出字符串.. 以上都需要相关机制来控制的,并不是直接在函数中写死.

3.2 通用模型:

func RegisterHandler(怎么取参数, 业务最终函数, 怎么处理业务结果) func(context *gin.Context) {
  return func(context *gin.Context) {
    // 参数到底是url、form、json的?是否需要做验证?也不一定会有参数.
    1. 参数 := 怎么取参数()
    // 到底怎么取?取什么?
    2. 业务结果 := 业务最终函数(context, 参数)
    // 执行完成了返回什么?返回json只是一种方式,保存文件也是一种方式.
    3. 怎么处理业务结果(业务结果)
  }
}:. 3个形参都是函数.. 为了和业务进行解耦,3个函数不能写死,是一个通用的.并不是某个产品、订单的函数.. 约定所有使用到对象都使用指针.    => 为了统一规范.. 不过过渡依赖gin的封装的便捷方法. => 尽量与框架无关,可移值性比较高的方式.

3.3 文件夹结构:

App 
└─ Services
   ├─ BookService.go    业务类(业务最终函数):
   │                    ①. 与架构无关,框架无关性代码.
   │                    ②. 不管是使用什么框架,使用grpc、go-mrico,这些代码是不变的.
   │
   ├─ BookEndpoint.go   Request和Response定义:
   │                    ①. 跟架构没有太大关系.
   │                    ②. 除了手工定义外,还可以通过proto文件自动生成(grpc).
   │
   └─ BookTransport.go  怎么取出参数和怎么处理响应:
                        ①. 和架构有关系的.

4. 列表&详情:

(1). BsyErp\Contract\main.go:

package main
import (
	"Contract.bsybx.com/App/Lib"
	"Contract.bsybx.com/App/Services"
	"Contract.bsybx.com/Init"
	"github.com/gin-gonic/gin"
)
func main () {
	router := gin.Default()
	v1 := router.Group("/v1")
	{
        // 获取book列表
		v1.Handle(Init.HTTP_METHOD_GET, "/books", Lib.RegisterHandler(
			Services.CreateBookListRequest(),
			Services.BookListEndPoint(&Services.BookService{}),
			Services.CreateBookListResponse(),
        ))
        // 获取book详情
		v1.Handle(Init.HTTP_METHOD_GET, "/book/:id", Lib.RegisterHandler(
			Services.CreateBookDetailRequest(),
			Services.BookDetailEndPoint(&Services.BookService{}),
			Services.CreateBookDetailResponse(),
		))
	}
	router.Run(Init.SERVER_ADDRESS)
}:. 下面(5)中如果Response方法优化了,这里可以优化为:
   bookResponse := Services.CreateBookResponse(),再将两个Response方法用变量替换.. bookService := &Services.BookService{},可以优化为传bookService.

(2). main.go优化如下:

bookResponse := Services.CreateBookResponse()
bookServer := &Services.BookService{}
bookListHandler := Lib.RegisterHandler(
  Services.CreateBookListRequest(),
  Services.BookListEndPoint(bookServer),
  bookResponse,
)
bookDetailHandler := Lib.RegisterHandler(
  Services.CreateBookDetailRequest(),
  Services.BookDetailEndPoint(bookServer),
  bookResponse,
)
v1.Handle(Init.HTTP_METHOD_GET, "/books", bookListHandler)
v1.Handle(Init.HTTP_METHOD_GET, "/book/:id", bookDetailHandler)

(3). BsyErp\Contract\App\Services\BookEndpoint.go:

package Services
import (
	"Contract.bsybx.com/App/Lib"
	"Contract.bsybx.com/Models"
	"context"
)
// v1/books?page=1&size=1
type BookListRequest struct {
	Page int  `form:"page"`
	Size int `form:"size"`
}
type BookListResponse struct {
	Result *Models.BooksList `json:"ResultData"`
}
// v1/book/12
type BookDetailRequest struct {
	// max可以做预估判断
	BookID int `uri:"id" binding:"required,gt=0,max=1000000"`
}
type BookDetailResponse struct {
	Result *Models.Books `json:"ResultData"`
}
func BookListEndPoint(book *BookService)  Lib.Endpoint {
   return func(ctx context.Context, request interface{}) (response interface{}, err error) {
	   req := request.(*BookListRequest)
	   return &BookListResponse{Result:book.BookList(req)}, nil
   }
}
func BookDetailEndPoint(book *BookService) Lib.Endpoint {
	return func(ctx context.Context, request interface{}) (interface{}, error) {
		req := request.(*BookDetailRequest)
		return &BookDetailResponse{Result:book.BookDetail(req)}, nil
	}
}:. BookListResponse与BookDetailResponse都是返回Result,可以优化:
   type BookResponse struct {
     Result interface{} `json:"ResultData"`
   }. 单从上面两个EndPoint方法来看,是冗余的,有优化空间.但是加上缓存后,业务逻辑是不一样的.

(4). BsyErp\Contract\App\Services\BookService.go:

package Services
import (
	"Contract.bsybx.com/Init"
	"Contract.bsybx.com/Models"
)
type BookService struct {
}
// 这个req.Size怎么去取?跟这个struct没有任何关系.只要知道从req中取Size就行了.
// 但是这个Size是怎么产生的,不关心,解耦了.
// 不能在这里使用gin的context来获取当前的get请求
func(s *BookService) BookList(req *BookListRequest) (*Models.BooksList, error) {
	books := &Models.BooksList{}
	err := Init.GetDB().Limit(req.Page).Offset(req.Size).Order("book_id desc").Find(books).Error
	if err != nil {
		return nil, err
	}
	return books, nil
}

// 这里是一个struct对应的方法,这个是类的方法,在实际运行中RegisterHandler,识别不了类方法,需要再来一个函数,把它变成最终的业务函数.
// 这个过程就是把这个struct的方法(类的方法)调用的过程,用一个函数来做一层包装.只不过,是让最终执行函数和框架再次解耦.最终的目的是为了确保RegisterHandler和框架与业务无关.
// 到底怎么执行,是由业务来决定的,这个方法永远是恒定的.
func(s *BookService) BookDetail(req *BookDetailRequest) (*Models.Books, []*Models.BookMetas, error) {
	book := &Models.Books{}
	if Init.GetDB().Find(book, req.BookID).RowsAffected != 1 {
		return nil, nil, fmt.Errorf("no body")
	}
	Models.NewBookMeta("click", "1", book.BookID).Save()
	metas := []*Models.BookMetas{}
	Init.GetDB().Where("item_id=?", book.BookID).Find(&metas)
	return book, metas, nil
}

(5). BsyErp\Contract\App\Services\BookTransport.go:

package Services
import (
	"Contract.bsybx.com/App/Lib"
	"fmt"
	"github.com/gin-gonic/gin"
)
func CreateBookListRequest() Lib.EncodeRequestFunc {
	return func(context *gin.Context) (i interface{}, e error) {
		req := &BookListRequest{}
		err := context.BindQuery(req)   // 和框架有关系了,获取一个get参数,并且绑定到BookListRequest上,会去寻找这个BookListRequest struct中是否有size。如果有绑定,否则出错。
		fmt.Println(req)
		if err != nil{
			return nil, err
		}
		return req, nil
	}
}
func CreateBookListResponse() Lib.DecodeResponseFunc  {
	return func(context *gin.Context, i interface{}) error {
		res := i.(*BookListResponse)       // 取出BookListResponse,后再断言
		context.JSON(200, res)
		return nil
	}
}
func CreateBookDetailRequest() Lib.EncodeRequestFunc {
	return func(context *gin.Context) (interface{}, error) {
		req := &BookDetailRequest{}
		fmt.Println(req)
		err := context.BindUri(req)
		fmt.Println(req)
		if err != nil {
			return nil, err
		}
		return req, nil
	}
}
func CreateBookDetailResponse() Lib.DecodeResponseFunc {
	return func(context *gin.Context, i interface{}) error {
		res := i.(*BookDetailResponse)
		context.JSON(200, res)
		return nil
	}
}:. 两个Response都是差不多逻辑,可以优化为:
   func CreateBookResponse() Lib.DecodeResponseFunc {
     return func(context *gin.Context, res interface{}) error {
       context.JSON(200, res)
       return nil
     }
   }

(6). 请求访问:

①. 列表:
   GET http://localhost/v1/books?page=1&size=1

②. 详情:
   GET http://localhost/v1/book/200
文章来源:https://blog.csdn.net/m0_68635815/article/details/135198480
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。