Go web 教程

Go Web 新手教程

你們好,我叫謝偉,是一名程序員。html

web 應用程序是一個各類編程語言一個很是流行的應用領域。前端

那麼 web 後臺開發涉及哪些知識呢?git

  • 模型設計:關係型數據庫模型設計
  • SQL、ORM
  • Restful API 設計

模型設計

web 後臺開發通常是面向的業務開發,也就說開發是存在一個應用實體:好比,面向的是電商領域,好比面向的是數據領域等,好比社交領域等。程序員

不一樣的領域,抽象出的模型各不相同,電商針對的可能是商品、商鋪、訂單、物流等模型,社交針對的可能是人、消息、羣組、帖子等模型。github

儘管市面是的數據庫很是繁多,不一樣的應用場景選擇不一樣的數據庫,但關係型數據庫依然是中小型企業的主流選擇,關係型數據庫對數據的組織很是友好。golang

可以快速的適用業務場景,只有數據達到某個點,產生某種瓶頸,好比數據量過多,查詢緩慢,這個時候,會選擇分庫、分表、主從模式等。web

數據庫模型設計依然是一個重要的話題。良好的數據模型,爲後續需求的持續迭代、擴展等,很是有幫助。sql

如何設計個良好的數據庫模型?數據庫

  • 遵循一些範式:好比著名的數據庫設計三範式
  • 容許少許冗餘

細講下來,無外乎: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 項目構建

固然你也能夠參考:github.com/golang-stan…

框架選擇

  • 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)
}
 
複製代碼

後者使用的是:godoc.org/gopkg.in/go… 校驗庫,gin web框架的參數校驗採用的也是這種方案。

覆蓋的場景,特別的多,使用者只須要關注結構體內 Tag 標籤的值便可。

  • 對數值型參數:校驗的方向有:一、 是否爲 0 ;二、 最大值,最小值(好比翻頁操做,每頁的顯示)三、區間、大於、小於、等
  • 對字符串型參數:校驗的方向有:一、是否爲 nil;二、枚舉或者特定值:eq="a"|eq="b" 等
  • 特定的場景:好比郵箱、顏色、Base6四、十六進制等

最經常使用的仍是數值型和字符串型

響應信息

先後端分離,最流行的數據交換格式是: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: 項目構建命令,包括一些測試、構建、運行啓動等

Go web 路線圖

相關文章
相關標籤/搜索