17 | 實現簡潔架構的 Biz 層

提示:

  • 所有體系課見專欄:Go 項目開發極速入門實戰課;
  • 歡迎加入 云原生 AI 實戰 星球,12+ 高質量體系課、20+ 高質量實戰項目助你在 AI 時代建立技術競爭力(聚焦于 Go、云原生、AI Infra);
  • 本節課最終源碼位于 fastgo 項目的 feature/s13 分支;
    更詳細的課程版本見:Go 項目開發中級實戰課:26 | 業務實現(3):實現 Biz 層代碼

Biz 層依賴 Store 層,所以實現了 Store 層代碼之后,便可以實現 Biz 層代碼。Biz 層代碼,主要用來實現系統中 REST 資源的各類業務操作,例如用戶資源的增刪改查等。

整個 fastgo 項目的設計較為規范,規范化的項目設計帶來的優點之一是開發方式的一致性和開發效率的提升。fastgo 項目 Biz 層的開發方式與 Store 層的開發方式保持一致。

API 接口定義

在開發 Biz 層代碼之前,需要先定義好 API 接口的請求入參和返回參數。為此,新建了 pkg/api/apiserver/v1/post.go 文件和 pkg/api/apiserver/v1/user.go 文件,分別保存了用戶接口和博客接口的請求入參和返回參數。

因為請求入參和返回參數(例如 CreateUserRequestCreateUserResponse)會提供給接口調用方(客戶端),所以需要將接口定義保存在 pkg/api 目錄下。另外,考慮到未來 fastgo 可能會加入多個服務,每個服務都有自己的 API 定義,fastgo 項目選擇了將每個服務的 API 定義保存在獨立的服務目錄下,例如 pkg/api/apiserver。

考慮到未來 API 接口的版本升級,fastgo 項目將接口進行了版本化處理,v1 版本的接口保存在 pkg/api/apiserver/v1 目錄下,v2 版本的接口保存在 pkg/api/apiserver/v2 目錄下。

IBiz 接口定義及實現

Biz 層代碼保存 internal/apiserver/biz/biz.go 文件中,接口名為 IBiz,定義如下:

// IBiz 定義了業務層需要實現的方法.
type IBiz interface {// 獲取用戶業務接口.UserV1() userv1.UserBiz// 獲取帖子業務接口.PostV1() postv1.PostBiz// 獲取帖子業務接口(V2版本).// PostV2() post.PostBiz
}

IBiz 接口包含了 User 資源和 Post 資源 v1 版本的接口,通過抽象工廠設計模式返回對應資源的接口。在 Go 項目開發中,業務層代碼的代碼量通常最大、變動最頻繁,并且隨著項目的迭代,可能會出現不兼容的變更。

這時需要對外暴露 v2 版本的 API 接口。因此,為了提高代碼的可維護性并保留未來的擴展能力,Biz 層代碼的存放結構如下:

internal/apiserver/biz/
├── biz.go
├── v1/ # v1 版本代碼實現
│   ├── post/ # 提高代碼可維護性,不同資源的代碼實現分別存放在不同的目錄中
│   │   └── post.go
│   └── user/
│       └── user.go
└── v2/ # 保留擴展能力:v2 代碼保存目錄

上述代碼,將不同版本的代碼保存在不同的版本化目錄中,不同 REST 資源的業務邏輯實現保存在跟資源對應的目錄中。不同資源的業務邏輯代碼均在其對應的目錄中實現,可以在目錄級別隔離不同資源的代碼實現,有利于提高代碼的穩定性,并降低維護的復雜度。

Biz 層依賴于 Store 層的實現,所以在創建 IBiz 實例時,需要傳入 IStore 類型的實例,IBiz 實例由 NewBiz 函數創建:

// biz 是 IBiz 的一個具體實現.
type biz struct {store store.IStore
}// 確保 biz 實現了 IBiz 接口.
var _ IBiz = (*biz)(nil)// NewBiz 創建一個 IBiz 類型的實例.
func NewBiz(store store.IStore) *biz {return &biz{store: store}
}

IBiz 的實現跟 IStore 的實現是保持一致,其他代碼,本節不再詳解。

UserBiz 接口定義及實現

User 資源的 Biz 層代碼實現位于 internal/apiserver/biz/v1/user/user.go 文件中,其接口定義為 UserBiz,代碼如下:

// UserBiz 定義處理用戶請求所需的方法.
type UserBiz interface {Create(ctx context.Context, rq *apiv1.CreateUserRequest) (*apiv1.CreateUserResponse, error)Update(ctx context.Context, rq *apiv1.UpdateUserRequest) (*apiv1.UpdateUserResponse, error)Delete(ctx context.Context, rq *apiv1.DeleteUserRequest) (*apiv1.DeleteUserResponse, error)Get(ctx context.Context, rq *apiv1.GetUserRequest) (*apiv1.GetUserResponse, error)List(ctx context.Context, rq *apiv1.ListUserRequest) (*apiv1.ListUserResponse, error)UserExpansion
}// UserExpansion 定義用戶操作的擴展方法.
type UserExpansion interface {
}

UserBiz 接口中的方法,同樣也分為了兩大類:標準資源 CURD 接口和擴展接口,擴展接口中實現了用戶登錄、Token 刷新、密碼修改等方法。實現 UserBiz 接口的 Go 結構體是 *userBiz

創建用戶:Create 方法實現

userBiz 結構體的 Create 方法實現如下:

// Create 實現 UserBiz 接口中的 Create 方法.
func (b *userBiz) Create(ctx context.Context, rq *apiv1.CreateUserRequest) (*apiv1.CreateUserResponse, error) {var userM model.User_ = copier.Copy(&userM, rq)if err := b.store.User().Create(ctx, &userM); err != nil {return nil, err}return &apiv1.CreateUserResponse{UserID: userM.UserID}, nil
}

為了提高開發效率,減少不必要的代碼量,Create 方法使用了 [github.com/jinzhu/copier](https://github.com/jinzhu/copier)Copy 函數給目標結構體變量 userM 賦值。

Create 方法中,通過 b.store.User().Create(ctx, &userM) 方法調用,將數據保存在數據庫中。

在 Go 項目開發中,數據庫禁止保存明文密碼。用戶密碼在入庫前需要進行加密處理。為了加密明文密碼字符串,fastgo 引入了 github.com/onexstack/onexstack/pkg/auth 包,auth 包中的 Encrypt 函數可以用來加密一個明文密碼字符串。

因為在入庫前都需要對明文密碼進行加密,所以,很自然的想到可以通過實現 GORM 框架的 BeforeCreate 鉤子來實現。修改 internal/apiserver/model/hook.go 文件,添加以下代碼:

import (..."github.com/onexstack/onexstack/pkg/auth"
)// BeforeCreate 在創建數據庫記錄之前加密明文密碼.
func (m *User) BeforeCreate(tx *gorm.DB) error {// Encrypt the user password.var err errorm.Password, err = auth.Encrypt(m.Password)if err != nil {return err}return nil
}

更新用戶:Update 方法實現

userBiz 結構體的 Update 方法實現如下:

// Update 實現 UserBiz 接口中的 Update 方法.
func (b *userBiz) Update(ctx context.Context, rq *apiv1.UpdateUserRequest) (*apiv1.UpdateUserResponse, error) {userM, err := b.store.User().Get(ctx, where.T(ctx))if err != nil {return nil, err}if rq.Username != nil {userM.Username = *rq.Username}if rq.Email != nil {userM.Email = *rq.Email}if rq.Nickname != nil {userM.Nickname = *rq.Nickname}if rq.Phone != nil {userM.Phone = *rq.Phone}if err := b.store.User().Update(ctx, userM); err != nil {return nil, err}return &apiv1.UpdateUserResponse{}, nil
}

在更新用戶時,根據是否傳入待更新的字段來判斷是否更新該字段。這樣的設計方式,可以通過一個更新接口實現字段的選擇性更新。

查詢用戶列表:List 方法實現

userBiz 結構體的 List 方法實現如下述代碼所示:

// List 實現 UserBiz 接口中的 List 方法.
func (b *userBiz) List(ctx context.Context, rq *apiv1.ListUserRequest) (*apiv1.ListUserResponse, error) {whr := where.P(int(rq.Offset), int(rq.Limit))count, userList, err := b.store.User().List(ctx, whr)if err != nil {return nil, err}var m sync.Mapeg, ctx := errgroup.WithContext(ctx)// 設置最大并發數量為常量 MaxConcurrencyeg.SetLimit(known.MaxErrGroupConcurrency)// 使用 goroutine 提高接口性能for _, user := range userList {eg.Go(func() error {select {case <-ctx.Done():return nildefault:count, _, err := b.store.Post().List(ctx, where.T(ctx))if err != nil {return err}converted := conversion.UserodelToUserV1(user)converted.PostCount = countm.Store(user.ID, converted)return nil}})}if err := eg.Wait(); err != nil {slog.ErrorContext(ctx, "Failed to wait all function calls returned", "err", err)return nil, err}users := make([]*apiv1.User, 0, len(userList))for _, item := range userList {user, _ := m.Load(item.ID)users = append(users, user.(*apiv1.User))}slog.DebugContext(ctx, "Get users from backend storage", "count", len(users))return &apiv1.ListUserResponse{TotalCount: count, Users: users}, nil
}

List 方法會查詢所有的用戶列表,并統計用戶所屬的博客數,這種遍歷多個列表,并且針對列表中每個元素都有耗時處理邏輯的代碼,可能會導致 List 方法執行時間較久。為了提高 List 方法的性能,List 方法中使用了 errgroup 包,并發查詢每個用戶的博客數。

在上述代碼中 eg.SetLimit 方法調用的作用是限制程序中同時運行的 Go 協程數量,以避免過多協程并發任務導致的系統資源過載(如高 CPU 和內存占用或高 I/O 消耗)。通過 eg.Go 啟動的 goroutine 會按照 SetLimit 的限制規則執行。當已經有 MaxErrGroupConcurrency 個任務在運行時,新任務會阻塞,直到某個正在運行的任務完成。

因為會并發處理 userList 列表中的每個元素,所以需要一個并發安全的數據類型,保存處理后的數據。Go 標準庫提供了 sync.Map 數據類型,該類型是并發安全的,可以直接在 Go 協程中使用 sync.MapStore 方法添加 key-value 對。在上述代碼中,使用 m.Store 保存了 *apiv1.User 類型的數據。

Store 層返回的數據類型為 *model.UserM,需要轉換為 Biz 層使用的數據類型 *apiv1.User*model.UserM*apiv1.User 數據類型之間的相互轉換,在 Biz 層經常會發生,為了提高代碼的可維護性,將這類轉換實現統一保存在 internal/apiserver/pkg/conversion 目錄下的 conversion 包中。

Store 層返回的用戶列表是降序排列的,為了保證 List 返回的列表也是降序排列的,上述代碼的最后,使用以下代碼段重新排列了 sync.Map 類型變量 m 中保存的數據:

for _, item := range userList {user, _ := m.Load(item.ID)users = append(users, user.(*apiv1.User))
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/72462.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/72462.shtml
英文地址,請注明出處:http://en.pswp.cn/web/72462.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

idea更新git代碼報錯No Git Roots

idea更新git代碼報錯&#xff1a; No Git Roots None of configured Git roots are under Git. The configured directory must have ".git directory in it.但是本地項目里是存在.git文件的&#xff0c;就是突然間不能更新代碼了 然后嘗試重新拉新項目代碼提示: Git i…

Webpack 知識點整理

? 1. 對 webpack 的理解&#xff1f;解決了什么問題&#xff1f; Webpack 是前端工程化領域的核心工具&#xff0c;其核心定位是模塊打包器&#xff08;Module Bundler&#xff09;&#xff0c;通過將各類資源&#xff08;JS、CSS、圖片等&#xff09;視為模塊并進行智能整合…

[Hello-CTF]RCE-Labs超詳細WP-Level13Level14(PHP下的0/1構造RCE命令簡單的字數限制RCE)

Level 13 源碼分析 這題又回到了 PHP重點關注preg_match("/[A-Za-z0-9\"%*,-.\/:;>?[\]^|]/", $cmd)禁用了所有數字, 并且回到了 PHP, 沒辦法用上一關的方法進行繞過但是比起上一關, 給我們少繞過了 &, ~, _似乎有其他方法 解題分析 利用 $(()) 和 …

Qt 控件概述 QWdiget 1.1

目錄 qrc機制 qrc使用 1.在項目中創建一個 qrc 文件 2.將圖片導入到qrc文件中 windowOpacity&#xff1a; cursor 光標 cursor類型 自定義Cursor font tooltip focusPolicy styleSheet qrc機制 之前提到使用相對路徑的方法來存放資源&#xff0c;還有一種更好的方式…

【eNSP實戰】將路由器配置為DHCP服務器

拓圖 要求&#xff1a; 為 office100 和 office200 分別配置地址池 AR1接口配置 interface GigabitEthernet0/0/0ip address 192.168.100.1 255.255.255.0 # interface GigabitEthernet0/0/1ip address 192.168.200.1 255.255.255.0 AR1路由器上創建office100地址池 [AR1…

數據結構——順序表seqlist

前言&#xff1a;大家好&#x1f60d;&#xff0c;本文主要介紹了數據結構——順序表部分的內容 目錄 一、線性表的定義 二、線性表的基本操作 三.順序表 1.定義 2. 存儲結構 3. 特點 四 順序表操作 4.1初始化 4.2 插入 4.2.1頭插 4.2.2 尾插 4.2.3 按位置插 4.3 …

OSPF | LSDB 鏈路狀態數據庫 / SPF 算法 / 實驗

注&#xff1a;本文為 “OSPF | LSDB / SPF ” 相關文章合輯。 LSDB 和 SPF 算法 瀟湘浪子的蹋馬骨湯 發布 2019-02-15 23:58:46 1. 鏈路狀態數據庫 (LSDB) 鏈路狀態協議除了執行洪泛擴散鏈路狀態通告&#xff08;LSA&#xff09;以及發現鄰居等任務外&#xff0c;其第三個任…

前端---CSS(前端三劍客)

1.基本語法規范 選擇器 {?條/N條聲明} ? 選擇器決定針對誰修改 (找誰) ? 聲明決定修改啥. (?啥) ? 聲明的屬性是鍵值對. 使? ; 區分鍵值對, 使? : 區分鍵和值 比如&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta…

【C++】 —— 筆試刷題day_6

刷題day_6&#xff0c;繼續加油哇&#xff01; 今天這三道題全是高精度算法 一、大數加法 題目鏈接&#xff1a;大數加法 題目解析與解題思路 OK&#xff0c;這道題題目描述很簡單&#xff0c;就是給我們兩個字符串形式的數字&#xff0c;讓我們計算這兩個數字的和 看題目我…

todolist docker 小工具

參考鏈接 前排提示 沒有中文&#xff0c;可使用瀏覽器 翻譯 前提 安裝docker安裝docker-compose 下載倉庫 git clone https://github.com/JordanKnott/taskcafe進行安裝 cd taskcafe docker-compose -p taskcafe up -d服務啟動后會監聽在 3333 端口上&#xff0c;通過瀏覽器…

Unity--GPT-SoVITS接入、處理GPTAPI的SSE響應流

GPT-SoVITS GPT-SoVITS- v2&#xff08;v3也可以&#xff0c;兩者對模型文件具有兼容&#xff09; 點擊后 會進入新的游覽器網頁 ----- 看了一圈&#xff0c;發現主要問題集中在模型的訓練很需要CPU&#xff0c;也就是模型的制作上&#xff0c;問題很多&#xff0c;如果有現有…

《TypeScript 快速上手:類型、編譯與嚴格模式的簡明教程》

一、TypeScript介紹 在引入編程社區 20 多年后&#xff0c;JavaScript 現在已成為有史以來應用最廣泛的跨平臺語言之一。JavaScript 最初是一種用于向網頁添加微不足道的交互性的小型腳本語言&#xff0c;現已發展成為各種規模的前端和后端應 用程序的首選語言。雖然用 JavaSc…

ROS2 系統架構

1.操作系統層 ros2是基于Linux、Windows、macOS系統建立的&#xff0c;這一層為ros2提供了各種基礎的硬件驅動&#xff0c;比如網卡驅動&#xff0c;常用USB驅動和常用攝像頭驅動等。 2.DDS實現層 ros2的核心通信是采用第三方的通信組件來實現的&#xff0c;這個第三方就是數…

【HTML】二、列表、表格

文章目錄 1、列表1.1 無序列表1.2 有序列表1.3 定義列表 2、表格2.1 定義2.2 表格結構標簽2.3 合并單元格 1、列表 列表分為&#xff1a; 無序列表有序列表定義列表&#xff1a;一個標題下有多個小分類 1.1 無序列表 ul嵌套li&#xff0c;ul是無序列表&#xff0c;li是列表…

redis zset基本介紹以及底層實現

ZSet&#xff08;Sorted Set&#xff09;有序集合 介紹 Redis 中的有序集合(Sorted Set)是在集合(Set)的基礎上,為每個成員關聯了一個分數(score)。這個分數可以用來對集合中的成員進行排序。 有序集合保留了集合不能有重復成員的特性&#xff08;成員不能重復&#xff0c;分值…

政策助力,3C 數碼行業數字化起航

政策引領&#xff0c;數字經濟浪潮來襲 在當今時代&#xff0c;數字經濟已成為全球經濟發展的核心驅動力&#xff0c;引領著新一輪科技革命和產業變革的潮流。我國深刻洞察這一發展趨勢&#xff0c;大力推進數字化經濟發展戰略&#xff0c;為經濟的高質量發展注入了強大動力。 …

IntelliJ IDEA 快捷鍵系列:重命名快捷鍵詳解

目錄 引言一、默認重命名快捷鍵1. Windows 系統?2. Mac 系統? 二、操作步驟與技巧1. 精準選擇重命名范圍?2. 智能過濾無關內容? 三、總結 引言 在代碼重構中&#xff0c;?重命名變量、類、方法? 是最常用的操作之一。正確使用快捷鍵可以極大提升開發效率。本文針對 ?Ma…

文檔搜索引擎

首先獲取很多網頁(爬蟲->一個http客戶端,發送http請求獲取http響應結果(就是網站))(批量化的獲取很多的頁面) 再根據用戶輸入的查詢詞,在網頁中進行查找 用戶輸入查詢詞之后,如何讓查詢詞和當前這些網頁進行匹配 ->使用倒排索引 倒排索引 1.文檔: 每個待搜索的網頁(被爬…

開源工具利器:Mermaid助力知識圖譜可視化與分享

在現代 web 開發中&#xff0c;可視化工具對于展示流程、結構和數據關系至關重要。Mermaid 是一款強大的 JavaScript 工具&#xff0c;它使用基于 Markdown 的語法來呈現可定制的圖表、圖表和可視化。對于展示流程、結構和數據關系至關重要。通過簡單的文本描述&#xff0c;你可…

C# --- LINQ

C# --- LINQ 什么是LINQFluent Syntax 和 SQL-Like QueryLINQ Operations 什么是LINQ LINQ的全稱為Language Integrated Query, 為各種查詢(包括對象查詢&#xff0c;數據庫查詢&#xff0c;XML查詢) 提供了統一模型.LINQ源于SQL&#xff0c;但比SQL更加強大&#xff0c;更加靈…