目录
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请求,可先经过一系列中间件处理,例如:Logger,Authorization,GZIP等。这个特性和NodeJs 的 Koa 框架很像。中间件机制也极大地提高了框架的可扩展性。
- 可以用于记录日志、请求时常和认证
- 异常处理:服务始终可用,不会宕机。Gin 可以捕获 panic,并恢复。而且有极为便利的机制处理HTTP请求过程中发生的错误。
- JSON:Gin可以解析并验证请求的JSON。这个特性对 Restful API 的开发尤其有用。
- 路由分组:例如将需要授权和不需要授权的API分组,不同版本的API分组。而且分组可嵌套,且性能不受影响。
- 渲染内置:原生支持JSON,XML和HTML的渲染。
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, float64 | 12.2 |
字节类型 | byte (等价于uint8) => 一个字节 | 'a' |
字符串类型 | string 在 Go 语言中,字符串使用 UTF8 编码 | 'hello' |
布尔值类型 | boolean | true 或 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、接口、映射或切片类型
类型 | 定义 | 零值 |
---|---|---|
bool | var variable bool | false |
int | var variable int | 0 |
string | var variable string | "" |
pointers | var a *int | nil |
slices | nil | |
maps | nil | |
channels | nil | |
functions | nil | |
interfaces | nil |
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.HealthCheck
、sd.DiskCheck
、sd.CPUCheck
、sd.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