Go Web 新手教程
大家好,我叫謝偉,是一名程序員。
web
應用程序是一個各種編程語言一個非常流行的應用領域。
那麼 web
後臺開發涉及哪些知識呢?
- 模型設計:關係型數據庫模型設計
- SQL、ORM
- Restful API 設計
模型設計
web 後臺開發一般是面向的業務開發,也就說開發是存在一個應用實體:比如,面向的是電商領域,比如面向的是數據領域等,比如社交領域等。
不同的領域,抽象出的模型各不相同,電商針對的多是商品、商鋪、訂單、物流等模型,社交針對的多是人、消息、羣組、帖子等模型。
儘管市面是的數據庫非常繁多,不同的應用場景選擇不同的數據庫,但關係型數據庫依然是中小型企業的主流選擇,關係型數據庫對數據的組織非常友好。
能夠快速的適用業務場景,只有數據達到某個點,產生某種瓶頸,比如數據量過多,查詢緩慢,這個時候,會選擇分庫、分表、主從模式等。
數據庫模型設計依然是一個重要的話題。良好的數據模型,爲後續需求的持續迭代、擴展等,非常有幫助。
如何設計個良好的數據庫模型?
- 遵循一些範式:比如著名的數據庫設計三範式
- 允許少量冗餘
細講下來,無外乎:1。 數據庫表設計 2。 數據庫字段設計、類型設計 3。 數據表關係設計:1對1,1對多,多對多
1。 數據庫表設計
表名
這個沒什麼講的,符合見聞之意的命名即可,但我依然建議,使用 database+實體
的形式。
比如:beeQuick_products
表示:數據庫:beeQuick
,表:products
真實的場景是,設計的:生鮮平臺:愛鮮蜂中商品的表
2。 數據庫字段設計
字段設計、類型設計
- 字段的個數:字段過多,後期需要進行拆表;字段過少,會涉及多表操作,所以拿捏尺度很重要,給個指標:少於12個字段吧。
- 如何設計字段?: 根據抽象的實體,比如教育系統:學生信息、老師信息、角色等,很容易知道表中需要哪些字段、字段類型。
- 如果你知道真實場景,儘量約束字段所佔的空間,比如:電話號碼 11 位,比如:密碼長度 不多於12位
外鍵設計
- 外鍵原本用來維護數據一致性,但真實使用場景並不會這麼用,而是依靠業務判斷,比如,將某條記錄的主鍵當作某表的某個字段
1對1,1對多,多對多關係
- 1對1: 某表的字段是另一個表的主鍵
type Order struct{
base
AccountId int64
}
- 1對多:某表的字段是另一個表的主鍵的集合
type Order struct {
base `xorm:"extends"`
ProductIds []int `xorm:"blob"`
Status int
AccountId int64
Account Account `xorm:"-"`
Total float64
}
- 多對多:使用第三張表維護多對多的關係
type Shop2Tags struct {
TagsId int64 `xorm:"index"`
ShopId int64 `xorm:"index"`
}
ORM
ORM 的思想是對象映射成數據庫表。
在具體的使用中:
1。 根據 ORM 編程語言和數據庫數據類型的映射,合理定義字段、字段類型
2。 定義表名稱
3。 數據庫表創建、刪除等
在 Go 中比較流行的 ORM 庫是: GORM 和 XORM ,數據庫表的定義等規則,主要從結構體字段和 Tag 入手。
字段對應數據庫表中的列名,Tag 內指定類型、約束類型、索引等。如果不定義 Tag, 則採用默認的形式。具體的編程語言類型和數據庫內的對應關係,需要查看具體的 ORM 文檔。
// XORM
type Account struct {
base `xorm:"extends"`
Phone string `xorm:"varchar(11) notnull unique 'phone'" json:"phone"`
Password string `xorm:"varchar(128)" json:"password"`
Token string `xorm:"varchar(128) 'token'" json:"token"`
Avatar string `xorm:"varchar(128) 'avatar'" json:"avatar"`
Gender string `xorm:"varchar(1) 'gender'" json:"gender"`
Birthday time.Time `json:"birthday"`
Points int `json:"points"`
VipMemberID uint `xorm:"index"`
VipMember VipMember `xorm:"-"`
VipTime time.Time `json:"vip_time"`
}
// GORM
type Account struct {
gorm.Model
LevelID uint
Phone string `gorm:"type:varchar" json:"phone"`
Avatar string `gorm:"type:varchar" json:"avatar"`
Name string `gorm:"type:varchar" json:"name"`
Gender int `gorm:"type:integer" json:"gender"` // 0 男 1 女
Birthday time.Time `gorm:"type:timestamp with time zone" json:"birthday"`
Points sql.NullFloat64
}
另一個具體的操作是: 完成數據庫的增刪改查,具體的思想,仍然是操作結構體對象,完成數據庫 SQL 操作。
當然對應每個模型的設計,我一般都會定義一個序列化結構體,真實模型的序列化方法是返回這個定義的序列化結構體。
具體來說:
// 定義一個具體的序列化結構體,注意名稱的命名,一致性
type AccountSerializer struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Phone string `json:"phone"`
Password string `json:"-"`
Token string `json:"token"`
Avatar string `json:"avatar"`
Gender string `json:"gender"`
Age int `json:"age"`
Points int `json:"points"`
VipMember VipMemberSerializer `json:"vip_member"`
VipTime time.Time `json:"vip_time"`
}
// 具體的模型的序列化方法返回定義的序列化結構體
func (a Account) Serializer() AccountSerializer {
gender := func() string {
if a.Gender == "0" {
return "男"
}
if a.Gender == "1" {
return "女"
}
return a.Gender
}
age := func() int {
if a.Birthday.IsZero() {
return 0
}
nowYear, _, _ := time.Now().Date()
year, _, _ := a.Birthday.Date()
if a.Birthday.After(time.Now()) {
return 0
}
return nowYear - year
}
return AccountSerializer{
ID: a.ID,
CreatedAt: a.CreatedAt.Truncate(time.Minute),
UpdatedAt: a.UpdatedAt.Truncate(time.Minute),
Phone: a.Phone,
Password: a.Password,
Token: a.Token,
Avatar: a.Avatar,
Points: a.Points,
Age: age(),
Gender: gender(),
VipTime: a.VipTime.Truncate(time.Minute),
VipMember: a.VipMember.Serializer(),
}
}
項目結構設計
├── cmd
├── configs
├── deployments
├── model
│ ├── v1
│ └── v2
├── pkg
│ ├── database.v1
│ ├── error.v1
│ ├── log.v1
│ ├── middleware
│ └── router.v1
├── src
│ ├── account
│ ├── activity
│ ├── brand
│ ├── exchange_coupons
│ ├── make_param
│ ├── make_response
│ ├── order
│ ├── product
│ ├── province
│ ├── rule
│ ├── shop
│ ├── tags
│ ├── unit
│ └── vip_member
└── main.go
└── Makefile
爲什麼要進行項目結構的組織?就問你個問題:雜亂的屋裏,找一件東西快,還是乾淨整齊的屋裏,找一件東西快?
合理的項目組織,利於項目的擴展,滿足多變的需求,這種模塊化的思維,其實在編程中也常出現,比如將整個系統根據功能劃分。
- cmd 用於 命令行
- configs 用於配置文件
- deployments 部署腳本,Dockerfile
- model 用於模型設計
- pkg 用於輔助的庫
- src 核心邏輯層,這一層,我的一般組織方式爲:按模型設計的實體劃分不同的文件夾,比如上文賬戶、活動、品牌、優惠券等,另外具體的處理邏輯,我又這麼劃分:
├── assistance.go // 輔助函數,如果重複使用的輔助函數,會提取到 pkg 層,或者 utils 層
├── controller.go // 核心邏輯處理層
├── param.go // 請求參數層:包括參數校驗
├── response.go // 響應信息
└── router.go // 路由
- main.go 函數入口
- Makefile 項目構建
當然你也可以參考:https://github.com/golang-standards/project-layout
框架選擇
- gin
- iris
- echo
...
主流的隨便選,問題不大。使用原生的也行,但你可能需要多寫很多代碼,比如路由的設計、參數的校驗:路徑參數、請求參數、響應信息處理等
Restful 風格的API開發
- 路由設計
- 參數校驗
- 響應信息
路由設計
儘管網上存在很多的 Restful 風格的 API 設計準則,但我依然推薦你看看下文的介紹。
域名(主機)
推薦使用專有的 API 域名下,比如:https://api.example.com
但實際上直接放在主機下:https://example.com/api
版本
需求會不斷的變更,接口也會在不斷的變更,所以,最好給 API 帶上版本:比如:https://example.com/api/v1
,表示 第一個版本。
有些會在頭部信息裏帶版本信息,不推薦,不直觀。
方式這麼些,但一定要統一。在頭部信息裏帶版本信息,那麼就一直這樣。如果在路路徑內,就一致在路徑內,統一非常重要。
請求方法
- POST: 在服務器上創建資源,對應數據庫操作是:create
- PATCH: 在服務器上更新資源,對應的數據庫操作是:update
- DELETE: 在服務器上刪除資源,對應的數據庫操作是:delete
- GET: 在服務器上獲取資源,對應的數據庫操作是:select
- 其他:不常用
路由設計
整體推薦:版本 + 實體(名詞)
的形式:
舉個例子:上文的項目結構中的 order
表示的是訂單實體。
那麼路由如何設計?
POST /api/v1/order
PATCH /api/v1/order/{order_id:int}
DELETE /api/v1/order/{order_id:int}
GET /api/v1/orders
儘管還存在其他方式,但我依然推薦需要保持一致性。
比如活動接口:
POST /api/v1/activity
PATCH /api/v1/activity/{activity_id:int}
DELETE /api/v1/activity/{activity_id:int}
GET /api/v1/activities
保持一致性。
參數校驗
路由設計中涉及的一個重要的知識點是:參數校驗
- 比如參數類型校驗
- 比如參數長度校驗
- 比如指定選項校驗
上文項目示例每個實體的接口具體的項目結構如下:
├── assistance.go
├── controller.go
├── param.go
├── response.go
└── router.go
- param.go 核心的就是組織接口中參數的定義、參數的校驗
參數校驗有兩種方式:1: 使用結構體方法實現校驗邏輯;2: 使用結構體中的 Tag 定義校驗。
type RegisterParam struct {
Phone string `json:"phone"`
Password string `json:"password"`
}
func (param RegisterParam) suitable() (bool, error) {
if param.Password == "" || len(param.Phone) != 11 {
return false, fmt.Errorf("password should not be nil or the length of phone is not 11")
}
if unicode.IsNumber(rune(param.Password[0])) {
return false, fmt.Errorf("password should start with number")
}
return true, nil
}
像這種方式,自定義參數結構體,結構體方法來進行參數的校驗。
缺點是:需要寫很多的代碼,要考慮很多的場景。
另外一種方式是:使用 結構體的 Tag 來實現。
type RegisterParam struct {
Phone string `form:"phone" json:"phone" validate:"required,len=11"`
Password string `form:"password" json:"password"`
}
func (r RegisterParam) Valid() error {
return validator.New().Struct(r)
}
後者使用的是:https://godoc.org/gopkg.in/go-playground/validator.v9 校驗庫,gin web框架的參數校驗採用的也是這種方案。
覆蓋的場景,特別的多,使用者只需要關注結構體內 Tag 標籤的值即可。
- 對數值型參數:校驗的方向有:1、 是否爲 0 ;2、 最大值,最小值(比如翻頁操作,每頁的顯示)3、區間、大於、小於、等
- 對字符串型參數:校驗的方向有:1、是否爲 nil;2、枚舉或者特定值:eq="a"|eq="b" 等
- 特定的場景:比如郵箱、顏色、Base64、十六進制等
最常用的還是數值型和字符串型
響應信息
前後端分離,最流行的數據交換格式是:json。儘管支持各種各種的響應信息,比如 html、xml、string、json 等。
構建 Restful 風格的API,我只推薦 json,方便前端或者客戶端的開發人員調用。
確定好數據交換的格式爲 json 之後,還需要哪些關注點?
- 狀態碼
- 具體的響應信息
{
"code": 200,
"data": {
"id": 1,
"created_at": "2019-06-19T23:14:11+08:00",
"updated_at": "2019-06-20T10:40:09+08:00",
"status": "已付款",
"phone": "18717711717",
"account_id": 1,
"total": 9.6,
"product_ids": [
2,
3
]
}
}
推薦統一使用上文的格式: code 用來表示狀態碼,data 用來表示具體的響應信息。
如果是存在錯誤,則推薦使用下面這種格式:
{
"code": 404,
"detail": "/v1/ordeda",
"error": "no route /v1/orderda"
}
狀態碼也區分很多種:
- 1XX: 接受到請求
- 2XX: 成功
- 3XX: 重定向
- 4XX: 客戶端錯誤
- 5XX: 服務端錯誤
根據具體的場景選擇狀態碼。
真實的應用是:在 pkg 包下定義一個 err 包,實現 Error 方法。
type ErrorV1 struct {
Detail string `json:"detail"`
Message string `json:"message"`
Code int `json:"code"`
}
type ErrorV1s []ErrorV1
func (e ErrorV1) Error() string {
return fmt.Sprintf("Detail: %s, Message: %s, Code: %d", e.Detail, e.Message, e.Code)
}
定義一些常用的錯誤信息和錯誤碼:
var (
// database
ErrorDatabase = ErrorV1{Code: 400, Detail: "數據庫錯誤", Message: "database error"}
ErrorRecordNotFound = ErrorV1{Code: 400, Detail: "記錄不存在", Message: "record not found"}
// body
ErrorBodyJson = ErrorV1{Code: 400, Detail: "請求消息體失敗", Message: "read json body fail"}
ErrorBodyIsNull = ErrorV1{Code: 400, Detail: "參數爲空", Message: "body is null"}
)
其他
- API 文檔:比較流行的是 swagger 文檔,文檔是其他開發人員瞭解接口的重要途徑,考慮到溝通成本,API 文檔必不可少。
- 日誌:日誌是方便開發人員查看問題的,也必不可少,業務量不復雜,日誌寫入文件中持久化即可;稍複雜的場景,可以選擇 ELK
- Dockerfile: web 應用,當然非常適合以容易的形式部署在主機上
- Makefile: 項目構建命令,包括一些測試、構建、運行啓動等