結合商業項目深入理解Go知識點

来源:https://www.cnblogs.com/wangzhongyang/archive/2022/12/29/17011962.html
-Advertisement-
Play Games

這篇文章更進一步,會結合電商前後臺API系統,把Go語言的知識點應用到商業項目中,讓大家結合實際的場景去理解,這樣應該對大家更有幫助! ...


這篇文章比較硬核,爆肝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,已把這個項目開源。

調用示例代碼

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天
   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)
   fmt.Printf("result:%v", TodayTotals)
   for i, date := range recent7Dates {
      for _, todayTotal := range TodayTotals {
         if date == todayTotal.Today {
            counts[i] = todayTotal.Total
         }
      }
   }
   if err != nil {
      return counts
   }
   return
}

工具類

受某位知乎大神的啟發,生成最近一周的日期我是這麼實現的:

從性能角度考慮可能不是最優寫法,但是理解成本肯定非常低:

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

事務處理

事務的應用場景很清晰:當我們提供的某個服務,需要操作多次DB,並且這些操作要具有原子性,要麼都成功,要麼都失敗。這種情況就需要事務處理。

事務處理的特點是:只要其中有一個環節失敗了,之前成功的DB操作也會回滾到之前的狀態。

事務處理實戰

比如我們創建訂單時就需要做事務處理,我們一個訂單可以添加多個商品,創建訂單時除了添加主訂單表,也會添加商品訂單表。

GoFrame的事務處理非常簡單:

  1. 只需要我們通過g.DB().Begin()開啟事務
  2. 在鏈式操作中通過.TX(tx)方法添加事務
  3. 在最後判斷是否有錯誤發生,有錯誤則通過Rollback()回滾事務,沒錯誤則通過Commit()方法提交事務。
func (s *orderService) Add(r *ghttp.Request, req *AddOrderReq) (res sql.Result, err error) {
   req.OrderInfo.UserId = gconv.Int(r.GetCtxVar(middleware.CtxAccountId))
   req.OrderInfo.Number = shared.GetOrderNum()

   tx, err := g.DB().Begin()
   if err != nil {
      return nil, errors.New("啟動事務失敗")
   }

   //defer方法最後執行 如果有報錯則回滾 如果沒有報錯,則提交事務
   defer func() {
      if err != nil {
         tx.Rollback()
      } else {
         tx.Commit()
      }
   }()

   //生成主訂單
   lastInsertId, err := dao.OrderInfo.Ctx(r.GetCtx()).TX(tx).InsertAndGetId(req.OrderInfo)
   if err != nil {
      return nil, err
   }
   //生成商品訂單
   for _, info := range req.OrderGoodsInfos {
      info.OrderId = gconv.Int(lastInsertId)
      _, err := dao.OrderGoodsInfo.Ctx(r.GetCtx()).TX(tx).Insert(info)
      if err != nil {
         return nil, err
      }
   }
   return
}

更多關於事務的知識點可以閱讀這篇文章:
# Go語言中比較優雅的寫法

靈活應用

需求

我們需要根據多個查詢條件去查詢商品,比如根據商品名稱和商品分類去查詢。

需求分析

我們來分析一下,客戶端會有幾種查詢場景?

  1. 根據商品名稱和商品分類2個條件查詢
  2. 只根據商品名稱查詢
  3. 只根據商品分類查詢
  4. 都沒有傳值,不命中查詢條件,返回全部商品

常規實現

做了需求分析之後,正常的思路就是寫if...else...判斷了:

whereCondition := gmap.New()
if req.Keyword != "" && req.CategoryId != 0 {
   whereCondition = g.Map{
      "name like": "%" + req.Keyword + "%",
      "level1_category_id =? OR level2_category_id =? OR level3_category_id =? ": g.Slice{req.CategoryId, req.CategoryId, req.CategoryId},
   }
} else if req.Keyword != "" {
   whereCondition = g.Map{
      "name like": "%" + req.Keyword + "%",
   }
} else if req.CategoryId != 0 {
   whereCondition = g.Map{
      "level1_category_id =? OR level2_category_id =? OR level3_category_id =? ": g.Slice{req.CategoryId, req.CategoryId, req.CategoryId},
   }
} else {
   whereCondition = g.Map{}
}

但是這種寫法太亂了,而且不容易擴展:如果我們再加一個查詢條件,不僅要新增一個else,就要改已經存在的if...else判斷,後面維護起來簡直是噩夢啊。

優化之後

在經過思考之後,使用map的set方法靈活賦值是個很好的選擇,優化後的代碼如下:

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

優化後的代碼異常清晰,如果我們再加新的查詢條件,只需要在代碼中再加一個if判斷就可以了。

我的感悟

我在學習map基礎用法的時候,並不能想到這種應用場景,這是很正常的。只有當真正開發商業項目,在具體需求的驅動之下,督促我們做優化,這時候會刺激我們回顧之前學到的知識點。結合實際需求幫助大家將之前學到的Go知識靈活運用,這是我開源這個項目的目的,也是我寫這篇文章的目的。

瞭解更多set知識

# GoFrame gset使用技巧總結 | 天然支持排序和有序遍歷、出棧、子集判斷、序列化、遍歷修改

好了,擴展的知識點就聊到這裡,下麵是對你學Go有幫助的學習資料,歡迎和我一起學習Go,實踐Go。

GitHub

本項目的GitHub地址,歡迎star、fork、復刻:

電商實戰項目V1版本

電商實戰項目V2版本

抱團取暖

公眾號:程式員升職加薪之旅

微信:wangzhongyang1993


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 資金核對的數據組裝-執行-應急鏈路,有著千萬級TPS併發量,同時由於資金業務特性,對系統可用性和準確性要求非常高;日常開發過程中會遇到各種各樣的高可用問題,也在不斷地嘗試做一些系統設計以及性能優化,在此期間總結了部分性能優化的經驗和方法,跟大家一起分享和交流。 ...
  • 家居網購項目實現010 以下皆為部分代碼,詳見 https://github.com/liyuelian/furniture_mall.git 24.bugFix-添加購物車按鈕動態處理 24.1需求分析/圖解 如某個家居的庫存量為0,前臺的“add to cart”按鈕顯示為“暫時缺貨” 後臺也要 ...
  • 1. C++ 98/03標準的for迴圈 在C++ 98/03標準中,如果要用 for 迴圈語句遍歷一個數組或者容器,只能套用如下結構: for(表達式 1; 表達式 2; 表達式 3){ //迴圈體 } 例如,下麵程式演示了用上述結構遍曆數組和容器的具體實現過程: #include <iostre ...
  • Python 是每個程式員都喜歡的語言,因為它易於編碼和易於閱讀的語法。但是,你知道 python 有一些很酷的技巧可以用來讓事情變得更簡單嗎?在今天的內容中,我將與你分享7 個你可能從未使用過的Python 技巧。 1、功能屬性 這種 hack 類似於類和對象概念。現在,可以聲明,也可以稍後在程式 ...
  • JZ67 把字元串轉換成整數(atoi) 題目 寫一個函數 StrToInt,實現把字元串轉換成整數這個功能。不能使用 atoi 或者其他類似的庫函數。傳入的字元串可能有以下部分組成: 1.若幹空格 2.(可選)一個符號字元('+' 或 '-') 3. 數字,字母,符號,空格組成的字元串表達式 4. ...
  • 1、阻塞 阻塞模式下,相關方法都會導致線程暫停 ServerSocketChannel.accept 會在沒有連接建立時讓線程暫停 SocketChannel.read 會在通道中沒有數據可讀時讓線程暫停 阻塞的表現其實就是線程暫停了,暫停期間不會占用 cpu,但線程相當於閑置 單線程下,阻塞方法之 ...
  • 用python爬取並分析《2021胡潤百富榜》的榜單數據! 1、python爬蟲講解(requests向介面請求)。 2、python數據分析講解(pandas數據分析及可視化畫圖)含:直方圖、柱形圖、餅圖、詞雲圖等。 ...
  • 安裝anaconda,進行數據標註 1.安裝前準備:下好安裝包和所需文件 https://www.aliyundrive.com/s/XyH2JQ5TjCz 提取碼: 3c2w 2.運行anaconda安裝包,解壓labelimg-master文件 3.把resources.py文件放到/label ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...