GoFrame系列:4、項目結構及開發明細
文章目錄
- GoFrame系列:4、項目結構及開發明細
- 1. 前言
- 2. 項目結構
- 3. 分層設計
- 3.1 控制器
- 3.2 業務邏輯
- 3.3 資料通路
- 3.4 模型定義
- 3.5 模闆解析
- 4. 資料庫設計
- 5. 包名設計
- 5.1 包名約定
- 5.2 包名設計
- 6. 控制器實作
- 6.1 結構化限制
- 6.2 結構體轉換
- 6.3 資料校驗
- 6.4 資料傳參
- 6.4 實作代碼
- 7. 上下文變量
- 7.1 結構定義
- 7.2 邏輯封裝
- 7.3 上下文變量注入
- 7.4 上下文變量使用
- 8. 中間件使用
- 8.1 跨域處理
- 8.2 鑒權處理
- 8.3 上下文注入
- 9. 業務邏輯封裝
- 9.1 職責劃分
- 9.2 資料校驗
- 9.3 内部引用
- 9.4 實作代碼
- 10. 資料檔案建立
- 10.1 代碼生成
- 10.2 `dao`使用
- 10.3 `model`使用
- 11. Swagger生成
- 11.1 `swagger`編寫
- 11.2 `swagger`生成
- 11.3 `swagger`插件
- 11.4 `swagger`文檔檢視
- 12. 基礎類庫存放
- 13. 最後
1. 前言
上節我們已經安裝demo并測試搭建該架構并運作的demo,也發現該架構搭建web背景是比較友善的,接下來我們了解應用的目錄結構并了解demo是如何進行開發的,以此來看一下我們後續自己開發web如何在該架構中展開。
從業務開發上基本分為四大部分:api、dao、model、service
2. 項目結構
如果是
Package
源碼包項目,開發者可随意定義目錄結構。
如果是業務類型項目,
GoFrame
官方推薦的
Go
項目目錄結構如下:
/
├── app
│ ├── api
│ ├── dao
│ ├── model
│ └── service
├── boot
├── config
├── docker
├── document
├── i18n
├── library
├── packed
├── public
├── router
├── template
├── Dockerfile
├── go.mod
└── main.go
目錄/檔案名稱 | 說明 | 描述 |
| 業務邏輯層 | 所有的業務邏輯存放目錄。 |
- | 業務接口 | 接收/解析使用者輸入參數的入口/接口層。 |
- | 資料通路 | 資料庫的通路操作,僅包含最基礎的資料庫 方法 |
- | 結構模型 | 資料結構管理子產品,管理資料實體對象,以及輸入與輸出資料結構定義 |
- | 邏輯封裝 | 業務邏輯封裝層,實作特定的業務需求,可供不同的包調用。 |
| 初始化包 | 用于項目初始化參數設定,往往作為 中第一個被 的包。 |
| 配置管理 | 所有的配置檔案存放目錄。 |
| 鏡像檔案 | 鏡像相關依賴檔案,腳本檔案等等。 |
| 項目文檔 | Documentation項目文檔,如: 設計文檔、幫助文檔等等。 |
| I18N國際化 | I18N國際化配置檔案目錄。 |
| 公共庫包 | 公共的功能封裝包,往往不包含業務需求實作。 |
| 打包目錄 | 将資源檔案打包的 檔案存放在這裡, 包初始化時會自動調用。 |
| 靜态目錄 | 僅有該目錄下的檔案才能對外提供靜态服務通路。 |
| 路由注冊 | 用于路由統一的注冊管理。 |
| 模闆檔案 | 模闆檔案存放的目錄。 |
| 鏡像描述 | 雲原生時代用于編譯生成Docker鏡像的描述檔案。 |
| 依賴管理 | 使用 包管理的依賴描述檔案。 |
| 入口檔案 | 程式入口檔案。 |
在實踐中,小夥伴們可以根據實際情況增删目錄。
注意:如果需要提供靜态服務,那麼所有靜态檔案都需要存放到
public
目錄下,僅有該目錄下的靜态檔案才能被外部直接通路。不推薦将程式目前運作目錄加入到靜态服務中。
項目建立推薦使用
GF
工具鍊
gf init
指令,具體請參考 開發工具 章節。
3. 分層設計
GoFrame
官方推薦的代碼分層設計。
3.1 控制器
控制器負責接收并響應用戶端的輸入與輸出,包括對輸入參數的過濾、轉換、校驗,對輸出資料結構的維護,并調用
service
實作業務邏輯處理。
控制器代碼位于
/app/api
。
3.2 業務邏輯
業務邏輯是需要封裝的,特别是一些可複用的業務邏輯,并被控制器調用實作業務邏輯處理。
邏輯封裝的代碼位于
/app/service
。
3.3 資料通路
資料通路代碼層負責所有的資料集合(資料表)通路收口,将資料集合按照面向對象的方式進行封裝。
資料通路的代碼位于
/app/dao
。
3.4 模型定義
模型定義代碼層負責維護所有的資料結構定義,包括所有的輸入輸出資料結構定義。
模型定義代碼層中僅包含資料結構定義,不包含任何的方法定義。
模型定義的代碼位于
/app/model
。
3.5 模闆解析
模闆解析是可選的,在實踐中往往可以采用
MVVM
的模式,例如使用
vue
/
react
等架構實作模闆解析。如果使用經典的模闆解析,可以通過
GoFrame
架構強大的模闆引擎實作模闆解析。
模闆檔案的存放于
/template
。
4. 資料庫設計
我們建立一個簡單的使用者表來做示範。
https://github.com/gogf/gf-demos/blob/master/document/sql/create.sql
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '使用者ID',
`passport` varchar(45) NOT NULL COMMENT '使用者賬号',
`password` varchar(45) NOT NULL COMMENT '使用者密碼',
`nickname` varchar(45) NOT NULL COMMENT '使用者昵稱',
`create_at` datetime DEFAULT NULL COMMENT '建立時間',
`update_at` datetime DEFAULT NULL COMMENT '更新時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
為簡化示例項目的接口實作複雜度,這裡的
password
沒有做任何加密處理,明文存放密碼資料。
5. 包名設計
5.1 包名約定
根據官方《Effective Go》建議,包名盡量采用言簡意赅的名稱(
short, concise, evocative
)。并且推薦通過不同的
import
路徑來區分相同包名的包引入。
5.2 包名設計
如果使用
GF
開發業務項目,那麼基本可以不用考慮包名設計的問題,因為從
v1.15
版本開始,
GF
官方推薦使用面向對象的封裝方式,項目中
app
目錄下往往隻存在
api
,
dao
,
model
,
service
四個包名,每個包内部通過對象的形式來封裝具體的資料模型或者業務邏輯。
6. 控制器實作
6.1 結構化限制
控制器的輸入與輸出使用了結構體定義進行限制,結構化維護輸入輸出資料結構是推薦的方式。例如:
// 賬号唯一性檢測請求參數,用于前後端互動參數格式約定
type UserApiCheckPassportReq struct {
Passport string `v:"required#賬号不能為空"`
}
雖然隻有一個參數,也采用了結構化定義,我們直接檢視該結構體便可得知該接口的輸入參數格式,而不用進入代碼中去分析,進而極大提高維護效率。
6.2 結構體轉換
結構體轉換可以使用
GetStruct
或者
Parse
方法,其中
Parse
同時可以執行資料校驗。結構體轉換方法的參數都可以給定一個結構體的空指針,内部會自動初始化結構體對象,轉換失敗(例如送出參數不存在)不會執行初始化。例如:
var (
data *model.UserApiSignInReq
)
if err := r.Parse(&data); err != nil {
response.JsonExit(r, 1, err.Error())
}
6.3 資料校驗
用戶端送出的資料都是不可信的,必須要做資料校驗。
可以通過給結構體綁定
v
的标簽進行設定校驗規則以及定義的錯誤提示。例如:
// 登入請求參數,用于前後端互動參數格式約定
type UserApiSignInReq struct {
Passport string `v:"required#賬号不能為空"`
Password string `v:"required#密碼不能為空"`
}
6.4 資料傳參
控制器負責接收、轉換、校驗、處理請求參數後,将所需的參數傳遞給調用的
service
對象方法,而不是直接将
Request
對象傳遞給
service
。例如:
func (a *apiUser) SignIn(r *ghttp.Request) {
var (
data *model.UserApiSignInReq
)
if err := r.Parse(&data); err != nil {
response.JsonExit(r, 1, err.Error())
}
if err := service.User.SignIn(r.Context(), data.Passport, data.Password); err != nil {
response.JsonExit(r, 1, err.Error())
} else {
response.JsonExit(r, 0, "ok")
}
}
6.4 實作代碼
https://github.com/gogf/gf-demos/blob/master/app/api/user.go
7. 上下文變量
上下文變量指的是标準庫的
context.Context
,是一個接口對象。主要用于
goroutine
的異步IO控制,以及流程變量傳遞。
在
Go
的
HTTP
請求流程中,不存在”全局變量”擷取請求參數的方式,隻有将上下文
context
變量傳遞到後續流程的方法中,而
context
上下文變量即包含了所有需要傳遞的共享變量。并且該
context
中的共享變量應當是事先約定的,并且往往存儲為對象指針形式。
7.1 結構定義
在該示例中,我們的上下文變量的資料結構定義為:
https://github.com/gogf/gf-demos/blob/master/app/model/context.go
// 請求上下文結構
type Context struct {
Session *ghttp.Session // 目前Session管理對象
User *ContextUser // 上下文使用者資訊
}
// 請求上下文中的使用者資訊
type ContextUser struct {
Id uint // 使用者ID
Passport string // 使用者賬号
Nickname string // 使用者名稱
}
7.2 邏輯封裝
由于該上下文對象也是和業務邏輯相關的,是以我們需要通過
service
對象将上下文變量封裝起來再供其他子產品使用。
https://github.com/gogf/gf-demos/blob/master/app/service/context.go
7.3 上下文變量注入
上下文的變量必須在請求一開始便注入到請求流程中,以便于其他方法調用,是以我們使用中間件來實作。
https://github.com/gogf/gf-demos/blob/168e049e58528413cd248c05c729840e552ece94/app/service/middleware.go#L15
7.4 上下文變量使用
約定俗成的,方法的第一個參數往往預留給
context.Context
類型參數使用,以便接受上下文變量,特别是
service
層的方法。例如:
https://github.com/gogf/gf-demos/blob/master/app/service/user.go
8. 中間件使用
8.1 跨域處理
允許跨域請求。
// 允許接口跨域請求
func (s *serviceMiddleware) CORS(r *ghttp.Request) {
r.Response.CORSDefault()
r.Middleware.Next()
}
8.2 鑒權處理
隻有在使用者登入後才可通過。
// 鑒權中間件,隻有登入成功之後才能通過
func (s *serviceMiddleware) Auth(r *ghttp.Request) {
if User.IsSignedIn(r.Context()) {
r.Middleware.Next()
} else {
r.Response.WriteStatus(http.StatusForbidden)
}
}
8.3 上下文注入
前一章節已介紹過。
9. 業務邏輯封裝
9.1 職責劃分
所有的業務邏輯實作均封裝于
service
層中,不推薦實作于控制器
api
中。
service
層的包名隻有一個,通過面向對象的方式進行封裝。
api
層在使用的時候隻會看到幾個公開的業務邏輯封裝對象。
9.2 資料校驗
與用戶端定義的輸入接口是由
api
層的代碼來做的校驗,
service
層的代碼僅對内部定義的參數進行必要的校驗;有時往往也不需要做參數校驗,認為内部調用的參數都是可信任的。例如:
注冊邏輯:
9.3 内部引用
在
service
内部的對象之間存在互相引用,直接使用對應的變量即可,例如:
9.4 實作代碼
https://github.com/gogf/gf-demos/blob/master/app/service/user.go
10. 資料檔案建立
10.1 代碼生成
該模型通過
GF
工具鍊的
gf gen dao
指令生成,提供了強大靈活的資料表操作方式。生成的資料檔案:
生成的檔案介紹請參考 gen 代碼生成 章節。
10.2 dao
使用
dao
通過
dao
包引用對應的對象即可,
dao
往往是被
service
代碼層調用。
查找
dao
中的
User
對象:
調用
User
對象的
Save
方法儲存資料到資料表中。
10.3 model
使用
model
通過
model
包引用對應的資料結構定義即可。
查找
model
中使用到
api
的請求資料結構
ApiUserSignInReq
:
通過
ApiUserSignInReq
接受請求參數:
11. Swagger生成
如果對使用
swagger
接口文檔比較感興趣可以參考此章節介紹,不需要的話可以忽略該章節。
swagger
接口文檔主要用于前後端的接口定義。
Golang
的
swagger
文檔通過注釋的形式編寫到
api
層的代碼中,使得接口文檔可以随着代碼一起維護,降低代碼與文檔不一緻的風險,并通過
gf-cli
工具生成:swagger API文檔生成。
11.1 swagger
編寫
swagger
swagger
的文法請參考第三方倉庫
swag
:https://github.com/swaggo/swag
目前僅此一家元件庫,
Golang Swagger
的
Golang
編寫體驗并不是特别友好,聊勝于無吧。
swagger
11.2 swagger
生成
swagger
我們這裡使用以下指令生成:
gf swagger --pack
其中
gf swagger
指令解析并生成
swagger.json
文檔到項目根目錄的
swagger
路徑下,同時這裡的
--pack
選項将
swagger.json
打包為
Golang
代碼檔案生成到項目根目錄的
packed
路徑下。
具體請參考 swagger API文檔生成 章節。
11.3 swagger
插件
swagger
我們這裡使用到了
GoFrame
的
swagger
插件:https://github.com/gogf/swagger
按照倉庫介紹說明,我們在
boot
啟動設定子產品中添加插件的注冊:
11.4 swagger
文檔檢視
swagger
随後可以啟動程式通路檢視
swagger
接口文檔頁面:http://127.0.0.1:8199/swagger
12. 基礎類庫存放
主要固定傳回資料格式及資料結構。
其中
JsonExit
與
Json
的差別在于,
JsonExit
調用時會輸出
JSON
資料後直接退出目前的路由方法;而
Json
在執行輸出後會繼續執行後續的路由方法邏輯。
package response
import (
"github.com/gogf/gf/net/ghttp"
)
// 資料傳回通用JSON資料結構
type JsonResponse struct {
Code int `json:"code"` // 錯誤碼((0:成功, 1:失敗, >1:錯誤碼))
Message string `json:"message"` // 提示資訊
Data interface{} `json:"data"` // 傳回資料(業務接口定義具體資料結構)
}
// 标準傳回結果資料結構封裝。
func Json(r *ghttp.Request, code int, message string, data ...interface{}) {
responseData := interface{}(nil)
if len(data) > 0 {
responseData = data[0]
}
r.Response.WriteJson(JsonResponse{
Code: code,
Message: message,
Data: responseData,
})
}
// 傳回JSON資料并退出目前HTTP執行函數。
func JsonExit(r *ghttp.Request, err int, msg string, data ...interface{}) {
Json(r, err, msg, data...)
r.Exit()
}