2024年Go最全结合商业项目深入理解Go知识点_goframe gcache,Golang面试题集锦

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

这篇文章更进一步,会结合电商前后台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,已把这个项目开源。

调用示例代码
func (s \*rotationService) UpdateMyPassword(r \*ghttp.Request, req \*UpdateMyPasswordReq) (res sql.Result, err error) {
   .
   .
   .
   //获得当前登录用户
   req.Id = gconv.Int(r.GetCtxVar(middleware.CtxAccountId))
   ctx := r.GetCtx()
   res, err = dao.AdminInfo.Ctx(ctx).WherePri(req.Id).Update(req)
   if err != nil {
      return nil, err
   }
   return
}

赋值示例代码

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

package middleware

import (
   "github.com/goflyfox/gtoken/gtoken"
   "github.com/gogf/gf/net/ghttp"
   "github.com/gogf/gf/util/gconv"
   "malu/library/response"
)

const (
   CtxAccountId      = "account\_id"       //token获取
   .
   .
   .
)

type TokenInfo struct {
   Id      int
    .
    .
    .
}

var GToken \*gtoken.GfToken

var MiddlewareGToken = tokenMiddleware{}

type tokenMiddleware struct{}

func (s \*tokenMiddleware) GetToken(r \*ghttp.Request) {
   var tokenInfo TokenInfo
   token := GToken.GetTokenData(r)
   err := gconv.Struct(token.GetString("data"), &tokenInfo)
   if err != nil {
      response.Auth(r)
      return
   }
   
   r.SetCtxVar(CtxAccountId, tokenInfo.Id)
    .
    .
    .
   r.Middleware.Next()
}

小技巧

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

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

接口缓存

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

读者提问

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

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

常规操作

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

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

链式操作

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

链式操作:取值
func (s \*rotationService) Detail(r \*ghttp.Request, req \*DetailReq) (res model.ArticleInfo, err error) {
   //查询时优先查询缓存
   cacheKey := ArticleDetailCacheKey + gconv.String(req.Id)
   err = dao.ArticleInfo.Ctx(r.GetCtx()).Cache(time.Hour, cacheKey).WherePri(req.Id).Scan(&res)
   if err != nil {
      return res, err
   }
   return
}

链式操作:更新值

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

func (s *rotationService) Update(r *ghttp.Request, req *UpdateArticleReq) (res sql.Result, err error) {
   ctx := r.GetCtx()
    .
    .
    .
   //更新缓存
   cacheKey := ArticleDetailCacheKey + gconv.String(req.Id)
   res, err = dao.ArticleInfo.Ctx(ctx).Cache(-1, cacheKey).WherePri(req.Id).Update(req)
   if err != nil {
      return nil, err
   }
   return
}

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

接口兼容处理

需求场景

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

取消收藏有2种情况:一种是根据收藏id
删除;另一种是根据收藏类型和文章id(或者商品id)删除

思考题

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

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

看下我们是如何实现的:

结构体

首先定义我们的请求结构体,允许通过收藏id删除;
或者根据类型和对象id删除(收藏类型:1商品 2文章)

type DeleteReq struct {
   Id       int `json:"id"`
   Type     int `json:"type"`
   ObjectId int `json:"object\_id"`
}

api层

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

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

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

func (\*collectionApi) Delete(r \*ghttp.Request) {
   var req \*DeleteReq
   if err := r.Parse(&req); err != nil {
      response.ParamErr(r, err)
   }
   
   if res, err := service.Delete(r.Context(), req); err != nil {
      response.Code(r, err)
   } else {
      response.SuccessWithData(r, res)
   }
}

service层

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

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

func (s *collectionService) Delete(ctx context.Context, req *DeleteReq) (res sql.Result, err error) {
   if req.Id != 0 {
      //根据收藏id删除
      res, err = dao.CollectionInfo.Ctx(ctx).WherePri(req.Id).Delete()
   } else {
      //根据类型和对象id删除
      res, err = dao.CollectionInfo.Ctx(ctx).
         Where(dao.CollectionInfo.Columns.Type, req.Type).
         Where(dao.CollectionInfo.Columns.ObjectId, req.ObjectId).
         Delete()
   }
   if err != nil {
      return nil, err
   }
   return
}

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

统计查询

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

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

"order\_total": [10, 0, 10, 20, 10, 0, 7],

我们如何实现呢?

service层

重点看这段查询语句

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()方法中除了可以指定查询字段,还可以使用查询函数,也可以指定别名:

func OrderTotal(ctx context.Context) (counts []int) {
   counts = []int{0, 0, 0, 0, 0, 0, 0}
   recent7Dates := shared.GetRecent7Date()
   TodayTotals := []TodayTotal{}
   //只取最近7天


![img](https://img-blog.csdnimg.cn/img_convert/8d7435b3c16a80c403f7117a248c951c.png)
![img](https://img-blog.csdnimg.cn/img_convert/cf552524e1658b2ed261dafb9c441fbd.png)
![img](https://img-blog.csdnimg.cn/img_convert/d15c77cf00eb0e6a48f43f03de58cfb0.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

:= shared.GetRecent7Date()
   TodayTotals := []TodayTotal{}
   //只取最近7天


[外链图片转存中...(img-C6j8UQh5-1715376448447)]
[外链图片转存中...(img-VTsGjyM8-1715376448447)]
[外链图片转存中...(img-TfsTF2x3-1715376448448)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

  • 20
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值