热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

gin项目结构_用go搭建高效restapi服务(使用postgresql,redis,gin,gorm...)

在本篇文章中,将会介绍如何利用go语言去搭建一个高效稳定的rest服务,以及如何去部署它。我们已经搭建好了轮子,好消息是,这
d4c8fe164e4080bb9f1f114aec439253.png

在本篇文章中,将会介绍如何利用go语言去搭建一个高效稳定的rest服务,以及如何去部署它。我们已经搭建好了轮子,好消息是,这一切都是开源的,你可以直接fork我们的项目

ruilisi/go-pangu​github.com
b9e5f9259cf43885b6558fb63313da6f.png

,从而节省额外开发的时间。系好安全带,我们要出发了。

(这篇教程可能并非最新的,在github项目里我们会实时更新这篇教程)

效率为王

Go有两个特性让我们去选择用它去开发后端的rest服务。一是Go在并发方面的高性能,在rest服务中,绝大多数的接口都会有很大的负荷,它们在每秒内都要承受成千上万的请求量,因而高并发性能成为一个非常重要的因素。二是Go语言是强类型语言,虽然在编写的过程中会稍微麻烦,但是它不会埋藏一些隐式的bug,从而能保证程序的鲁棒性。同时,完善的包管理,类C的写法都使得任何程序员都能迅速切换技术栈,而不会有太大的负担。

简要介绍

本服务在开发中使用了如下的包/工具,不用感到无从下手,接下的内容我将会一步步开始解释这些是如何发挥他们的作用。

名字描述
Go最近几年最为流行的新兴语言,简单的同时拥有极高的并发性能。
Gin基于Go语言的web框架,方便灵活的中间件,强大的数据绑定,以及极高的性能
GormGo语言的全功能ORM库,用于操作数据库
GinkgoGinkgo是一个BDD风格的Go测试框架,旨在帮助你有效地编写富有表现力的全方位测试。
JWTSON Web Tokens,是目前最流行的跨域认证解决方案。
Postgres高性能开源数据库,当整体负载达到很高时依旧能有强大的性能
Redis内存数据库,拥有极高的性能
Docker开发、部署、运行应用的虚拟化平台

前期准备

工欲善其事必先利其器,在正式开始开发前,我们需要一件趁手的开发工具,这里我推荐在mac或者linux的环境中在终端下去开发。我们需要使用两个工具,一个是tmux,另一个就是spacevim。

tmux是一个窗口多开的工具,它能够在开许多的窗口,同时还能在窗口里轻松的分屏,能够极大的提升开发的效率。

下图就是我在窗口1里分了3个小屏,主要进行开发,窗口2则是我看的一些其他的一些包或者工具。窗口3则是一些其他服务,以及数据库查询所在。

8591da978830b6cb981361c3eb056294.png

spacevim是一个集成的VIM IDE它能够轻松的管理插件以及相关的配置,它为不同的语言提供了不同的开发模块,这些模块支持了代码自动补全, 语法检查、格式化、调试、REPL 等特性。

https://github.com/SpaceVim/SpaceVim​github.com

这里有我们整理的相关快捷键的文档,它可能会跟你使用的有所不同,但这些都是我们在日常开发中觉得非常舒适的配置,你可以在配置文件中修改快捷键。

Tmux&Vim常用快捷指令 · 语雀​www.yuque.com
0e46fc5f9dd57ed3964f6878399453c4.png

如果你还不会使用这两个工具,那么可以去看一下的教程或是文档。

Tmux 使用教程 - 阮一峰的网络日志​www.ruanyifeng.com
27685d256b2f2b169c10a8100eeadd51.png
使用文档 | SpaceVim​spacevim.org
1f1ef9bd0ad377e6c27fbbe4ee81592b.png

我们还需要进行一个准备,就是有一些开发的网站可能会被block或是速度比较慢,这个时候我们可能就需要额外的一些方法去解决这个问题,你可以在vagrant虚拟机里完成上述配置以及解决这个问题,碍于篇幅,这里我们就不去赘述这个问题了。

准备好数据库

我们这里需要使用两个数据库,一个是postgresql数据库,一个就是redis数据库。为什么会需要这两个数据库呢?因为我们的rest api服务有两种数据类型,一种是长效的,也就是需要一直保存的数据,像是用户的信息。还有一种就是短期的热数据,像是token,短信code,这些数据很快就会过期,因而我们把它们储存在内存数据库redis中,能极大提升效率,也不会让这些数据去污染postgresql数据库,加大其的负担。

如何使用它们的方法这里也不在赘述,如果你有学习过sql,那么这两个数据库你都能很快上手。我这写了一个常用命令文档

postgresql 和 redis · 语雀​www.yuque.com
0e46fc5f9dd57ed3964f6878399453c4.png

开启服务

接下来我们拉取项目,我们首先来观察一下整个项目的结构是什么样的。

文件功能
application.yml配置文件,包含基本信息
args包含获取url的params的函数
conf获取配置文件的函数
controllerrouter使用的handler控件,包含各种操作具体内容
dbdb操作,像是打开数据库
jwtjwt相关内容 包含生成jwt与验证jwt的函数
main.go程序主函数,执行时增加-db参数可选择不同的内容,create创建数据库,migrate更新表结构,drop删除数据库
middleware中间件,验证token是否正确
models基础的结构以及一些基本的数据库操作
params数据绑定的结构
redis包含连接redis和redis操作函数
router路由
test测试

我们直接开干,首先就是来看根目录下的配置文件application.yml

DEVISE_JWT_SECRET_KEY: RANDOM_SECRET
HTTP_PORT: 3002
BASE_DATABASE_URL: postgres://postgres:postgres@localhost:5432
DATABASE_URL: postgres://postgres:postgres@localhost:5432/go_pangu_development?sslmode=disable
DATABASE_TESTURL: postgres://postgres:postgres@localhost:5432/go_pangu_test?sslmode=disable
REDIS_URL: redis://localhost:6379/7
WORKERS: CollectIpWorker,QcloudSmsWorker
SMS_APPID:
SMS_APPKEY:
SMS_TEMPID:
SMS_APPLYTEMPID:

这里的你需要更改的就是 postgres:// 后跟的账号和密码,其他的都可以不去修改。

接着运行主程序

go run main.go -db=create //创建主数据库跟测试数据库
go run main.go -db=migrate //创建表
go run main.go //运行服务
go run main.go -db=drop //删库跑路

如果一切顺利,就会如下图所示

4780f643774fd1c76b0181b232815dcb.png
成功运行结果

我们尝试请求一下这个服务,就是用最简单的ping,你可以在浏览器,也可以使用postman(建议)。如果成功返回pong,那么我们的服务就一切正常。与此同时,服务器也会留下请求记录。

d7c7a591030f40738f1baedb70882d1b.png
在postman里发起请求
89f52b27ea1b1aa38f8b8010f4c5d533.png
请求成功

路由

路由被统一写在了router文件夹下

func SetupRouter() *gin.Engine {//前几行的config能够解决前端可能产生的跨域请求问题。router := gin.Default()config := cors.DefaultConfig()config.ExposeHeaders = []string{"Authorization"}config.AllowCredentials = trueconfig.AllowAllOrigins = trueconfig.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"}router.Use(cors.New(config))router.GET("/ping", service.PingHandler)router.POST("/sms", service.SMSHandler)//router.GET("")//每个地址都对应一个handler控件,handler控件都被放在了controller文件夹里。authorized := router.Group("/")authorized.Use(middleware.Auth("user")) {authorized.GET("/auth_ping", service.AuthPingHandler)}users := router.Group("/users"){users.POST("/sign_up", service.SignUpHandler)users.POST("/sign_in", service.SignInHandler)}users.Use(middleware.Auth("user")){users.POST("/change_password", service.ChangePasswordHandler)}return router
}
......

后面起的几个路由,当使用middleware.Auth的时候,则是使用了jwt token来验证身份。这些token在登录时会生成,而生成token的过程中用到了数据库的操作,所以我们先来看数据库操作。

数据库操作

postgresql

postgresql 的相关内容在db,models这两个文件中。

db 文件夹负责连接数据库后声明一个DB变量让其他package中函数操作中使用。

main函数的create migrate功能所使用的函数也在这里。

var DB *gorm.DBtype PGExtension struct {Extname string
}func Open(env string) {var err errorvar url stringurl = conf.GetEnv("DATABASE_URL")if env == "test" {url = conf.GetEnv("DATABASE_TESTURL")}if DB, err = gorm.Open(postgres.Open(url), &gorm.Config{}); err != nil {panic(err.Error())}
}
......

models 文件夹定义了数据库的表结构,以及一些常用的数据库操作,像是查询,删除一些数据。我们使用的是gorm包。

GORM 指南​gorm.io

package modelsimport ("errors""fmt""go-pangu/db""gorm.io/gorm"
)
//``里的内容是在json转换时的标识,Model里的内容会在gorm操作时自动生成。
type Model struct {ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`CreatedAt time.TimeUpdatedAt time.Time
}type User struct {ModelEmail string `gorm:"index:idx_email,unique"`EncryptedPassword string
}func FindUserByEmail(email string) (*User, SearchResult) {var user Userresult := Result(db.DB.Where("email = ?", email).First(&user).Error)return &user, result}
......

redis

redis 的相关内容主要在redis文件夹中,其中主要包括了连接redis数据库以及进行redis数据库操作的函数。

var RDB *redis.Client
var ctx = context.Background()func newRDB() *redis.Client {u, err := url.Parse(conf.GetEnv("REDIS_URL"))if err != nil {panic(err.Error())}password, _ := u.User.Password()db, err := strconv.Atoi(u.Path[1:])if err != nil {panic("Redis url format error")}return redis.NewClient(&redis.Options{Addr: u.Host,Password: password, // no password setDB: db, // use default DB})
}func ConnectRedis() {RDB = newRDB()_, err := RDB.Ping(ctx).Result()if err != nil {panic(err)}
}func getRDB() *redis.Client {if RDB != nil {return RDB}return newRDB()
}func Get(key string) string {value, err := getRDB().Get(ctx, key).Result()if err != nil {fmt.Println(err)}return value
}
func Set(key string, value interface{}) {_, err := getRDB().Set(ctx, key, value, 0).Result()if err != nil {fmt.Println(err)}
}
......

token

有些接口,只能在用户进行登录之后才能进行调用,怎么来判断用户是否登录了呢。我们这里使用的是jwt,json web token。生成token,验证token以及注销token的函数都放在了jwt文件夹中了。这些操作生成token都被存储在了redis数据库中。

如果你不熟悉jwt,可以看这篇文档。

JSON Web Token 入门教程​www.ruanyifeng.com
ce3ffee2dc60d89013321c9120210177.png

type Payload struct {Device string `json:"device,omitempty"`Scp string `json:"scp,omitempty"`jwt.StandardClaims
}func GenPayload(device, scp, sub string) Payload {now := time.Now()return Payload{Device: device,Scp: scp,StandardClaims: jwt.StandardClaims{ExpiresAt: now.Add(1 * time.Hour).Unix(),Id: uuid.New().String(),NotBefore: now.Unix(),IssuedAt: now.Unix(),Subject: sub,},}
}func JwtRevoked(payload Payload) bool {return _redis.Exists(fmt.Sprintf("user_blacklist:%s:%s", payload.Subject, payload.Id))
}func RevokeJwt(payload Payload) {expiration := payload.ExpiresAt - payload.IssuedAt_redis.SetEx(fmt.Sprintf("user_blacklist:%s:%s", payload.Subject, payload.Id), payload.Id, time.Duration(expiration)*time.Second)
}func RevokeLastJwt(payload Payload) {lastJwt := _redis.Get(fmt.Sprintf("user_jwt:%s", payload.Subject))if lastJwt != "" {arr := strings.Split(lastJwt, ":")jti, expStr := arr[0], arr[len(arr)-1]exp, err := strconv.ParseInt(expStr, 10, 64)if err != nil {exp = time.Now().Unix()}payload.Id = jtipayload.IssuedAt = time.Now().Unix()payload.ExpiresAt = expRevokeJwt(payload)}
}
......

在登录的过程中,我们生成token

payload := jwt.GenPayload(deviceType, "user", user.ID.String())
tokenString := jwt.Encoder(payload)
jwt.OnJwtDispatch(payload)c.Header("Authorization", "Bearer "+tokenString)

最后我们写一个中间件,来进行判断中间件是否合法,其中sub是用户的uuid,scp是用户类型。

func Auth(scp string) gin.HandlerFunc {return func(c *gin.Context) {bear := c.Request.Header.Get("Authorization")token := strings.Replace(bear, "Bearer ", "", 1)sub, scope, err := jwt.Decoder(token)if err != nil {c.Abort()c.String(http.StatusUnauthorized, err.Error())} else {if scope != scp {controller.StatusError(c, http.StatusUnauthorized, "unauthorized", "invalid scope")c.Abort()}c.Set("sub", sub)c.Set("scp", scope)c.Next()}}
}

handler控件

这个部分就是路由实际使用的控件了,也就是写入接口请求后的逻辑的所在。它们被放在controller文件夹下。主要负责就是处理请求的参数后进行各种操作,像是数据库操作来获取数据进行返回。在路由部分我们已经进行了token处理,所以在handler里我们可以直接获取用户类型和他的uuid。

这里还用到了params文件夹,这里面保存了进行数据绑定的结构体。当然也可以使用map[string]interface{}类型的变量来绑定一切数据,但是需要类型断言,有时候比较繁琐,如何取舍,看实际情况而定。

func ChangePasswordHandler(c *gin.Context) {//变量sub, _ := c.Get("sub")scp, _ := c.Get("scp")var change params.ChangePasswordvar oldEncryptedPassword stringvar user *models.User//绑定数据if err := c.ShouldBind(&change); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}if change.Password != change.PasswordConfirm {c.JSON(http.StatusBadRequest, gin.H{"status": "password and password confirm not match"})return}//修改密码,生成新密码的加密字符串switch scp {case "user":user, _ = models.FindUserByColum("id", sub)oldEncryptedPassword = user.EncryptedPassword}err := bcrypt.CompareHashAndPassword([]byte(oldEncryptedPassword), []byte(change.OriginPassword))if err != nil {c.JSON(http.StatusBadRequest, gin.H{"status": "origin password error"})return}hash, err := bcrypt.GenerateFromPassword([]byte(change.Password), bcrypt.DefaultCost)if err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}encryptedPassword := string(hash)var payload jwt.Payloadswitch scp {case "user"://保存数据库,生成新tokendb.DB.Model(&user).Updates(models.User{EncryptedPassword: encryptedPassword})payload = jwt.GenPayload("", "user", user.ID.String())for _, device := range conf.DEVICE_TYPES {payload.Device = devicejwt.RevokeLastJwt(payload)}}c.JSON(http.StatusOK, gin.H{"status": "update password success"})
}

读取配置文件

为了获取配置文件的内容,conf文件夹下有如下的函数

func GetEnv(env string) string {return fmt.Sprintf("%v", viper.Get(env))
}func init() {viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")// any approach to require this configuration into your program.str, _ := os.Getwd()str = strings.Replace(str, "/test", "", 1)str = strings.Replace(str, "/controller", "", 1)url := str + "/application.yml"data, err := ioutil.ReadFile(url)if err != nil {panic(err)}viper.ReadConfig(bytes.NewBuffer(data))
}

当需要使用时,只要像这样调用就行

url = conf.GetEnv("DATABASE_URL")

测试

测试在test文件夹下,这里我们使用的是ginkgo来进行测试,Ginkgo是一个BDD风格的Go测试框架,旨在帮助你有效地编写富有表现力的全方位测试。进入测试文件夹,使用ginkgo就可以测试,不用担心可能会打乱数据库内容,之前我们在运行 go run main.go 时就额外生成了一个测试数据库,不会影响在正常开发数据里的内容。

如果你不了解ginkgo可以看这篇

Ginkgo学习笔记​blog.gmem.cc

运行结果如下

87708632f40841715b02ff721e1778a5.png
测试结果

部署

使用make release或者debug 后 会生成可执行的server,接着就可以使用docker部署在服务器上。

相关文件在根目录下的Makefile Dockerfile

FROM alpine:3.10
WORKDIR /app
ADD dist/go-pangu-amd64-release-linux go-pangu
COPY application.yml .
RUN chmod +x go-pangu
CMD ["/app/go-pangu"]




推荐阅读
  • binlog2sql,你该知道的数据恢复工具
    binlog2sql,你该知道的数据恢复工具 ... [详细]
  • H5技术实现经典游戏《贪吃蛇》
    本文将分享一个使用HTML5技术实现的经典小游戏——《贪吃蛇》。通过H5技术,我们将探讨如何构建这款游戏的两种主要玩法:积分闯关和无尽模式。 ... [详细]
  • Docker安全策略与管理
    本文探讨了Docker的安全挑战、核心安全特性及其管理策略,旨在帮助读者深入理解Docker安全机制,并提供实用的安全管理建议。 ... [详细]
  • CRZ.im:一款极简的网址缩短服务及其安装指南
    本文介绍了一款名为CRZ.im的极简网址缩短服务,该服务采用PHP和SQLite开发,体积小巧,约10KB。本文还提供了详细的安装步骤,包括环境配置、域名解析及Nginx伪静态设置。 ... [详细]
  • 流处理中的计数挑战与解决方案
    本文探讨了在流处理中进行计数的各种技术和挑战,并基于作者在2016年圣何塞举行的Hadoop World大会上的演讲进行了深入分析。文章不仅介绍了传统批处理和Lambda架构的局限性,还详细探讨了流处理架构的优势及其在现代大数据应用中的重要作用。 ... [详细]
  • flea,frame,db,使用,之 ... [详细]
  • 本文详细介绍如何在华为鲲鹏平台上构建和使用适配ARM架构的Redis Docker镜像,解决常见错误并提供优化建议。 ... [详细]
  • 本文回顾了作者在求职阿里和腾讯实习生过程中,从最初的迷茫到最后成功获得Offer的心路历程。文中不仅分享了个人的面试经历,还提供了宝贵的面试准备建议和技巧。 ... [详细]
  • Nginx 启动命令及 Systemctl 配置详解
    本文详细介绍了在未配置和已配置 Systemctl 的情况下启动 Nginx 的方法,并提供了详细的配置步骤和命令示例。 ... [详细]
  • 本文介绍了.hbs文件作为Ember.js项目中的视图层,类似于HTML文件的功能,并详细讲解了如何在Ember.js应用中集成Bootstrap框架及其相关组件的方法。 ... [详细]
  • Requests库的基本使用方法
    本文介绍了Python中Requests库的基础用法,包括如何安装、GET和POST请求的实现、如何处理Cookies和Headers,以及如何解析JSON响应。相比urllib库,Requests库提供了更为简洁高效的接口来处理HTTP请求。 ... [详细]
  • 调试利器SSH隧道
    在开发微信公众号或小程序的时候,由于微信平台规则的限制,部分接口需要通过线上域名才能正常访问。但我们一般都会在本地开发,因为这能快速的看到 ... [详细]
  • 从理想主义者的内心深处萌发的技术信仰,推动了云原生技术在全球范围内的快速发展。本文将带你深入了解阿里巴巴在开源领域的贡献与成就。 ... [详细]
  • 本文详细介绍了如何正确设置Shadowsocks公共代理,包括调整超时设置、检查系统限制、防止滥用及遵守DMCA法规等关键步骤。 ... [详细]
  • PHP面试题精选及答案解析
    本文精选了新浪PHP笔试题及最新的PHP面试题,并提供了详细的答案解析,帮助求职者更好地准备PHP相关的面试。 ... [详细]
author-avatar
手机用户2502937541
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有