①. 网站数百万的商品表、商品类别表,完成对应API的开发.
②. 三层架构模式解耦.
③. 目的:
a. 在有限的资源内完成尽可能的优化系统性能和科学节约资源消耗.
b. 尽可能的提高QPS.
c. 经常CPU资源100%,可能需要程序优化、系统优化、硬件扩容优化.
(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
}
①. 三层架构模式,并不是MVC,在go-kit中也使用了.
②. 考虑到可扩展性,整个业务代码写完后,框架都变化了.使用其它的框架.需要快速修改和切换.
a. 需要有一定的模式.
b. 有了一定的架构后,代码基本上不用变.只要改相关的接口部分.
c. gin web框架的代码,如果换成微服务框架grpc,使用proto文件生成对应的文件,移植起来比较方便.
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返回吗?是否还有其它方式,并不仅仅是输出字符串.
⑤. 以上都需要相关机制来控制的,并不是直接在函数中写死.
func RegisterHandler(怎么取参数, 业务最终函数, 怎么处理业务结果) func(context *gin.Context) {
return func(context *gin.Context) {
// 参数到底是url、form、json的?是否需要做验证?也不一定会有参数.
1. 参数 := 怎么取参数()
// 到底怎么取?取什么?
2. 业务结果 := 业务最终函数(context, 参数)
// 执行完成了返回什么?返回json只是一种方式,保存文件也是一种方式.
3. 怎么处理业务结果(业务结果)
}
}
注:
①. 3个形参都是函数.
②. 为了和业务进行解耦,这3个函数不能写死,是一个通用的.并不是某个产品、订单的函数.
④. 约定所有使用到对象都使用指针. => 为了统一规范.
⑤. 不过过渡依赖gin的封装的便捷方法. => 尽量与框架无关,可移值性比较高的方式.
App
└─ Services
├─ BookService.go 业务类(业务最终函数):
│ ①. 与架构无关,框架无关性代码.
│ ②. 不管是使用什么框架,使用grpc、go-mrico,这些代码是不变的.
│
├─ BookEndpoint.go Request和Response定义:
│ ①. 跟架构没有太大关系.
│ ②. 除了手工定义外,还可以通过proto文件自动生成(grpc).
│
└─ BookTransport.go 怎么取出参数和怎么处理响应:
①. 和架构有关系的.
(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