GVKun编程网logo

Gin实践 连载七 Golang优雅重启HTTP服务(golang 重启自己)

7

这篇文章主要围绕Gin实践连载七Golang优雅重启HTTP服务和golang重启自己展开,旨在为您提供一份详细的参考资料。我们将全面介绍Gin实践连载七Golang优雅重启HTTP服务的优缺点,解答

这篇文章主要围绕Gin实践 连载七 Golang优雅重启HTTP服务golang 重启自己展开,旨在为您提供一份详细的参考资料。我们将全面介绍Gin实践 连载七 Golang优雅重启HTTP服务的优缺点,解答golang 重启自己的相关问题,同时也会为您带来Gin实践 连载一 Golang介绍与环境安装、Gin实践 连载九 将Golang应用部署到Docker、Golang Gin实践 连载十七 用 Nginx 部署 Go 应用、Golang Gin实践 连载十三 优化你的应用结构和实现Redis缓存的实用方法。

本文目录一览:

Gin实践 连载七 Golang优雅重启HTTP服务(golang 重启自己)

Gin实践 连载七 Golang优雅重启HTTP服务(golang 重启自己)

优雅的重启服务

在前面编写案例代码时,我相信你会想到

每次更新完代码,更新完配置文件后
就直接这么 ctrl+c 真的没问题吗,ctrl+c到底做了些什么事情呢?

在这一节中我们简单讲述 ctrl+c 背后的信号以及如何在Gin优雅的重启服务,也就是对 HTTP 服务进行热更新

项目地址:https://github.com/EDDYCJY/go...

ctrl + c

内核在某些情况下发送信号,比如在进程往一个已经关闭的管道写数据时会产生 SIGPIPE信号

在终端执行特定的组合键可以使系统发送特定的信号给此进程,完成一系列的动作

命令 信号 含义
ctrl + c SIGINT 强制进程结束
ctrl + z SIGTSTP 任务中断,进程挂起
ctrl + \ SIGQUIT 进程结束 和 dump core
ctrl + d EOF
SIGHUP 终止收到该信号的进程。若程序中没有捕捉该信号,当收到该信号时,进程就会退出(常用于 重启、重新加载进程)

因此在我们执行ctrl + c关闭gin服务端时,会强制进程结束,导致正在访问的用户等出现问题

常见的 kill -9 pid 会发送 SIGKILL 信号给进程,也是类似的结果

信号

本段中反复出现信号是什么呢?

信号是 Unix 、类 Unix 以及其他 POSIX 兼容的操作系统中进程间通讯的一种有限制的方式

它是一种异步的通知机制,用来提醒进程一个事件(硬件异常、程序执行异常、外部发出信号)已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程。此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数

所有信号

$ kill -l
 1) SIGHUP     2) SIGINT     3) SIGQUIT     4) SIGILL     5) SIGTRAP
 6) SIGABRT     7) SIGBUS     8) SIGFPE     9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIgalRM    15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP
21) SIGTTIN    22) SIGTTOU    23) SIGURG    24) SIGXcpu    25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF    28) SIGWINCH    29) SIGIO    30) SIGPWR
31) SIGSYS    34) SIGRTMIN    35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10    45) SIGRTMIN+11    46) SIGRTMIN+12    47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15    50) SIGRTMAX-14    51) SIGRTMAX-13    52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10    55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5    60) SIGRTMAX-4    61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX

怎样算优雅

目的

  • 不关闭现有连接(正在运行中的程序)
  • 新的进程启动并替代旧进程
  • 新的进程接管新的连接
  • 连接要随时响应用户的请求,当用户仍在请求旧进程时要保持连接,新用户应请求新进程,不可以出现拒绝请求的情况

流程

1、替换可执行文件或修改配置文件

2、发送信号量 SIGHUP

3、拒绝新连接请求旧进程,但要保证已有连接正常

4、启动新的子进程

5、新的子进程开始 Accet

6、系统将新的请求转交新的子进程

7、旧进程处理完所有旧连接后正常结束

实现优雅重启

endless

Zero downtime restarts for golang HTTP and HTTPS servers. (for golang 1.3+)

我们借助 fvbock/endless 来实现 Golang HTTP/HTTPS 服务重新启动的零停机

endless server 监听以下几种信号量:

  • syscall.SIGHUP:触发 fork 子进程和重新启动
  • syscall.SIGUSR1/syscall.SIGTSTP:被监听,但不会触发任何动作
  • syscall.SIGUSR2:触发 hammerTime
  • syscall.SIGINT/syscall.SIGTERM:触发服务器关闭(会完成正在运行的请求)

endless 正正是依靠监听这些信号量,完成管控的一系列动作

安装

go get -u github.com/fvbock/endless

编写

打开 gin-blog 的 main.go文件,修改文件:

package main

import (
    "fmt"
    "log"
    "syscall"

    "github.com/fvbock/endless"

    "gin-blog/routers"
    "gin-blog/pkg/setting"
)

func main() {
    endless.DefaultReadTimeOut = setting.ReadTimeout
    endless.DefaultWriteTimeOut = setting.WriteTimeout
    endless.DefaultMaxHeaderBytes = 1 << 20
    endPoint := fmt.Sprintf(":%d",setting.HTTPPort)

    server := endless.NewServer(endPoint,routers.InitRouter())
    server.BeforeBegin = func(add string) {
        log.Printf("Actual pid is %d",syscall.Getpid())
    }

    err := server.ListenAndServe()
    if err != nil {
        log.Printf("Server err: %v",err)
    }
}

endless.NewServer 返回一个初始化的 endlessServer 对象,在 BeforeBegin 时输出当前进程的 pid,调用 ListenAndServe 将实际“启动”服务

验证

编译
$ go build main.go
执行
$ ./main
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
...
Actual pid is 48601

启动成功后,输出了pid为 48601;在另外一个终端执行 kill -1 48601 ,检验先前服务的终端效果

[root@localhost go-gin-example]# ./main
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:    export GIN_MODE=release
 - using code:    gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /auth                     --> ...
[GIN-debug] GET    /api/v1/tags              --> ...
...

Actual pid is 48601

...

Actual pid is 48755
48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection

可以看到该命令已经挂起,并且 fork 了新的子进程 pid48755

48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection

大致意思为主进程(pid为48601)接受到 SIGTERM 信号量,关闭主进程的监听并且等待正在执行的请求完成;这与我们先前的描述一致

唤醒

这时候在 postman 上再次访问我们的接口,你可以惊喜的发现,他“复活”了!

Actual pid is 48755
48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection


$ [GIN] 2018/03/15 - 13:00:16 | 200 |     188.096µs |   192.168.111.1 | GET      /api/v1/tags...

这就完成了一次正向的流转了

你想想,每次更新发布、或者修改配置文件等,只需要给该进程发送SIGTERM信号,而不需要强制结束应用,是多么便捷又安全的事!

问题

endless 热更新是采取创建子进程后,将原进程退出的方式,这点不符合守护进程的要求

http.Server - Shutdown()

如果你的Golang >= 1.8,也可以考虑使用 http.Server 的 Shutdown 方法

package main

import (
    "fmt"
    "net/http"
    "context"
    "log"
    "os"
    "os/signal"
    "time"


    "gin-blog/routers"
    "gin-blog/pkg/setting"
)

func main() {
    router := routers.InitRouter()

    s := &http.Server{
        Addr:           fmt.Sprintf(":%d",setting.HTTPPort),Handler:        router,ReadTimeout:    setting.ReadTimeout,WriteTimeout:   setting.WriteTimeout,MaxHeaderBytes: 1 << 20,}

    go func() {
        if err := s.ListenAndServe(); err != nil {
            log.Printf("Listen: %s\n",err)
        }
    }()
    
    quit := make(chan os.Signal)
    signal.Notify(quit,os.Interrupt)
    <- quit

    log.Println("Shutdown Server ...")

    ctx,cancel := context.WithTimeout(context.Background(),5 * time.Second)
    defer cancel()
    if err := s.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown:",err)
    }

    log.Println("Server exiting")
}

小结

在日常的服务中,优雅的重启(热更新)是非常重要的一环。而 GolangHTTP 服务方面的热更新也有不少方案了,我们应该根据实际应用场景挑选最合适的

参考

本系列示例代码

  • go-gin-example

本系列目录

  • 连载一 Golang介绍与环境安装
  • 连载二 搭建Blog API's(一)
  • 连载三 搭建Blog API's(二)
  • 连载四 搭建Blog API's(三)
  • 连载五 使用JWT进行身份校验
  • 连载六 编写一个简单的文件日志
  • 连载七 Golang优雅重启HTTP服务
  • 连载八 为它加上Swagger
  • 连载九 将Golang应用部署到Docker
  • 连载十 定制 GORM Callbacks
  • 连载十一 Cron定时任务
  • 连载十二 优化配置结构及实现图片上传
  • 连载十三 优化你的应用结构和实现Redis缓存
  • 连载十四 实现导出、导入 Excel
  • 连载十五 生成二维码、合并海报
  • 连载十六 在图片上绘制文字
  • 连载十七 用 Nginx 部署 Go 应用
  • 番外 Golang交叉编译
  • 番外 请入门 Makefile

拓展阅读

  • manners
  • graceful
  • grace
  • plugin: new package for loading plugins · golang/go@0cbb12f · GitHub

Gin实践 连载一 Golang介绍与环境安装

Gin实践 连载一 Golang介绍与环境安装

若有任何问题或建议,欢迎及时交流和碰撞。我的公众号是 【脑子进煎鱼了】,GitHub 地址:https://github.com/eddycjy。

Golang介绍与环境安装

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.

Gin是用Golang开发的一个微框架,类似Martinier的API,重点是小巧、易用、性能好很多,也因为 httprouter 的性能提高了40倍。

准备环节

一、安装Golang

首先,根据对应的操作系统选择安装包下载,

在这里我使用的是Centos 64位系统

wget https://studygolang.com/dl/golang/go1.9.2.linux-amd64.tar.gz

tar -zxvf go1.9.2.linux-amd64.tar.gz

mv go/ /usr/local/

配置 /etc/profile

vi /etc/profile

添加环境变量GOROOT和将GOBIN添加到PATH中

export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin

添加环境变量GOPATH(这个可按实际情况设置目录位置)

export GOPATH=/usr/local/go/path

配置完毕后,执行命令令其生效

source /etc/profile

在控制台输入go version,若输出版本号则安装成功

那么大家会有些疑问,纠结go本身有什么东西,我们刚刚设置的环境变量是什么?

1、 go本身有什么东西

首先,我们在解压的时候会得到一个名为go的文件夹,其中包括了所有Go语言相关的一些文件,在这下面又包含很多文件夹和文件,我们来简单说明其中主要文件夹的作为:

  • api:用于存放依照Go版本顺序的API增量列表文件。这里所说的API包含公开的变量、常量、函数等。这些API增量列表文件用于Go语言API检查
  • bin:用于存放主要的标准命令文件(可执行文件),包含gogodocgofmt
  • blog:用于存放官方博客中的所有文章
  • doc:用于存放标准库的HTML格式的程序文档。我们可以通过godoc命令启动一个Web程序展示这些文档
  • lib:用于存放一些特殊的库文件
  • misc:用于存放一些辅助类的说明和工具
  • pkg:用于存放安装Go标准库后的所有归档文件(以.a结尾的文件)。注意,你会发现其中有名称为linux_amd64的文件夹,我们称为平台相关目录。这类文件夹的名称由对应的操作系统和计算架构的名称组合而成。通过go install命令,Go程序会被编译成平台相关的归档文件存放到其中
  • src:用于存放Go自身、Go标准工具以及标准库的所有源码文件
  • test:存放用来测试和验证Go本身的所有相关文件

2、 刚刚设置的环境变量是什么

  • GOROOT:Go的根目录
  • GOPATH:用户工作区
  • PATH下增加$GOROOT/bin:Gobin下会存放可执行文件,我们把他加入PATH中就可以直接在命令行使用

3、 工作区是什么?

这在Go中是一个非常重要的概念,在一般情况下,Go源码文件必须放在工作区中,也就是说,我们写的项目代码都必须放在我们所设定的工作区中,虽然对于命令源码文件来说,这不是必须的。但我们大多都是前一种情况。工作区其实就是一个对应特定工程的目录,它应包含3个子目录:src目录、pkg目录、bin目录

  • src:用于以代码包的形式组织并保存Go源码文件
  • pkg:用于存放通过go install命令安装后的代码包的归档文件(.a 结尾的文件)
  • bin:与pkg目录类似,在通过go install命令完成安装后,保存由Go命令源码文件生成的可执行文件

4、 什么是命令源码文件?

如果一个源码文件被声明属于main代码包,且该文件代码中包含无参数声明和结果声明的main函数,则它就是命令源码文件。命令源码文件可通过go run命令直接启动运行

二、安装Govendor

If using go1.5, ensure GO15VENDOREXPERIMENT=1 is set.

在命令行下执行安装

go get -u github.com/kardianos/govendor

等待一会,安装成功后。

我们cd /usr/local/go/path(第三方依赖包,会默认安装在GOPATH的第一个目录下)目录,

执行ls,可以在工作区中看到binpkgsrc三个目录。这就是我们上面一小节所说的工作区了!

那么,我们所安装的govendor去哪里了呢?

答案就在工作区内,所生成的代码包大概是这样。我们所需要的是编译好的可执行文件,在/usr/local/go/path/bin中。

path/
├── bin
│   └── govendor
├── pkg
│   └── linux_amd64
│       └── github.com
│           └── kardianos
│               └── govendor
│                   ├── ...
└── src
    └── github.com
        └── kardianos
            └── govendor
                ├── ...

大家还记得我们先前在环境变量PATH中设置了GOBIN,

我们现在要做的就是把工作区中bin目录下的可执行文件govendor给移动过去,或者你可以将$GOPATH的BIN目录给加入环境变量中

那样就可以直接在命令行直接执行govendor

mv /usr/local/go/path/bin/govendor /usr/local/go/bin/

移动成功后,在命令行执行govendor -version,若出现版本号,则成功

#govendor -version
$ v1.0.9

在这里为什么单独挑出一节来讲govendor呢?

大家可以想想,虽然我们在本地开发,利用$GOPATH达到安装第三方依赖包,然后去使用他,似乎也没有什么问题。

但是在实际的多人协作及部署中是有问题的

  • 每一个新来的人都要go get很多次
  • 拉下来的版本还可能不一样
  • 线上部署更麻烦了

因此我们在这简单的使用govendor来解决这个问题,在这个项目完成的最后,你只需govendor initgovendor add +external就能美滋滋的把依赖包都放到项目的vendor目录中,就能把它一同传上你的版本库里了,是不是很方便呢。

当然了,目前官方推荐的包管理工具就有十几种,大家可以适当考察一下,这个不在本篇的范围内。

三、安装Gin

在命令行下执行安装

go get -u github.com/gin-gonic/gin

检查/usr/local/go/path中是否存在gin的代码包

四、测试Gin是否安装成功

编写一个test.go文件

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}

执行test.go

go run test.go

访问$HOST:8080/ping,若返回{"message":"pong"}则正确

curl 127.0.0.1:8080/ping

至此,我们的环境安装都基本完成了:)

具体gin的介绍从连载二开始,会讲解Demo所涉及的知识点!

参考

本系列示例代码

  • go-gin-example

本系列目录

  • 连载一 Golang介绍与环境安装
  • 连载二 搭建Blog API''s(一)
  • 连载三 搭建Blog API''s(二)
  • 连载四 搭建Blog API''s(三)
  • 连载五 使用JWT进行身份校验
  • 连载六 编写一个简单的文件日志
  • 连载七 Golang优雅重启HTTP服务
  • 连载八 为它加上Swagger
  • 连载九 将Golang应用部署到Docker
  • 连载十 定制 GORM Callbacks
  • 连载十一 Cron定时任务
  • 连载十二 优化配置结构及实现图片上传
  • 连载十三 优化你的应用结构和实现Redis缓存
  • 连载十四 实现导出、导入 Excel
  • 连载十五 生成二维码、合并海报
  • 连载十六 在图片上绘制文字
  • 连载十七 用 Nginx 部署 Go 应用
  • 番外 Golang交叉编译
  • 番外 请入门 Makefile

相关文档

  • Gin
  • Gin Web Framework
  • Go并发编程实战
  • govendor

Gin实践 连载九 将Golang应用部署到Docker

Gin实践 连载九 将Golang应用部署到Docker

项目地址:https://github.com/EDDYCJY/go... (快上车,支持一波)
原文地址:将Golang应用部署到Docker

注:

  • 开始前你需要安装好 docker,配好镜像源
  • 本章节源码在 f-20180324-docker 分支上
  • 从本章节开始 项目目录都以 go-gin-example 为基准(请配合自己本地项目灵活变动)

介绍

在这里简单介绍下Docker,建议深入学习

image

Docker 是一个开源的轻量级容器技术,让开发者可以打包他们的应用以及应用运行的上下文环境到一个可移植的镜像中,然后发布到任何支持Docker的系统上运行。 通过容器技术,在几乎没有性能开销的情况下,Docker 为应用提供了一个隔离运行环境

  • 简化配置
  • 代码流水线管理
  • 提高开发效率
  • 隔离应用
  • 快速、持续部署

接下来我们正式开始对项目进行 docker 的所需处理和编写,每一个大标题为步骤大纲

Golang

一、编写Dockerfile

go-gin-example 项目根目录创建 Dockerfile 文件,写入内容

FROM golang:latest

WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example
COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example
RUN go build .

EXPOSE 8000
ENTRYPOINT ["./go-gin-example"]

作用

golang:latest 镜像为基础镜像,将工作目录设置为 $GOPATH/src/go-gin-example,并将当前上下文目录的内容复制到 $GOPATH/src/go-gin-example

在进行 go build 编译完毕后,将容器启动程序设置为 ./go-gin-example,也就是我们所编译的可执行文件

注意 go-gin-exampledocker 容器里编译,并没有在宿主机现场编译

说明

Dockerfile 文件是用于定义 Docker 镜像生成流程的配置文件,文件内容是一条条指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建;这些指令应用于基础镜像并最终创建一个新的镜像

你可以认为用于快速创建自定义的 Docker 镜像

1、 FROM

指定基础镜像(必须有的指令,并且必须是第一条指令)

2、 WORKDIR

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如果目录不存在,WORKDIR 会帮你建立目录

3、COPY

格式:

COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置

4、RUN

用于执行命令行命令

格式:RUN <命令>

5、EXPOSE

格式为 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务

在 Dockerfile 中写入这样的声明有两个好处

  • 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射
  • 运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口

6、ENTRYPOINT

ENTRYPOINT 的格式和 RUN 指令格式一样,分为两种格式

  • exec 格式:
<ENTRYPOINT> "<CMD>"
  • shell 格式:
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]

ENTRYPOINT 指令是指定容器启动程序及参数

二、构建镜像

go-gin-example 的项目根目录下执行 docker build -t gin-blog-docker .

该命令作用是创建/构建镜像,-t 指定名称为 gin-blog-docker. 构建内容为当前上下文目录

$ docker build -t gin-blog-docker .
Sending build context to Docker daemon 96.39 MB
Step 1/6 : FROM golang:latest
 ---> d632bbfe5767
Step 2/6 : WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example
 ---> 56294f978c5d
Removing intermediate container e112997b995d
Step 3/6 : COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example
 ---> 3b60960120cf
Removing intermediate container 63e310b3f60c
Step 4/6 : RUN go build .
 ---> Running in 52648a431450

 ---> 7bfbeb301fea
Removing intermediate container 52648a431450
Step 5/6 : EXPOSE 8000
 ---> Running in 98f5b387d1bb
 ---> b65bd4076c65
Removing intermediate container 98f5b387d1bb
Step 6/6 : ENTRYPOINT ./go-gin-example
 ---> Running in c4f6cdeb667b
 ---> d8a109c7697c
Removing intermediate container c4f6cdeb667b
Successfully built d8a109c7697c

三、验证镜像

查看所有的镜像,确定刚刚构建的 gin-blog-docker 镜像是否存在

$ docker images
REPOSITORY              TAG                 IMAGE ID            CREATED              SIZE
gin-blog-docker         latest              d8a109c7697c        About a minute ago   946 MB
docker.io/golang        latest              d632bbfe5767        8 days ago           779 MB
...

四、创建并运行一个新容器

执行命令 docker run -p 8000:8000 gin-blog-docker

$ docker run -p 8000:8000 gin-blog-docker
dial tcp 127.0.0.1:3306: connect: connection refused
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:    export GIN_MODE=release
 - using code:    gin.SetMode(gin.ReleaseMode)

...
Actual pid is 1

运行成功,你以为大功告成了吗?

你想太多了,仔细看看控制台的输出了一条错误 dial tcp 127.0.0.1:3306: connect: connection refused

我们研判一下,发现是 Mysql 的问题,接下来第二项我们将解决这个问题

Mysql

一、拉取镜像

Docker 的公共仓库 Dockerhub 下载 MySQL 镜像(国内建议配个镜像)

$ docker pull mysql

二、创建并运行一个新容器

运行 Mysql 容器,并设置执行成功后返回容器ID

$ docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=rootroot -d mysql
8c86ac986da4922492934b6fe074254c9165b8ee3e184d29865921b0fef29e64

连接 Mysql

初始化的 Mysql 应该如图

image

Golang + Mysql

一、删除镜像

由于原本的镜像存在问题,我们需要删除它,此处有几种做法

  • 删除原本有问题的镜像,重新构建一个新镜像
  • 重新构建一个不同 nametag 的新镜像

删除原本的有问题的镜像,-f 是强制删除及其关联状态

若不执行 -f,你需要执行 docker ps -a 查到所关联的容器,将其 rm 解除两者依赖关系

$ docker rmi -f gin-blog-docker
Untagged: gin-blog-docker:latest
Deleted: sha256:d8a109c7697c3c2d9b4de7dbb49669d10106902122817b6467a031706bc52ab4
Deleted: sha256:b65bd4076c65a3c24029ca4def3b3f37001ff7c9eca09e2590c4d29e1e23dce5
Deleted: sha256:7bfbeb301fea9d8912a4b7c43e4bb8b69bdc57f0b416b372bfb6510e476a7dee
Deleted: sha256:3b60960120cf619181c1762cdc1b8ce318b8c815e056659809252dd321bcb642
Deleted: sha256:56294f978c5dfcfa4afa8ad033fd76b755b7ecb5237c6829550741a4d2ce10bc

二、修改配置文件

将项目的配置文件 conf/app.ini,内容修改为

#debug or release
RUN_MODE = debug

[app]
PAGE_SIZE = 10
JWT_SECRET = 233

[server]
HTTP_PORT = 8000
READ_TIMEOUT = 60
WRITE_TIMEOUT = 60

[database]
TYPE = mysql
USER = root
PASSWORD = rootroot
HOST = mysql:3306
NAME = blog
TABLE_PREFIX = blog_

三、重新构建镜像

重复先前的步骤,回到 gin-blog 的项目根目录下执行 docker build -t gin-blog-docker .

四、创建并运行一个新容器

关联

Q:我们需要将 Golang 容器和 Mysql 容器关联起来,那么我们需要怎么做呢?

A:增加命令 --link mysql:mysqlGolang 容器与 Mysql 容器互联;通过 --link可以在容器内直接使用其关联的容器别名进行访问,而不通过IP,但是--link只能解决单机容器间的关联,在分布式多机的情况下,需要通过别的方式进行连接

运行

执行命令 docker run --link mysql:mysql -p 8000:8000 gin-blog-docker

$ docker run --link mysql:mysql -p 8000:8000 gin-blog-docker
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:    export GIN_MODE=release
 - using code:    gin.SetMode(gin.ReleaseMode)
...
Actual pid is 1

结果

检查启动输出、接口测试、数据库内数据,均正常;我们的 Golang 容器和 Mysql 容器成功关联运行,大功告成 :)


Review

思考

虽然应用已经能够跑起来了

但如果对 GolangDocker 有一定的了解,我希望你能够想到至少2个问题

  • 为什么 gin-blog-docker 占用空间这么大?(可用 docker ps -as | grep gin-blog-docker 查看)
  • Mysql 容器直接这么使用,数据存储到哪里去了?

创建超小的Golang镜像

Q:第一个问题,为什么这么镜像体积这么大?

A:FROM golang:latest 拉取的是官方 golang 镜像,包含Golang的编译和运行环境,外加一堆GCC、build工具,相当齐全

这是有问题的,我们可以不在Golang容器中现场编译的,压根用不到那些东西,我们只需要一个能够运行可执行文件的环境即可

构建Scratch镜像

Scratch镜像,简洁、小巧,基本是个空镜像

一、修改Dockerfile
FROM scratch

WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example
COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example

EXPOSE 8000
CMD ["./go-gin-example"]
二、编译可执行文件
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o go-gin-example .

编译所生成的可执行文件会依赖一些库,并且是动态链接。在这里因为使用的是 scratch 镜像,它是空镜像,因此我们需要将生成的可执行文件静态链接所依赖的库

三、构建镜像
$ docker build -t gin-blog-docker-scratch .
Sending build context to Docker daemon 133.1 MB
Step 1/5 : FROM scratch
 ---> 
Step 2/5 : WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example
 ---> Using cache
 ---> ee07e166a638
Step 3/5 : COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example
 ---> 1489a0693d51
Removing intermediate container e3e9efc0fe4d
Step 4/5 : EXPOSE 8000
 ---> Running in b5630de5544a
 ---> 6993e9f8c944
Removing intermediate container b5630de5544a
Step 5/5 : CMD ./go-gin-example
 ---> Running in eebc0d8628ae
 ---> 5310bebeb86a
Removing intermediate container eebc0d8628ae
Successfully built 5310bebeb86a

注意,假设你的Golang应用没有依赖任何的配置等文件,是可以直接把可执行文件给拷贝进去即可,其他都不必关心

这里可以有好几种解决方案

  • 依赖文件统一管理挂载
  • go-bindata 一下

...

因此这里如果解决了文件依赖的问题后,就不需要把目录给 COPY 进去了

四、运行
$ docker run --link mysql:mysql -p 8000:8000 gin-blog-docker-scratch
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:    export GIN_MODE=release
 - using code:    gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /auth                     --> github.com/EDDYCJY/go-gin-example/routers/api.GetAuth (3 handlers)
...

成功运行,程序也正常接收请求

接下来我们再看看占用大小,执行 docker ps -as 命令

$ docker ps -as
CONTAINER ID        IMAGE                     COMMAND                  ...         SIZE
9ebdba5a8445        gin-blog-docker-scratch   "./go-gin-example"       ...     0 B (virtual 132 MB)
427ee79e6857        gin-blog-docker           "./go-gin-example"       ...     0 B (virtual 946 MB)

从结果而言,占用大小以Scratch镜像为基础的容器完胜,完成目标

Mysql挂载数据卷

倘若不做任何干涉,在每次启动一个 Mysql 容器时,数据库都是空的。另外容器删除之后,数据就丢失了(还有各类意外情况),非常糟糕!

数据卷

数据卷 是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v 这个命令

数据卷 是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:

  • 数据卷 可以在容器之间共享和重用
  • 对 数据卷 的修改会立马生效
  • 对 数据卷 的更新,不会影响镜像
  • 数据卷 默认会一直存在,即使容器被删除
注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的 数据卷。

如何挂载

首先创建一个目录用于存放数据卷;示例目录 /data/docker-mysql,注意 --name 原本名称为 mysql 的容器,需要将其删除 docker rm

$ docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=rootroot -v /data/docker-mysql:/var/lib/mysql -d mysql
54611dbcd62eca33fb320f3f624c7941f15697d998f40b24ee535a1acf93ae72

创建成功,检查目录 /data/docker-mysql,下面多了不少数据库文件

验证

接下来交由你进行验证,目标是创建一些测试表和数据,然后删除当前容器,重新创建的容器,数据库数据也依然存在(当然了数据卷指向要一致)

我已验证完毕,你呢?

参考

本系列示例代码

  • go-gin-example

本系列目录

  • 连载一 Golang介绍与环境安装
  • 连载二 搭建Blog API''s(一)
  • 连载三 搭建Blog API''s(二)
  • 连载四 搭建Blog API''s(三)
  • 连载五 使用JWT进行身份校验
  • 连载六 编写一个简单的文件日志
  • 连载七 Golang优雅重启HTTP服务
  • 连载八 为它加上Swagger
  • 连载九 将Golang应用部署到Docker
  • 连载十 定制 GORM Callbacks
  • 连载十一 Cron定时任务
  • 连载十二 优化配置结构及实现图片上传
  • 连载十三 优化你的应用结构和实现Redis缓存
  • 连载十四 实现导出、导入 Excel
  • 连载十五 生成二维码、合并海报
  • 连载十六 在图片上绘制文字
  • 连载十七 用 Nginx 部署 Go 应用
  • 番外 Golang交叉编译
  • 番外 请入门 Makefile

书籍

  • Docker —— 从入门到实践

Golang Gin实践 连载十七 用 Nginx 部署 Go 应用

Golang Gin实践 连载十七 用 Nginx 部署 Go 应用

原文地址:Golang Gin实践 连载十七 用 Nginx 部署 Go 应用

前言

如果已经看过前面 “十六部连载,两部番外”,相信您的能力已经有所提升

那么,现在今天来说说简单部署后端服务的事儿

做什么

在本章节,我们将简单介绍 Nginx 以及使用 Nginx 来完成对 go-gin-example 的部署,会实现反向代理和简单负载均衡的功能

Nginx

是什么

Nginx 是一个 Web Server,可以用作反向代理、负载均衡、邮件代理、TCP / UDP、HTTP 服务器等等,它拥有很多吸引人的特性,例如:

  • 以较低的内存占用率处理 10,000 多个并发连接(每10k非活动HTTP保持活动连接约2.5 MB )
  • 静态服务器(处理静态文件)
  • 正向、反向代理
  • 负载均衡
  • 通过OpenSSL 对 TLS / SSL 与 SNI 和 OCSP 支持
  • FastCGI、SCGI、uWSGI 的支持
  • WebSockets、HTTP/1.1 的支持
  • Nginx + Lua

安装

请右拐谷歌或百度,安装好 Nginx 以备接下来的使用

简单讲解

常用命令

  • nginx:启动 Nginx
  • nginx -s stop:立刻停止 Nginx 服务
  • nginx -s reload:重新加载配置文件
  • nginx -s quit:平滑停止 Nginx 服务
  • nginx -t:测试配置文件是否正确
  • nginx -v:显示 Nginx 版本信息
  • nginx -V:显示 Nginx 版本信息、编译器和配置参数的信息

涉及配置

1、 proxy_pass:配置反向代理的路径。需要注意的是如果 proxy_pass 的 url 最后为
/,则表示绝对路径。否则(不含变量下)表示相对路径,所有的路径都会被代理过去

2、 upstream:配置负载均衡,upstream 默认是以轮询的方式进行负载,另外还支持四种模式,分别是:

(1)weight:权重,指定轮询的概率,weight 与访问概率成正比

(2)ip_hash:按照访问 IP 的 hash 结果值分配

(3)fair:按后端服务器响应时间进行分配,响应时间越短优先级别越高

(4)url_hash:按照访问 URL 的 hash 结果值分配

部署

在这里需要对 nginx.conf 进行配置,如果你不知道对应的配置文件是哪个,可执行 nginx -t 看一下

$ nginx -t
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful

显然,我的配置文件在 /usr/local/etc/nginx/ 目录下,并且测试通过

反向代理

反向代理是指以代理服务器来接受网络上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。(来自百科)

image

配置 hosts

由于需要用本机作为演示,因此先把映射配上去,打开 /etc/hosts,增加内容:

127.0.0.1       api.blog.com

配置 nginx.conf

打开 nginx 的配置文件 nginx.conf(我的是 /usr/local/etc/nginx/nginx.conf),我们做了如下事情:

增加 server 片段的内容,设置 server_name 为 api.blog.com 并且监听 8081 端口,将所有路径转发到 http://127.0.0.1:8000/

worker_processes  1;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       8081;
        server_name  api.blog.com;

        location / {
            proxy_pass http://127.0.0.1:8000/;
        }
    }
}

验证

启动 go-gin-example

回到 go-gin-example 的项目下,执行 make,再运行 ./go-gin-exmaple

$ make
github.com/EDDYCJY/go-gin-example
$ ls
LICENSE        README.md      conf           go-gin-example middleware     pkg            runtime        vendor
Makefile       README_ZH.md   docs           main.go        models         routers        service
$ ./go-gin-example 
...
[GIN-debug] DELETE /api/v1/articles/:id      --> github.com/EDDYCJY/go-gin-example/routers/api/v1.DeleteArticle (4 handlers)
[GIN-debug] POST   /api/v1/articles/poster/generate --> github.com/EDDYCJY/go-gin-example/routers/api/v1.GenerateArticlePoster (4 handlers)
Actual pid is 14672
重启 nginx
$ nginx -t
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
$ nginx -s reload
访问接口

image

如此,就实现了一个简单的反向代理了,是不是很简单呢

负载均衡

负载均衡,英文名称为Load Balance(常称 LB),其意思就是分摊到多个操作单元上进行执行(来自百科)

你能从运维口中经常听见,XXX 负载怎么突然那么高。 那么它到底是什么呢?

其背后一般有多台 server,系统会根据配置的策略(例如 Nginx 有提供四种选择)来进行动态调整,尽可能的达到各节点均衡,从而提高系统整体的吞吐量和快速响应

如何演示

前提条件为多个后端服务,那么势必需要多个 go-gin-example,为了演示我们可以启动多个端口,达到模拟的效果

为了便于演示,分别在启动前将 conf/app.ini 的应用端口修改为 8001 和 8002(也可以做成传入参数的模式),达到启动 2 个监听 8001 和 8002 的后端服务

配置 nginx.conf

回到 nginx.conf 的老地方,增加负载均衡所需的配置。新增 upstream 节点,设置其对应的 2 个后端服务,最后修改了 proxy_pass 指向(格式为 http:// + upstream 的节点名称)

worker_processes  1;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    upstream api.blog.com {
        server 127.0.0.1:8001;
        server 127.0.0.1:8002;
    }

    server {
        listen       8081;
        server_name  api.blog.com;

        location / {
            proxy_pass http://api.blog.com/;
        }
    }
}
重启 nginx
$ nginx -t
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
$ nginx -s reload

验证

再重复访问 http://api.blog.com:8081/auth?username={USER_NAME}}&password={PASSWORD},多访问几次便于查看效果

目前 Nginx 没有进行特殊配置,那么它是轮询策略,而 go-gin-example 默认开着 debug 模式,看看请求 log 就明白了

image

image

总结

在本章节,希望您能够简单习得日常使用的 Web Server 背后都是一些什么逻辑,Nginx 是什么?反向代理?负载均衡?

怎么简单部署,知道了吧。

参考

本系列示例代码

  • go-gin-example

本系列目录

  • 连载一 Golang介绍与环境安装
  • 连载二 搭建Blog API''s(一)
  • 连载三 搭建Blog API''s(二)
  • 连载四 搭建Blog API''s(三)
  • 连载五 使用JWT进行身份校验
  • 连载六 编写一个简单的文件日志
  • 连载七 Golang优雅重启HTTP服务
  • 连载八 为它加上Swagger
  • 连载九 将Golang应用部署到Docker
  • 连载十 定制 GORM Callbacks
  • 连载十一 Cron定时任务
  • 连载十二 优化配置结构及实现图片上传
  • 连载十三 优化你的应用结构和实现Redis缓存
  • 连载十四 实现导出、导入 Excel
  • 连载十五 生成二维码、合并海报
  • 连载十六 在图片上绘制文字
  • 连载十七 用 Nginx 部署 Go 应用
  • 番外 Golang交叉编译
  • 番外 请入门 Makefile

Golang Gin实践 连载十三 优化你的应用结构和实现Redis缓存

Golang Gin实践 连载十三 优化你的应用结构和实现Redis缓存

原文地址:优化你的应用结构和实现Redis缓存
项目地址:https://github.com/EDDYCJY/go...

如果对你有所帮助,欢迎点个 Star

前言

之前就在想,不少教程或示例的代码设计都是一步到位的(也没问题)

但实际操作的读者真的能够理解透彻为什么吗?左思右想,有了今天这一章的内容,我认为实际经历过一遍印象会更加深刻

规划

在本章节,将介绍以下功能的整理:

  • 抽离、分层业务逻辑:减轻 routers/*.go 内的 api方法的逻辑(但本文暂不分层 repository,这块逻辑还不重)
  • 增加容错性:对 gorm 的错误进行判断
  • Redis缓存:对获取数据类的接口增加缓存设置
  • 减少重复冗余代码

问题在哪?

在规划阶段我们发现了一个问题,这是目前的伪代码:

if ! HasErrors() {
    if ExistArticleByID(id) {
        DeleteArticle(id)
        code = e.SUCCESS
    } else {
        code = e.ERROR_NOT_EXIST_ARTICLE
    }
} else {
    for _, err := range valid.Errors {
        logging.Info(err.Key, err.Message)
    }
}

c.JSON(http.StatusOK, gin.H{
    "code": code,
    "msg":  e.GetMsg(code),
    "data": make(map[string]string),
})

如果加上规划内的功能逻辑呢,伪代码会变成:

if ! HasErrors() {
    exists, err := ExistArticleByID(id)
    if err == nil {
        if exists {
            err = DeleteArticle(id)
            if err == nil {
                code = e.SUCCESS
            } else {
                code = e.ERROR_XXX
            }
        } else {
            code = e.ERROR_NOT_EXIST_ARTICLE
        }
    } else {
        code = e.ERROR_XXX
    }
} else {
    for _, err := range valid.Errors {
        logging.Info(err.Key, err.Message)
    }
}

c.JSON(http.StatusOK, gin.H{
    "code": code,
    "msg":  e.GetMsg(code),
    "data": make(map[string]string),
})

如果缓存的逻辑也加进来,后面慢慢不断的迭代,岂不是会变成如下图一样?

image

现在我们发现了问题,应及时解决这个代码结构问题,同时把代码写的清晰、漂亮、易读易改也是一个重要指标

如何改?

在左耳朵耗子的文章中,这类代码被称为 “箭头型” 代码,有如下几个问题:

1、我的显示器不够宽,箭头型代码缩进太狠了,需要我来回拉水平滚动条,这让我在读代码的时候,相当的不舒服

2、除了宽度外还有长度,有的代码的 if-else 里的 if-else 里的 if-else 的代码太多,读到中间你都不知道中间的代码是经过了什么样的层层检查才来到这里的

总而言之,“箭头型代码”如果嵌套太多,代码太长的话,会相当容易让维护代码的人(包括自己)迷失在代码中,因为看到最内层的代码时,你已经不知道前面的那一层一层的条件判断是什么样的,代码是怎么运行到这里的,所以,箭头型代码是非常难以维护和Debug的。

简单的来说,就是让出错的代码先返回,前面把所有的错误判断全判断掉,然后就剩下的就是正常的代码了

(注意:本段引用自耗子哥的 如何重构“箭头型”代码,建议细细品尝)

落实

本项目将对既有代码进行优化和实现缓存,希望你习得方法并对其他地方也进行优化

第一步:完成 Redis 的基础设施建设(需要你先装好 Redis)

第二步:对现有代码进行拆解、分层(不会贴上具体步骤的代码,希望你能够实操一波,加深理解)

Redis

一、配置

打开 conf/app.ini 文件,新增配置:

...
[redis]
Host = 127.0.0.1:6379
Password =
MaxIdle = 30
MaxActive = 30
IdleTimeout = 200

二、缓存 Prefix

打开 pkg/e 目录,新建 cache.go,写入内容:

package e

const (
    CACHE_ARTICLE = "ARTICLE"
    CACHE_TAG     = "TAG"
)

三、缓存 Key

(1)、打开 service 目录,新建 cache_service/article.go

写入内容:传送门

(2)、打开 service 目录,新建 cache_service/tag.go

写入内容:传送门

这一部分主要是编写获取缓存 KEY 的方法,直接参考传送门即可

四、Redis 工具包

打开 pkg 目录,新建 gredis/redis.go,写入内容:

package gredis

import (
    "encoding/json"
    "time"

    "github.com/gomodule/redigo/redis"

    "github.com/EDDYCJY/go-gin-example/pkg/setting"
)

var RedisConn *redis.Pool

func Setup() error {
    RedisConn = &redis.Pool{
        MaxIdle:     setting.RedisSetting.MaxIdle,
        MaxActive:   setting.RedisSetting.MaxActive,
        IdleTimeout: setting.RedisSetting.IdleTimeout,
        Dial: func() (redis.Conn, error) {
            c, err := redis.Dial("tcp", setting.RedisSetting.Host)
            if err != nil {
                return nil, err
            }
            if setting.RedisSetting.Password != "" {
                if _, err := c.Do("AUTH", setting.RedisSetting.Password); err != nil {
                    c.Close()
                    return nil, err
                }
            }
            return c, err
        },
        TestOnBorrow: func(c redis.Conn, t time.Time) error {
            _, err := c.Do("PING")
            return err
        },
    }

    return nil
}

func Set(key string, data interface{}, time int) (bool, error) {
    conn := RedisConn.Get()
    defer conn.Close()

    value, err := json.Marshal(data)
    if err != nil {
        return false, err
    }

    reply, err := redis.Bool(conn.Do("SET", key, value))
    conn.Do("EXPIRE", key, time)

    return reply, err
}

func Exists(key string) bool {
    conn := RedisConn.Get()
    defer conn.Close()

    exists, err := redis.Bool(conn.Do("EXISTS", key))
    if err != nil {
        return false
    }

    return exists
}

func Get(key string) ([]byte, error) {
    conn := RedisConn.Get()
    defer conn.Close()

    reply, err := redis.Bytes(conn.Do("GET", key))
    if err != nil {
        return nil, err
    }

    return reply, nil
}

func Delete(key string) (bool, error) {
    conn := RedisConn.Get()
    defer conn.Close()

    return redis.Bool(conn.Do("DEL", key))
}

func LikeDeletes(key string) error {
    conn := RedisConn.Get()
    defer conn.Close()

    keys, err := redis.Strings(conn.Do("KEYS", "*"+key+"*"))
    if err != nil {
        return err
    }

    for _, key := range keys {
        _, err = Delete(key)
        if err != nil {
            return err
        }
    }

    return nil
}

在这里我们做了一些基础功能封装

1、设置 RedisConn 为 redis.Pool(连接池)并配置了它的一些参数:

  • Dial:提供创建和配置应用程序连接的一个函数
  • TestOnBorrow:可选的应用程序检查健康功能
  • MaxIdle:最大空闲连接数
  • MaxActive:在给定时间内,允许分配的最大连接数(当为零时,没有限制)
  • IdleTimeout:在给定时间内将会保持空闲状态,若到达时间限制则关闭连接(当为零时,没有限制)

2、封装基础方法

文件内包含 Set、Exists、Get、Delete、LikeDeletes 用于支撑目前的业务逻辑,而在里面涉及到了如方法:

(1)RedisConn.Get():在连接池中获取一个活跃连接

(2)conn.Do(commandName string, args ...interface{}):向 Redis 服务器发送命令并返回收到的答复

(3)redis.Bool(reply interface{}, err error):将命令返回转为布尔值

(4)redis.Bytes(reply interface{}, err error):将命令返回转为 Bytes

(5)redis.Strings(reply interface{}, err error):将命令返回转为 []string

在 redigo 中包含大量类似的方法,万变不离其宗,建议熟悉其使用规则和 Redis命令 即可

到这里为止,Redis 就可以愉快的调用啦。另外受篇幅限制,这块的深入讲解会另外开设!

拆解、分层

在先前规划中,引出几个方法去优化我们的应用结构

  • 错误提前返回
  • 统一返回方法
  • 抽离 Service,减轻 routers/api 的逻辑,进行分层
  • 增加 gorm 错误判断,让错误提示更明确(增加内部错误码)

编写返回方法

要让错误提前返回,c.JSON 的侵入是不可避免的,但是可以让其更具可变性,指不定哪天就变 XML 了呢?

1、打开 pkg 目录,新建 app/request.go,写入文件内容:

package app

import (
    "github.com/astaxie/beego/validation"

    "github.com/EDDYCJY/go-gin-example/pkg/logging"
)

func MarkErrors(errors []*validation.Error) {
    for _, err := range errors {
        logging.Info(err.Key, err.Message)
    }

    return
}

2、打开 pkg 目录,新建 app/response.go,写入文件内容:

package app

import (
    "github.com/gin-gonic/gin"

    "github.com/EDDYCJY/go-gin-example/pkg/e"
)

type Gin struct {
    C *gin.Context
}

func (g *Gin) Response(httpCode, errCode int, data interface{}) {
    g.C.JSON(httpCode, gin.H{
        "code": httpCode,
        "msg":  e.GetMsg(errCode),
        "data": data,
    })

    return
}

这样子以后如果要变动,直接改动 app 包内的方法即可

修改既有逻辑

打开 routers/api/v1/article.go,查看修改 GetArticle 方法后的代码为:

func GetArticle(c *gin.Context) {
    appG := app.Gin{c}
    id := com.StrTo(c.Param("id")).MustInt()
    valid := validation.Validation{}
    valid.Min(id, 1, "id").Message("ID必须大于0")

    if valid.HasErrors() {
        app.MarkErrors(valid.Errors)
        appG.Response(http.StatusOK, e.INVALID_PARAMS, nil)
        return
    }

    articleService := article_service.Article{ID: id}
    exists, err := articleService.ExistByID()
    if err != nil {
        appG.Response(http.StatusOK, e.ERROR_CHECK_EXIST_ARTICLE_FAIL, nil)
        return
    }
    if !exists {
        appG.Response(http.StatusOK, e.ERROR_NOT_EXIST_ARTICLE, nil)
        return
    }

    article, err := articleService.Get()
    if err != nil {
        appG.Response(http.StatusOK, e.ERROR_GET_ARTICLE_FAIL, nil)
        return
    }

    appG.Response(http.StatusOK, e.SUCCESS, article)
}

这里有几个值得变动点,主要是在内部增加了错误返回,如果存在错误则直接返回。另外进行了分层,业务逻辑内聚到了 service 层中去,而 routers/api(controller)显著减轻,代码会更加的直观

例如 service/article_service 下的 articleService.Get() 方法:

func (a *Article) Get() (*models.Article, error) {
    var cacheArticle *models.Article

    cache := cache_service.Article{ID: a.ID}
    key := cache.GetArticleKey()
    if gredis.Exists(key) {
        data, err := gredis.Get(key)
        if err != nil {
            logging.Info(err)
        } else {
            json.Unmarshal(data, &cacheArticle)
            return cacheArticle, nil
        }
    }

    article, err := models.GetArticle(a.ID)
    if err != nil {
        return nil, err
    }

    gredis.Set(key, article, 3600)
    return article, nil
}

而对于 gorm 的 错误返回设置,只需要修改 models/article.go 如下:

func GetArticle(id int) (*Article, error) {
    var article Article
    err := db.Where("id = ? AND deleted_on = ? ", id, 0).First(&article).Related(&article.Tag).Error
    if err != nil && err != gorm.ErrRecordNotFound {
        return nil, err
    }

    return &article, nil
}

习惯性增加 .Error,把控绝大部分的错误。另外需要注意一点,在 gorm 中,查找不到记录也算一种 “错误” 哦

最后

显然,本章节并不是你跟着我敲系列。我给你的课题是 “实现 Redis 缓存并优化既有的业务逻辑代码”

让其能够不断地适应业务的发展,让代码更清晰易读,且呈层级和结构性

如果有疑惑,可以到 go-gin-example 看看我是怎么写的,你是怎么写的,又分别有什么优势、劣势,取长补短一波?

参考

本系列示例代码

  • go-gin-example

本系列目录

  • 连载一 Golang介绍与环境安装
  • 连载二 搭建Blog API''s(一)
  • 连载三 搭建Blog API''s(二)
  • 连载四 搭建Blog API''s(三)
  • 连载五 使用JWT进行身份校验
  • 连载六 编写一个简单的文件日志
  • 连载七 Golang优雅重启HTTP服务
  • 连载八 为它加上Swagger
  • 连载九 将Golang应用部署到Docker
  • 连载十 定制 GORM Callbacks
  • 连载十一 Cron定时任务
  • 连载十二 优化配置结构及实现图片上传
  • 连载十三 优化你的应用结构和实现Redis缓存
  • 连载十四 实现导出、导入 Excel
  • 连载十五 生成二维码、合并海报
  • 连载十六 在图片上绘制文字
  • 连载十七 用 Nginx 部署 Go 应用
  • 番外 Golang交叉编译
  • 番外 请入门 Makefile

推荐阅读

  • 如何重构“箭头型”代码

今天关于Gin实践 连载七 Golang优雅重启HTTP服务golang 重启自己的介绍到此结束,谢谢您的阅读,有关Gin实践 连载一 Golang介绍与环境安装、Gin实践 连载九 将Golang应用部署到Docker、Golang Gin实践 连载十七 用 Nginx 部署 Go 应用、Golang Gin实践 连载十三 优化你的应用结构和实现Redis缓存等更多相关知识的信息可以在本站进行查询。

本文标签: