博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Go web 教程
阅读量:5742 次
发布时间:2019-06-18

本文共 8605 字,大约阅读时间需要 28 分钟。

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: 项目构建命令,包括一些测试、构建、运行启动等

转载于:https://juejin.im/post/5d0bb02ae51d455070226fb2

你可能感兴趣的文章
Django博客教程(五):处理 http 请求完全解读
查看>>
haproxy负载均衡算法
查看>>
selinux(实验环境:redhat7.0)
查看>>
Nginx防盗链、Nginx访问控制、Nginx解析php相关配置、Nginx代理
查看>>
linux下SS命令使用
查看>>
solaris学习1:磁盘续
查看>>
安装hadoop+zookeeper
查看>>
关于docker
查看>>
【C#】基于webservice的身份验证,如何创建新项目
查看>>
centos安装zeromq, jzmq
查看>>
【我的技术我做主】如何解决市场部门和测试部门之间的冲突
查看>>
EIGRP中AD与FD的区别
查看>>
统计字符串、乘法口诀
查看>>
confluence 使用plantuml报错
查看>>
How to batch create VM disks
查看>>
***常见复杂SQL语句(含统计类SQL)
查看>>
Java本地文件操作(二)文件夹的创建、删除、重命名
查看>>
FlashFXP5.2主动模式(PORT)
查看>>
Docker 环境 Storage Pool 用完解决方案:resize-device-mapper
查看>>
第五讲 python函数
查看>>