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: 项目构建命令,包括一些测试、构建、运行启动等