Python+Go实践(电商架构一)

文章目录

  • 简介
    • 架构分析
  • 接口管理
  • peewee
    • CURD
  • Gin
    • 获取请求参数
    • protobuf
    • 验证表单
    • 中间件
    • 静态文件
    • 优雅退出
  • 小结

简介

  • 电商系统:后台管理+前端商城

架构分析

  • 传统的单体架构,以Django为例;之前写过flask开发微视频网站就是这样的架构
    Python+Go实践(电商架构一)_第1张图片
  • 痛点分析及演进
    • 单体不是只有一个机器,而是整个服务是一个整体,没有拆分
    • 代码管理繁琐,紧耦合(代码都整在一起,一个文件,像一坨屎),不同开发人员可能会冲突,测试成本高
      Python+Go实践(电商架构一)_第2张图片
    • 即使系统解耦,将不同的服务代码分开写,仍存在问题
      Python+Go实践(电商架构一)_第3张图片
    • 模块化复用代码(不同端口提供不同服务),减少代码重复;但仍然用相同的数据库,性能出现瓶颈,不太好
      Python+Go实践(电商架构一)_第4张图片
  • 微服务架构,独立性;有各自的数据库,随时上下线,但内部访问还有问题
    Python+Go实践(电商架构一)_第5张图片
  • 痛点分析及演进
    • 分层设计,使用rpc内部访问;上下两层都是微服务,大型系统可能成百上千个
      Python+Go实践(电商架构一)_第6张图片
    • 微服务终极架构,为保证正常服务,需要注册中心、服务发现、配置中心、链路追踪等
      Python+Go实践(电商架构一)_第7张图片
    • 当然,还需要网关组件,只靠NGINX搞不定
      Python+Go实践(电商架构一)_第8张图片
  • 接下来分别从接口管理、web框架、grpc等方面逐级实现

接口管理

  • 目前广泛采用前后端分离的结构,通过json交互,所以文档变的很重要
  • 当后端没有开发好接口时,前端不得不使用mock系统测试,可能会和后端的文档出现不一致
  • yapi就可以解决这个问题,还可以定义测试集,一键测试
    Python+Go实践(电商架构一)_第9张图片
    • 小结:yapi就是管理前后端API的工具;按照文档给前端调(返回数据),调后端(测试)
  • 使用docker部署
    • 参考GitHub的链接,关于docker的操作可以看我的笔记
      # 安装docker-compose
      pip install docker-compose
      sudo ln -s /usr/local/python37/bin/docker-compose /usr/bin/docker-compose
      
      # 会下载两个镜像
      # 直接创建好了容器的
      git clone https://github.com/Ryan-Miao/docker-yapi.git
      cd docker-yapi
      docker-compose up
      # yml文件里的`command: "node /my-yapi/vendors/server/app.js"`第一次启动先注释掉,配置好之后再打开
      
    • 部署,完成之后停止docker进程,修改command,docker-compose up,就可以在3000端口访问了
      Python+Go实践(电商架构一)_第10张图片
    • 访问后点击使用文档,学一学;登录用户名是上面配置的,默认密码是ymfe.org
  • 使用
    • 推荐看使用文档(不过你也不愿意看是不)
    • 创建项目,这个就是咱们的项目哈,不是项目内分组;基本路径先不设置
    • 进入项目,可以添加分类(文件夹),管理接口
    • 预览:Mock地址就是给前端用的,带上规定的请求参数,看看返回的数据是否符合要求
      Python+Go实践(电商架构一)_第11张图片
    • 运行:会提示安装Chrome插件;测后端的,会传递请求的参数;可以做环境配置
    • 编辑:设置请求携带的参数,这其实就是文档的核心;后续:前端按这里规定的给参数,返回给它想要的值(mock,随机值;让前端测试先通过);后端传入请求参数看能不能返回想要的值(给前端返回的啥,按道理这就应该返回相应的值;类似postman);
    • 以POST为例,在设置中打开mock严格模式和开启json5(因为json不能限定类型,会让前端难做)
      Python+Go实践(电商架构一)_第12张图片
    • 高级mock的功能可以看文档,它的优先级最高
  • 添加测试集合,选择接口
  • 数据管理,导入json数据、导出文档,很方便
  • 具体的流程等用到就清楚了~

peewee

  • 上层web使用go实现,底层srv使用python实现;开发是从底层向上哈
  • python通过peewee访问数据库,这里简单介绍一下
  • 安装:pip install peewee -i https://pypi.douban.com/simplepip install pymysql (node03)
  • 使用,官方文档
    • 本质就是借助ORM,需要用python代码定义一些field啥的
    • Meta设置表的元数据,比如表名(一般和class名一致)、数据库名
      from peewee import *
      import datetime
      
      # 数据库
      db = MySQLDatabase('peewee',host ='127.0.0.1',user='root',passwd='root');
      
      # 表1
      class User(Model):
          username = CharField(unique=True)	# 没设置主键,会自动增加id字段作为主键;可以用primary_key=True设置
      
          class Meta:
              database = db
      
      class Tweet(Model):
          user = ForeignKeyField(User, backref='tweets')	# 多端
          message = TextField()
          created_date = DateTimeField(default=datetime.datetime.now)
          is_published = BooleanField(default=True)
      
          class Meta:
              database = db
              
      if __name__=="__main__":
      	db.connect()
      	db.create_tables([User, Tweet])
      
    • field的属性有些必须要设置
      Python+Go实践(电商架构一)_第13张图片
    • 创建后再修改表结构需要migration,但是peewee没有Django那么完善,手动改即可;mysql基础操作可以看笔记

CURD

  • 添加和查询
    #1. 添加
    charlie = User.create(username='charlie')
    huey = User(username='huey')
    huey.save()	# 可以add,可以update
    
    Tweet.create(user=charlie, message='My first tweet')
    
    # 多条add
    data_source = [
        {'field1': 'val1-1', 'field2': 'val1-2'},
        {'field1': 'val2-1', 'field2': 'val2-2'},
        # ...
    ]
    for data_dict in data_source:
        Model.create(**data_dict)
    
    
    #2. 查询
    User.get(User.username == 'charlie')
    
    usernames = ['charlie', 'huey', 'mickey']
    users = User.select().where(User.username.in_(usernames))
    tweets = Tweet.select().where(Tweet.user.in_(users))
    
    # We could accomplish the same using a JOIN:
    tweets = (Tweet
              .select()
              .join(User)
              .where(User.username.in_(usernames)))
    
    # How many tweets were published today? 聚合
    tweets_today = (Tweet
                    .select()
                    .where(
                        (Tweet.created_date >= datetime.date.today()) &
                        (Tweet.is_published == True))
                    .count())
    
    # Paginate the user table and show me page 3 (users 41-60).
    User.select().order_by(User.username).paginate(3, 20)
    
    # Order users by the number of tweets they've created:
    tweet_ct = fn.Count(Tweet.id)
    users = (User
             .select(User, tweet_ct.alias('ct'))
             .join(Tweet, JOIN.LEFT_OUTER)
             .group_by(User)
             .order_by(tweet_ct.desc()))
    
  • 更新删除
    # 需要改变表的定义,加个age
    User.update(age=20).where(User.username=="charlie").execute()	# 返回 affected rows
    Counter.update(count=Counter.count + 1).where(Counter.url == request.url)
    
    # 删除
    user = User.get(User.id == 1)
    user.delete_instance()
    
    query = Tweet.delete().where(Tweet.creation_date < one_year_ago)	# 返回SQL语句(Model)
    query.execute() 
    
  • 更多功能
    class BaseModel(Model):
    	add_time = DateTimeField(default=datetime.datetime.now, verbose_name="添加时间")
        class Meta:
            database = db
            
    # 可以继承,帮助我们管理不同表重复的字段   
    class Person(BaseModel):
        name = CharField()
    
    # insert方法
    p_id = Person.insert({'name': 'bobby'}).execute() # 插入一条数据,返回主键
    # 和save不同,如果使用insert就需要指明所以字段值,Base中的add_time会失效
    # 建议使用insert_many,insert会发起多条网络请求
    data = [
        {'facid': 9, 'name': 'Spa', 'membercost': 20, 'guestcost': 30,'initialoutlay': 100000, 'monthlymaintenance': 800},
        {'facid': 10, 'name': 'Squash Court 2', 'membercost': 3.5,'guestcost': 17.5, 'initialoutlay': 5000, 'monthlymaintenance': 80}]
    query = Facility.insert_many(data) # 插入了多个
    
    class Person(Model):
        first = CharField()
        last = CharField()
    
        class Meta:
            primary_key = CompositeKey('first', 'last')	# 复合主键
    
    class Pet(Model):
        owner_first = CharField()
        owner_last = CharField()
        pet_name = CharField()
    
    	# 复合外键
        class Meta:
            constraints = [SQL('FOREIGN KEY(owner_first, owner_last) REFERENCES person(first, last)')]
    
  • 还有很多查询方法:模糊、条件、多表、聚合;再议吧,都是MySQL的基础操作

Gin

  • go语言web框架,类似python的flask;go语言还有一个对标Django的beego框架;学习的话建议互补,没必要重复学两个轻量/重量frame
  • 安装:go get -u github.com/gin-gonic/gin,如果使用go module,无需安装,自动同步
  • helloworld
    package main
    
    import "github.com/gin-gonic/gin"
    
    func main() {
    	// 实例化一个server,类似flask的Flask(__name__)
        r := gin.Default()
        r.GET("/ping", func(c *gin.Context) {
            c.JSON(200, gin.H{
                "message": "pong",
            })
        })
        r.Run() // listen and serve on 0.0.0.0:8080
    }
    
    // 改进如下
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func pong(c *gin.Context) {	// 依附这个struct,方便使用其方法
    	//c.JSON(200, gin.H{
    	//	"message": "pong",
    	//})
    	c.JSON(http.StatusOK, map[string]string{"message": "pong"})
    }
    
    func main() {
    	r := gin.Default()
    	r.GET("/ping", pong)
    	r.Run(":8083") // listen and serve on 0.0.0.0:8080
    }
    
    • 可以下载解压Chrome的扩展程序jsonview,方便查看
  • URL配置
    // 这些restful接口很有用
    func main() {
        // 使用默认中间件创建一个gin路由器
        // logger and recovery (crash-free) 中间件
        router := gin.Default()
    
        router.GET("/someGet", getting)
        router.POST("/somePost", posting)
        router.PUT("/somePut", putting)
        router.DELETE("/someDelete", deleting)
        router.PATCH("/somePatch", patching)
        router.HEAD("/someHead", head)
        router.OPTIONS("/someOptions", options)
    
        // 默认启动的是 8080端口,也可以自己定义启动端口
        router.Run()
        // router.Run(":3000") for a hard coded port
    }
    
  • 路由分组,将前缀相同的路由合并
    Python+Go实践(电商架构一)_第14张图片
  • 路由参数
    // 严格匹配,以 / 为分割
    // 如果是"/user/:name/:action/add"则最后必须写上add才能匹配
    r.GET("/user/:name/:action/", func(c *gin.Context) {
        name := c.Param("name")
        action := c.Param("action")
        c.String(http.StatusOK, "%s is %s", name, action)
    })
    
    // 参数匹配可以通过请求方法、路径名称、参数个数等区分开;看例子
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    	"testing"
    )
    
    func goodsList(c *gin.Context) {
    	c.JSON(http.StatusOK, map[string]string{"message": "list"})
    }
    func goodsDetail(c *gin.Context) {
    	name := c.Param("name")
    	action := c.Param("action")
    	c.String(http.StatusOK, "%s is %s", name, action)
    }
    func goodsCreate(c *gin.Context) {
    	c.JSON(http.StatusOK, map[string]string{"message": "add"})
    }
    
    func TestParams(t *testing.T) {
    	// 如果用gin.New(),不会开启日志logger和异常recovery中间件
    	router := gin.Default()
    	goodsGroup := router.Group("/goods")
    	{
    		goodsGroup.GET("/:name", goodsDetail)	// 这里并不会和list冲突哈,如果访问/goods/list优先匹配/list(全匹配)
    		goodsGroup.GET("/list", goodsList)
    		goodsGroup.POST("/list", goodsCreate) // 和GET方法区分
    	}
    	router.Run(":8083") // listen and serve on 0.0.0.0:8080
    }
    
    
    // 用的很少,会匹配后续所有内容
    r.GET("/user/:name/*action", func(c *gin.Context) {
      	name := c.Param("name")
        action := c.Param("action")
        c.String(http.StatusOK, "%s is %s", name, action)
    })
    // 随着框架的更新使用上可能不太一样,都需要在实践过程中调试/看源码,这些不属于智力内容,掌握方法即可
    
    // 那怎么限制参数的类型呢?
    
  • 匹配指定类型的路由参数
    type Person struct {
    	// 这里可以做很多丰富的配置
        ID int`uri:"id" binding:"required,uuid"`
        Name string `uri:"name" binding:"required"`
    }
    
    func main() {
        route := gin.Default()
        route.GET("/:name/:id", func(c *gin.Context) {
            var person Person
            if err := c.ShouldBindUri(&person); err != nil {
                c.JSON(400, gin.H{"msg": err})
                return
            }
            c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
        })
        route.Run(":8088")
    }
    

获取请求参数

  • 上面是路由参数(路径里的),这里将怎么获取get/post的参数
  • GET参数又叫查询字符串参数,使用Query方法
    func TestParams2(t *testing.T) {
    	router := gin.Default()
    
    	// 匹配的url格式:  /welcome?firstname=Roy&lastname=Kun
    	router.GET("/welcome", func(c *gin.Context) {
    		firstname := c.DefaultQuery("firstname", "Guest") // 如果不传递,就使用默认值
    		lastname := c.Query("lastname")                   // 是 c.Request.URL.Query().Get("lastname") 的简写
    
    		c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
    	})
    	router.Run(":8080")
    }
    
  • POST参数,使用requests发起请求;这个demo里:传递什么返回什么
    func TestParams3(t *testing.T) {
    	router := gin.Default()
    
    	router.POST("/form_post", func(c *gin.Context) {
    		message := c.PostForm("message")
    		nick := c.DefaultPostForm("nick", "anonymous") // 此方法可以设置默认值
    		// 获取什么返回什么
    		c.JSON(200, gin.H{
    			"status":  "posted",
    			"message": message,
    			"nick":    nick,
    		})
    	})
    	router.Run(":8080")
    }
    
    # 这个需要python发送post请求,浏览器只能发起get请求
    import requests
    
    rsp = requests.post("http://127.0.0.1:8083/form_post", data={
    	"message": "Hello",
    	"nick": "Roy"
    })
    print(rsp.text)
    
  • 混合获取,发起post请求
    func TestParams4(t *testing.T) {
    	router := gin.Default()
    
    	router.POST("/post", func(c *gin.Context) {
    		id := c.Query("id")
    		page := c.DefaultQuery("page", "0")
    		name := c.PostForm("name")
    		message := c.PostForm("message")
    
    		fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
    	})
    	router.Run(":8083")
    }
    

protobuf

  • 返回JSON不一定非要gin.H,可以使用struct,还能配合json-tag,改变key名称
    func TestParams5(t *testing.T) {
    	r := gin.Default()
    	r.GET("/someJSON", func(c *gin.Context) {
    		c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
    	})
    	r.GET("/moreJSON", func(c *gin.Context) {
    		// You also can use a struct,只要是键值对;很灵活
    		var msg struct {
    			Name    string `json:"user"`
    			Message string
    			Number  int
    		}
    		msg.Name = "Lena"
    		msg.Message = "hey"
    		msg.Number = 123
    		c.JSON(http.StatusOK, msg)
    	})
    	r.Run(":8080")
    	
    	// 返回值如下
    	{
    		user: "Lena",
    		Message: "hey",
    		Number: 123
    	}
    }
    
  • 通过protobuf传递请求参数和函数返回值,就是序列化数据的方式变了,gin也直接支持protobuf:c.ProtoBuf()
    // user.proto
    syntax = "proto3";
    option go_package = ".;proto";
    
    message Teacher {
        string name = 1;
        repeated string course = 2;
    }
    // 生成go文件
    
    package main
    
    import (
        "github.com/gin-gonic/gin"
        "net/http"
        "start/gin_t/proto"	// 导入proto生成的文件
    )
    
    func main() {
    	r := gin.Default()
    	r.GET("/someProtoBuf", func(c *gin.Context) {
    	    courses := []string{"python", "django", "go"}
    	    // The specific definition of protobuf is written in the testdata/protoexample file.
    	    data := &proto.Teacher{
    	        Name: "Roy",
    	        Course:  courses,
    	    }
    	    // Note that data becomes binary data in the response
    	    // Will output protoexample.Test protobuf serialized data
    	    c.ProtoBuf(http.StatusOK, data)	// 相当于c.JSON 返回数据,只不过是序列化的方式变了
    	    // Proto只管序列化,其他的交给gin传输
    	    // 不必太纠结细节,要有框架思维
    	})
    	r.Run(":8083")
    }
    
    // 使用python发起请求,请求过来的是二进制文件,还要用proto文件(和go中的一致即可)生成的方法解析
    
    Python+Go实践(电商架构一)_第15张图片

验证表单

  • 和flask类似,gin需要集成表单验证的框架,官方文档,具体怎么限制都可以参考这里
  • 用起来也比较简单,使用struct定义字段的验证规则,控制器函数用ShouldBind方法进行Form验证
    // 如果只支持json,就不需要指定xml、form
    // 主要是binding的值在做限制
    type Login struct {
        User     string `form:"user" json:"user" xml:"user"  binding:"required"`
        Password string `form:"password" json:"password" xml:"password" binding:"required"`
    }
    
    func main() {
        router := gin.Default()
        // Example for binding JSON ({"user": "manu", "pas": "123"})
        router.POST("/loginJSON", func(c *gin.Context) {
            var json Login
            if err := c.ShouldBindJSON(&json); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                return
            }
            
            if json.User != "manu" || json.Password != "123" {
                c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
                return
            } 
            
            c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
    
    		_ = router.Run(":8083")
        })
    }
    
    # pip install -i http://pypi.douban.com/simple/ requests --trusted-host pypi.douban.com
    import requests
    
    rsp = requests.post("http://127.0.0.1:8083/login", json={
        "user":"Roy",
        "pas":"123"
    })
    print(rsp.text)
    
  • 来个注册的验证
    // 用json起别名
    type SignUpParam struct {
    	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
    	Name       string `json:"name" binding:"required"`
    	Email      string `json:"email" binding:"required,email"`
    	Password   string `json:"password" binding:"required"`
    	RePassword string `json:"re_password" binding:"required,eqfield=Password"`
    	// 也可以放在前端ajax验证
    }
    
    func TestParams7(t *testing.T) {
    	router := gin.Default()
    	router.POST("/signup", func(c *gin.Context) {
    		var json SignUpParam
    		if err := c.ShouldBind(&json); err != nil {
    			c.JSON(http.StatusOK, gin.H{
    				"msg": err.Error(),
    			})
    		}
    		c.JSON(http.StatusOK, gin.H{"status": "you are signed up"})
    		// 保存入库等业务逻辑代码...
    	})
    	_ = router.Run(":8083")
    }
    
    rsp = requests.post("http://127.0.0.1:8083/signup", json={
        "age": 18,
        "name": "Roy",
        "email": "[email protected]",
        "pas": "123",
        "re_password": "123"
    })
    print(rsp.text)
    
  • 但是上面返回的信息都是英文,因为validator支持国际语言,可以配置成中文;这里还是借助第三方包实现gin的翻译interface,定制很方便
    import (
    	"fmt"
    	"github.com/gin-gonic/gin"
    	"github.com/gin-gonic/gin/binding"
    	"github.com/go-playground/locales/en"
    	"github.com/go-playground/locales/zh"
    	ut "github.com/go-playground/universal-translator"
    	"github.com/go-playground/validator/v10"
    	en_translation "github.com/go-playground/validator/v10/translations/en"
    	zh_translation "github.com/go-playground/validator/v10/translations/zh"
    	"net/http"
    	"testing"
    )
    var trans ut.Translator
    
    func InitTrans(locale string) (err error) {
    	// 修改gin框架中的validator引擎属性,实现定制
    	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    		zhT := zh.New() // 中文翻译器
    		enT := en.New()
    		uni := ut.New(enT, zhT, enT) // 第一个参数是备用的语言环境
    		trans, ok = uni.GetTranslator(locale)
    		if !ok {
    			return fmt.Errorf("%s", locale)
    		}
    		// 借助go-playground包,实现gin的翻译接口
    		switch locale {
    		case "en":
    			en_translation.RegisterDefaultTranslations(v, trans)
    		case "zh":
    			zh_translation.RegisterDefaultTranslations(v, trans)
    		default:
    			en_translation.RegisterDefaultTranslations(v, trans)
    		}
    		return
    	}
    	return err
    }
    
    func TestParams7(t *testing.T) {
    	// 中文
    	if err := InitTrans("zh"); err != nil {
    		fmt.Println("初始化翻译器错误")
    		return
    	}
    	router := gin.Default()
    	router.POST("/signup", func(c *gin.Context) {
    		var json SignUpParam
    		if err := c.ShouldBind(&json); err != nil {
    			errs, _ := err.(validator.ValidationErrors)
    			c.JSON(http.StatusBadRequest, gin.H{
    				"msg": errs.Translate(trans),
    			})
    			return
    		}
    		c.JSON(http.StatusOK, gin.H{"status": "you are signed up"})
    		// 保存入库等业务逻辑代码...
    	})
    	_ = router.Run(":8083")
    }
    // 返回:{"msg":{"SignUpParam.Email":"Email必须是一个有效的邮箱"}},还需要改一下json格式
    
  • 修改返回的json格式(json格式化),还是在InitTrans定义,这部分类似中间件,当使用了我们实现了接口的自定义的翻译器时,就会触发这里注册的一系列操作
    func TestParams7(t *testing.T) {
    	if err := InitTrans("zh"); err != nil {
    		fmt.Println("初始化翻译器错误")
    		return
    	}
    	router := gin.Default()
    	router.POST("/signup", func(c *gin.Context) {
    		var json SignUpParam
    		if err := c.ShouldBind(&json); err != nil {
    			errs, _ := err.(validator.ValidationErrors)	// 断言?
    			c.JSON(http.StatusBadRequest, gin.H{
    				"msg": remove(errs.Translate(trans)),
    			})	
    			return
    		}
    		c.JSON(http.StatusOK, gin.H{"status": "you are signed up"})
    		// 保存入库等业务逻辑代码...
    	})
    	_ = router.Run(":8083")
    }
    
    var trans ut.Translator
    
    func InitTrans(locale string) (err error) {
    	// 修改gin框架中的validator引擎属性,实现定制
    	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    		// 注册一个获取json的tag的方法,获取键的字符串,例如:"SignUpParam.age",但想要的是age,再定义各remove函数就好
    		v.RegisterTagNameFunc(func(field reflect.StructField) string {
    			name := strings.SplitN(field.Tag.Get("json"), ",", 2)[0]	// 测试出真知
    			if name == "-" {
    				return ""
    			}
    			return name
    		})
    		zhT := zh.New() // 中文翻译器
    		enT := en.New()
    		uni := ut.New(enT, zhT, enT) // 第一个参数是备用的语言环境
    		trans, ok = uni.GetTranslator(locale)
    		if !ok {
    			return fmt.Errorf("%s", locale)
    		}
    		switch locale {
    		case "en":
    			// 注册一个翻译的方法,改一下go默认的翻译规则
    			en_translation.RegisterDefaultTranslations(v, trans)
    		case "zh":
    			zh_translation.RegisterDefaultTranslations(v, trans)
    		default:
    			en_translation.RegisterDefaultTranslations(v, trans)
    		}
    		return
    	}
    	return err
    }
    
    func remove(fields map[string]string) map[string]string {
    	// 去掉部分前面的json字段
    	rsp := map[string]string{}
    	for field, err := range fields {
    		// 找到 . 的下一级,就是最后一级;这种只适合两级比如 Login.user
    		rsp[field[strings.Index(field, ".")+1:]] = err
    	}
    	return rsp
    }
    

中间件

  • 之所以说类似中间件,是因为这里还是通过调用InitTranserrs.Translate(trans)触发转换的,代码侵入性很强,gin中有很多实用的中间件;类似python装饰器
    // 中间件测试
    func TestMD1(t *testing.T) {
    	// 来个Engine
    	router := gin.New()
    	// 使用logger和recovery中间件,可以查看这个Use,传入...HandlerFunc即可
    	router.Use(gin.Logger(), gin.Recovery()) // 默认全局
    
    	// 如何限制某个路由或某组路由使用中间件呢?自定义个HandlerFunc
    	auth := router.Group("/goods")
    	// 主要是这个Use在触发func的逻辑,真正实现中间件的效果
    	auth.Use(Auth)
    }
    
    func Auth(ctx *gin.Context) { // type HandlerFunc func(*Context)
    
    }
    
  • 来一个标准的自定义中间件函数(HandlerFunc)
    // 一般都是这种形式,不直接返回HandlerFunc,而是调用封装的函数
    func MyMiddleware() gin.HandlerFunc {
    	return func(context *gin.Context) {
    		t := time.Now()
    		context.Set("newFeature", "Roy")	// 扩展的逻辑
    		context.Next() // 继续执行原本的逻辑,类似python装饰器中被装饰函数
    		end := time.Since(t)
    		fmt.Printf("耗时:%v\n", end)
    		status := context.Writer.Status()
    		fmt.Println("状态:", status)
    	}
    }
    func TestMD2(t *testing.T) {
    	// 来个Engine
    	router := gin.Default()
    	router.GET("/ping", func(c *gin.Context) {
    		c.JSON(http.StatusOK, gin.H{
    			"message": "pong",
    		})
    	})
    	router.Use(MyMiddleware())
    	router.Run(":8083")
    }
    
    1
  • 源码解析
    • 有个问题你发现了吗?我们以判断token验证登录为例
      func TokenReq() gin.HandlerFunc {
      	return func(context *gin.Context) {
      		var token string
      		for k, v := range context.Request.Header {
      			if k == "X-Token" { // 首字母必须大写
      				token = v[0] // type Header map[string][]string
      			}
      		}
      		if token != "roykun" {
      			context.JSON(http.StatusUnauthorized, gin.H{
      				"msg": "未登录",
      			})
      			return // 如果执行这里应该不会Next才对
      		}
      		context.Next()
      	}
      }
      
      func TestMD3(t *testing.T) {
      	// 来个Engine
      	router := gin.Default()
      	router.Use(TokenReq()) // 要写在路由定义的前面
      
      	router.GET("/ping", func(c *gin.Context) {
      		c.JSON(http.StatusOK, gin.H{
      			"message": "pong",
      		})
      	})
      
      	router.Run(":8083")
      }
      // 用python请求,浏览器有跨域问题
      
      Python+Go实践(电商架构一)_第16张图片
    • 这里header信息不正确,执行return,按道理不应该返回message!如果要不执行后续逻辑,只能context.Abort()
    • 为什么会这样呢?那就要看Use()这个核心方法了;本质上,是将所有的中间件加入队列
      Python+Go实践(电商架构一)_第17张图片
    • 所以当我们Next()时也是转到下一个中间件,return并不能跳出这个队列!所以其他中间件的Next还是会走到原func的逻辑
      Python+Go实践(电商架构一)_第18张图片
    • 所以只能Abort,可以看看源码的逻辑,会将index赋值为abortIndex

静态文件

  • 静态资源(图片、样式、html文件等)通过模板文件解决,官方文档
  • 其实就两个问题,(1) 拿到静态文件,(2) 将变量填充进去;最后返回给客户端
    func main() {
    	// 创建一个默认的路由引擎
    	r := gin.Default()
    	// 配置模板(拿到静态文件)
    	r.LoadHTMLFiles("templates/index.tmpl")
    
    	r.GET("/index", func(c *gin.Context) {
    		c.HTML(http.StatusOK, "index.tmpl", gin.H{
    			"title": "RoyKun", // 填充变量
    		})
    	})
    
    	r.Run(":8083")
    }
    
    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Titletitle>
    head>
    <body>
    	
        <h1>{{.title}}h1>
    body>
    html>
    
  • 但此时会报HTTP ERROR 500错误,模板文件找不到;这是因为我们的代码运行底层要编译
    // 测试EXE文件路径;使用IDE Run启动
    dir, _ := filepath.Abs(filepath.Dir(os.Args[0])) // 当前文件
    fmt.Println("curr:", dir)	// C:\Users\Windows10\AppData\Local\Temp\GoLand
    // 也就是说可执行文件会放在这个路径下,和我们main.go及templates根本不是一个地方,所以会出现文件找不到的异常
    
  • 解决办法就是,(1) 使用绝对路径,(2) 在terminal执行main.exe启动server
    • 推荐第二种方式,这也是服务上线后启动的方式:go build main.go
  • 当有多个静态文件时,使用LoadHTMLFiles有点力不从心,可以用LoadHTMLGlob,并使用{{define "name"}}(起别名)对应逻辑函数中的name,唯一定位模板文件
    func main() {
    	// 创建一个默认的路由引擎
    	r := gin.Default()
    	// 配置模板
    	r.LoadHTMLGlob("templates/**/*") // 代表二级目录下的所有文件
    
    	r.GET("/index", func(c *gin.Context) {
    		// 这个name和模板中的define字段一致即可,不一定非要按路径,但是这样可读性好!
    		c.HTML(http.StatusOK, "myindex.html", gin.H{
    			"title": "index",
    		})
    	})
    
    	r.GET("/goods", func(c *gin.Context) {
    		c.HTML(http.StatusOK, "goods/list.html", gin.H{
    			"title": "gets/goods",
    		})
    	})
    
    	r.GET("/users", func(c *gin.Context) {
    		c.HTML(http.StatusOK, "users/list.html", gin.H{
    			"title": "gets/users",
    		})
    	})
    
    	// 启动HTTP服务,默认在0.0.0.0:8080启动服务
    	r.Run(":8083")
    }
    
    
    {{define "myindex.html"}}
    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Titletitle>
    head>
    <body>
        <h1>{{.title}}h1>
    body>
    html>
    {{end}}
    
    
    {{define "goods/list.html"}}
    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>商品title>
    head>
    <body>
        <h1>商品h1>
    body>
    html>
    {{end}}
    
    
    {{define "users/list.html"}}
    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>用户title>
    head>
    <body>
        <h1>用户h1>
    body>
    html>
    {{end}}
    
  • 上面都是说模板文件,还有其他的静态文件,比如样式、图片、js等,怎么引入呢?
    // 配置静态文件夹路径 第一个参数是前缀(相当于api的作用),第二个是真实文件夹路径
    r.StaticFS("/static", http.Dir("./static"))
    // 还可以定义image、js等api
    
    // html中,带/static的,后面的部分会到./static下找
    <link rel="stylesheet" href="/static/demo.css">
    
    Python+Go实践(电商架构一)_第19张图片

优雅退出

  • 优雅的停止或重启是很有必要的,该通知的通知到位,别突然没了,搞的很麻烦很尴尬~
    func TestMD3(t *testing.T) {
    	// 来个Engine
    	router := gin.Default()
    
    
    	router.GET("/ping", func(c *gin.Context) {
    		c.JSON(http.StatusOK, gin.H{
    			"message": "pong",
    		})
    	})
    
    	go func() {
    		router.Run(":8083")
    	}()
    
    	quit := make(chan os.Signal) // 用来接收os信号的channel
    	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)	// ctrl-c kill
    	<-quit // 只要能取到协程来的消息
    
    	// 处理主协程逻辑
    	fmt.Println("关闭server中...")
    }
    
  • 这里就是把Run放到了协程处理,主协程通过chan监听消息(ctrl-c,kill命令)

小结

  • 这部分主要是架构,前后端接口管理,python操作数据库,gin框架介绍

你可能感兴趣的:(Go,python,golang,架构)