执行go协程时, 是没有返回值的, 这时候需要用到go语言中特色的channel来获取到返回值. 通过channel拿到返回值有两种处理形式, 一种形式是具有go风格特色的, 即发送给一个for channelselect channel的独立goroutine中, 由该独立的goroutine来处理函数的返回值. 还有一种传统的做法, 就是将所有goroutine的返回值都集中到当前函数, 然后统一返回给调用函数.

阅读全文 »

前篇文章中介绍了zap日志包的basic用法, 在zap.Config的配置中, 有OutputPathsErrorOutputPaths分别可以指定日志输出目标, 一般情况下, 日志会输出到stdout stderr 或 本地文本文件. 本篇文章介绍自定义协议的日志输出目标

阅读全文 »

zap 和 logrus 是 go 语言中日志包的佼佼者. 两者都推荐使用结构化的日志打印, 从速度上来说, zap 要比 logrus 快很多. zap 的官方案例中, 介绍了三种使用方式, 分别是 AdvancedConfiguration BasicConfigurationPresets 本篇文章介绍 zap 包的 Presets 用法

阅读全文 »

上篇文章中介绍了轮询调度算法, 忽略后端元素的差异性, 将请求均匀的”涂抹”到后端节点. 本篇介绍更为复杂的加权轮询调度算法, 可以根据后端节点实际承载能力, 单独调整每个节点的权重

阅读全文 »

实现轮询调度算法(Round-Robin/rr)的原理是通过取模拿到数组/切片的下标, 从而提取出下一次即将调度的元素. 公式为i = (i + 1) mod n
i为数组下标, n为数组长度
轮询调度算法的优点是无状态, 均匀的将请求调度到后端的每个节点, 并不关心后端每个节点的实际差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import "fmt"

// 声明后端元素的数据结构
type volume struct {
name string
quota int
}

// 声明数据类型存放元素的Slice
type volumes []volume

// 声明并赋值索引下标
var i = -1

// 轮询调度算法
func (v volumes) Select() string {
i = (i + 1) % len(v)
return v[i].name
}

func main(){
v1 := volume{
name: "a",
quota: 4,
}
v2 := volume{
name: "b",
quota: 4,
}
v3 := volume{
name: "c",
quota: 8,
}
v4 := volume{
name: "d",
quota: 2,
}

vo := volumes{
v1, v2, v3, v4,
}

for j := 0; j < 20; j++ {
fmt.Println(j+1, vo.Select())
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1 a
2 b
3 c
4 d
5 a
6 b
7 c
8 d
9 a
10 b
11 c
12 d
13 a
14 b
15 c
16 d
17 a
18 b
19 c
20 d

通过运行结果可以看出, 每次Select请求都被均匀的调度到”后端的节点”. 考虑到后端节点可能存在的差异性, 下篇文章将介绍更为复杂的加权轮询调度算法

最大公约数简介

最大公约数(GCD/Greatest Common Divisor)指几个整数中共有约数中最大的一个

欧几里德算法

欧几里德算法又称辗转相除法, 该算法用于计算两个整数的最大公约数. 定理如下:

1
gcd(a,b) = gcd(b,a mod b)

意思就是说: ab的最大公约数 = ba➗b的余数的最大公约数

  • a➗b的余数为0时, 最大公约数为b
  • a➗b的余数不为0时, 则将a=b, b=a➗b的余数, 递归运算, 直到余数为0

具体证明的过程在此不再赘述, 如有兴趣可以自行谷歌一下, 下面直接给出基于go语言的实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func gcd(a,b int) int {
if b == 0 {
return a
}
return gcd(b, a % b)
}

func main() {
fmt.Println(gcd(5, 10))
}

运行结果:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

func gcd(a,b int) int {
if b == 0 {
return a
}
return gcd(b, a % b)
}

func GetGcd(n []int) int {
// 先取得Slice中的第一个数, 并假设第一个数为最大公约数
g := n[0]
// 依次与Slice中的每个数字求最大公约数
// 这里i=1, 因为第0个数字已经取出并假设为最大公约数
for i:=1; i<len(n)-1; i++ {
oldGcd := g
// 使用两个数字比较的最大公约数函数进行计算, 得出当前两个数字的最大公约数
// 循环开始后, 依次将当前最大公约数与后面的数字一一进行运算, 求最大公约数
g = gcd(g, n[i])
fmt.Printf("第%d次运算, 当前最大公约数为%d, 与%d运算得出的新最大公约数为: %d \n", i, oldGcd, n[i], g)
}
return g
}

func main() {
nums := []int{4,4,6,2,8,10}
fmt.Printf("运算结果: \n\t数列: %v 的最大公约数为: %d", nums, GetGcd(nums))
}

运行结果:

1
2
3
4
5
6
第1次运算, 当前最大公约数为4, 与4运算得出的新最大公约数为: 4 
第2次运算, 当前最大公约数为4, 与6运算得出的新最大公约数为: 2
第3次运算, 当前最大公约数为2, 与2运算得出的新最大公约数为: 2
第4次运算, 当前最大公约数为2, 与8运算得出的新最大公约数为: 2
运算结果:
数列: [4 4 6 2 8 10] 的最大公约数为: 2

intstr 这个包是偶然在k8s client-go中发现的, 在编写deployment结构体定义时, apiv1.Container实例下LivenessProbe中用到的Port, 就是intstr.IntOrString. 特地看下来这个函数, 发现这个函数恰巧解决了一直困扰我的一个问题: 在restful接口中, 如果要求json格式body体内传递port端口参数, 那么port这个字段, 我应该在服务端定义成整型好呢, 还是字符串类型好呢, 一直很纠结~ 有了intstr.IntOrString再也不用为这事儿发愁了

简介

以往我们接收jsonyaml字符串要转成结构体时, 数据类型必须一一对应. 比如数据中的端口字段, 如果json 中传递是的整型, 则为以下样式 {"port": 8080}; 如果传递的是字符串类型, 则为以下形式{"port": "8080"}

服务端接收到这些字符串后, 需要将数据与结构体中的字段一一绑定, 当然数据类型也必须一致. 如果在服务端定义了port字段为int, 但是json字符串传递过来的是{"port": "8080"}, 则绑定数据时将会报错. 反之也一样, 数据类型必须一致.

我自己开发过得项目中, 很多接口的body体内都出现了端口字段, 有的时候定义为整型, 有的时候定义为字符串类型, api doc里也是有的写得整型, 有的写的字符串, 每次都纠结用什么类型好

intstr.IntOrString 数据类型顾名思义, 该类型可以既可以接受整型, 也可以接受字符串类型. 再也不用纠结这种用整型和字符串类型都合适的字段到底用什么数据类型了

intstr.IntOrString 不仅可以兼容接收整型或字符串类型, 而且可以通过其内置的函数, 在两个数据类型之间自由转换

json/yaml串转结构体

以下demo的场景为, 拿到json/yaml串, 转换成结构体的使用场景

端口字段为整型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/util/intstr"
)

type service struct {
Name string `json:"name"`
Host string `json:"host"`
Port intstr.IntOrString `json:"port"`
}

func main() {

// 定义json字符串, port字段的值设置为整型
jsonStr := `{"name": "nginx", "host": "192.168.1.2", "port": 8080}`

nginxService := &service{}

// 将json字符串的数据, 绑定到结构体实例中
json.Unmarshal([]byte(jsonStr), nginxService)

fmt.Printf("提取数据类型: %T 实际数据类型(编号): %v 值: %#v \n", nginxService.Port.IntVal, nginxService.Port.Type, nginxService.Port.IntVal)
fmt.Printf("提取数据类型: %T 实际数据类型(编号): %v 值: %#v \n", nginxService.Port.StrVal, nginxService.Port.Type, nginxService.Port.StrVal)
}

运行结果:

1
2
提取数据类型: int32 实际数据类型(编号): 0 值: 8080 
提取数据类型: string 实际数据类型(编号): 0 值: ""

上面的例子中, json字符串中的端口字段为整型, 绑定到结构体的实例后, 通过nginxService.Port.Type 可以取得json串中实际传递的是哪种数据类型, 在本例中, 调用nginxService.Port.Type的结果是0, 通过查看源码得知, 0表示json串中传递的数据是整型

1
2
3
4
5
6
7
8
// 源码定义
// Type represents the stored type of IntOrString.
type Type int

const (
Int Type = iota // The IntOrString holds an int.
String // The IntOrString holds a string.
)

在实际使用时, 可以先判断nginxService.Port.Type的结果是0, 还是1

  • 如果是0, 则取整型nginxService.Port.IntVal
  • 如果是1, 则取字符串类型nginxService.Port.StrVal

还有一种更直接的用法, 就是根据需要, 显式的类型转换

比如上面的例子中, json传递的端口值为整型, 但是服务端绑定到结构体之后, 当使用该端口时, 需要该端口值得数据类型为字符串类型, 你可以直接这样做:

fmt.Printf("%#v", nginxService.Port.String())

运行结果:

"8080"

可以看到, 整型的数据, 已经帮你转换成为字符串类型, 后期使用非常灵活, 可以随时提取两种数据类型的值

端口为字符串类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/util/intstr"
)

type service struct {
Name string `json:"name"`
Host string `json:"host"`
Port intstr.IntOrString `json:"port"`
}

func main() {

// 定义json字符串, port字段的值设置为字符串类型
jsonStr := `{"name": "nginx", "host": "192.168.1.2", "port": "8080""}`

nginxService := &service{}

// 将json字符串的数据, 绑定到结构体实例中
json.Unmarshal([]byte(jsonStr), nginxService)

fmt.Printf("提取数据类型: %T 实际数据类型(编号): %v 值: %#v \n", nginxService.Port.IntVal, nginxService.Port.Type, nginxService.Port.IntVal)
fmt.Printf("提取数据类型: %T 实际数据类型(编号): %v 值: %#v \n", nginxService.Port.StrVal, nginxService.Port.Type, nginxService.Port.StrVal)
fmt.Printf("值: %#v 实际数据类型: %T \t \n", nginxService.Port.String(), nginxService.Port.String())
fmt.Printf("值: %#v 实际数据类型: %T \t \n", nginxService.Port.String(), nginxService.Port.IntValue())
}

运行结果:

1
2
3
4
提取数据类型: int32 实际数据类型(编号): 1 值: 0 
提取数据类型: string 实际数据类型(编号): 1 值: "8080"
值: "8080" 实际数据类型: string
值: "8080" 实际数据类型: int

结构体转json/yaml

数据类型: 整型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/util/intstr"
)

type service struct {
Name string `json:"name"`
Host string `json:"host"`
Port intstr.IntOrString `json:"port"`
}

func main() {

nginxService := service{
Name: "nginx",
Host: "192.168.1.2",
Port: intstr.IntOrString{
Type: 0, // 这里设置为0, 意为数据类型为整型
IntVal: 2222,
StrVal: "1111",
},
}

jsonBytes, _ := json.Marshal(nginxService)
fmt.Println(string(jsonBytes))

}

注意上面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
2
3
4
5
nginxService := service{
Name: "nginx",
Host: "192.168.1.2",
Port: intstr.IntOrString{IntVal: 2222},
}

数据类型: 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/util/intstr"
)

type service struct {
Name string `json:"name"`
Host string `json:"host"`
Port intstr.IntOrString `json:"port"`
}

func main() {

nginxService := service{
Name: "nginx",
Host: "192.168.1.2",
Port: intstr.IntOrString{
Type: 1, // 这里设置为1, 意为数据类型为str
IntVal: 2222,
StrVal: "1111",
},
}

jsonBytes, _ := json.Marshal(nginxService)
fmt.Println(string(jsonBytes))

}

注意上面intstr.IntOrString.Type的值设置为1, 运行结果:

1
{"name":"nginx","host":"192.168.1.2","port":"1111"}

intstr.IntOrString.Type的值设置为1, 表示该结构体实例内的数据为字符串类型, 故转json时, 按照StrVal中的值来生成字符串