Go 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、是否为 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 路线图

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 去年有段时间得空,就把谷歌GAE的API权威指南看了一遍,收获颇丰,特别是在自己几乎独立开发了公司的云数据中心之后...
    骑单车的勋爵阅读 20,477评论 0 41
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,932评论 6 13
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,702评论 0 15
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,256评论 0 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,092评论 1 32