[Go]三、最简单的RestFul API服务器

目录

1. REST Web 框架选择

1.1、Gin 特性

2、安装gin

3、第一个Gin程序

 3.1、常见问题

4、GO的数据类型

4.1、变量的定义

4.2、简单类型

4.3、Go语言类型的转换

4.4、指针

4.5、nil零值

4.5.1、什么是nil呢?

5、API服务器健康状态自检

5.1、服务器健康有哪些?

5.2. 定义路由分组用于服务器健康检查

5.3、 服务器健康检查实现

5.4、安装依赖并测试

5.4.1、测试

5.5、启动apiserver时自检

5.5.1、测试


1. REST Web 框架选择

要编写一个 RESTful 风格的 API 服务器,首先需要一个 RESTful Web 框架,经过调研选择了 GitHub star 数最多的 Gin。采用轻量级的 Gin 框架,具有如下优点: 高性能 、 扩展性强 、 稳定性强 、相对而言比较 简洁 (查看性能对 比)。关于 Gin 的更多介绍可以参考 Golang 微框架 Gin 简介。

Gin 是使用 Go/golang 语言实现的 HTTP Web 框架。接口简洁,性能极高。截止 1.4.0 版本,包含测试代码,仅14K,其中测试代码 9K 左右,也就是说框架源码仅 5K 左右。

1.1、Gin 特性

  • 快速:路由不使用反射,基于Radix树,内存占用少。
  • 中间件HTTP请求,可先经过一系列中间件处理,例如:LoggerAuthorizationGZIP等。这个特性和NodeJs Koa 框架很像。中间件机制也极大地提高了框架的可扩展性。
    • 可以用于记录日志、请求时常和认证
  • 异常处理:服务始终可用,不会宕机。Gin 可以捕获 panic,并恢复。而且有极为便利的机制处理HTTP请求过程中发生的错误。
  • JSONGin可以解析并验证请求的JSON。这个特性对 Restful API 的开发尤其有用。
  • 路由分组:例如将需要授权和不需要授权的API分组,不同版本的API分组。而且分组可嵌套,且性能不受影响。
  • 渲染内置:原生支持JSONXMLHTML的渲染。

2、安装gin

手动安装Gin,也可以使用包模块管理,自动安装

[root@nginx-kafaka03 apiserver]# go get -u -v github.com/gin-gonic/gin

# 这个命令会解决一些依赖包

-v :打印出被构建的代码包的名字 -u :已存在相关的代码包,强行更新代码包及其依赖包

注意我们的代码不能放在$GOPATH(即这个路径:/root/code)

3、第一个Gin程序

构建了一个测试账号系统(后面统称为 apiserver )

创建apiserver文件夹,在里新建文件 main.go 。

这里的文件名可以不叫main.go,但是由于它是主运行文件,所以按惯例可以命名为main.go。

[root@nginx-kafaka03 apiserver]# cat main.go 
package main

import (
	"fmt"
	// 导入gin
	"github.com/gin-gonic/gin"
)
func main() {
	fmt.Println("vim-go")
	// 生成一个实例,这个实例即WSGI应用程序
	g := gin.Default()
	// 定义请求,第一个参数是请求路径;第二个参数是函数
	g.GET("/", func(c *gin.Context) {
		// 将数据交给Context的Render ==》返回数据
		c.String(200, "Hello,三创人")
	})  
	// 让应用运行在本地服务器上,默认监听端口是8080
	g.Run("0.0.0.0:8000") // listen and serve on 0.0.0.0:800
}

1. 首先,我们使用了 gin.Default() 生成了一个实例,这个实例即 WSGI 应用程序。

2. 接下来,我们使用 r.Get("/", ...) 声明了一个路由,告诉 Gin 什么样的URL 能触发传入的函数,这个函数返回我们想要显示在用户浏览器中的信息。

3. 最后用 r.Run() 函数来让应用运行在本地服务器上,默认监听端口是 8080,可以传入参数设置端口,例如r.Run(":8000") 即运行在 8080端口。

  • 初始化
[root@nginx-kafaka03 apiserver]# go mod init apiserver
go: creating new go.mod: module apiserver
  • 运行
[root@nginx-kafaka03 apiserver]# go run main.go
vim-go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[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    /                         --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:8000
[GIN] 2022/07/23 - 10:56:11 | 200 |       31.22µs |  192.168.29.128 | GET      "/"
[GIN] 2022/07/23 - 10:56:24 | 200 |      43.794µs |    192.168.29.1 | GET      "/"

起来之后,不要去动它,让他一直运行。只有这样,我们才能够访问到接口。 

  • 在浏览器访问 http://ip:8000

把脚本中的go.Run改为127.0.0.1:8000。然后重新运行go run main.go。

[root@nginx-kafaka03 apiserver]# curl http://127.0.0.1:8000
Hello,三创人[root@nginx-kafaka03 apiserver]# 

 0.0.0.0 代表本机上的任意ip地址;127.0.0.1 代表自己的lo接口(回环接口),只能本机访问,其他机器访问不了

 3.1、常见问题

  • 常见问题
    • 异常关闭
    • netstat 

在本地浏览器上访问Linux服务器上启动的服务不成功,成功访问的条件:

1、网络通:即ip能ping通,且端口通。这里一般大概率是防火墙禁止了端口访问,所以可以将防火墙关闭。

service firewalld stop

2、端口监听正常:确保端口是启动的

[root@nginx-kafaka03 apiserver]# netstat -anplut|grep 8000
tcp6       0      0 :::8000                 :::*                    LISTEN      2064/main    
[root@nginx-kafaka03 apiserver]# lsof -i:8000
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
main    2064 root    3u  IPv6  28117      0t0  TCP *:irdmi (LISTEN)

4、GO的数据类型

Go的数据类型分为四大类:

  • 基础类
    • 数字、字符串、布尔类
  • 聚合类型
    • 数组、结构体
  • 引用类型
    • 指针、slice、map、函数、管道
  • 接口类型

4.1、变量的定义

Go 语言是静态类型的,变量声明时必须明确变量的类型。Go 语言与其他语言显著不同的一个地方在于,Go 语言的类型在变量后面。比如 java 中,声明一个整体一般写成 int a = 1,在 Go 语言中,需要这么写:

  • 格式1:var identifier type

这种关键字的写法一般用于声明全局变量

var a int         // 如果没有赋值,默认为0
var a int = 1     // 声明时赋值
var a = 1         // 声明时赋值

var a = 1,因为 1 是 int 类型的,所以赋值时,a 自动被确定为 int 类型,所以类型名可以省略不写

  • 格式2:还有一种更简单的表达

这种声明方式只能在函数体中使用

// 这种方式相对来说用得更多一些
a := 1
msg := "Hello World!"
  • 格式3:一次声明多个变量var identifier1, identifier2 type
var b, c int = 1, 2

注意:

  • 变量需要定义后在使用
  • := 左侧的变量不应该是已经被声明过的,否则会导致编译错误。
  • 变量定义后没有使用,那么编译会失败"a declared but not used"
  • go会根据值确定类型,也会根据类型自动确定值

4.2、简单类型

类型类型
整型类型int(取决于操作系统), int8, int16, int32, int64, uint8, uint16, …10
浮点数类型float32, float6412.2
字节类型byte (等价于uint8) => 一个字节'a'
字符串类型string 在 Go 语言中,字符串使用 UTF8 编码'hello'
布尔值类型booleantrue 或 false

我们看下面的例子:

package main

import (
	"fmt"
	// 反射模块,核心包括两方面:类型(reflect.Type)、值(reflect.Value)
	"reflect"
)

func main() {
	fmt.Println("vim-go")
	var a int
	var b int = 1
	var c = 1
	fmt.Println(a, b, c)
	d := 1
	msg := "hello world"
	fmt.Println(d, msg)
	// 查看变量的类型
	e := 3.14
	fmt.Println(reflect.TypeOf(e))
	fmt.Println(reflect.TypeOf(msg))
	fmt.Println(reflect.TypeOf(a))
}

##### 运行结果
vim-go
0 1 1
1 hello world
float64
string
int

4.3、Go语言类型的转换

Go 不支持隐式转换类型

类型转换用于将一种数据类型的变量转换为另外一种类型的变量。Go 语言类型转换基本格式如下:

type_name(expression)
  • type_name 为类型
  • expression 为表达式

以下实例中将整型转化为浮点型,并计算结果,将结果赋值给浮点型变量:

package main

import "fmt"

func main() {
  var sum int = 17
  var count int = 5
  var mean float32

  mean = float32(sum)/float32(count)
  fmt.Printf("mean 的值为: %f\n",mean)
}

以上实例执行输出结果为:

mean 的值为: 3.400000

4.4、指针

指针即某个值的地址,类型定义时使用符号*,对一个已经存在的变量,使用&获得该变量的地址。

str := "Golang"
var p *string = &str // p 是指向 str 的指针
*p = "Hello"
fmt.Println(str) // 修改了 p,str 的值也发生了改变

// 运行结果为
Hello

一般来说,指针通常在函数传递参数,或者给某个类型定义新的方法时使用。Go 语言中,参数是按值传递的,如果不使用指针,函数内部将会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果参数使用指针,对参数的传递将会影响到外部变量。

例如:

func add(num int) {
	num += 1
}

func realAdd(num *int) {
	*num += 1
}

func main() {
	num := 100
	add(num)
	fmt.Println(num)  // 100,num 没有变化

	realAdd(&num)
	fmt.Println(num)  // 101,指针传递,num 被修改
}
  • reflect.TypeOf().Kind() 可以知道某个变量的类型,我们可以看到,字符串是以 byte 数组形式保存的,类型是 uint8,占1个 byte,打印时需要用 string 进行类型转换,否则打印的是编码值。
  • 因为字符串是以 byte 数组的形式存储的,所以,str2[2] 的值并不等于。str2 的长度 len(str2) 也不是 4,而是 8( Go 占 2 byte,语言占 6 byte)。

正确的处理方式是将 string 转为 rune 数组

str2 := "Go语言"
runeArr := []rune(str2)
fmt.Println(reflect.TypeOf(runeArr[2]).Kind()) // int32
fmt.Println(runeArr[2], string(runeArr[2]))    // 35821 语
fmt.Println("len(runeArr):", len(runeArr))    // len(runeArr): 4

转换成 []rune 类型后,字符串中的每个字符,无论占多少个字节都用 int32 来表示,因而可以正确处理中文。

4.5、nil零值

nil是go语言中预先的标识符,我们可以直接使用nil,而不用声明它。

  • nil的常用写法:
file,err := funcName(xxx)
if err!= nil{
	// do something....
	fmt.Println("代码有错误")
}
  • 获取函数返回值,其中当err不等于nil的时候,说明出现某些错误了,需要我们对这个错误进行一些处理
  • 如果err等于nil说明运行正常。

4.5.1、什么是nil呢?

nil的意思是无,或者是零值。

在Go语言中,如果你声明了一个变量但是没有对它进行赋值操作,那么这个变量就会有一个类型的默认零值。这是每种类型都有不同对应的零值:

nil的值类型必须是指针、通道、func、接口、映射或切片类型

类型定义零值
boolvar variable boolfalse
intvar variable int0
stringvar variable string""
pointersvar a *intnil
slicesnil
mapsnil
channelsnil
functionsnil
interfacesnil

5、API服务器健康状态自检

5.1、服务器健康有哪些?

思考: 服务器健康有哪些?如何检查?

  • 磁盘空间
  • CPU状态
  • MEM状态
  • 服务状态等

5.2. 定义路由分组用于服务器健康检查

由于后期我们会实现很多路由对应的处理函数,如果量大的话,router文件会变得非常大

因此,我们也可以将处理函数放到handler目录中

”apiserver/handler/sd“ 此目录将用于保存服务器检查相关处理函数

**注意:**短小的处理函数可以直接编写匿名函数放在router中,长函数建议拆分

apiserver/router/router.go

    // 加载模块-处理函数模块化
    "apiserver/handler/sd"

    // 在Load函数中添加    
    //  -modify here- 添加健康检查的handler
    svcd := g.Group("/sd")
    {   
        svcd.GET("/health", sd.HealthCheck)
        svcd.GET("/disk", sd.DiskCheck)
        svcd.GET("/cpu", sd.CPUCheck)
        svcd.GET("/ram", sd.RAMCheck)
    }

该代码块定义了一个叫 sd 的路由分组,在该分组下注册了 /health/disk/cpu/ram HTTP 路径,分别路由到 sd.HealthChecksd.DiskChecksd.CPUChecksd.RAMCheck 函数。

sd 分组主要用来检查 API Server 的状态:健康状况、服务器硬盘、CPU 和内存使用量。

main() 函数通过调用 router.Load 函数来加载路由,路由映射到具体的处理函数

树结构图:

[root@nginx-kafaka03 apiserver]# tree
.
├── go.mod
├── go.sum
├── handler
│   └── sd
│       └── check.go
├── main.go
└── router
    └── router.go

5.3、 服务器健康检查实现

apiserver/handler/sd/check.go

编写几个检查函数

package sd

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/shirou/gopsutil/cpu"
	"github.com/shirou/gopsutil/disk"
	"github.com/shirou/gopsutil/load"
	"github.com/shirou/gopsutil/mem"
)

// 定义常量
const (
	B  = 1
	KB = 1024 * B
	MB = 1024 * KB
	GB = 1024 * MB
)

// HealthCheck shows `OK` as the ping-pong result.
func HealthCheck(c *gin.Context) {
	message := "OK"
	// http.StatusOK => 所有HTTP状态码都对应到一个名字 (源码)
	c.String(http.StatusOK, "\n"+message)
}

// DiskCheck checks the disk usage.
func DiskCheck(c *gin.Context) {
    // 可查看disk.Usage的源代码,此处有2个返回值,*UsageStat, erro
	u, _ := disk.Usage("/")

	usedMB := int(u.Used) / MB
	usedGB := int(u.Used) / GB
	totalMB := int(u.Total) / MB
	totalGB := int(u.Total) / GB
	usedPercent := int(u.UsedPercent)

	status := http.StatusOK
	text := "OK"

	if usedPercent >= 95 {
		status = http.StatusInternalServerError
		text = "CRITICAL"
	} else if usedPercent >= 90 {
		status = http.StatusTooManyRequests
		text = "WARNING"
	}

	message := fmt.Sprintf("%s - Free space: %dMB (%dGB) / %dMB (%dGB) | Used: %d%%", text, usedMB, usedGB, totalMB, totalGB, usedPercent)
	c.String(status, "\n"+message)
}

// CPUCheck checks the cpu usage.
func CPUCheck(c *gin.Context) {
	cores, _ := cpu.Counts(false)

	a, _ := load.Avg()
	l1 := a.Load1
	l5 := a.Load5
	l15 := a.Load15

	status := http.StatusOK
	text := "OK"

	if l5 >= float64(cores-1) {
		status = http.StatusInternalServerError
		text = "CRITICAL"
	} else if l5 >= float64(cores-2) {
		status = http.StatusTooManyRequests
		text = "WARNING"
	}

	message := fmt.Sprintf("%s - Load average: %.2f, %.2f, %.2f | Cores: %d", text, l1, l5, l15, cores)
	c.String(status, "\n"+message)
}

// RAMCheck checks the disk usage.
func RAMCheck(c *gin.Context) {
	u, _ := mem.VirtualMemory()

	usedMB := int(u.Used) / MB
	usedGB := int(u.Used) / GB
	totalMB := int(u.Total) / MB
	totalGB := int(u.Total) / GB
	usedPercent := int(u.UsedPercent)

	status := http.StatusOK
	text := "OK"

	if usedPercent >= 95 {
		status = http.StatusInternalServerError
		text = "CRITICAL"
	} else if usedPercent >= 90 {
		status = http.StatusTooManyRequests
		text = "WARNING"
	}

	message := fmt.Sprintf("%s - Free space: %dMB (%dGB) / %dMB (%dGB) | Used: %d%%", text, usedMB, usedGB, totalMB, totalGB, usedPercent)
	c.String(status, "\n"+message)
}

5.4、安装依赖并测试

  • mod tidy 会自动检查依赖并下载需要的内容,非常nice

    go mod tidy 
    

    这里主要是安装了:

    go get github.com/shirou/gopsutil/cpu
    go get github.com/shirou/gopsutil/disk
    go get github.com/shirou/gopsutil/load
    go get github.com/shirou/gopsutil/mem

5.4.1、测试

  • 命令行测试
# curl http://localhost:8000/sd/health

OK
  • 浏览器测试

5.5、启动apiserver时自检

上面我们已经实现了几个接口用于获取服务器状态,但是,它需要我们主动访问才能获取状态,那么我们如何能在有问题时,直接收到提醒呢?

  • 定时任务/监控系统:编写监控脚本,有问题时提醒(邮件/短信/电话/微信/钉钉...)

    这部分在Linux部分会详细学习...

  • 启动服务时:主动检查,有问题直接停掉服务,提醒管理员

    有时候 API 进程起来不代表 API 服务器正常,如API 进程存在,但是服务器却不能对外提供服务。因此在启动 API 服务器时,如果能够最后做一个自检会更好些。

在 apiserver 中添加了自检程序,通过自检可以最大程度地保证启动后的 API 服务器处于健康状态。

apiserver/main.go

定义pingServer用于检查/sd/health是否正常访问

// pingServer pings the http server to make sure the router is working.
func pingServer() error {
    // 如果函数正常运行 --> 返回值nil
    // 如果函数出错 -> error
	for i := 0; i < 10; i++ {
		// 请求/sd/health => Get返回值有两个
		resp, err := http.Get("http://127.0.0.1:8000" + "/sd/health")
		log.Print("Waiting for the router, retry in 1 second.")
		// 如果返回200,则表示启动成功,直接返回nil
		if err == nil && resp.StatusCode == 200 {
			return nil
		}
		
		// 否则1秒后重试
		log.Print("Waiting for the router, retry in 1 second.")
		time.Sleep(time.Second)
	}
	// 尝试10次,均失败则返回一个错误
	return errors.New("Cannot connect to the router.")
}
  • 在 pingServer() 函数中,http.Get 向 http://127.0.0.1:8080/sd/health 发送 HTTP GET 请求

  • 如果函数正确执行并且返回的 HTTP StatusCode 为 200,则说明 API 服务器可用。

  • 如果超过指定次数,服务还是不能访问,pingServer会 返回errors,表示API服务器不可用。

拓展知识:标准库-log:Go语言标准库之log - 二十三岁的有德 - 博客园

拓展知识:标准库-time: https://www.jianshu.com/p/9d5636d34f17

拓展知识:标准库-常用的http请求操作: golang常用的http请求操作 - 腾讯云开发者社区-腾讯云

apiserver/main.go

调用pingServer检查服务是否正常


func main() {
	...

	// 调用协程函数,检查服务健康状态
	go func() {
		if err := pingServer(); err != nil {
			log.Fatal("The router has no response, or it might took too long to start up.", err)
		}
		log.Print("The router has been deployed successfully.")
	}()
    ...
    // 这个程序要放在g.Run的上面
	// 让应用运行在本地服务器上,默认监听端口是 8080
	g.Run(":8000") // listen and serve on 0.0.0.0:8000
}

  • 在启动 HTTP 端口前 go 一个 pingServer 协程(后台并行执行的一个任务)
  • 启动 HTTP 端口后,该协程不断地 ping /sd/health 路径
  • 如果成功,则输出部署成功提示
  • 如果失败次数超过一定次数,则终止 HTTP 服务器进程

拓展知识:go协程:https://www.jianshu.com/p/4ae2281927d7

5.5.1、测试

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FanMY_71

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值