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 文档。
// XORMtype 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"`}复制代码
// GORMtype 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 项目构建
当然你也可以参考:
框架选择
- 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/orderPATCH /api/v1/order/{order_id:int}DELETE /api/v1/order/{order_id:int}GET /api/v1/orders复制代码
尽管还存在其他方式,但我依然推荐需要保持一致性。
比如活动接口:
POST /api/v1/activityPATCH /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)} 复制代码
后者使用的是: 校验库,gin web框架的参数校验采用的也是这种方案。
覆盖的场景,特别的多,使用者只需要关注结构体内 Tag 标签的值即可。
- 对数值型参数:校验的方向有:1、 是否为 0 ;2、 最大值,最小值(比如翻页操作,每页的显示)3、区间、大于、小于、等
- 对字符串型参数:校验的方向有:1、是否为 你来;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 []ErrorV1func (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: 项目构建命令,包括一些测试、构建、运行启动等