29

Go web 教程

 4 years ago
source link: https://www.tuicool.com/articles/EJNRFfQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
YJJreeM.jpg!web

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 路线图


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK