目录
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、测试
要编写一个 RESTful 风格的 API 服务器,首先需要一个 RESTful Web 框架,经过调研选择了 GitHub star 数最多的 Gin。采用轻量级的 Gin 框架,具有如下优点: 高性能 、 扩展性强 、 稳定性强 、相对而言比较 简洁 (查看性能对 比)。关于 Gin 的更多介绍可以参考 Golang 微框架 Gin 简介。
Gin 是使用 Go/golang 语言实现的 HTTP Web 框架。接口简洁,性能极高。截止 1.4.0 版本,包含测试代码,仅14K,其中测试代码 9K 左右,也就是说框架源码仅 5K 左右。
手动安装Gin,也可以使用包模块管理,自动安装
[root@nginx-kafaka03 apiserver]# go get -u -v github.com/gin-gonic/gin
# 这个命令会解决一些依赖包
-v :打印出被构建的代码包的名字 -u :已存在相关的代码包,强行更新代码包及其依赖包
注意我们的代码不能放在$GOPATH(即这个路径:/root/code)
构建了一个测试账号系统(后面统称为 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 "/"
起来之后,不要去动它,让他一直运行。只有这样,我们才能够访问到接口。
把脚本中的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接口(回环接口),只能本机访问,其他机器访问不了
在本地浏览器上访问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)
Go的数据类型分为四大类:
Go 语言是静态类型的,变量声明时必须明确变量的类型。Go 语言与其他语言显著不同的一个地方在于,Go 语言的类型在变量后面。比如 java 中,声明一个整体一般写成 int a = 1
,在 Go 语言中,需要这么写:
这种关键字的写法一般用于声明全局变量
var a int // 如果没有赋值,默认为0
var a int = 1 // 声明时赋值
var a = 1 // 声明时赋值
var a = 1
,因为 1 是 int 类型的,所以赋值时,a 自动被确定为 int 类型,所以类型名可以省略不写
这种声明方式只能在函数体中使用
// 这种方式相对来说用得更多一些
a := 1
msg := "Hello World!"
var identifier1, identifier2 type
var b, c int = 1, 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
Go 不支持隐式转换类型
类型转换用于将一种数据类型的变量转换为另外一种类型的变量。Go 语言类型转换基本格式如下:
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
指针即某个值的地址,类型定义时使用符号*,对一个已经存在的变量,使用&获得该变量的地址。
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 被修改
}
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 来表示,因而可以正确处理中文。
nil是go语言中预先的标识符,我们可以直接使用nil,而不用声明它。
file,err := funcName(xxx)
if err!= nil{
// do something....
fmt.Println("代码有错误")
}
- 获取函数返回值,其中当err不等于
nil
的时候,说明出现某些错误了,需要我们对这个错误进行一些处理- 如果err等于
nil
说明运行正常。
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 |
拓展:https://www.jianshu.com/p/dd80f6be7969
思考: 服务器健康有哪些?如何检查?
由于后期我们会实现很多路由对应的处理函数,如果量大的话,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
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)
}
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
# curl http://localhost:8000/sd/health
OK
上面我们已经实现了几个接口用于获取服务器状态,但是,它需要我们主动访问才能获取状态,那么我们如何能在有问题时,直接收到提醒呢?
定时任务/监控系统:编写监控脚本,有问题时提醒(邮件/短信/电话/微信/钉钉...)
这部分在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
}
pingServer
协程(后台并行执行的一个任务)/sd/health
路径拓展知识:go协程:https://www.jianshu.com/p/4ae2281927d7