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

news2024/11/23 8:33:11

文章目录

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

简介

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

架构分析

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

接口管理

  • 目前广泛采用前后端分离的结构,通过json交互,所以文档变的很重要
  • 当后端没有开发好接口时,前端不得不使用mock系统测试,可能会和后端的文档出现不一致
  • yapi就可以解决这个问题,还可以定义测试集,一键测试
    1
    • 小结: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端口访问了
      2
    • 访问后点击使用文档,学一学;登录用户名是上面配置的,默认密码是ymfe.org
  • 使用
    • 推荐看使用文档(不过你也不愿意看是不)
    • 创建项目,这个就是咱们的项目哈,不是项目内分组;基本路径先不设置
    • 进入项目,可以添加分类(文件夹),管理接口
    • 预览:Mock地址就是给前端用的,带上规定的请求参数,看看返回的数据是否符合要求
      3
    • 运行:会提示安装Chrome插件;测后端的,会传递请求的参数;可以做环境配置
    • 编辑:设置请求携带的参数,这其实就是文档的核心;后续:前端按这里规定的给参数,返回给它想要的值(mock,随机值;让前端测试先通过);后端传入请求参数看能不能返回想要的值(给前端返回的啥,按道理这就应该返回相应的值;类似postman);
    • 以POST为例,在设置中打开mock严格模式和开启json5(因为json不能限定类型,会让前端难做)
      4
    • 高级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的属性有些必须要设置
      1
    • 创建后再修改表结构需要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
    }
    
  • 路由分组,将前缀相同的路由合并
    1
  • 路由参数
    // 严格匹配,以 / 为分割
    // 如果是"/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中的一致即可)生成的方法解析
    
    2

验证表单

  • 和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": "ss2@qq.com",
        "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请求,浏览器有跨域问题
      
      2
    • 这里header信息不正确,执行return,按道理不应该返回message!如果要不执行后续逻辑,只能context.Abort()
    • 为什么会这样呢?那就要看Use()这个核心方法了;本质上,是将所有的中间件加入队列
      3
    • 所以当我们Next()时也是转到下一个中间件,return并不能跳出这个队列!所以其他中间件的Next还是会走到原func的逻辑
      4
    • 所以只能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>Title</title>
    </head>
    <body>
    	<!--使用填充过来的变量,返回给client-->
        <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")
    }
    
    <!--default/index.tmpl-->
    {{define "myindex.html"}}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>{{.title}}</h1>
    </body>
    </html>
    {{end}}
    
    <!--goods/list.tmpl-->
    {{define "goods/list.html"}}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>商品</title>
    </head>
    <body>
        <h1>商品</h1>
    </body>
    </html>
    {{end}}
    
    <!--users/list.tmpl-->
    {{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">
    
    5

优雅退出

  • 优雅的停止或重启是很有必要的,该通知的通知到位,别突然没了,搞的很麻烦很尴尬~
    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框架介绍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/190620.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Oracle数据库安装配置和卸载

Oracle数据库是一种网络上的数据库, 它在网络上支持多用户, 支持服务器/客户机等部署(或配置) 服务器与客户机是软件概念, 它们与计算机硬件不存在一一对应的关系. 即: 同一台计算机既可以充当服务器又可以充当客户机, 或者, 一台计算机只充当服务器或只充当充当客户机. Orac…

JAVA 老掉牙的面试问题 线程交替打印问题,分析实操一下

前言 新的一年了&#xff0c;首先是祝大家新年新气象&#xff0c;工作顺利&#xff0c;生活美满 。 万物复苏的一年&#xff0c; 大家都蠢蠢欲动&#xff01; 金三银四快来了&#xff0c; 机会多不多&#xff1f; ‘’满大街机会‘’ 好了不啰嗦&#xff0c;最近有比较多的小伙…

开关电源-TL431与光耦组成的电压反馈电路-TL431工作过程分析

开关电源&#xff1a;TL431与光耦组成的电压反馈电路 #开关电源#开关电源最基本的要求是输入电压变化时&#xff0c;输出电压保持恒定&#xff0c;而与此相关的测试如电压调整率、负载调整率等也是衡量开关电源性能的重要指标&#xff0c;实现输出电压恒定的方式是反馈&#x…

【Catalyst 9000上EEM的部署方法和示例,你看明白了吗?】

新的一年 新的征程 新的课程开班 等你来学&#xff01; EEM&#xff08;Embedded Event Manager&#xff09;作为一个自动化的脚本部署在设备上&#xff0c;可以根据指定的trigger来自动完成提前布置的任务&#xff0c;如信息的收集或特定的操作。一个完整的EEM需要包含Na…

Python学习-----起步2(变量与转义符)

目录 前言 Python变量/常量使用规则 基本数据变量类型 整形&#xff08;int&#xff09; 浮点型&#xff08;float&#xff09; 字符串类型&#xff08;str&#xff09; 布尔类型&#xff08;bool&#xff09; 转义符 习题 前言 在我们学习C语言时&#xff0c;我们学了很…

对象之间的拷贝

这里写目录标题Lambda 的 builderidea 自动生成插件 GenerateAllSetterMapStructDozer在开发的时候经常会有业务代码之间有很多的 JavaBean 之间的相互转化&#xff0c;比如 PO/DTO/VO/QueryParam 之间的转换问题&#xff0c;本文总结一下各种转换方法Lambda 的 builder 使用构…

有幸得到Alibaba内部Gradle实战笔记,趁机狂补

虽然一直在接触 Gradle&#xff0c;但对 Gradle 始终都有一些陌生感&#xff0c;表现在日常的开发中就是不敢随便改 build.gradle 文件&#xff0c;一旦 sync 出错&#xff0c;只会复制错误找谷歌&#xff0c;可是解决方案也不一定能够完美解决自己的问题。还有就是不熟悉 Grad…

黑客为什么都有一台笔记本电脑?

用一位程序员的话俩说&#xff1a;虽然我不是黑客&#xff0c;但是我也喜欢弄一些编程&#xff0c;对我而言&#xff0c;电脑就是我的工具&#xff0c;我的系统里面肯定有许多我自己写的程序&#xff0c;在我做任何操作的时候&#xff0c;我肯定使用我熟悉的软件&#xff0c;这…

创建者模式—工厂模式

目录 1.工厂模式 1.1概述 1.2简单工厂模式 1.2.1结构 1.2.2实现 1.2.3优缺点 1.2.4静态工厂 1.3工厂方法模式 1.3.1概念 1.3.2结构 1.3.3实现 1.3.4优缺点 1.4抽象工厂模式 1.4.1概念 1.4.2结构 1.4.3实现 1.4.4优缺点 1.工厂模式 1.1概述 需求&#xff1a;设…

易基因|细菌全基因组甲基化纳米孔测序(ONT):技术推介

大家好&#xff0c;这是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。今天跟大家介绍一下易基因的新产品&#xff1a;细菌全基因组甲基化纳米孔测序&#xff08;ONT&#xff09;。表观修饰不需要改变DNA序列便能实现对性状的改变&#xff0c;表观修饰的改变与基…

什么是事务?什么是索引?什么是视图?

目录 一、事务 二、视图 1 、视图概念 2、为什么要使用视图 3 、性能问题 4 、定义视图 5、查看视图 6、删除视图 三、索引 1、引入索引的问题 2、索引是什么 3、索引为什么选择b树 一、事务 事务是什么&#xff1f; 事务是一个操作序列&#xff0c;这些操作要么都…

Python+Selenium+Unittest 之selenium2--元素定位1-简介

这篇先说下webdriver的几种元素定位方式&#xff0c;让大家有个大概的了解&#xff0c;UI自动化说白了就是使用代码代替人工去进行操作&#xff0c;在页面上&#xff0c;执行人员可以直接对看到的任何地方进行点击、拖动等操作&#xff0c;而代码的话需要识别到到底要点那个按钮…

生产制造业ERP管理系统对于制造企业的好处有哪些?

任何一家企业在管理当中都存在或多或少的问题&#xff0c;这些问题对企业的发展都形成了一定的阻碍。在生产制造业当中&#xff0c;由于每日的繁重的生产计划和大量的生产作业&#xff0c;使得企业管理存在一些问题&#xff0c;这些问题不利于生产的有序进行&#xff0c;从而阻…

图表控件LightningChart.NET 系列教程(八):用代码创建图表

LightningChart.NET SDK 是一款高性能数据可视化插件工具&#xff0c;由数据可视化软件组件和工具类组成&#xff0c;可支持基于 Windows 的用户界面框架&#xff08;Windows Presentation Foundation&#xff09;、Windows 通用应用平台&#xff08;Universal Windows Platfor…

【微服务】分布式事务Seata

分布式事务Seata1.分布式事务问题1.1.本地事务1.2.分布式事务2.理论基础2.1.CAP定理2.1.1.一致性2.1.2.可用性2.1.3.分区容错2.1.4.矛盾2.2.BASE理论2.3.解决分布式事务的思路3.初识Seata3.1.Seata的架构3.2.部署TC服务3.3.微服务集成Seata3.3.1.引入依赖3.3.2.配置TC地址3.3.3…

商用清洁机器人:科沃斯“破圈”、高仙机器人“纵深”

配图来自Canva可画 正所谓科技改变生活&#xff0c;机器人在人们日常生活中出现的频率正在逐步提高。同时&#xff0c;随着智能技术的不断迭代升级、用户需求的增多&#xff0c;机器人的应用场景逐步拓宽、功能形态也愈发多样化&#xff0c;比如配送机器人、医疗机器人、教育机…

Android 12.0 启动app时设置密码锁

1.前言 1.1概述 在12.0的系统产品开发中,对于限制某些app的启动的功能中,在项目中的需求是在点击app启动的时候,根据包名设置密码锁,当输入正确的密码的时候来启动这个app,否则 就不能启动这个app,达到限制使用app的目的,这就需要在app启动的时候,检测app的包名,然后在…

分布式架构的必问理论

基础理论&#xff1a; CAP理论&#xff1a; CAP理论是分布式系统设计中最基础、也是最为关键的理论&#xff0c;它指出&#xff0c;分布式数据存储不可能同时满足以下三个条件。 一致性&#xff08;Consistency&#xff09;&#xff1a;每次读取要么获得最近写入的数据&…

OAuth2 (二)

目录 创建项目结构 父工程 客户 认证服务器 资源拥有者 资源服务器 创建项目结构 演示代码下载&#xff1a; https://gitee.com/lisenaq/oauth2-example.git 演示客户发请求&#xff1a; http://localhost:8080/client/getCode 父工程 父工程有:子模块需要重新导入该…

CVE-2022-34916 Apache Flume 远程代码执行漏洞分析

项目介绍 Apache Flume 是一个分布式的&#xff0c;可靠的&#xff0c;并且可用于高效地收集&#xff0c;汇总和移动大量日志数据的软件。它具有基于流数据流的简单而灵活的体系结构。它具有可调的可靠性机制以及许多故障转移和恢复机制&#xff0c;并且具有健壮性和容错性。它…