Go web 教程

4,728

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 项目构建

当然你也可以参考: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 标签的值即可。

  • 对数值型参数:校验的方向有: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: 项目构建命令,包括一些测试、构建、运行启动等

Go web 路线图