这篇文章比较硬核,爆肝5千字,把之前整理的知识点都串起来了。建议先收藏,慢慢看。

前言

上一篇文章 #【Go WEB进阶实战】开源的电商前后台API系统 很受大家欢迎,有好多小伙伴私信我问题:“gtoken真不错,能不能再多讲讲?”、“接口怎么设计Cache好?”、“代码规范讲一下吧”、“这个系统有没有前端页面?”,等等....

那我就再写一篇作为补充喽,小伙伴们还有什么问题欢迎在评论区留言。

之前整理过一篇可能是全网最用心的「Go学习建议和资料汇总」,也深受大家好评,大家可以先收藏慢慢学。

这篇文章更进一步,会结合电商前后台API系统,把Go语言的知识点应用到商业项目中,让大家结合实际的场景去理解,这样应该对大家更有帮助!

小提示:这篇文章的重点不是把各个知识点讲透,而是为了让大家理解各个知识点在商业项目中的应用。如果你的基础比较薄弱,每个知识点的最后也都附上了详解链接,方便大家去查漏补缺。

下面就开始和我进阶实战吧:

登录鉴权

我们在上一篇文章中有介绍,系统的登录鉴权是通过gtoken实现的,有的小伙伴没有搞清楚登录信息存储在哪里?我们是如何获得当前登录用户的信息?

首先gtoken的数据默认使用内存缓存gcache,这种缓存会随着服务的终止而销毁,当重启服务时,之前缓存的数据就丢失了;gtoken也支持使用redis,比如我们的项目中就是使用了gredis,将登录信息存储在redis中进行管理。

更多关于gtoken的知识点可以看这篇专题文章:# 通过阅读源码解决项目难题:GToken替换JWT实现SSO单点登录

如果你基础比较弱的话,我还录制了视频教程:# 【视频】登录鉴权的三种方式:token、jwt、session实战分享

下面聊聊如何获得登录用户信息的问题:

我们使用Go语言无论开发http项目还是rpc项目,上下文都是很重要的概念,用于共享变量和链路跟踪

我们通过Context上下文对象在一次请求中设置用户信息,共享变量,进而实现在后续链路中都能获得当前登录用户的信息:

Context上下文

以修改密码举例:

我们通过ghttp.Request的实例r,调用GetCtxVar() 方法。

比如:r.GetCtxVar(middleware.CtxAccountId),通过这种方式我们就可以获得登录用户信息了

小提示:为了行文清晰,让大家更直观的看到和知识点相关的代码,不重要的代码会用三个竖着的.省略。完整的代码可以fork文末的GitHub,已把这个项目开源。

调用示例代码

  1. func (s *rotationService) UpdateMyPassword(r *ghttp.Request, req *UpdateMyPasswordReq) (res sql.Result, err error) {
  2. .
  3. .
  4. .
  5. //获得当前登录用户
  6. req.Id = gconv.Int(r.GetCtxVar(middleware.CtxAccountId))
  7. ctx := r.GetCtx()
  8. res, err = dao.AdminInfo.Ctx(ctx).WherePri(req.Id).Update(req)
  9. if err != nil {
  10. return nil, err
  11. }
  12. return
  13. }

赋值示例代码

赋值的核心代码也很简单,就是通过 r.SetCtxVar(key, value) 方法,就能把变量赋值到context中了

  1. package middleware
  2. import (
  3. "github.com/goflyfox/gtoken/gtoken"
  4. "github.com/gogf/gf/net/ghttp"
  5. "github.com/gogf/gf/util/gconv"
  6. "malu/library/response"
  7. )
  8. const (
  9. CtxAccountId = "account_id" //token获取
  10. .
  11. .
  12. .
  13. )
  14. type TokenInfo struct {
  15. Id int
  16. .
  17. .
  18. .
  19. }
  20. var GToken *gtoken.GfToken
  21. var MiddlewareGToken = tokenMiddleware{}
  22. type tokenMiddleware struct{}
  23. func (s *tokenMiddleware) GetToken(r *ghttp.Request) {
  24. var tokenInfo TokenInfo
  25. token := GToken.GetTokenData(r)
  26. err := gconv.Struct(token.GetString("data"), &tokenInfo)
  27. if err != nil {
  28. response.Auth(r)
  29. return
  30. }
  31. r.SetCtxVar(CtxAccountId, tokenInfo.Id)
  32. .
  33. .
  34. .
  35. r.Middleware.Next()
  36. }

小技巧

  1. 在架构设计中,在哪个场景下设置Context是非常重要的:上下文的变量必须在请求一开始便注入到请求流程中,以便于其他方法调用,所以我们在中间件中来实现是比较优雅的选择
  2. 结合实际场景,我们设置到Context中的变量可以是指针类型,因为任何地方获取到这个指针,不仅可以获取到里面的数据,而且能够直接修改里面的数据
  3. 建议养成好习惯:在service层的方法中,第一个参数必传context.Context对象或者*ghttp.Request对象。这样有利于我们后续扩展,能够方便的通过context共享数据,而且还能进行链路追踪

更详细的介绍看这里:# GoFrame 如何优雅的共享变量 | Context的使用

接口缓存

关于接口缓存,有小伙伴提出这样的疑问?

当然要设计接口数据缓存了,而且在GoFrame中还有比较优雅的实践方式:链式操作设置缓存。

我们给查询接口添加缓存的思路是这样的:

常规操作

  1. 定义缓存key
  2. 根据缓存key查询是否有值
    • 有值返回缓存中的值,不查询DB
    • 无值,查询DB,写入缓存
  3. 返回数据
  1. func (s *rotationService) Detail(r *ghttp.Request, req *DetailReq) (res model.ArticleInfo, err error) {
  2. cacheKey := ArticleDetailCacheKey + gconv.String(req.Id)
  3. res := Cache::get(cacheKey)
  4. if(!res){
  5. err = dao.ArticleInfo.Ctx(r.GetCtx()).WherePri(req.Id).Scan(&res)
  6. if err != nil {
  7. return res, err
  8. }
  9. Cache::set(cacheKey,res,time.Hour)
  10. }
  11. return
  12. }

GoFrame为我们提供了非常优雅的链接操作:

链式操作

我们只需要在链式查询中使用Cache()方法,设置缓存时间和缓存key就可以了,GoFrame为我们实现了上述常规操作中的繁琐操作:

链式操作:取值

  1. func (s *rotationService) Detail(r *ghttp.Request, req *DetailReq) (res model.ArticleInfo, err error) {
  2. //查询时优先查询缓存
  3. cacheKey := ArticleDetailCacheKey + gconv.String(req.Id)
  4. err = dao.ArticleInfo.Ctx(r.GetCtx()).Cache(time.Hour, cacheKey).WherePri(req.Id).Scan(&res)
  5. if err != nil {
  6. return res, err
  7. }
  8. return
  9. }

链式操作:更新值

更新操作只需要将Cache()方法的第一个参数过期时间设置为负数,就会清空缓存

  1. func (s *rotationService) Update(r *ghttp.Request, req *UpdateArticleReq) (res sql.Result, err error) {
  2. ctx := r.GetCtx()
  3. .
  4. .
  5. .
  6. //更新缓存
  7. cacheKey := ArticleDetailCacheKey + gconv.String(req.Id)
  8. res, err = dao.ArticleInfo.Ctx(ctx).Cache(-1, cacheKey).WherePri(req.Id).Update(req)
  9. if err != nil {
  10. return nil, err
  11. }
  12. return
  13. }

除了这个典型的场景,我们项目的热门商品是通过LRU缓存淘汰策略实现的,小伙伴们可以看这篇详解一探究竟:# GoFrame gcache使用实践 | 缓存控制 淘汰策略

接口兼容处理

需求场景

我们电商系统的文章和商品都支持收藏和取消收藏

取消收藏有2种情况:一种是根据收藏id

删除;另一种是根据收藏类型和文章id(或者商品id)删除

思考题

我们根据上述的需求是设计两个接口分别实现呢?还是只设计一个接口兼容实现呢?

我倾向于只使用一种接口,兼容实现:这样不仅减少代码量,而且后期有逻辑调整时,只修改一处代码就可以了。

看下我们是如何实现的:

结构体

首先定义我们的请求结构体,允许通过收藏id删除;

或者根据类型和对象id删除(收藏类型:1商品 2文章)

  1. type DeleteReq struct {
  2. Id int `json:"id"`
  3. Type int `json:"type"`
  4. ObjectId int `json:"object_id"`
  5. }

api层

然后我们编写api层,这部分代码很简单,所有的api层代码都是这种规范

  1. 定义请求参数结构体
  2. 解析请求参数,做数据校验,有问题直接返回错误;正常则继续向下执行
  3. 调用service层对应的方法,传入上下文context和请求体
  4. 根据service层的返回结果决定是返回错误码,还是返回数据。

小技巧:所有的api层都是这样的思路,我们的逻辑处理一般写在service中

  1. func (*collectionApi) Delete(r *ghttp.Request) {
  2. var req *DeleteReq
  3. if err := r.Parse(&req); err != nil {
  4. response.ParamErr(r, err)
  5. }
  6. if res, err := service.Delete(r.Context(), req); err != nil {
  7. response.Code(r, err)
  8. } else {
  9. response.SuccessWithData(r, res)
  10. }
  11. }

service层

最后我们编写service层代码,实现取消收藏接口兼容的重点也在这里了

我们根据传入的id做判断,如果id不为0,根据收藏id删除;否则的话就根据传入的type类型区别是文章还是商品,根据ObjectId确定要删除对象的id。

  1. func (s *collectionService) Delete(ctx context.Context, req *DeleteReq) (res sql.Result, err error) {
  2. if req.Id != 0 {
  3. //根据收藏id删除
  4. res, err = dao.CollectionInfo.Ctx(ctx).WherePri(req.Id).Delete()
  5. } else {
  6. //根据类型和对象id删除
  7. res, err = dao.CollectionInfo.Ctx(ctx).
  8. Where(dao.CollectionInfo.Columns.Type, req.Type).
  9. Where(dao.CollectionInfo.Columns.ObjectId, req.ObjectId).
  10. Delete()
  11. }
  12. if err != nil {
  13. return nil, err
  14. }
  15. return
  16. }

小技巧:我们查询条件的字段都是通过这种方式取值的:dao.CollectionInfo.Columns.Type,而不会写死字符串type,原因是如果我们的字段有修改,前者这种写法可以一改全改;而后者写死字符串的方式很难找全要修改的地方,维护成本比较高。

统计查询

咱们想一个复杂点的场景,进阶实战一下GoFrame ORM的使用:

我们需要查询最近7天每天的订单量,如果当天没有订单就返回0。期望的数据结构是这样的:

  1. "order_total": [10, 0, 10, 20, 10, 0, 7],

我们如何实现呢?

service层

重点看这段查询语句

  1. err := dao.OrderInfo.Ctx(ctx).Where(dao.OrderInfo.Columns.CreatedAt+" >= ", shared.GetBefore7Date()).Fields("count(id) total,date_format(created_at, '%Y-%m-%d') today").Group("today").Scan(&TodayTotals)

在GoFrame中 where的第二个参数如果传数组,默认就是where in查询;

我们在Fields()方法中除了可以指定查询字段,还可以使用查询函数,也可以指定别名:

  1. func OrderTotal(ctx context.Context) (counts []int) {
  2. counts = []int{0, 0, 0, 0, 0, 0, 0}
  3. recent7Dates := shared.GetRecent7Date()
  4. TodayTotals := []TodayTotal{}
  5. //只取最近7天
  6. err := dao.OrderInfo.Ctx(ctx).Where(dao.OrderInfo.Columns.CreatedAt+" >= ", shared.GetBefore7Date()).Fields("count(*) total,date_format(created_at, '%Y-%m-%d') today").Group("today").Scan(&TodayTotals)
  7. fmt.Printf("result:%v", TodayTotals)
  8. for i, date := range recent7Dates {
  9. for _, todayTotal := range TodayTotals {
  10. if date == todayTotal.Today {
  11. counts[i] = todayTotal.Total
  12. }
  13. }
  14. }
  15. if err != nil {
  16. return counts
  17. }
  18. return
  19. }

工具类

受某位知乎大神的启发,生成最近一周的日期我是这么实现的:

从性能角度考虑可能不是最优写法,但是理解成本肯定非常低:

  1. //生成最近一周的日期
  2. func GetRecent7Date() (dates []string) {
  3. gt := gtime.New(time.Now())
  4. dates = []string{
  5. gt.Format("Y-m-d"), //今天
  6. gt.Add(-gtime.D * 1).Format("Y-m-d"), //1天前
  7. gt.Add(-gtime.D * 2).Format("Y-m-d"),
  8. gt.Add(-gtime.D * 3).Format("Y-m-d"),
  9. gt.Add(-gtime.D * 4).Format("Y-m-d"),
  10. gt.Add(-gtime.D * 5).Format("Y-m-d"),
  11. gt.Add(-gtime.D * 6).Format("Y-m-d"), //6天前
  12. }
  13. return
  14. }

事务处理

事务的应用场景很清晰:当我们提供的某个服务,需要操作多次DB,并且这些操作要具有原子性,要么都成功,要么都失败。这种情况就需要事务处理。

事务处理的特点是:只要其中有一个环节失败了,之前成功的DB操作也会回滚到之前的状态。

事务处理实战

比如我们创建订单时就需要做事务处理,我们一个订单可以添加多个商品,创建订单时除了添加主订单表,也会添加商品订单表。

GoFrame的事务处理非常简单:

  1. 只需要我们通过g.DB().Begin()开启事务
  2. 在链式操作中通过.TX(tx)方法添加事务
  3. 在最后判断是否有错误发生,有错误则通过Rollback()回滚事务,没错误则通过Commit()方法提交事务。
  1. func (s *orderService) Add(r *ghttp.Request, req *AddOrderReq) (res sql.Result, err error) {
  2. req.OrderInfo.UserId = gconv.Int(r.GetCtxVar(middleware.CtxAccountId))
  3. req.OrderInfo.Number = shared.GetOrderNum()
  4. tx, err := g.DB().Begin()
  5. if err != nil {
  6. return nil, errors.New("启动事务失败")
  7. }
  8. //defer方法最后执行 如果有报错则回滚 如果没有报错,则提交事务
  9. defer func() {
  10. if err != nil {
  11. tx.Rollback()
  12. } else {
  13. tx.Commit()
  14. }
  15. }()
  16. //生成主订单
  17. lastInsertId, err := dao.OrderInfo.Ctx(r.GetCtx()).TX(tx).InsertAndGetId(req.OrderInfo)
  18. if err != nil {
  19. return nil, err
  20. }
  21. //生成商品订单
  22. for _, info := range req.OrderGoodsInfos {
  23. info.OrderId = gconv.Int(lastInsertId)
  24. _, err := dao.OrderGoodsInfo.Ctx(r.GetCtx()).TX(tx).Insert(info)
  25. if err != nil {
  26. return nil, err
  27. }
  28. }
  29. return
  30. }

更多关于事务的知识点可以阅读这篇文章:

# Go语言中比较优雅的写法

灵活应用

需求

我们需要根据多个查询条件去查询商品,比如根据商品名称和商品分类去查询。

需求分析

我们来分析一下,客户端会有几种查询场景?

  1. 根据商品名称和商品分类2个条件查询
  2. 只根据商品名称查询
  3. 只根据商品分类查询
  4. 都没有传值,不命中查询条件,返回全部商品

常规实现

做了需求分析之后,正常的思路就是写if...else...判断了:

  1. whereCondition := gmap.New()
  2. if req.Keyword != "" && req.CategoryId != 0 {
  3. whereCondition = g.Map{
  4. "name like": "%" + req.Keyword + "%",
  5. "level1_category_id =? OR level2_category_id =? OR level3_category_id =? ": g.Slice{req.CategoryId, req.CategoryId, req.CategoryId},
  6. }
  7. } else if req.Keyword != "" {
  8. whereCondition = g.Map{
  9. "name like": "%" + req.Keyword + "%",
  10. }
  11. } else if req.CategoryId != 0 {
  12. whereCondition = g.Map{
  13. "level1_category_id =? OR level2_category_id =? OR level3_category_id =? ": g.Slice{req.CategoryId, req.CategoryId, req.CategoryId},
  14. }
  15. } else {
  16. whereCondition = g.Map{}
  17. }

但是这种写法太乱了,而且不容易扩展:如果我们再加一个查询条件,不仅要新增一个else,就要改已经存在的if...else判断,后面维护起来简直是噩梦啊。

优化之后

在经过思考之后,使用map的set方法灵活赋值是个很好的选择,优化后的代码如下:

  1. whereCondition := gmap.New()
  2. if req.Keyword != "" {
  3. whereCondition.Set(dao.GoodsInfo.Columns.Name+" like ", "%"+req.Keyword+"%")
  4. }
  5. if req.CategoryId != 0 {
  6. whereCondition.Set("level1_category_id =? OR level2_category_id =? OR level3_category_id =? ", g.Slice{req.CategoryId, req.CategoryId, req.CategoryId})
  7. }

优化后的代码异常清晰,如果我们再加新的查询条件,只需要在代码中再加一个if判断就可以了。

我的感悟

我在学习map基础用法的时候,并不能想到这种应用场景,这是很正常的。只有当真正开发商业项目,在具体需求的驱动之下,督促我们做优化,这时候会刺激我们回顾之前学到的知识点。结合实际需求帮助大家将之前学到的Go知识灵活运用,这是我开源这个项目的目的,也是我写这篇文章的目的。

了解更多set知识

# GoFrame gset使用技巧总结 | 天然支持排序和有序遍历、出栈、子集判断、序列化、遍历修改

好了,扩展的知识点就聊到这里,下面是对你学Go有帮助的学习资料,欢迎和我一起学习Go,实践Go。

GitHub

本项目的GitHub地址,欢迎star、fork、复刻:

电商实战项目V1版本

电商实战项目V2版本

抱团取暖

公众号:程序员升职加薪之旅

微信:wangzhongyang1993

结合商业项目深入理解Go知识点的更多相关文章

  1. atitit 商业项目常用模块技术知识点 v3 qc29

    atitit 商业项目常用模块技术知识点 v3 qc29 条码二维码barcodebarcode 条码二维码qrcodeqrcode 条码二维码dm码生成与识别 条码二维码pdf147码 条码二维码z ...

  2. 首个hybird商业项目踩坑总结

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  3. 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(4)

    chsakell分享了一个前端使用AngularJS,后端使用ASP.NET Web API的项目. 源码: https://github.com/chsakell/spa-webapi-angula ...

  4. 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(3)

    chsakell分享了一个前端使用AngularJS,后端使用ASP.NET Web API的项目. 源码: https://github.com/chsakell/spa-webapi-angula ...

  5. 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(2)

    chsakell分享了一个前端使用AngularJS,后端使用ASP.NET Web API的项目. 源码: https://github.com/chsakell/spa-webapi-angula ...

  6. 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(1)

    chsakell分享了一个前端使用AngularJS,后端使用ASP.NET Web API的项目. 源码: https://github.com/chsakell/spa-webapi-angula ...

  7. 手把手教Android商业项目-即时通讯-i美聊

    [课程概况] 手把手教你从无到有的完整实现一个Android商业项目,是目前整个市场上所没有的课程,废话不多说,请往下看. [项目概况] 项目名称:i美聊 所属领域:移动社交 即时通讯   代码行数: ...

  8. Java编译运行环境讨论(复古但能加深对Java项目的理解)

    Java编译运行环境讨论(复古但能加深对Java项目的理解) 如今我们大多数情况都会使用IDE来进行Java项目的开发,而一个如今众多优秀的IDE已经能够帮助我们自动的部署并调试运行我们的Java程序 ...

  9. BBS+Blog项目流程及补充知识点

    项目流程: 1. 产品需求 (1)基于用户认证组件和Ajax实现登陆验证(图片验证码) (2)基于forms组件和Ajax实现注册功能 (3)设计系统首页(文章列表渲染) (4)设计个人站点页面 (5 ...

  10. 浅谈对ionic项目的理解

    在思考怎么将客户端app连接到服务器的时候,就在想ionic项目的本质是什么,一开始因为ionic serve这一命令,我以为它自己就是个服务器,但是后来一细想又感觉不是这样,不然客户端又该怎么和服务 ...

随机推荐

  1. 洛谷P2880 [USACO07JAN] Balanced Lineup G(树状数组/线段树)

    维护区间最值的模板题. 1.树状数组 1 #include<bits/stdc++.h> 2 //树状数组做法 3 using namespace std; 4 const int N=5 ...

  2. 鸟哥linux私房菜习题总结

    第零章 1.请找出世界上跑得最快的超级计算机的相关信息? 系统名称:天河二号. 所在位置:中国广州中山大学超算中心. 使用的cpu型号与规格:采用的是Intel的Ivy Bridge处理器与Xeon ...

  3. MyBatis(入参的类型和日志记录)

    入参的类型是对象 1. 新增的参数是对象 2. 空值的处理,占位符 字段,jdbcType=VARCHAR          字符串 字段,jdbcType=DATE                  ...

  4. 【多线程那些事儿】如何使用C++写一个线程安全的单例模式?

    如何写一个线程安全的单例模式? 单例模式的简单实现 单例模式大概是流传最为广泛的设计模式之一了.一份简单的实现代码大概是下面这个样子的: class singleton { public: stati ...

  5. Docker | redis集群部署实战

    前面已经简单熟悉过redis的下载安装使用,今天接着部署redis集群(cluster),简单体会一下redis集群的高可用特性. 环境准备 Redis是C语言开发,安装Redis需要先将Redis的 ...

  6. 一、Go语言开篇介绍

    Go语言开篇介绍 Go语言 是Google公司 在2007开发一种静态强类型.编译型语言,并在 2009 年正式对外发布. Go语言以其近C的执行性能和近解析型语言的开发效率,以及近乎于完美的编译速度 ...

  7. vulnhub靶场之THALES: 1

    准备: 攻击机:虚拟机kali.本机win10. 靶机:THALES: 1,网段地址我这里设置的桥接,所以与本机电脑在同一网段,下载地址:https://download.vulnhub.com/th ...

  8. Terminal(oh-my-zsh) 美化

    如果你使用Mac进行开发,那么Terminal.app应该是你使用非常频繁的app了.初体验Terminal时你可能觉得单调乏味,阅读密密麻麻的内容也很费劲.但是如果你跟着我一起配置它,就会发现你平时 ...

  9. 使用kubeadm方式搭建K8S集群

    使用kubeadm方式搭建K8S集群 kubeadm是官方社区推出的一个用于快速部署kubernetes集群的工具. 这个工具能通过两条指令完成一个kubernetes集群的部署: # 创建一个 Ma ...

  10. Go语言核心36讲18

    你很棒,已经学完了关于Go语言数据类型的全部内容.我相信你不但已经知晓了怎样高效地使用Go语言内建的那些数据类型,还明白了怎样正确地创造自己的数据类型. 对于Go语言的编程知识,你确实已经知道了不少了 ...