GOPHER_AVATARS.jpg
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, 是否为 你来; 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: 项目构建命令, 包括一些测试, 构建, 运行启动等
Go Web 路线图
来源: http://www.tuicool.com/articles/EJNRFfQ