很多文档中都反复强调了, go语言中没有继承, 只有组合, 活用组合的特性可以让代码更加优雅. 在go语言中, struct中的匿名字段实现了组合的特性
go语言实现加权轮询调度算法
上篇文章中介绍了轮询调度算法, 忽略后端元素的差异性, 将请求均匀的”涂抹”到后端节点. 本篇介绍更为复杂的加权轮询调度算法, 可以根据后端节点实际承载能力, 单独调整每个节点的权重
go语言实现轮询调度算法
实现轮询调度算法(Round-Robin/rr)的原理是通过取模拿到数组/切片的下标, 从而提取出下一次即将调度的元素. 公式为
i = (i + 1) mod n
i
为数组下标,n
为数组长度
轮询调度算法的优点是无状态, 均匀的将请求调度到后端的每个节点, 并不关心后端每个节点的实际差异
1 | package main |
运行结果:
1 | 1 a |
通过运行结果可以看出, 每次Select
请求都被均匀的调度到”后端的节点”. 考虑到后端节点可能存在的差异性, 下篇文章将介绍更为复杂的加权轮询调度算法
go语言实现最大公约数算法
最大公约数简介
最大公约数(GCD/Greatest Common Divisor)指几个整数中共有约数中最大的一个
欧几里德算法
欧几里德算法又称辗转相除法, 该算法用于计算两个整数的最大公约数. 定理如下:
1 | gcd(a,b) = gcd(b,a mod b) |
意思就是说: a
和b
的最大公约数 = b
和a➗b的余数
的最大公约数
- 当
a➗b的余数
为0时, 最大公约数为b - 当
a➗b的余数
不为0时, 则将a=b
,b=a➗b的余数
, 递归运算, 直到余数为0
具体证明的过程在此不再赘述, 如有兴趣可以自行谷歌一下, 下面直接给出基于go语言的实现代码
1 | package main |
运行结果:
1 | 5 |
解析:
第一次调用:
调用gcd(5, 10)
则a=5, b=10
, 进入函数后判断if b == 0
, 此时b=10
, 判断条件为假, 递归调用gcd(b, a % b)
, 此时实际值为 gcd(10, 5 % 10)
, 5与10计算取余
商0, 余5, 则实际调用参数为gcd(10, 5)
第二次调用:
调用gcd(10, 5)
则a=10, b=5
, 进入函数后判断if b == 0
, 此时b=5
, 判断条件为假, 递归调用gcd(b, a % b)
, 此时实际值为 gcd(5, 10 % 5)
, 10与5计算取余
商2, 余0, 则实际调用参数为gcd(5, 0)
第三次调用:
调用gcd(5, 0)
则a=5, b=0
, 进入函数后判断if b == 0
, 此时b=0
, 判断条件为真, 直接返回a
此时a
即为两个数的最大公约数
多个数求最大公约数
实现思路: 先求出头两个数的最大公约数, 再拿计算出的最大公约数依次与后面所有的数求最大公约数
1 | package main |
运行结果:
1 | 第1次运算, 当前最大公约数为4, 与4运算得出的新最大公约数为: 4 |
go语言巧用intstr来处理数字与字符类型
intstr
这个包是偶然在k8s client-go中发现的, 在编写deployment
结构体定义时,apiv1.Container
实例下LivenessProbe
中用到的Port
, 就是intstr.IntOrString
. 特地看下来这个函数, 发现这个函数恰巧解决了一直困扰我的一个问题: 在restful
接口中, 如果要求json格式body
体内传递port
端口参数, 那么port
这个字段, 我应该在服务端定义成整型好呢, 还是字符串类型好呢, 一直很纠结~ 有了intstr.IntOrString
再也不用为这事儿发愁了
简介
以往我们接收json
或yaml
字符串要转成结构体时, 数据类型必须一一对应. 比如数据中的端口字段, 如果json
中传递是的整型, 则为以下样式 {"port": 8080}
; 如果传递的是字符串类型, 则为以下形式{"port": "8080"}
服务端接收到这些字符串后, 需要将数据与结构体中的字段一一绑定, 当然数据类型也必须一致. 如果在服务端定义了port
字段为int
, 但是json
字符串传递过来的是{"port": "8080"}
, 则绑定数据时将会报错. 反之也一样, 数据类型必须一致.
我自己开发过得项目中, 很多接口的body
体内都出现了端口字段, 有的时候定义为整型, 有的时候定义为字符串类型, api doc里也是有的写得整型, 有的写的字符串, 每次都纠结用什么类型好
intstr.IntOrString
数据类型顾名思义, 该类型可以既可以接受整型, 也可以接受字符串类型. 再也不用纠结这种用整型和字符串类型都合适的字段到底用什么数据类型了
intstr.IntOrString
不仅可以兼容接收整型或字符串类型, 而且可以通过其内置的函数, 在两个数据类型之间自由转换
json/yaml串转结构体
以下demo的场景为, 拿到json/yaml
串, 转换成结构体的使用场景
端口字段为整型
1 | package main |
运行结果:
1 | 提取数据类型: int32 实际数据类型(编号): 0 值: 8080 |
上面的例子中, json字符串中的端口字段为整型, 绑定到结构体的实例后, 通过nginxService.Port.Type
可以取得json串中实际传递的是哪种数据类型, 在本例中, 调用nginxService.Port.Type
的结果是0
, 通过查看源码得知, 0
表示json串中传递的数据是整型
1 | // 源码定义 |
在实际使用时, 可以先判断nginxService.Port.Type
的结果是0, 还是1
- 如果是0, 则取整型
nginxService.Port.IntVal
- 如果是1, 则取字符串类型
nginxService.Port.StrVal
还有一种更直接的用法, 就是根据需要, 显式的类型转换
比如上面的例子中, json传递的端口值为整型, 但是服务端绑定到结构体之后, 当使用该端口时, 需要该端口值得数据类型为字符串类型, 你可以直接这样做:
fmt.Printf("%#v", nginxService.Port.String())
运行结果:
"8080"
可以看到, 整型的数据, 已经帮你转换成为字符串类型, 后期使用非常灵活, 可以随时提取两种数据类型的值
端口为字符串类型
1 | package main |
运行结果:
1 | 提取数据类型: int32 实际数据类型(编号): 1 值: 0 |
结构体转json/yaml
数据类型: 整型
1 | package main |
注意上面intstr.IntOrString.Type
的值设置为0
, 运行结果:
1 | {"name":"nginx","host":"192.168.1.2","port":2222} |
intstr.IntOrString.Type
的值设置为0
, 表示该结构体实例内的数据为整型, 故转json时, 按照IntVal
中的值来生成字符串
*注意: * 如果没有显式为intstr.IntOrString.Type
赋值, 则该值默认为0, 默认会取IntVal
中的数据, 也就是说, 可以简写为以下形式
1 | nginxService := service{ |
数据类型: 字符串
1 | package main |
注意上面intstr.IntOrString.Type
的值设置为1
, 运行结果:
1 | {"name":"nginx","host":"192.168.1.2","port":"1111"} |
intstr.IntOrString.Type
的值设置为1
, 表示该结构体实例内的数据为字符串类型, 故转json时, 按照StrVal
中的值来生成字符串
go语言结构体多字段多因素排序
上篇结构体排序的文档介绍了如果依据多个字段进行单因素排序, 举例来说, 一个学生实例, 可以单独按照姓名排序, 可以单独按照年龄排序, 可以单独按照体重排序. 本篇文章介绍如果进行多因素排序, 也就是按照姓名排序, 当姓名相同时, 按照年龄排序…
以官方文档的demo来做演示
1 | package main |
运行结果:
1 | By user: [{dmr C 100} {glenda Go 200} {gri Go 100} {gri Smalltalk 80} {ken C 150} {ken Go 200} {r Go 100} {r C 150} {rsc Go 200}] |
与上一篇文章<go语言结构体排序>(简称A)中<结构体多字段单独排序>的代码区别 (本篇简称B):
A:
1 | type By func(i, j student) bool |
B:
1 | type multiSorter struct { |
A B两篇中各截取了部分代码, 均涉及两项操作
- 生成数据集
- 排序
A篇的操作为: 单独声明一个By
数据类型, 为该数据类型编写方法, 方法中的第一步为生成数据集, 第二步为排序操作. 调用时By(name).Sort(students)
中的By(name)
是类型转换操作, 后续的.Sort()
方法是调用By
自己的Sort
方法得到的排序结果
B篇的操作为: 单独编写一个OrderBy
函数, 该OrderBy
函数仅用于生成数据集并返回, 需要注意的是, 这里生成的数据集, 仅包含有排序函数, 不包含实际数据. 由于数据集类型本身自带Sort
方法, 所以返回的数据可以直接调用Sort
方法执行排序操作, 排序之前, 将参数中的数据填充到数据集中, 形成完整的数据集. 调用时OrderedBy(user).Sort(changes)
中的OrderedBy(user)
与A篇不同, 此处不是类型转换, 而是函数调用, 后续的.Sort()
方法是由OrderedBy()
函数返回的multiSorter
对象, 调用multiSorter
中的Sort
方法得到的排序结果
A B两篇的排序方法略有差别, 但本质都是一样的, 核心操作都是生成数据集
和排序
, 只是B篇中, Less方法需要根据多因素排序, 略微复杂一些. 上面的例子是官方的demo, 当然我们也可以完全按照A篇的风格改写
1 | // 删除multiSorter数据类型中的Sort方法 |
这样修改之后, 调用方式也发生了变更
OrderedBy()
是一个类型转换的操作, 根据上面的声明, 它只能转换[]lessFunc
类型的数据, 所以需要先声明并赋值[]lessFunc{}
对象, 然后才能交给OrderedBy()
函数进行类型转换. 转换后其自身拥有Sort
方法可供调用
1 | OrderedBy([]lessFunc{user}).Sort(changes) |
以下是A篇风格的完整代码
1 | package main |
go语言结构体排序
有了go语言中的排序接口, 可以很容易的对结构体的任意一个字段进行排序, 具体以哪个字段排序, 定义在该结构体Slice的
Less
方法中. 一个结构体的Slice仅可以实现一次sort
接口, 如果一个结构体中有多个字段需要单独进行排序, 最粗鲁的办法只能是定义多个Slice类型, 为每个类型都编写不同的Less
方法
约定关键字:
多字段单独排序: 一个结构体中, 需要有多个字段作为依据, 单独进行排序, 单个字段排序时不受其他字段排序策略的影响
多字段多因素排序: 一个结构体中, 对第一个字段排序时, 如果该字段的数值相等, 按其他字段再次进行排序操作(例如: 按年龄进行排序, 年龄相同的按体重排序)
结构体单字段排序
1 | package main |
运行结果:
1 | [{c 3 40} {a 5 30} {d 7 28} {b 8 35}] |
结构体多字段单独排序
多字段需要排序的, 最粗鲁的办法是为每一个需要排序的字段单独声明一个数据集类型, 然后在每个数据集类型中实现其对应的Less
方法
1 | package main |
运行结果:
1 | [{a 5 30} {b 8 35} {c 3 40} {d 7 28}] // 按姓名排序 |
这个排序的办法的劣势是需要写很多重复代码, 通过这个例子也可以看出, 按不动字段排序实际只是Less
方法的实现不同
结构体多字段单独排序(推荐)
这里介绍一个更优雅的方式, 解决上面多字段排序需要编写n * 3
方法的问题
1 | package main |
运行结果:
1 | By name: [{a 5 30} {b 8 35} {c 3 40} {d 7 28}] |
这个多字段单独排序的方法, 省去了需要重复编写Len()``Swap()
方法的麻烦, 也是sort
包官方推荐的多字段排序案例
结构体多字段单独排序2(推荐)
官方文档中还介绍了另一种排序方式, 通过Go语言独特的组合特性来实现sort接口的三个方法
1 | package main |
go语言sort排序基本用法
go语言中预置了
[]int
[]float64
[]String
三种数据类型的排序函数, 如果需要对其他数据类型进行排序, 需要自己实现sort interface
内置数据类型排序
[]int 类型排序
1 | func main() { |
运行结果:
1 | ------------ Int Slice ------------ |
[]float64 类型排序
1 | func main() { |
运行结果:
1 | ------------ Float64 Slice ------------ |
[]string 类型排序
1 | func main() { |
运行结果:
1 | ------------ String Slice ------------ |
其他数据类型排序
int8
int16
int32
int64
float32
虽然也是常用的数据类型, 但是这些数据类型需要自己实现排序接口
1 | // A type, typically a collection, that satisfies sort.Interface can be |
[]int32 类型排序
1 | type Int32Slice []int32 |
运行结果:
1 | [3 19 26 56 78] |
[]float32 类型排序
1 | type Float32Slice []float32 |
运行结果:
1 | [12.78 53.2 78.5 98.5] |
gin添加自定义中间件
中间件的作用
在我们熟悉的一个完整的http
请求中, 客户端先向服务端发送的http request
, 服务端接收到request
之后, 根据实际需要, 给客户端返回http response
站在服务端的角度来看, 就是接收request
, 响应response
, 而中间件的作用, 就在request
与response
之间
- 收到
request
之后, 在request
还未到达处理的handler
之前, 可以通过中间件做出一系列的数据处理, 可以看做是前置校验或过滤 request
到达handler
之后, 中间件还可以根据需要对响应体做统一的数据处理
gin内置的中间件
1 | func BasicAuth(accounts Accounts) HandlerFunc |
中间件的作用域
1 | r := gin.Default() //默认带Logger(), Recovery()这两个内置中间件 |
全局作用域
1 | r := gin.New() //不带任何中间件 |
组作用域
1 | r := gin.New() |
单个路由作用域
1 | r := gin.New() |
自定义中间件
- 第一种形式
1 | //定义中间件 |
1 | //引用中间件 |
注意: r.Use()
中的中间件函数没有()
- 第二种形式
1 | func middleware2() gin.HandlerFunc { |
1 | //引用中间件 |
数据传递
gin.Context
中提供了Set``Get
函数用来存储和提取数据
1 | //中间件set数据 |
1 | //handler中get数据 |
Set
方法存储的值为interface{}
类型, Get
方法中提供以下常用的数据类型方便提取数据
1 | func (c *Context) GetBool(key string) (b bool) |
拦截request请求
- 为
request
请求添加header
1 | //中间件 |
1 | //handler |
- 为
response
响应添加header
1 | func middleware2() gin.HandlerFunc { |
追加handler处理后的动作
1 | package main |
1 | curl http://127.0.0.1:8080/ |
c.Next()
c.Next()
函数允许我们在中间件中控制调度的逻辑, 每执行到c.Next()
一次, 执行权就会别切换到其他中间件或handler
函数中
1 | package main |
运行:
1 | go run main.go |
访问:
1 | curl http://127.0.0.1:8080 # 返回结果: ok |
程序日志:
1 | start middleware1 |
依据注册中间件的顺序, 每次遇到c.Next()
函数就跳转到下一个中间件执行, 直到跳转到最后一个中间件, 再次遇到c.Next()
的时候, 就跳到handler
主函数中运行, 当handler
执行完毕后, 再逐层向上返回, 继续执行c.Next()
之后的代码
c.Abord()
c.Abord()
用来终止request
, 阻止其到达handler
一般情况下用在鉴权与认证的中间件中
1 | package main |
访问后, 程序日志:
1 | start middleware1 |
可以看到即使在middleware2
中执行了c.Abord()
, c.Abord()
后面的代码依然被完整的执行, 并且返回到了middleware1
跳转的地方, 继续执行了后续代码
go语言中的常量与枚举
go语言中使用关键字
const
来定义常量. 最近在项目开发中, 在ovirt-go sdk中经常会用到其内置常量, 善用go的常量, 不仅可以增加代码的可读性, 还能省去一些”小麻烦”~
常量的定义
常量可以单个定义
1 | const HttpStatusOk = 200 |
也可以批量定义
1 | const ( |
常量的优势
go在代码编译前, 变量的类型就已经确定, 并不可更改. 但是常量就比较特殊, 常量在代码编译的时候, 类型才会确定下来, 下面来举个例子🌰
- 变量
1 | package main |
执行结果:
1 | # command-line-arguments |
很显然, 代码在编译前, a,b
两个变量的数据类型已经确定, 两个不同的数据类型, 是不予许进行运算操作的.
- 常量
1 | package main |
运行结果:
1 | type: int |
如果其中一个数字是常量的情况, 运行结果就大不相同了, 可以看到, 该程序可以正常执行, 而且拿到了正确的结果. 这是因为, 虽然b
变量在编译前确定了数据类型, 但是a
常量并没有, a
常量是在编译时才根据实际情况确定了数据类型, 常量会根据编译时的运算, 自动执行类型转换
上面的例子中, 声明了a
常量, 同时声明了aa
变量, 赋值均为1
, 通过编译执行, 得到aa
的数据类型自动判定为int
, 而a
常量却可以先和float32
类型的b
运算, 而后又和int32
类型的c
进行运算. 说明a
常量的数据类型, 是在编译时, 根据实际的运算需要而变化的
所以, 项目中的一些数字可以多多定义为常量, 这样在运算的时候, 也可以减少显式的数据类型转换
枚举
go 语言中没有专门的枚举关键字, 是通过常量const
关键字实现的, 枚举的标志性关键字为iota
.
iota
关键字在const
关键字出现的时候, 被重置为0
, 且const
中每新增一行常量的声明, iota
计数器将被+1
1 | package main |
运行结果:
1 | 0 |
同行声明的情况
1 | package main |
运行结果:
1 | 0 |
注意: iota
仅会在新增的常量声明中才会自增, 声明在同一行的常量, iota
的值是相同的
跨行声明的情况
- 情况1
1 | package main |
运行结果:
1 | 0 |
const
批量声明的特点, 如果后面的常量没有显式赋值, 则它们的值等于上一个显式赋值的值
- 情况2
1 | package main |
运行结果:
1 | 0 |
由于iota
的特点, 每新增一行const
的定义, 计数器就会自增1, 所以即使中间显式赋值了一些常量, 依然不会影响iota
的自增
枚举的应用
自定义自增大小
1 | package main |
运行结果:
1 | 2 |
数据单位运算
1 | package main |
运行结果:
1 | 1 |
- b = 1 << (10 * iota)
第一个常量: iota被初始化为0, 则表达式为1 << (10 * 0)
–> 1 << 0
, 1 向左位移0位, 还是1
- kb
第二个常量: 没有显式赋值, 该值应该等于上一行显式赋值的”值”, 由于上一行显式赋值的值为一个表达式, 所以将该表达式完整的继承下来. 且该行为新增常量声明, 所以iota
变量自增1, 则赋值表达式实际为: 1 << (10 * 1)
–> 1 << 10
, 1 向左位移 10 位, 二进制表示为10000000000
(2的10次方), 转换为10进制为1024
, 所以 kb 经过表达式运算后, 值为: 1024
- mb
第三个常量: 同理, iota 自增 1 后, 赋值表达式实际为: 1 << (10 * 2)
—> 1 << 20
, 1 向左位移 20 位, 二进制表示为100000000000000000000
(2的20次方), 转换为10进制为1048576
- 以此类推