golang基础

变量和常量

如何定义变量

单声明变量

var名称类型是声明单个变量的语法

  • 第一种,指定变量类型,声明后若不赋值,使用默认值
1
2
var name string
name ="cwz"
  • 第二种,根据值自行判定变量类型(类型推断Type inference)

如果一个变量有一个初始值,Go将自动能够使用初始值来推断该变量的类型。因此,如果变量具有初始值,则可以省略变量声明中的类型

1
var name ="cwz"
  • 第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的(多个变量同时声明时,至少保证一个是新变量),否则会导致编译错误(简短声明)
1
2
3
var a int = 10
var b = 10
c := 10

这种方式只能被用在函数体内,而不可以用于全局变量的声明与赋值

1
2
3
4
5
6
7
8
package main
var a = "cwz"
var b string = "b"
var c bool

func main(){
println(a, b, c)
}

多声明变量

  • 第一种,以逗号分隔,声明与赋值分开,若不赋值,存在默认值
1
2
var name1, name2, name3 type
name1, name2, name3 = v1, v2, v3
  • 第二种,直接赋值,下面的变量类型可以是不同的类型
1
var name1, name2, name3 = v1, v2, v3
  • 第三种,集合类型
1
2
3
4
var (
name string
age int
)

注意:

  • 变量必须先定义才能使用
  • go语言是静态语言,要求变量的类型和赋值的类型必须一致
  • 变量名不能冲突。同一个作用于域内不能冲突
  • 简短定义方式,左边的变量名至少有一个是新的,而且不能定义全局变量
  • 变量定义了就要使用,否则无法通过编译

如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20 就是不被允许的,编译器会提示错误 no new variables on left side of :=,但是 a = 20 是可以的,因为这是给相同的变量赋予一个新的值。

单纯地给 a 赋值也是不够的,这个值必须被使用,所以使用在同一个作用域中,已存在同名的变量,之后的声明初始化会退化为赋值操作。但这个前提是,最少要有一个新的变量被定义,且在同一作用域,例如,下面的x就是新定义的变量:

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

import (
"fmt"
)

func main() {
x := 140
fmt.Println(&x)
x, y := 200, "abc"
fmt.Println(&x, x)
fmt.Print(y)
}

匿名变量

在使用多重赋值时候,如果不需要在左值中接收变量,可以使用匿名变量_,python中也经常有这种用法

1
2
3
4
5
6
7
8
func GetData() (int, int) {
return 100, 200
}
func main(){
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)
}

常量的定义

常量

常量是一个简单值的标识符,在程序运行时,不会被修改的量

1
2
显式类型定义: const b string = "abc"
隐式类型定义: const b = "abc"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
const LENGTH int = 10
const WIDTH int = 5
var area int
const a, b, c = 1, false, "str" //多重赋值

area = LENGTH * WIDTH
fmt.Printf("面积为 : %d", area)
fmt.Println(a, b, c)
}

常量可以作为枚举组成常量组:

1
2
3
4
5
const (
Unknown = 0
Female = 1
Male = 2
)

常量组中如不指定类型和初始化值,则与上一行非空常量值相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

func main() {
const (
x u· = 16
y
s = "abc"
z
)
fmt.Printf("%T,%v\n", y, y)
fmt.Printf("%T,%v\n", z, z)
}

// 输出:
uint16,16
string,abc

常量的注意事项:

  • 常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
  • 不曾使用的常量,在编译的时候,是不会报错的

iota

iota,是特殊常量,可以认为是一个可以被编译器修改的常量

iota 可以被用作枚举值:

1
2
3
4
5
const (
a = iota
b = iota
c = iota
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

1
2
3
4
5
const (
a = iota
b
c
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}

// 0 1 2 ha ha 100 100 7 8

注意:

  • 如果中断iota自增,则必须显式恢复。且后续自增值按行序递增
  • 自增默认是int类型,可以自行进行显示指定类型
  • 数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址

go基本数据类型

bool类型

布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true

数值类型

整数型

类型 有无符号 长度
int8 8 位 (-128 到 127)
int16 16位 (-32768 到 32767)
int32 32 位整型 (-2147483648 到 2147483647)
int64 64 位整型 (-9223372036854775808 到 9223372036854775807)
uint8 8 位整型 (0 到 255) 8位都用于表示数值
uint16 16 位整型 (0 到 65535)
uint32 32 位整型 (0 到 4294967295)
uint64 64 位整型 (0 到 18446744073709551615)

go语言中,int是一种动态类型,取决于机器本身是多少位,64位机器就是int64,占8个字节:

1
2
3
var age int = 18
fmt.Println(age)
fmt.Println(unsafe.Sizeof(age)) // 8

浮点型

  • float32 32位浮点型数
  • float64 64位浮点型数
1
2
3
4
// float类型, float类型最大数
//var weight float32 = 70.5
fmt.Println(math.MaxFloat32) // 3.4028234663852886e+38
fmt.Println(math.MaxFloat64) // 1.7976931348623157e+308

其他类型

  • byte 等于 uint8,主要用来处理ASCII码的处理
  • rune 等于 int32,主要处理中文字符
1
2
3
4
5
6
7
8
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

字符

Golang中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存。

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

import (
"fmt"
)

func main() {

var a byte
a = 'a'
//输出ascii对应码值 97
fmt.Println(a)
fmt.Printf("a=%c", a)
}

字符常量只能使用单引号括起来,例如:var a byte = 'a' var a int = 'a'

字符本质是一个数字, 可以进行加减乘除

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

import (
"fmt"
"reflect"
)

func main() {

a := 'a'

//这里注意一下 1. a+1可以和数字计算 2.a+1的类型是32 3. int类型可以直接变成字符

fmt.Println(reflect.TypeOf(a+1)) // int32
fmt.Printf("a+1=%c", a+1) // a+1=b
}

字符串

字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。

数据类型的转换

简单的转换操作

1
2
3
4
// 基本类型转换
a := int(3.0)
fmt.Println(a)
// 这样是可以的

但是,在go语言中不支持变量间的隐式类型转换

看一段C程序代码:

1
2
3
4
5
6
7
8
9
#include <iostream>
using namespace std;

int main(){
int a = 5;
float b = 6.2;
a = b;
count<<a<<endl;
}

这样输出的结果就是6

再看一看golang:

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
var b int = 5.0
fmt.Println(b)
}

这样可以运行成功。需要说明的是:var b int = 5.0中,对于b来说是一个变量,5.0是一个常量。常量到变量是会进行隐式类型转换

1
2
3
4
c := 5.7
fmt.Printf("%T\n", c) // float64
var d int = c // 这里就会产生错误,cannot use c (type float64) as type int in assignment。这里 c 和 d 都是变量。变量赋值给另一个变量,两种类型不一致,是不会做隐式转换的
fmt.Println(d)

说明:

  • Go允许在底层结构相同的两个类型之间互转

  • 不是所有数据类型都能转换的,例如字母格式的string类型”abcd”转换为int肯定会失败

  • 低精度转换为高精度时是安全的,高精度的值转换为低精度时会丢失精度。例如int32转换为int16,float32转换为int,这样会丢失精度

  • 这种简单的转换方式不能对int(float)和string进行互转,要跨大类型转换,可以使用strconv包提供的函数

strconv转换

Itoa和Atoi

  • Itoa int转换为字符串
1
println("a" + strconv.Itoa(32))  // a32
  • string转换为int:Atoi
1
2
3
4
// 字符串转int aoti
data, _:=strconv.Atoi("12")
fmt.Println(data)
//由于string可能无法转换为int,所以这个函数有两个返回值:第一个返回值是转换成int的值,第二个返回值判断是否转换成功。

Parse类函数

Parse类函数用于转换字符串为给定类型的值:ParseBool()、ParseFloat()、ParseInt()、ParseUint()

1
2
3
4
b, err := strconv.ParseBool("true")
f, err := strconv.ParseFloat("3.1415", 64) // 转换成float64
i, err := strconv.ParseInt("-42", 10, 64)
u, err := strconv.ParseUint("42", 10, 64)

ParseInt()和ParseUint()有3个参数:

1
2
func ParseInt(s string, base int, bitSize int) (i int64, err error)
func ParseUint(s string, base int, bitSize int) (uint64, error)

说明:

  • bitSize参数表示转换为什么位的int/uint,有效值为0、8、16、32、64。当bitSize=0的时候,表示转换为int或uint类型。例如bitSize=8表示转换后的值的类型为int8或uint8。
  • base参数表示以什么进制的方式去解析给定的字符串,有效值为0、2-36。当base=0的时候,表示根据string的前缀来判断以什么进制去解析:0x开头的以16进制的方式去解析,0开头的以8进制方式去解析,其它的以10进制方式解析。

Format类函数

将给定类型格式化为string类型:FormatBool()、FormatFloat()、FormatInt()、FormatUint()

1
2
3
4
s := strconv.FormatBool(true)
s := strconv.FormatFloat(3.1415, 'E', -1, 64)
s := strconv.FormatInt(-42, 16) //表示将-42转换为16进制数,转换的结果为-2a。
s := strconv.FormatUint(42, 16)

第二个参数base指定将第一个参数转换为多少进制,有效值为2<=base<=36。当指定的进制位大于10的时候,超出10的数值以a-z字母表示。例如16进制时,10-15的数字分别使用a-f表示,17进制时,10-16的数值分别使用a-g表示。

FormatFloat()参数众多:

1
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
  • bitSize表示f的来源类型(32:float32、64:float64),会据此进行舍入
  • fmt表示格式:’f’(-ddd.dddd)、’b’(-ddddp±ddd,指数为二进制)、’e’(-d.dddde±dd,十进制指数)、’E’(-d.ddddE±dd,十进制指数)、’g’(指数很大时用’e’格式,否则’f’格式)、’G’(指数很大时用’E’格式,否则’f’格式)
  • prec控制精度(排除指数部分):对’f’、’e’、’E’,它表示小数点后的数字个数;对’g’、’G’,它控制总的数字个数。如果prec 为-1,则代表使用最少数量的、但又必需的数字来表示f

go的运算符和表达式

算术运算符

1
+ - * / %(求余) ++ --

关系运算符

1
== != > < >= <=

逻辑运算符

&& 所谓逻辑与运算符。如果两个操作数都非零,则条件变为真
\ \ 所谓的逻辑或操作。如果任何两个操作数是非零,则条件变为真
! 所谓逻辑非运算符。使用反转操作数的逻辑状态。如果条件为真,那么逻辑非操后结果为假
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
package main

import "fmt"

func main() {
var a bool = true
var b bool = false
if ( a && b ) {
fmt.Printf("第一行 - 条件为 true\n" )
}
if ( a || b ) {
fmt.Printf("第二行 - 条件为 true\n" )
}
/* 修改 a 和 b 的值 */
a = false
b = true
if ( a && b ) {
fmt.Printf("第三行 - 条件为 true\n" )
} else {
fmt.Printf("第三行 - 条件为 false\n" )
}
if ( !(a && b) ) {
fmt.Printf("第四行 - 条件为 true\n" )
}
}

位运算符

位运算符对整数在内存中的二进制位进行操作

下表列出了位运算符&, |, ^的计算:

p q p & q p \ q p ^ q
0 0 0 0 0
0 1 0 1 1
1 1 1 1 0
1 0 0 1 1

Go 语言支持的位运算符如下表所示。假定 A 为60,B 为13,A = 0011 1100,B = 0000 1101:

运算符 描述 实例
& 按位与运算符”&”是双目运算符。 其功能是参与运算的两数各对应的二进位相与。 (A & B) 结果为 12, 二进制为 0000 1100
\ 按位或运算符”\ “是双目运算符。 其功能是参与运算的两数各对应的二进位相或 (A \ B) 结果为 61, 二进制为 0011 1101
^ 按位异或运算符”^”是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (A ^ B) 结果为 49, 二进制为 0011 0001
<< 左移运算符”<<”是双目运算符。左移n位就是乘以2的n次方。 其功能把”<<”左边的运算数的各二进位全部左移若干位,由”<<”右边的数指定移动的位数,高位丢弃,低位补0。 A << 2 结果为 240 ,二进制为 1111 0000
>> 右移运算符”>>”是双目运算符。右移n位就是除以2的n次方。 其功能是把”>>”左边的运算数的各二进位全部右移若干位,”>>”右边的数指定移动的位数。 A >> 2 结果为 15 ,二进制为 0000 1111

赋值运算符

运算符 描述 实例
= 简单的赋值运算符,将一个表达式的值赋给一个左值 C = A + B 将 A + B 表达式结果赋值给 C
+= 相加后再赋值 C += A 等于 C = C + A
-= 相减后再赋值 C -= A 等于 C = C - A
*= 相乘后再赋值 C = A 等于 C = C A
/= 相除后再赋值 C /= A 等于 C = C / A
%= 求余后再赋值 C %= A 等于 C = C % A
<<= 左移后赋值 C <<= 2 等于 C = C << 2
>>= 右移后赋值 C >>= 2 等于 C = C >> 2
&= 按位与后赋值 C &= 2 等于 C = C & 2
^= 按位异或后赋值 C ^= 2 等于 C = C ^ 2
\ = 按位或后赋值 C \ = 2 等于 C = C \ 2

其他运算符

运算符 描述 实例
& 返回变量存储地址 &a; 将给出变量的实际地址。
* 指针变量。 *a; 是一个指针变量

字符串的相关操作

基本操作

字符串的长度len

1
2
var name string = "cwz:中国人"
fmt.Println(len(name)) // 13

字符串,返回的是字节的长度,中文一个字符占3个字节,所以长度为13

所以使用rune来存储中文字符:

1
2
3
var name string = "cwz:中国人"
name_arr := []rune(name)
fmt.Println(len(name_arr)) // 7

转义符

转义字符 意义 ASCII码值(十进制)
\n 换行(LF) ,将当前位置移到下一行开头 010
\r 回车(CR) ,将当前位置移到本行开头 013
\t 水平制表(HT) (跳到下一个TAB位置) 009
\\ 代表一个反斜线字符’’\’ 092
\' 代表一个单引号(撇号)字符 039
\" 代表一个双引号字符 034
\? 代表一个问号 063

字符串子串的操作

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
package main

import (
"fmt"
"strings"
)

func main() {

var name string = "cwz:你好啊aac"
var date string = "2021\\01\\12"
fmt.Println(name, date) // cwz:你好啊aac 2021\01\12

// 是否包含字符串
fmt.Println(strings.Contains(name, "c")) // true

fmt.Println(strings.Index(name, "你")) // 4

// 统计出现的次数
fmt.Println(strings.Count(name, "c")) // 2
// 前缀和后缀
fmt.Println(strings.HasPrefix(name, "c")) // true
fmt.Println(strings.HasSuffix(name, "h")) // false

// 大小写转换
fmt.Println(strings.ToUpper("hello")) // HELLO
fmt.Println(strings.ToLower("AAAA")) // aaaa

// 字符串的比较
fmt.Println(strings.Compare("he", "ha")) // 1

// 去掉空格
fmt.Println(strings.TrimSpace(" hhh ")) // hhh

// split方法
fmt.Println(strings.Split("192.189.211.0", "."))

// 合并 join
arrs := strings.Split("192.189.211.0", ".") // [192 189 211 0]
fmt.Println(strings.Join(arrs, "-")) // 192-189-211-0

// 字符串替换
fmt.Println(strings.Replace("cwz:18", "18", "20", 1)) // cwz:20
}

字符串的输入输出格式化

缺省格式和类型

格式化后的效果 动词 描述
[0 1] %v 缺省格式
[]int64{0, 1} %#v go语法打印
[]int64 %T 类型打印
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"strconv"
)

func main() {

// printf 格式化输出
name := "cwz"
age := 18
fmt.Println("name:" + name + ",age:" + strconv.Itoa(age)) // name:cwz,age:18
fmt.Printf("name:%v, age:%v\n", name, age) // name:cwz, age:18
fmt.Printf("name:%#v, age:%#v\n", name, age) // name:"cwz", age:18
fmt.Printf("name:%T, age:%T\n", name, age) // name:string, age:int
}

整型(缩进, 进制类型, 正负符号)

格式化后的效果 动词 描述
15 %d 十进制
+15 %+d 必须显示正负符号
␣␣15 %4d Pad空格(宽度为4,右对齐)
15␣␣ %-4d Pad空格 (宽度为4,左对齐)
1111 %b 二进制
17 %o 八进制
f %x 16进制,小写

字符(有引号, Unicode)

格式化后的效果 动词 描述
A %c 字符
‘A’ %q 有引号的字符
U+0041 %U Unicode
U+0041 ‘A’ %#U Unicode 有引号
1
2
data := 65
fmt.Printf("%q\n", data) // 'A'

浮点(缩进, 精度, 科学计数)

格式化后的效果 动词 描述
1.234560e+02 %e 科学计数
123.456000 %f 十进制小数

条件语句和循环语句

go语言的常用控制流程有if和for,没有while, 而switch和goto是为了简化代码,降低重复代码,属于扩展的流程控制

if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}

if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
} else {
/* 在布尔表达式为 false 时执行 */
}

if 布尔表达式1 {
/* 在布尔表达式1为 true 时执行 */
} else if 布尔表达式2{
/* 在布尔表达式1为 false ,布尔表达式2为true时执行 */
} else{
/* 在上面两个布尔表达式都为false时,执行*/
}

for

Go 语言的 For 循环有 3 种形式,只有其中的一种使用分号

1
2
3
4
5
6
7
8
//和 C 语言的 for 一样:
for init; condition; post { }

//和 C 的 while 一样:
for condition { }

//和 C 的 for(;;) 一样:
for { }
1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main(){
sum := 0
for i := 1; i < 10; i++ {
sum += 1
}
fmt.Println(sum) // 9
}

for循环的range格式:

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main(){
name := "hello"
for _,value:=range name{
fmt.Printf("%c", value)
}
}

goto语句

Go 语言的 goto 语句可以无条件地转移到过程中指定的行。

goto 语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。

但是,在结构化程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
/* 定义局部变量 */
var a int = 10

/* 循环 */
LOOP: for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a + 1
goto LOOP
}
fmt.Printf("a的值为 : %d\n", a)
a++
}
}

使用 goto 退出多层循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
for x := 0; x < 10; x++ {
for y := 0; y < 10; y++ {
if y == 2 {
// 跳转到标签
goto breakHere
}
}
}
// 手动返回, 避免执行进入标签
return
// 标签
breakHere:
fmt.Println("done")
}

switch语句

switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。

1
2
3
4
5
6
7
8
switch var1 {
case val1:
...
case val2:
...
default:
...
}
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 main() {
/* 定义局部变量 */
var grade string = "B"
var marks int = 90

switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}

switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" );
}
fmt.Printf("你的等级是 %s\n", grade );
}

不同的 case 表达式使用逗号分隔:

1
2
3
4
5
var a = "mum"
switch a {
case "mum", "daddy":
fmt.Println("family")
}

分支表达式:

1
2
3
4
5
var r int = 11
switch {
case r > 10 && r < 20:
fmt.Println(r)
}

常用复杂的数据类型

go语言中的数组

Go 语言提供了数组类型的数据结构。

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。

数组元素可以通过索引(位置)来读取(或者修改),索引从0开始,第一个元素索引为 0,第二个索引为 1,以此类推。数组的下标取值范围是从0开始,到长度减1。

数组一旦定义后,大小不能更改。

声明和初始化数组

1
2
3
// 数组的声明
var courses [10] string
var courses2 = [5]string{"redis","django","flask"}

初始化数组中 {} 中的元素个数不能大于 [] 中的数字。如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:

1
var balance = []float32{1000.0, 2.0, 3.4, 7.0, 50.0}

数组的其他创建方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a [4] float32 // 等价于:var arr2 = [4]float32{}
fmt.Println(a) // [0 0 0 0]

var b = [5] string{"ruby", "王二狗", "rose"}
fmt.Println(b) // [ruby 王二狗 rose ]

var c = [5] int{'A', 'B', 'C', 'D', 'E'} // byte
fmt.Println(c) // [65 66 67 68 69]

d := [...] int{1,2,3,4,5}// 根据元素的个数,设置数组的大小
fmt.Println(d)//[1 2 3 4 5]

e := [5] int{4: 100} // [0 0 0 0 100]
fmt.Println(e)

f := [...] int{0:1, 4:1, 9:1} // [1 0 0 0 1 0 0 0 0 1]
fmt.Println(f)

取值

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
course := [5]string{"golang", "mysql"}
fmt.Println(course[0]) // golang
}

for遍历数组

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

import "fmt"

func main() {
var a = [3]int{1, 3, 5}
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
}
// 循环打印出1,3,5

for range 遍历数组

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

import "fmt"

func main() {
var a = [3]int{1, 3, 5}
for i, v := range a {
fmt.Println(i,v) // i是索引,v是值
}
}

/*
0 1
1 3
2 5
*/

数组是值类型

Go中的数组是值类型,而不是引用类型。这意味着当它们被分配给一个新变量时,将把原始数组的副本分配给新变量。如果对新变量进行了更改,则不会在原始数组中反映。

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

import "fmt"

func main() {
a := [...]string{"USA", "China", "India", "Germany", "France"}
b := a // a copy of a is assigned to b
b[0] = "Singapore"
fmt.Println("a is ", a) // a is [USA China India Germany France]
fmt.Println("b is ", b) // b is [Singapore China India Germany France]
}

go语言中的切片

  • Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活、功能强悍的内置类型切片(“动态数组”)。
  • 与数组相比,切片的长度是不固定的;可以追加元素,在追加时可能使切片的容量增大

Go的切片类型为处理同类型数据序列提供一个方便而高效的方式。 切片有些类似于其他语言中的数组,但是有一些不同寻常的特性。

定义切片

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 "fmt"

func main() {
// 第一种方法,直接定义
var courses []string // 定义了一个切片
fmt.Printf("%T\n", courses) // []string

// 第二种方法,字面量
courses2 := []string{"django", "scrapy", "flask"}
fmt.Printf("%T", courses2)

//第三种,使用make
courses3 := make([]string, 5)
fmt.Println(len(courses3))

// 第四种方法,通过数组变成一个切片
var course4 = [5]string{"python","flask","django","golang","beego"}
subCourse := course4[1:4]
fmt.Printf("%T\n", subCourse) // []string

// 第五种方法:new
subCourse2 := *new([]int)
fmt.Printf("%T", subCourse2)

}

使用方法

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 "fmt"

func main() {
var course4 = [5]string{"python","flask","django","golang","beego"}
subCourse := course4[1:4]
subCourse3 := subCourse[1:3]
// append 追加元素
subCourse3 = append(subCourse3, "abc")
fmt.Println(subCourse3) // [django golang abc]
appendCourses := []string{"a", "b", "c"}
subCourse3 = append(subCourse3, appendCourses...)
fmt.Println(subCourse3)


// 拷贝的时候 目标对象长度需要设置
subCourse4 := make([]string, 2)
fmt.Println(len(subCourse4)) // 2
copy(subCourse4, subCourse3)
fmt.Println(subCourse4) // [django golang]


// 删除元素
deleteCourses := [5]string{"hello", "abc", "123", "golang", "word"}
courseSlice := deleteCourses[:]
courseSlice = append(courseSlice[:1], courseSlice[2:]...)
fmt.Println(courseSlice)
}

slice的内部原理

Go 语言中的的 slice 是在数组之上的抽象数据类型,要理解slice就需要先理解数组。

1
2
3
var a [4]int
a[0] = 1
i := a[0]

数组定义了长度和元素类型。例如,[4]int 类型表示一个由四个整数组成的数组。数组的大小是固定的,长度是数组类型的一部分( [4]int[5]int 是完全不同的类型)

类型 [4]int 在内存中用四个连续的整数表示。

Go 语言中,数组是值传递。当分配或传递数组值的时候,实际上会复制整个数组

slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。

在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int[4]int 就是不同的类型。

而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关

如果使用make创建slice,首先会先创建一个底层的数组。所以修改slice的值会影响到底层数组,也就是说:切片和数组是公用一块内存的。

切片的扩容

  • append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 ... 传入 slice,直接追加一个切片。
  • append函数返回值是一个新的slice,Go编译器不允许调用了 append 函数后不使用返回值。
  • 使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 len-1 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。
  • 这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 slice 的容量是留了一定的 buffer 的。否则,每次添加元素的时候,都会发生迁移,成本太高。
  • 新 slice 预留的 buffer 大小是有一定规律的,并不是简单的:

当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。

这样描述是不对的。

  • append 函数会在切片容量不够的情况下,会调用 growslice 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置
  • 扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。后半部分还对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍

文章推荐:https://juejin.cn/post/6844903811429957646

map的使用

创建map

要求所有的key的数据类型相同,所有value数据类型相同(注:key与value可以有不同的数据类型)

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
package main

import "fmt"

func main() {
// 1、字面值
m1 := map[string]string{
"m1": "v1",
}
fmt.Printf("%v\n", m1) // map[m1:v1]

// 2、使用make创建
m2 := make(map[string]string)
m2["m2"] = "v2"
fmt.Printf("%v\n", m2) // map[m2:v2]

// 3、定义一个空的map
m3 := map[string]string{}
fmt.Printf("%v\n", m3)

// map中的key不是所有的类型都支持,该类型需要支持 == 或者 != 操作
//a := []int{1,2,3}
//b := []int{1,2,3}
// 两个切片之间是不能比较的,所以slice是不能做map的key的

}

map常见操作

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
package main

import "fmt"

func main() {

// map的基本操作
m := map[string]string{
"a": "va",
"b": "vb",
}

// 增加,修改
m["c"] = "vc"
m["b"] = "vb1"
fmt.Printf("%v\n", m) // map[a:va b:vb1 c:vc]

// 查询
v, ok := m["d"]
fmt.Println(v, ok) // map[a:va b:vb1 c:vc] false

// 删除
//delete(m, "a")
//fmt.Printf("%v", m) // map[b:vb1 c:vc]

// 遍历
for k, v := range m{
fmt.Println(k, v)
}

}

map 遍历的顺序是随机的,使用for range遍历的时候,k,v使用的同一块内存

将go中的map按key进行排序:

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 (
"fmt"
"sort"
)

func main() {
fruits := map[string]int{
"oranges": 100,
"apples": 200,
"banans": 300,
}
// 将key转移到切片,将切片进行排序
var a [] string
for key := range fruits {
a = append(a, key)
}
sort.Strings(a)
for _, key := range a {
fmt.Printf("%s:%v\n", key, fruits[key])
}
}

/*
apples:200
banans:300
oranges:100
*/

go语言的指针

什么是指针

抛砖引玉:

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

import "fmt"

func swap(a int, b int) {
// 用于交换a和b
c := a
a = b
b = c
}

func main() {
a := 10
b := 20
swap(a, b)
fmt.Println(a, b) // 10 20

}

上述代码并没有将a和b交换。我们要明白,像int这种基本类型是一个值传递,main函数中的变量和swap函数的就完全没有关系,main函数中将参数传递到swap是先拷贝一份数据,将拷贝的数据传递过去。所以就有了指针。

对于内存来说,每一个字节其实都有地址,可以通过16进制打印出来。

如上图所示,变量b的值为156,而b的内存地址为0x1040a124,变量a存储了b的地址,就称a指向了b

指针的操作

1
2
3
4
// 现在有一种特殊的变量类型,这个变量只能保存地址
var ip *int // 这个变量里面只能保存地址类型这种值
ip = &a
fmt.Println(ip) // 0xc00000a0b8

&操作符用于获取变量的地址。如果在类型前面加*,表示指向这个类型的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {

a := 10

// 现在有一种特殊的变量类型,这个变量只能保存地址
var ip *int // 这个变量里面只能保存地址类型这种值
ip = &a
fmt.Println(ip) // 0xc00000a0b8

// 如果要修改指针指向的变量的值,用法也比较特殊
*ip = 50
fmt.Println(a) // 50
// 如何定义指针变量,如果修改指针变量指向内存中的值
fmt.Printf("ip所指向的内存空间地址是:%p,内存中的值:%d", ip, *ip)
// ip所指向的内存空间地址是:0xc00000a0b8,内存中的值:50

}

数组指针和指针数组

数组指针:指向数组导入指针

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var a [3]int = [3]int{1, 2, 3}
var b *[3]int = &a
fmt.Println(b)
}
// &[1 2 3]

指针数组:数组里面放入指针

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

import "fmt"

func main() {
var x, y, z = 1, 2, 3
var b [3]*int = [3]*int{&x, &y, &z}
fmt.Println(b)
}

// [0xc00001e0c8 0xc00001e0e0 0xc00001e0e8]

指针的默认值是nil,一般判断:

1
if ip != nil{}

交换a和b:

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

import "fmt"

func swap(a *int, b *int) {
// 用于交换a和b
c := *a
*a = *b
*b = c
}

func main() {
a := 10
b := 20
swap(&a, &b)
fmt.Println(a, b) // 20 10

}

指向指针的指针

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

import "fmt"

func main() {
var a int = 123
var b *int = &a
var c **int = &b
var d ***int = &c

fmt.Println(d) // 0xc00000e030
fmt.Println(*d) // 0xc00000e028
fmt.Println(**d) // 0xc00001e0c8
fmt.Println(***d) // 123
}

go的nil

先看一个现象:

1
2
var p *int  // 申明了一个变量p,但是这个变量没有初始值,没有内存,保留了一个占位符,但这个占位符实际上没有在内存中分配
*p = 10 // 往没有分配的内存赋了一个10,p没有空间

上述代码会报错。这是因为初始的时候没有分配空间。当然这是对于默认值为nil的类型来说的。

像int byte rune float bool string 都有默认值,所以初始化的时候一开始申明就会分配内存;但是像指针,切片,map,接口这些默认值为nil,没有一开始就分配内存。

new函数和make函数

对于指针这些默认值为nil的类型来说,如何一开始申明的时候就分配内存?

可以使用new函数:

1
2
var p *int = new(int)  // go的编译器就知道先申请一块内存空间,这里的内存值全部为0
*p = 10

当然除了new函数,还可以使用make函数:

1
2
var info map[string]string = make(map[string]string)
info["name"] = "cwz"

make和new的区别:

  • 两者都是内存的分配,但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,不会初始化内存,只会将内存置零。
  • make返回的还是这三个引用类型的实例;而new返回的是指向类型的指针

new函数图解

  • p变量仍然是占空间的,使用指针会增加额外空间
  • go语言遇到new之后会调用操作系统的接口要一块空余的内存空间,操作系统会把内存分配好,并把这块地址返回给go,go把这块内存地址放好

go的函数

函数的定义

语法:

1
2
3
func 函数名(参数1 类型,参数2 类型) 返回值 {
函数体
}

go定义函数的方法有几种:

  • 方法1
1
2
3
func add(a, b int) int {
return a + b
}

带参数的函数,两个参数类型相同,可以省略

  • 方法2
1
2
3
func add2(a, b int) int {
return a + b
}

有返回值

  • 方法3
1
2
3
4
func add2(a, b int) (sum int) {
sum = a + b
return
}

以给返回值命名,return 可以不用带返回值

  • 方法4
1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func test(a string, b int) (string, int) {
return a, b
}

func main() {
fmt.Println(test("cwz", 3))
}
// cwz 3

返回多个值

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 (
"errors"
"fmt"
)

func div(a, b int) (result int, err error) {
if b == 0 {
err = errors.New("被除数不能为0")
} else {
result = a / b
}

return result, err
}

func main() {

result, err := div(12, 3)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(result)
}

}

参数的设置

通过省略号来接收任意长度的参数

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

import "fmt"

// 通过省略号去动态设置多个参数值
func f1(params ...int) (sum int) {

for _, v := range params {
sum += v
}
return
}

func main() {

fmt.Println(f1(1,2,3,4,5))
fmt.Println(f1(1,2,3))
}

通过省略号将slice打散

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

// 通过省略号去动态设置多个参数值
func f1(params ...int) (sum int) {

for _, v := range params {
sum += v
}
return
}

func main() {

slice := []int{1,2,3,4,5}
fmt.Println(f1(slice...)) // 可以将slice打散成int,传过去

}

创建长度不定的数组

1
2
arr := [...]int{1,2,3}
fmt.Printf("%T", arr) // [3]int

匿名函数

go 中我们也可以使用匿名函数,经常用在一些临时的小函数中:

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

import "fmt"

func test() {
// 匿名函数直接加括号
func () {
fmt.Println("我是匿名函数")
}()
}

func main() {
test()
}
// 我是匿名函数

函数类型

go 里边函数其实也是一等公民,函数本身也是一种类型,所以我们可以定义一个函数然后赋值给一个变量:

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

import "fmt"

func main() {
res := func(a string) {fmt.Println(a)}
res("hello golang")
fmt.Printf("%T", res)
}

// hello golang
// func(string)

函数这个类型,它的参数,返回值,都是类型的一部分

1
2
3
4
5
var a func()
var b func(a,b int)
var c func(a,b int)int
var d func(a,b int)(int,string)
// 这几个的函数类型是不一样的

使用type 给函数类型重命名:

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

import "fmt"

type sub func(a, b int) int

func subImpl(a, b int) int {
return a - b
}

func main() {
var mySub sub = subImpl
fmt.Println(mySub(3,2)) // 1
}

闭包函数

闭包函数满足两个条件:

  • 定义在函数内部
  • 函数体代码对外部作用域有引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func test() {
su:="golang"
addSu := func(name string) string {
return name + su // 这里使用到了 su 这个变量,所以 addSu 就是一个闭包
}
fmt.Println(addSu("hello, "))
}

func main() {
test()
}

// hello, golang

go中没有装饰器语法糖,所以要利用闭包实现装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"
// 测试函数
func help(name string) {
fmt.Println("原函数", name)
}

// 装饰器函数
func decorator(a func(name string)) func(string) {
res:= func(name string) {
fmt.Println("装饰器函数") // 装饰器代码
a(name)
}
return res
}

func main() {
help := decorator(help)
help("hello golang")
}

go的异常处理

错误处理

在python中使用try / except来进行异常的捕获和处理。还可以通过异常栈来追踪异常的调用信息从而帮助我们修复异常代码。

在go中,错误用内建的error类型来表示,错误值可以存储在变量里,作为函数的返回值。

示例:

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

import (
"fmt"
"os"
)

func main() {
f, err := os.Open("1.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(f.Name(), "成功打开")
}

在 go 的惯例中,一般函数多个返回值的最后一个值用来返回错误,返回 nil 表示没有错误,调用者通过检查返回的错误是否是 nil 就知道是否需要处理错误了。

go的错误error类型

error 是 go 的一个内置的接口类型,比如你可以使用开发工具跳进去看下 error 的定义:

1
2
3
4
5
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}

error 的定义很简单,只要我们自己实现了一个类型的 Error() 方法返回一个字符串,就可以当做错误类型了。举一个简单小例子, 比如计算两个整数相除,我们知道除数是不能为 0 的,这个时候我们就可以写个函数:

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

import (
"errors" // 使用内置的 errors
"fmt"
)


func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}

func main() {
// fmt.Println(testDefer())
a, b := 1, 0
res, err := Divide(a, b)
if err != nil {
fmt.Println(err) // error 类型实现了 Error() 方法可以打印出来
}
fmt.Println(res)
}

defer语句

defer语句的使用

go 中提供了一个 defer 语句用来延迟一个函数(匿名函数)或者方法的执行

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

import "fmt"

func main() {
defer fmt.Println("我最后执行") // 延迟执行
fmt.Println("来了")
}

/*
来了
我最后执行
*/

需要注意的是:derfer之后只能是函数调用,不能是表达式

函数里可以使用多个 defer 语句,如果有多个 defer,它们会按照先进后出(Last In First Out)的顺序执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func testDefer() string {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
fmt.Println("函数体")
return "test"
}

func main() {
fmt.Println(testDefer())
}

/*
函数体
defer 2
defer 1
test
*/

defer语句的细节

defer语句执行时的拷贝机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// defer语句执行时的拷贝机制
test := func() {
fmt.Println("test1")
}
defer test()
test = func() {
fmt.Println("test2")
}
fmt.Println("test3")

/*
test3
test1
*/

值传递:

1
2
3
4
5
6
x := 10
defer func(a int) {
fmt.Println(a)
}(x)
x++
// 10

这个defer把函数的逻辑和变量值都压入栈中,函数和值都有了,就与外面的x++无关了。

引用传递:

1
2
3
4
5
6
x := 10
defer func(a *int) {
fmt.Println(*a)
}(&x)
x++
// 11

压栈的时候压的是函数的代码和函数里的参数值,这里参数值是一个指针,指向x的值,外部改了值之后,函数里是顺着指针指向那块内存取值的。x已经变成11了,所以打印的也是11。

1
2
3
4
5
6
7
x := 10
// 此处的defer函数并没有参数,函数内部使用的值是全局的值
defer func() {
fmt.Println(x)
}()
x++
// 11

这里压栈是把函数压入是没有参数的,函数里的逻辑是指向x的,x是外部的x,指向的是x的变量并不是x的值。

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
package main

import "fmt"

func handle() int {
x := 10
defer func() {
x++
}()
return x
}

func handle2() *int {
a := 10
b := &a
defer func() {
*b++
}()
return b
}


func main() {
fmt.Println(handle()) // 10
fmt.Println(*handle2()) // 11
}

在 return 之前会临时保存 x 的值

总结:

  • defer本质是是注册了一个延迟函数,defer函数的执行顺序已经确定
  • defer没有嵌套
  • 如果函数的返回值是无名的(不带命名返回值),则go语言会在执行return的时候会执行一个类似创建一个临时变量作为保存return值的动作
  • 而有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作,而后续的defer函数会改变这个返回值(虽然defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return)

go的异常处理

go 的异常处理机制 panic(恐慌)/recover(恢复),一般我们使用的是错误处理(error)而不是 panic。因为只有非常严重的场景 下才会发生 panic 导致代码退出

平常我们使用的 web 框架,一般即使出错了,我们也希望整个进程继续执行,而不是直接退出无法处理用户请求。 比如 python 的 web 框架,如果遇到了业务代码没有捕获的异常,框架会给我们捕获然后返回给客户端 500 的状态码表示代码有错。

go 里区分对待异常(panic)和错误(error)的,绝大部分场景下我们使用的都是错误,只有少数场景下发生了严重错误我们想让整个进程都退出了才会使用异常。

比如除法函数的例子,如果我们碰到了个除数为 0 被认为是严重错误,也可以使用 panic 抛出异常:

1
2
3
4
5
6
func div(a, b int) (int, error) {
if b == 0 {
panic("被除数为0")
}
return a / b, nil
}

如果我们传入除数0,但是又不想让进程退出呢?go 还提供了一个 recover 函数用来从异常中恢复,比如使用 recover 可以把一个 panic 包装成为 error 再返回,而不是让进程退出:

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 "fmt"

func div(a, b int) (int, error) {
if b == 0 {
panic("被除数为0")
}
return a / b, nil
}

func main() {
a := 10
b := 0
defer func() {
err := recover()
if err != nil {
fmt.Printf("异常被捕获到:%v\n", err)
}
fmt.Println("正常执行")
}()

fmt.Println(div(a, b))
}

/*
异常被捕获到:被除数为0
正常执行
*/

使用panic的坑:

  • panic会引起主线程的挂掉,同时会导致其他协程都挂掉
  • 在父协程中无法捕获子协程中出现的异常

go语言中的结构体

type的几种使用常见

  • 给一个类型定义别名
1
2
3
type myByte = byte
var b myByte
fmt.Printf("%T\n", b) // uint8
  • 基于一个已有的类型定义一个新的类型
1
2
3
type myInt int
var i myInt
fmt.Printf("%T\n", i) // main.myInt
  • 定义结构体
1
2
3
type Course struct {

}
  • 定义接口
1
2
3
type Callable interface {

}
  • 定义函数别名
1
type handle func(str string)

结构体的定义

go语言不支持传统的面向对象,go没有类class这个概念,但是go的结构体也可以实现面向对象的特性。

结构体只能定义数据,把数据聚合起来。

结构体的格式如下:

1
2
3
4
type 结构体名字 struct {
属性 类型
属性 类型
}

示例:

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

import "fmt"

type Person struct {
name string
age int
}

func main() {
p := Person{"张三", 30}
fmt.Println(p, p.name, p.age)
}
// {张三 30} 张三 30

这里面有一些细节,值得注意:

  • 在其他语言中,比如java是通过关键字public、private等来限制成员变量的访问权限的。在go语言中,它是通过结构体内部变量的大小写来决定访问权限的,首字母大写是公开变量,首字母小写是内部变量(只有同属于一个包下的代码才能直接访问)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

// 大小写表示公有和私有
type Person struct {
Name string
Age int
}

func main() {
// 实例化 kv形式
var p Person = Person{
Name: "张三",
Age: 30,
}
fmt.Println(p.Name, p.Age)
}
// 张三 30

结构体的实例化

  • kv形式 实例化结构体
1
2
3
4
5
6
7
8
9
type Person struct {
Name string
Age int
}

var p Person = Person{
Name: "张三",
Age: 30,
}
  • 顺序形式
1
2
3
4
5
type Person struct {
Name string
Age int
}
p := Person{"张三", 30}

一个指针指向结构体,通过结构体指针获取对象的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"unsafe"
)

type Course struct {
Name string
Price int
Url string
}

func main() {
c3 := &Course{"gin", 100, "https://www.baidu.com"}
fmt.Println((*c3).Name)
fmt.Println(c3.Name)
// 这里其实是go语言的语法糖 go语言内部会将 c3.Name转换成 (*c3).Name
}

结构体的零值

结构体是值类型,零值是属性的零值

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

import "fmt"

type Person struct {
name string
age int
}

func main() {
var p Person
fmt.Println(p) // { 0}
p.name = "cwz" // 赋值
p.age = 23
fmt.Println(p, p.name, p.age) // {cwz 23} cwz 23
}

多种方式零值初始化:

1
2
3
4
5
6
var c5 Course = Course{}
var c6 Course
var c7 *Course = new(Course)
fmt.Println(c5.Price)
fmt.Println(c6.Price)
fmt.Println(c7.Price)

go语言中结构体无处不在

先来看一下这个现象:

1
2
fmt.Println(unsafe.Sizeof(""))  // 16
fmt.Println(unsafe.Sizeof("asddddddddddddddddd")) // 16

空字符串和有值的字符串占用空间是一样的。

通过观察 Go 语言的底层源码,可以发现所有的 Go 语言内置的高级数据结构都是由结构体来完成的。

字符串头的结构体

在 64 位机器上将会占用 16 个字节

1
2
3
4
type string struct {
Data uintptr // 指针占8个长度
Len int // 64位系统 占8个长度
}

slice的结构体

在 64 位机器上将会占用 24 个字节

1
2
3
4
5
6
7
type slice struct {
array unsafe.Pointer // 底层数组地址
len int // 长度
cap int // 容量
}
s1 := []string{"django", "flask", "sanic", "fastapi"}
fmt.Println("切片占用的内存:", unsafe.Sizeof(s1)) // 切片占用的内存: 24

map的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
type hmap struct {
count int
...
buckets unsafe.Pointer // hash桶地址
...
}

m1 := map[string]string {
"k1": "v1",
"k2": "v2",
"k3": "v3",
}
fmt.Println(unsafe.Sizeof(m1)) // 8

数组只有「体」,切片除了「体」之外,还有「头」部。切片的头部和内容体是分离的,使用指针关联起来。看一下以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"
import "unsafe"

type ArrayStruct struct {
value [10]int
}

type SliceStruct struct {
value []int
}

func main() {
var as = ArrayStruct{[...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
var ss = SliceStruct{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
fmt.Println(unsafe.Sizeof(as), unsafe.Sizeof(ss))
// 80 24
}

结构体绑定方法

基本语法:

1
func (t Type) methodName(parameter list) {}

定义方法

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

import "fmt"

type Person struct {
name string
age int
}

// 给结构体绑定方法
func (p Person) printName() {
fmt.Println(p.name)
}


func main() {
p := Person{
name: "reese",
age: 34,
}
p.printName()
}

Go不是一个纯粹的面向对象编程语言,而且Go也不支持类。因此,基于类型的方法是一种实现和类 相似行为的途径。

结构体的两种接收形式

值接收

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
package main

import "fmt"

// 结构体的方法只能和结构体在同一个包中

type Person struct {
Name string
Age int
Address string
}

func (p Person) printPersonInfo() {
fmt.Printf("姓名:%s, 年龄:%d, 地址:%s", p.Name, p.Age, p.Address)
}

func (p *Person) setAge(age int) {
p.Age = age
}

func main() {
p1 := Person{"cwz", 19, "上海"}
Person.printPersonInfo(p)
Person.setAge(p, 20) // 参数是结构体,结构体是值传递,无法改

}

函数的参数传递都是值进行传递,也就是会拷贝参数的值,我们想要修改传入的值,就需要传递一个指针。

指针接收

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
package main

import "fmt"

// 结构体的方法只能和结构体在同一个包中

type Person struct {
Name string
Age int
Address string
}

func (p Person) printPersonInfo() {
fmt.Printf("姓名:%s, 年龄:%d, 地址:%s", p.Name, p.Age, p.Address)
}

// 指针类型接收器修改年龄
func (p *Person) setAge(age int) {
p.Age = age
}

func main() {
p1 := Person{"cwz", 19, "上海"}
//p1.setAge(20)
(&p1).setAge(20) // p1.setAge(20)和(&p1).setAge(20)是一样的
fmt.Println(p1.Age)

}
  • 不管是值类型接收器还是指针类型接收器,都可以用值和指针来调用
  • 指针接收器使用场景:
    • 结构体数据比较大,拷贝一个结构体的代价过大

内嵌结构体实现继承类似的效果

内嵌结构体

结构体作为一种变量它可以放进另外一个结构体作为一个字段来使用,这种内嵌结构体的形式在 Go 语言里称之为「组合」。下面我们来看看内嵌结构体的基本使用方法

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
package main

import "fmt"

type Person struct {
Name string
Age int
Hobby Hobby // person多一个爱好的信息,将另一个结构体的变量放进来
}

type Hobby struct {
Id int
Name string
}

func (p Person) personInfo() {
fmt.Printf("姓名:%s, 年龄:%d, 爱好:%s", p.Name, p.Age, p.Hobby.Name)
}

func main() {
p1 := Person{
Name: "reese",
Age: 18,
Hobby: Hobby{
Id: 1,
Name: "跑步",
},
}
p1.personInfo()
}

// 姓名:reese, 年龄:18, 爱好:跑步

匿名内嵌结构体

还有一种特殊的内嵌结构体形式,内嵌的结构体不提供名称。

这时外面的结构体将直接继承内嵌结构体所有的内部字段和方法,就好像把子结构体的一切全部都揉进了父结构体一样。

匿名的结构体字段将会自动获得以结构体类型的名字命名的字段名称

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
package main

import "fmt"

type Address struct {
city, state string
}
type People struct {
name string
age int
Address // 匿名字段
}

func main() {
var p People = People{
name: "cwz",
age: 19,
Address: Address{ // 因为Address是匿名结构体,所以需要这样声明
"Shanghai", "Jingan",
},
}

fmt.Println(p)
}
// {cwz 19 {Shanghai Jingan}}

如果嵌入结构的字段和外部结构的字段相同,那么想要修改嵌入结构的字段值需要加上外部结构中声明的嵌入结构名称

结构体的标签

结构体的字段除了名字和类型外,还可以有一个可选的标签(tag)。

  • 它是一个附属于字段的字符串,可以是文档或其他的重要标记
  • 比如在解析json或生成json文件时,常用到encoding/json包,它提供一些默认标签。
  • 例如:omitempty标签可以在序列化的时候忽略0值或者空值
  • -标签的作用是不进行序列化,其效果和和直接将结构体中的字段写成小写的效果一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"encoding/json"
"fmt"
)

type Info struct {
Name string
Age int `json:"age,omitempty"`
Gender string
}

func main() {
info := Info{
Name: "cwz",
Gender: "男",
}
re, _ := json.Marshal(info)
fmt.Println(string(re))
}
// {"Name":"cwz","Gender":"男"}

自定义标签,对字段进行处理

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
package main

import (
"encoding/json"
"fmt"
"reflect"
)

type Info struct {
Name string `orm:"name, max_length=10, min_length=3"`
Age int `orm:"age, min=18, max=80"`
Gender string `orm:"gender, required"`
}

func main() {
info := Info{
Name: "cwz",
Gender: "男",
}
re, _ := json.Marshal(info)
fmt.Println(string(re))

// 通过反射包去识别这些tag

t := reflect.TypeOf(info)

for i := 0; i < t.NumField(); i++ {
field := t.Field(i) // 获取结构体的每一个字段
tag := field.Tag.Get("orm")
fmt.Printf("%d. %v (%v), tag: '%v'\n", i+1, field.Name, field.Type.Name(), tag)
}

}

/*
1. Name (string), tag: 'name, max_length=10, min_length=3'
2. Age (int), tag: 'age, min=18, max=80'
3. Gender (string), tag: 'gender, required'
*/

go语言的接口

go语言的接口(protol 协议) 设计参考了鸭子类型 和 java的接口来设计的

鸭子类型

鸭子类型在python中是很常见的,python本身是同时支持鸭子类型和面向对象的。而且python本身就是基于鸭子类型设计的一门语言。

看一个经典的鸭子类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal:
def walk(self):
pass

def speak(self):
pass


class Dog():
def walk(self):
pass

def speak(self):
pass


class Cat(Animal):
pass


dog = Dog()

python语言本身的设计是基于鸭子类型来实现的,所以 Dog到底是不是Animal,不是由这个继承关系来确定的,而是这个类是否实现了和Animal一样的方法。

Animal实际上只是定义了一些方法的名称而已,其他的任何类只要实现了这个Animal里面的方法,那这个类就是Animal类型。

1
2
3
4
5
6
7
8
9
10
class Company:
def __init__(self, employee_list):
self.employee = employee_list

def __iter__(self):
return iter(self.employee)

company = Company(["tom", "bob", "jane"])
if isinstance(company, Iterable):
print("company是iterable类型")

Company没有继承Iterable,但是都实现了__iter__方法,所以就说company是iterable类型。

它的类型不是由它继承关系确定的,是由实现了指定名称的方法

go的接口是一种抽象类型

接口是一个协议,比如我要定义一个程序员接口:会写代码、能解决bug,这就认为是程序员了,其他的属性一律不管:

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
package main

import "fmt"

// 接口是一个协议
type Programmer interface {
Coding() string // 方法只是申明
Debug() string
}

type Pythoner struct {
}

func (p Pythoner) Coding() string {
fmt.Println("python开发者")
return "python开发者"
}

func (p Pythoner) Debug() string {
fmt.Println("我会python的debug")
return "我会python的debug"
}

func main() {
// 接口帮我们完成了go语言的多态
var pro Programmer = Pythoner{}
pro.Coding()
}

我们先声明了一个接口类型的值,只要实现了这个接口的 struct 变量,都可以赋值给它, 而调用方法的时候,go 会根据实际类型选择使用哪个 struct 的方法。

go的接口是一种抽象类型,只是申明方法,struct是具象的。 使用的时候将具象的赋值给抽象的。

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
package main

import (
"fmt"
)

type Test interface {
Tester()
}

type MyFloat float64

func (m MyFloat) Tester() {
fmt.Println(m)
}

func describe(t Test) {
fmt.Printf("接口类型 %T value %v\n", t, t)
}

func main() {
var t Test
f := MyFloat(23.4)
t = f
describe(t)
t.Tester()
}

/*
输出结果:
接口类型 main.MyFloat value 23.4
23.4
*/

Test 接口只有一个方法Tester(),而MyFloat 类型实现了该接口。main函数中,变量f(MyFloat类型)赋值给了t(Test类型)。现在t的具体类型为MyFloat,而t的值为23.4。describe函数打印出了接口的具体类型和值。

接口支持组合

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 Programmer interface {
Coding() string
Debug() string
}

type Designer interface {
Design() string
}

type Pythoner struct {
}

func (p Pythoner) Coding() string {
fmt.Println("python开发者")
return "python开发者"
}

func (p Pythoner) Debug() string {
fmt.Println("我会python的debug")
return "我会python的debug"
}

func (p Pythoner) Manage() string {
fmt.Println("不好意思,管理我也懂")
return "不好意思,管理我也懂"
}

func (p Pythoner) Design() string {
fmt.Println("我是一个python开发者,我也会ui设计")
return "我是一个python开发者,我也会ui设计"
}

type Manager interface {
Programmer
Designer
Manage() string
}

func main() {
// 接口组合
var m Manager = Pythoner{}
m.Debug()

}

上述代码定义了 Manager接口,包含了Programmer、Designer接口,以及自己的Manage方法。

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
49
50
51
52
53
package main

import "fmt"

type Programmer interface {
Coding() string
Debug() string
}

type Designer interface {
Design() string
}

type Pythoner struct {
UIDesigner
}

type UIDesigner struct {
}

func (d UIDesigner) Design() string {
fmt.Println("我会ui设计")
return "我会ui设计"
}

func (p Pythoner) Coding() string {
fmt.Println("python开发者")
return "python开发者"
}

func (p Pythoner) Debug() string {
fmt.Println("我会python的debug")
return "我会python的debug"
}

func (p Pythoner) Manage() string {
fmt.Println("不好意思,管理我也懂")
return "不好意思,管理我也懂"
}


type Manager interface {
Programmer
Designer
Manage() string
}

func main() {
var m Manager = Pythoner{}
m.Design()
// struct组合完成了接口

}

结构体组合实现了所有接口方法

空接口

没有包含方法的接口称为空接口。控接口表示为 interface{}。由于空接口没有方法,所以 所有类型都实现了空接口。

应用场景如下:

  • 可以把任何类型都赋值给空接口变量
1
2
3
4
5
6
7
type Person struct {
name string
age int
}
i = Person{}
i = 10
i = "cwz"
  • 参数传递
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
package main

import "fmt"

func printAnything(i interface{}) {
fmt.Printf("%v\n", i)
}

func main() {

var i interface{}

//2、参数传递,写出了动态函数的感觉
i = 10
printAnything(i)
i = "cwz"
printAnything(i)
i = []string{"aa", "bb"}
printAnything(i)

}
/*
10
cwz
[aa bb]
*/
  • 空接口可以作为map的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {

//3、空接口可以作为map的值,本来map的key的类型和value的类型都是要各自相同的
var personInfo = make(map[string]interface{})
personInfo["name"] = "cwz"
personInfo["age"] = 19
personInfo["hobby"] = [3]string{"跑步", "游泳"}
fmt.Printf("%v", personInfo)

}
// map[age:19 hobby:[跑步 游泳 ] name:cwz]

接口的类型断言

类型断言的语法格式如下:

1
2
instance, ok := interfaceVal.(RealType) 
// 如果 ok 为 true 的话,接口值就转成了我们需要的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func printA(i interface{}) {
if s, ok := i.(string); ok {
fmt.Printf("%s(字符串)\n", s)
}

if a, ok := i.(int); ok {
fmt.Printf("%d(整数)\n", a)
}
}

func main() {

x := 10
printA(x)

}
// 10(整数)

判断传的值是什么类型的

使用if判断写起来代码比较不工整,可以使用switch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func doSomething(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("%s(字符串)\n", v)
case int:
fmt.Printf("%d(整数)\n", v)
}
}

func main() {

x := 10
doSomething(x)

}

举一个应用场景:一个存储的接口,可能会使用很多的方式存储,比如本地存储,后期可能迁移到阿里云存储

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

import "fmt"

type AliOss struct {
}

type LocalFile struct {
}

func store(x interface{}) {
switch v := x.(type) {
case AliOss:
// 此处要做一些特殊的处理
case LocalFile:
// 本地存储
}

}

func main() {

}

实现sort排序

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
package main

import (
"fmt"
"sort"
)

type Person struct {
Name string
Age int
}

type PersonA []Person

func (p PersonA) Len() int {
return len(p)
}

func (p PersonA) Less(i, j int) bool {
return p[i].Age < p[j].Age
}

func (p PersonA) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}

func main() {
pp := PersonA{
Person{"cwz", 42},
Person{"reese", 37},
Person{"neo", 29},
}
// 通过sort来排序
sort.Sort(pp)
for _, v := range pp {
fmt.Println(v)
}

}
/*
{neo 29}
{reese 37}
{cwz 42}
*/

go语言的反射

反射介绍

反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

Go程序在运行期使用reflect包访问程序的反射信息。

reflect包

在Go语言的反射机制中,任何接口值都由是一个具体类型具体类型的值两部分组成的。

在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Typereflect.Value两部分组成,并且reflect包提供了reflect.TypeOfreflect.ValueOf两个函数来获取任意对象的Value和Type。

  • reflect.TypeOf(i) :获得接口值的类型
  • reflect.ValueOf(i):获得接口值的值

Typeof

在Go语言中,使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"reflect"
)

func reflectType(x interface{}) {
typeOf := reflect.TypeOf(x)
fmt.Printf("type:%v\n", typeOf)
}

func main() {

var a int32 = 20
reflectType(a) // type:int32
var b float64 = 3.1415
reflectType(b) // type:float64
}
获取类别Kind()

在反射中关于类型还划分为两种:

  • 类型Type
  • 种类Kind

我们可以使用type关键字构造很多自定义类型,Kind种类就是指底层的类型。

但在反射中,当需要区分指针、结构体等类型时,就会用到种类(Kind)

翻看Kind的源码:

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
type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)

举例说明,定义指针和结构体类型,通过反射查看它们的类型和种类:

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
package main

import (
"fmt"
"reflect"
)

type myInt int64

func reflectType(x interface{}) {
t := reflect.TypeOf(x)
fmt.Printf("type:%v, kind:%v\n", t.Name(), t.Kind())
}

func main() {
var a *float32 // 指针
var b myInt // 自定义类型
var c rune // 类型别名
reflectType(a) // type:, kind:ptr
reflectType(b) // type:myInt, kind:int64
reflectType(c) // type:int32, kind:int32

type person struct {
name string
age int
}
p := person{
name: "cwz",
age: 18,
}

reflectType(p) // type:person, kind:struct
}

上述代码中,a指针的.Name()是空。

Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空。

ValueOf

reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。

reflect.Value类型提供的获取原始值的方法如下:

方法 说明
Interface() interface {} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64 将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
Bool() bool 将值以 bool 类型返回
Bytes() []bytes 将值以字节数组 []bytes 类型返回
String() string 将值以字符串类型返回
通过反射获取值
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
package main

import (
"fmt"
"reflect"
)

func reflectValue(x interface{}) {
valueOf := reflect.ValueOf(x)
k := valueOf.Kind()
switch k {
case reflect.Int64:
// valueOf.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
fmt.Printf("type is int64, value is %d\n", int64(valueOf.Int()))
case reflect.Float32:
// valueOf.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
fmt.Printf("type is float32, value is %f\n", float32(valueOf.Float()))
case reflect.Float64:
// valueOf.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
fmt.Printf("type is float64, value is %f\n", float64(valueOf.Float()))
}
}
func main() {
var a float32 = 3.14
var b int64 = 100
reflectValue(a) // type is float32, value is 3.140000
reflectValue(b) // type is int64, value is 100

// 将int类型的原始值转换为reflect.Value类型
c := reflect.ValueOf(10)
fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}
通过反射设置值

想要在函数中通过反射修改变量的值,需要注意的是:

  • 函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的Elem()方法来获取指针对应的值。
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 (
"fmt"
"reflect"
)

func reflectSetValue1(x interface{}) {
v := reflect.ValueOf(x)
if v.Kind() == reflect.Int64 {
v.SetInt(200) //修改的是副本,reflect包会引发panic
}
}
func reflectSetValue2(x interface{}) {
v := reflect.ValueOf(x)

// 反射中使用 Elem()方法获取指针对应的值
if v.Elem().Kind() == reflect.Int64 {
v.Elem().SetInt(200)
}
}
func main() {
var a int64 = 100
// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
reflectSetValue2(&a)
fmt.Println(a) // 200
}

isNil()和isValid()

1
func (v Value) IsNil() bool

IsNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic

1
func (v Value) IsValid() bool

IsValid()返回v是否持有一个值。如果v是Value零值会返回false,此时v除了IsValid、String、Kind之外的方法都会导致panic

IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效

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
package main

import (
"fmt"
"reflect"
)

func main() {
// *int类型空指针
var a *int
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil()) // var a *int IsNil: true
// nil值
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) // nil IsValid: false

// 实例化一个匿名结构体
b := struct{}{}
// 尝试从结构体中查找"abc"字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid()) // 不存在的结构体成员: false
// 尝试从结构体中查找"abc"方法
fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid()) // 不存在的结构体方法: false

// map
c := map[string]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid()) // map中不存在的键: false
}

结构体反射

与结构体相关的方法

任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()Field()方法获得结构体成员的详细信息。

reflect.Type中与获取结构体成员相关的的方法如下表所示。

方法 说明
Field(i int) StructField 根据索引,返回索引对应的结构体字段的信息。
NumField() int 返回结构体成员字段数量。
FieldByName(name string) (StructField, bool) 根据给定字符串返回字符串对应的结构体字段的信息。
FieldByIndex(index []int) StructField 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField,bool) 根据传入的匹配函数匹配需要的字段。
NumMethod() int 返回该类型的方法集中方法的数目
Method(int) Method 返回该类型方法集中的第i个方法
MethodByName(string)(Method, bool) 根据方法名返回该类型方法集中的方法

StructField类型

StructField类型用来描述结构体中的一个字段的信息

1
2
3
4
5
6
7
8
9
10
11
type StructField struct {
// Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
// 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string
PkgPath string
Type Type // 字段的类型
Tag StructTag // 字段的标签
Offset uintptr // 字段在结构体中的字节偏移量
Index []int // 用于Type.FieldByIndex时的索引切片
Anonymous bool // 是否匿名字段
}

结构体反射示例

当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息

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
package main

import (
"fmt"
"reflect"
)

type student struct {
Name string `json:"name"`
Score int `json:"score"`
}

func main() {
stu1 := student{
Name: "小王子",
Score: 90,
}

t := reflect.TypeOf(stu1)
fmt.Println(t.Name(), t.Kind()) // student struct
// 通过for循环遍历结构体的所有字段信息
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
}

// 通过字段名获取指定结构体字段信息
if scoreField, ok := t.FieldByName("Score"); ok {
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
}
}
/*
student struct
name:Name index:[0] type:string json tag:name
name:Score index:[1] type:int json tag:score
name:Score index:[1] type:int json tag:score
*/

go语言的包管理

go的import各种姿势

单行导入和多行导入

在 Go 语言中,一个包可包含多个 .go 文件(这些文件必须得在同一级文件夹中),只要这些 .go 文件的头部都使用 package 关键字声明了同一个包。

导入包的方式分为两种:

  • 单行导入
1
2
import "fmt"
import "io/ioutil"
  • 多行导入
1
2
3
4
import (
"fmt"
"io/ioutil"
)

使用点的方式导入

这种格式相当于把fmt包直接合并到当前程序,在使用fmt包内的方法时可以不用加前缀fmt.

1
2
3
4
5
import . "fmt"

func main() {
Println("hello, world")
}

但这种用法,会有一定的隐患,就是导入的包里可能有函数,会和我们自己的函数发生冲突

使用别名

我们导入了两个具有同一包名的包时产生冲突,此时这里为其中一个包定义别名

1
2
3
4
import (
"crypto/rand"
mrand "math/rand" // 将名称替换为mrand避免冲突
)

包的初始化

每个包都允许有一个 init 函数,当这个包被导入时,会执行该包的这个 init 函数,做一些初始化任务。

对于 init 函数的执行有几点需要注意:

  • init 函数优先于 main 函数执行
  • init()函数在程序运行时自动被调用执行,不能在代码中主动调用它
  • 同一个包甚至同一个源文件,可以有多个 init 函数
  • 同一个包内的多个 init 顺序是不受保证的

包初始化执行的顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

var a int = 10

const pi = 3.14

func init() {
fmt.Println("init:", a)
}

func main() {
fmt.Println("hello world")
}
/*
init: 10
hello world
*/

可以看出,包的初始化顺序是:

  • 包作用域的常量和变量(常量优先于变量)——> init() ——> main()

init()函数的执行顺序:

Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。

在运行时,被最后导入的包会最先初始化并调用其init()函数:

包的匿名导入

当我们导入一个包时,如果这个包没有被使用到,在编译时,是会报错的。

如果想执行这个包的初始化的init函数,又不希望使用这个包内部的数据,就可以使用匿名引用。

使用方法如下,下划线为空白标识符,并不能被访问

1
import _ ”PackageTest/calc“

包不能出现环形引用的情况,比如包a引用了包b,包b引用了包c,如果包c又引用了包a,则编译不能通过

包的重复引用是允许的,比如包a引用了包b和包c,包b和包c都引用了包d。这种场景相当于重复引用了包d,这种情况是允许的,并且go的编译器保证包d的init函数只会执行一次。

go导入的是路径而不是包

  • 导入时,是按照目录导入。导入目录后,可以使用这个目录下的所有包。
  • 出于习惯,包名和目录名通常会设置成一样,所以会让你有一种你导入的是包的错觉

Go Modules的应用

之前Go 语言的的包依赖管理一直都被大家所诟病。Go官方也在一直在努力为开发者提供更方便易用的包管理方案,从最初的 GOPATH 到 GO VENDOR,再到最新的 GO Modules,虽然走了不少的弯路,但最终还是拿出了 Go Modules 这样像样的解决方案。

go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具

GOPATH方案

可以将其理解为工作目录,在这个工作目录下,通常有如下的目录结构:

  • bin:存放编译后生成的二进制可执行文件
  • pkg:存放编译后生成的 .a 文件
  • src:存放项目的源代码

将所有的包放在$GOPATH/src 目录下进行管理的方式就称为gopath模式。

这想想就觉得不行,这样包管理起来肯定很混乱

go vendor模式的过渡

vendor模式在 go1.5开始支持,相当于gopath的一个补丁方案

GOPATH 方案下不同项目下无法使用多个版本库,vendor的解决方案就是:

  • 在每个项目下都创建一个 vendor 目录,每个项目所需的依赖都只会下载到自己vendor目录下,项目之间的依赖包互不影响
  • 在编译时,v1.5 的 Go 在你设置了开启 GO15VENDOREXPERIMENT=1 (注:这个变量在 v1.6 版本默认为1,但是在 v1.7 后,已去掉该环境变量,默认开启 vendor 特性,无需你手动设置)后,会提升 vendor 目录的依赖包搜索路径的优先级(相较于 GOPATH)。

这样包的搜索顺序由高到低是这样的:

  • 当前包下的 vendor 目录
  • 向上级目录查找,直到找到 src 下的 vendor 目录
  • 在 GOROOT 目录下查找
  • 在 GOPATH 下面查找依赖包

当然这只是GOPATH方案的一个补丁方法,并没有从本质上解决包管理混乱的情况。

go mod

go modules 在 v1.11 版本正式推出,在v1.14 版本中,官方正式发话,称其已经足够成熟,可以应用于生产上。

从 v1.11 开始,go env 多了个环境变量: GO111MODULE ,这里的 111,其实就是 v1.11 的象征标志

GO111MODULE 是一个开关,通过它可以开启或关闭 go mod 模式:

  • GO111MODULE=off禁用模块支持,编译时会从GOPATHvendor文件夹中查找包。
  • GO111MODULE=on启用模块支持,编译时会忽略GOPATHvendor文件夹,只根据 go.mod下载依赖。
  • GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,自动开启模块支持。

开启go mod的命令:

1
go env -w GO111MODULE="on"

常用的 go mod 命令:

  • go mod download 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
  • go mod edit 编辑go.mod文件
  • go mod graph 打印模块依赖图
  • go mod init 初始化当前文件夹, 创建go.mod文件
  • go mod tidy 增加缺少的module,删除无用的module
  • go mod vendor 将依赖复制到vendor下
  • go mod verify 校验依赖
  • go mod why 解释为什么需要依赖

go.mod文件记录了项目所有的依赖信息,结构如下:

1
2
3
4
5
6
7
8
module github.com/cwz/studygo/module-test

go 1.16

require (
github.com/gin-gonic/gin v1.4.0
github.com/go-sql-driver/mysql v1.4.1
)
  • 第一行:模块的引用路径
  • 第二行:项目使用的go版本
  • 第三行:项目所需的直接依赖包及其版本

go语言的编码规范

  • 代码规范不是强制的,也就是你不遵循代码规范写出来的代码运行也是完全没有问题的
  • 代码规范目的是方便团队形成一个统一的代码风格,提高代码的可读性,规范性和统一性。本规范将从命名规范,注释规范,代码风格和 Go 语言提供的常用的工具这几个方面做一个说明
  • 规范并不是唯一的,也就是说理论上每个公司都可以制定自己的规范,不过一般来说整体上规范差异不会很大

命名规范

命名是代码规范中很重要的一部分,统一的命名规则有利于提高的代码的可读性,好的命名仅仅通过命名就可以获取到足够多的信息。

  • 当命名(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中 如Java的 public)
  • 命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中 如Java的 private )

包名:package

保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。包名应该为小写单词,不要使用下划线或者混合大小写。

1
2
package model
package main

文件名

尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词

1
user_model.go

结构体命名

  • 采用驼峰命名法,首字母根据访问控制大写或者小写
  • struct 申明和初始化格式采用多行
1
2
3
4
5
6
7
8
9
10
11
// 多行申明
type User struct{
Username string
Email string
}

// 多行初始化
u := User{
Username: "admin",
Email: "admin@admin.com",
}

接口命名

  • 命名规则基本和上面的结构体类型
  • 单个函数的结构名以 “er” 作为后缀,例如 Reader , Writer
1
2
3
type Reader interface {
Read(p []byte) (n int, err error)
}

变量命名

和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则:

  • 如果变量为私有,且特有名词为首个单词,则使用小写,如 apiClient
  • 其它情况都应当使用该名词原有的写法,如 APIClient、repoID、UserID
  • 错误示例:UrlArray,应该写成 urlArray 或者 URLArray
  • 若变量类型为 bool 类型,则名称应以 has, is, can 或 allow 开头
1
2
3
4
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool

常量命名

常量均需使用全部大写字母组成,并使用下划线分词

1
const APP_VER = "1.0"

如果是枚举类型的常量,需要先创建相应类型:

1
2
3
4
5
6
type Scheme string

const (
HTTP Scheme = "http"
HTTPS Scheme = "https"
)

注释规范

Go提供C风格的/* */块注释和C ++风格的//行注释。行注释是常态;块注释主要显示为包注释,但在表达式中很有用或禁用大量代码。

go 语言自带的 godoc 工具可以根据注释生成文档,生成可以自动生成对应的网站(golang.org 就是使用 godoc 工具直接生成的),注释的质量决定了生成的文档的质量。每个包都应该有一个包注释,在package子句之前有一个块注释。对于多文件包,包注释只需要存在于一个文件中,任何一个都可以。包评论应该介绍包,并提供与整个包相关的信息。它将首先出现在godoc页面上,并应设置下面的详细文档

包注释

每个包都应该有一个包注释,一个位于package子句之前的块注释或行注释。包如果有多个go文件,只需要出现在一个go文件中(一般是和包同名的文件)即可。 包注释应该包含下面基本信息(请严格按照这个顺序,简介,创建人,创建时间):

  • 包的基本简介(包名,简介)
  • 创建者,格式: 创建人: rtx 名
  • 创建时间,格式:创建时间: yyyyMMdd
1
2
3
// util 包, 该包包含了项目共用的一些常量,封装了项目中一些共用函数。
// 创建人: hanru
// 创建时间: 20190419

结构体、接口 注释

每个自定义的结构体或者接口都应该有注释说明,该注释对结构进行简要介绍,放在结构体定义的前一行,格式为: 结构体名, 结构体说明。同时结构体内的每个成员变量都要有说明,该说明放在成员变量的后面(注意对齐),实例如下:

1
2
3
4
5
// User , 用户对象,定义了用户的基础信息
type User struct{
Username string // 用户名
Email string // 邮箱
}

函数注释

每个函数都应该有注释说明,函数的注释应该包括三个方面(严格按照此顺序撰写):

  • 简要说明,格式说明:以函数名开头,“,”分隔说明部分
  • 参数列表:每行一个参数,参数名开头,“,”分隔说明部分
  • 返回值: 每行一个返回值
1
2
3
4
5
6
7
// NewtAttrModel , 属性数据层操作类的工厂方法
// 参数:
// ctx : 上下文信息
// 返回值:
// 属性操作类指针
func NewAttrModel(ctx *common.Context) *AttrModel {
}

代码逻辑注释

对于一些关键位置的代码逻辑,或者局部较为复杂的逻辑,需要有相应的逻辑说明,方便其他开发者阅读该段代码

注释风格

统一使用中文注释,对于中英文字符之间严格使用空格分隔, 这个不仅仅是中文和英文之间,英文和中文标点之间也都要使用空格分隔,例如:

1
// 从 Redis 中批量读取属性,对于没有读取到的 id , 记录到一个数组里面,准备从 DB 中读取

上面 Redis 、 id 、 DB 和其他中文字符之间都是用了空格分隔

  • 建议全部使用单行注释
  • 和代码的规范一样,单行注释不要过长,禁止超过 120 字符

import规范

import在多行的情况下,goimports会自动帮你格式化,但是我们这里还是规范一下import的一些规范,如果你在一个文件里面引入了一个package,还是建议采用如下格式:

1
2
3
import (
"fmt"
)

如果你的包引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包:

1
2
3
4
5
6
7
8
9
10
11
import (
"encoding/json"
"strings"

"github.com/astaxie/beego"
"github.com/go-sql-driver/mysql"

"myproject/models"
"myproject/controller"
"myproject/utils"
)

有顺序的引入包,不同的类型采用空格分离,第一种实标准库,第二是第三方包,第三是自己的项目包

1
2
3
4
5
// 这是不好的导入
import “../net”

// 这是正确的做法
import “github.com/repo/proj/src/net”

错误处理

  • 错误处理的原则就是不能丢弃任何有返回err的调用,不要使用 _ 丢弃,必须全部处理。接收到错误,要么返回err,或者使用log记录下来
  • 尽早return:一旦有错误发生,马上返回
  • 尽量不要使用panic,除非你知道你在做什么
  • 错误描述如果是英文必须为小写,不需要标点结尾
  • 采用独立的错误流进行处理
1
2
3
4
5
6
7
8
9
10
11
12
// 错误写法
if err != nil {
// error handling
} else {
// normal code
}

// 正确写法
if err != nil {
// error handling
return // or continue, etc.
}

go的并发编程

Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。

Go语言还提供channel在多个goroutine间进行通信。goroutinechannel是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。

goroutine

在python、java中实现并发编程并不轻松,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换。

但是Go中的goroutine机制使得在语言层面已经内置了调度和上下文切换的机制。当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了。

简单使用goroutine

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

启动单个goroutine

1
2
3
4
5
6
7
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
hello()
fmt.Println("main goroutine done!")
}

这个示例中hello函数和下面的语句是串行的,执行的结果是打印完Hello Goroutine!后打印main goroutine done!

接下来我们在调用hello函数前面加上关键字go,也就是启动一个goroutine去执行hello这个函数。

1
2
3
4
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
}

这一次的执行结果只打印了main goroutine done!,并没有打印Hello Goroutine!

在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束。

最简单的方法就是sleep:

1
2
3
4
5
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
time.Sleep(time.Second*2)
}

启动多个goroutine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"sync"
)
var wg sync.WaitGroup

func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {

for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}

goroutine调度问题

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建十万左右的goroutine也是可以的。

goroutine调度

GMP是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程:

  • G 就是goroutine,里面除了存放本goroutine信息外, 还有与所在P的绑定等信息
  • P 管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  • M 是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的

P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

更多了解GMP:

GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

Go语言中的操作系统线程和goroutine的关系:

  1. 一个操作系统线程对应用户态多个goroutine。
  2. go程序可以同时使用多个操作系统线程。
  3. goroutine和OS线程是多对多的关系,即m:n。

channel

Go语言的并发模型是CSP(Communicating Sequential Processes)提倡通过通信共享内存而不是通过共享内存而实现通信

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是一种类型,是引用类型,申明channel类型的格式如下:

1
var 变量 chan 元素类型
1
2
3
var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

初始化channel

通道是引用类型,通道类型的空值是 nil 。

1
2
var ch chan int
fmt.Println(ch) // <nil>

channel使用 make 初始化之后才能使用:

1
2
3
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

上面创建的都是无缓冲的channel,也可以指定缓冲大小:

1
ch7 := make(chan int, 2)

channel的操作

先定义一个channel:

1
ch := make(chan int)

一些操作:

1
2
3
4
5
6
ch <- 10  // 把10发送到ch中

x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果

close(ch) // 关闭通道

关闭后的通道有以下特点:

  • 对一个关闭的通道再发送值就会导致panic
  • 对一个关闭的通道进行接收,会一直获取值直到通道为空
  • 对一个关闭的并且没有值的通道,执行接收操作会得到对应类型的零值
  • 关闭一个已经关闭的通道会导致panic

无缓冲的通道

无缓冲的通道又称为阻塞的通道

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main(){
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}

上面的代码运行会产生一个经典的错误:

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
D:/workspace/golang_codes/test1/test.go:8 +0x65

使用channel不当,经常会出现deadlock!

我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。

上面的代码会阻塞在ch <- 10这一行代码形成死锁,其中一个解决的方法就是启用一个goroutine去接受值:

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

import "fmt"

func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道

有缓冲的通道

我们可以在使用make函数初始化通道的时候为其指定通道的容量:

1
2
3
4
5
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。

for range从通道取值

当向通道中发送完数据时,我们可以通过close函数来关闭通道。

当通道被关闭时,再往该通道发送值会引发panic,从该通道取值的操作会先取完通道中的值,再然后取到的值一直都是对应类型的零值。那如何判断一个通道是否被关闭了呢?

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
package main

import (
"fmt"
)

func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
}

从上面的例子中我们看到有两种方式在接收值的时候判断该通道是否被关闭,不过我们通常使用的是for range的方式。使用for range遍历通道,当通道被关闭的时候就会退出for range

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

Go语言中提供了单向通道来处理这种情况:

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
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}

func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}

func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
  • chan <-int 是一个只写单向通道,可以对其执行发送操作但是不能执行接收操作
  • <-chan int是一个只读单向通道,可以对其执行接收操作但是不能执行发送操作

在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来是不可以的。

worker pool(goroutine池)

在工作中我们通常会使用可以指定启动的goroutine数量–worker pool模式,控制goroutine的数量,防止goroutine泄漏和暴涨。

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
package main

import (
"fmt"
"time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker:%d start job:%d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("worker:%d end job:%d\n", id, j)
results <- j * 2
}
}

func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 开启3个goroutine
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 5个任务
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// 输出结果
for a := 1; a <= 5; a++ {
<-results
}
}

select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:

1
2
3
4
5
6
7
for{
// 尝试从ch1接收值
data, ok := <-ch1
// 尝试从ch2接收值
data, ok := <-ch2

}

这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。

select的使用类似于switch语句,有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。

select会一直等待,直到某个case的通信操作完成时,就执行case分支对应的语句。

1
2
3
4
5
6
7
8
9
10
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}
1
2
3
4
5
6
7
8
9
10
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}

使用select语句能提高代码的可读性。

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个。
  • 对于没有caseselect{}会一直等待,可用于阻塞main函数。

select的应用场景

select有很重要的一个应用就是超时处理。我们知道,如果没有case需要处理,select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况。

下面这个例子我们会在2秒后往channel c1中发送一个数据,但是select设置为1秒超时,因此我们会打印出timeout 1,而不是result 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"time"
)

func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}

其实它利用的是time.After方法,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。

并发锁

有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)

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

import (
"fmt"
"sync"
)

var x int64
var wg sync.WaitGroup

func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}

上面的代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:

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
package main

import (
"fmt"
"sync"
)

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

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
49
50
package main

import (
"fmt"
"sync"
"time"
)

var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)

func write() {
// lock.Lock() // 加互斥锁
rwlock.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设写操作耗时10毫秒
rwlock.Unlock() // 解写锁
// lock.Unlock() // 解互斥锁
wg.Done()
}

func read() {
// lock.Lock() // 加互斥锁
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解读锁
// lock.Unlock() // 解互斥锁
wg.Done()
}

func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}

for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}

wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}

sync.WaitGroup

在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:

方法名 功能
(wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

1
2
3
4
5
6
7
8
9
10
11
12
var wg sync.WaitGroup

func hello() {
defer wg.Done()
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(1)
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
wg.Wait()
}

需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针

go语言中的Context

Context,也叫上下文,它的接口定义如下:

1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}

可以看到 Context 接口共有 4 个方法:

  • Deadline:返回的第一个值是 截止时间,到了这个时间点,Context 会自动触发 Cancel 动作。返回的第二个值是 一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消 Context。
  • Done:返回一个只读的通道(只有在被cancel后才会返回),类型为 struct{}。当这个通道可读时,意味着parent context已经发起了取消请求,根据这个信号,开发者就可以做一些清理动作,退出goroutine。
  • Err:返回 context 被 cancel 的原因。
  • Value:返回被绑定到 Context 的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。

简单使用Context

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
package main

import (
"context"
"fmt"
"time"
)

func monitor(ctx context.Context, number int) {
for {
select {
// 其实可以写成 case <- ctx.Done()
// 这里仅是为了让你看到 Done 返回的内容
case v :=<- ctx.Done():
fmt.Printf("监控器%v,接收到通道值为:%v,监控结束。\n", number,v)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}

func main() {
ctx, cancel := context.WithCancel(context.Background())

for i :=1 ; i <= 5; i++ {
go monitor(ctx, i)
}

time.Sleep( 1 * time.Second)
// 关闭所有 goroutine
cancel()

// 等待5s,若此时屏幕没有输出 <正在监控中> 就说明所有的goroutine都已经关闭
time.Sleep( 5 * time.Second)

fmt.Println("主程序退出!!")

}

这里面的关键代码,也就三行:

  • 第一行:以 context.Background() 为 parent context 定义一个可取消的 context
1
ctx, cancel := context.WithCancel(context.Background())
  • 第二行:然后你可以在所有的goroutine 里利用 for + select 搭配来不断检查 ctx.Done() 是否可读,可读就说明该 context 已经取消,你可以清理 goroutine 并退出了。
1
case <- ctx.Done():
  • 第三行:当你想到取消 context 的时候,只要调用一下 cancel 方法即可。这个 cancel 就是我们在创建 ctx 的时候返回的第二个值。
1
cancel()

运行结果输出如下:

1
2
3
4
5
6
7
8
9
10
11
监控器2,正在监控中...
监控器1,正在监控中...
监控器5,正在监控中...
监控器3,正在监控中...
监控器4,正在监控中...
监控器5,接收到通道值为:{},监控结束。
监控器4,接收到通道值为:{},监控结束。
监控器2,接收到通道值为:{},监控结束。
监控器1,接收到通道值为:{},监控结束。
监控器3,接收到通道值为:{},监控结束。
主程序退出!!

根Context

创建 Context 必须要指定一个 父 Context。

Go 已经帮我们实现了2个,我们代码中最开始都是以这两个内置的context作为最顶层的parent context,衍生出更多的子Context。

1
2
3
4
5
6
7
8
9
10
11
12
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)

func Background() Context {
return background
}

func TODO() Context {
return todo
}
  • 一个是Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context,它不能被取消。
  • 一个是TODO,如果我们不知道该使用什么Context的时候,可以使用这个,但是实际应用中,暂时还没有使用过这个TODO。
  • 他们两个本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

Context 的继承衍生

上面在定义我们自己的 Context 时,我们使用的是 WithCancel 这个方法。

除它之外,context 包还有其他几个 With 系列的函数:

1
2
3
4
5
6
7
func WithCancel(parent Context) (ctx Context, cancel CancelFunc){}

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc){}

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc){}

func WithValue(parent Context, key, val interface{}) Context{}

这四个函数有一个共同的特点,就是第一个参数,都是接收一个 父context

通过一次继承,就多实现了一个功能,比如使用 WithCancel 函数传入 根context ,就创建出了一个子 context,该子context 相比 父context,就多了一个 cancel context 的功能

WithDeadline
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
package main

import (
"context"
"fmt"
"time"
)

func monitor(ctx context.Context, number int) {
for {
select {
case <-ctx.Done():
fmt.Printf("监控器%v,监控结束。\n", number)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}

func main() {
ctx01, cancel := context.WithCancel(context.Background())
ctx02, cancel := context.WithDeadline(ctx01, time.Now().Add(1*time.Second))

defer cancel()

for i := 1; i <= 5; i++ {
go monitor(ctx02, i)
}

time.Sleep(5 * time.Second)
if ctx02.Err() != nil {
fmt.Println("监控器取消的原因: ", ctx02.Err())
}

fmt.Println("主程序退出!!")
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
监控器4,正在监控中...
监控器5,正在监控中...
监控器3,正在监控中...
监控器2,正在监控中...
监控器1,正在监控中...
监控器5,监控结束。
监控器3,监控结束。
监控器1,监控结束。
监控器4,监控结束。
监控器2,监控结束。
监控器取消的原因: context deadline exceeded
主程序退出!!
WithTimeout

WithTimeout 和 WithDeadline 使用方法及功能基本一致,都是表示超过一定的时间会自动 cancel context,

唯一不同的地方:

  • WithDeadline 传入的第二个参数是 time.Time 类型,它是一个绝对的时间,意思是在什么时间点超时取消
  • WithTimeout 传入的第二个参数是 time.Duration 类型,它是一个相对的时间,意思是多长时间后超时取消
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
package main

import (
"context"
"fmt"
"time"
)

func monitor(ctx context.Context, number int) {
for {
select {
case <-ctx.Done():
fmt.Printf("监控器%v,监控结束。\n", number)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}

func main() {
ctx01, cancel := context.WithCancel(context.Background())

// 相比例子1,仅有这一行改动
ctx02, cancel := context.WithTimeout(ctx01, 1*time.Second)

defer cancel()

for i := 1; i <= 5; i++ {
go monitor(ctx02, i)
}

time.Sleep(5 * time.Second)
if ctx02.Err() != nil {
fmt.Println("监控器取消的原因: ", ctx02.Err())
}

fmt.Println("主程序退出!!")
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
监控器5,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器3,正在监控中...
监控器4,正在监控中...
监控器2,监控结束。
监控器3,监控结束。
监控器4,监控结束。
监控器1,监控结束。
监控器5,监控结束。
监控器取消的原因: context deadline exceeded
主程序退出!!

go网络编程

go实现TCP通信

服务端代码:

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
package main

import (
"bufio"
"fmt"
"net"
)

func process(conn net.Conn) {
defer conn.Close() // 处理完之后需要关闭连接
// 针对当前的连接做数据的发送和接收操作
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:])
if err != nil {
fmt.Printf("read from conn failed, err: %v\n", err)
break
}
recv := string(buf[:n])
fmt.Println("接收到的数据:", recv)
_, _ = conn.Write([]byte("ok")) // 把收到的数据返回给客户端
}
}

func main() {
// 1、启动服务
listen, err := net.Listen("tcp", "127.0.0.1:50070")
if err != nil {
fmt.Printf("listen failed, err: %v\n", err)
return
}
defer listen.Close()
for {
// 2、等待客户端来建立连接
conn, err := listen.Accept()
if err != nil {
fmt.Printf("accept failed, err: %v\n", err)
continue
}
// 3、启动一个单独的goroutine去处理连接
go process(conn)
}

}

客户端代码:

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
package main

import (
"bufio"
"fmt"
"net"
"os"
"strings"
)

func main() {
// 1、于服务端建立连接
conn, err := net.Dial("tcp", "127.0.0.1:50070")
if err != nil {
fmt.Printf("dail failed, err: %v\n", err)
return
}
// 2、利用该连接进行数据的发送和接收
input := bufio.NewReader(os.Stdin)
for {
s, _ := input.ReadString('\n')
s = strings.TrimSpace(s)
if strings.ToUpper(s) == "Q" {
return
}
// 给服务端发消息
_, err := conn.Write([]byte(s))
if err != nil {
fmt.Printf("send failed, err: %v\n", err)
return
}
// 从服务端接收回复的消息
var buf [1024]byte
n, err := conn.Read(buf[:])
if err != nil {
fmt.Printf("read failed, err: %v\n", err)
return
}
fmt.Println("收到服务端回复:", string(buf[:n]))
}
}

go实现实现UDP通信

服务端代码:

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
package main

import (
"fmt"
"net"
)

func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 50080,
})
if err != nil {
fmt.Printf("listen failed, err: %v\n", err)
return
}
defer listen.Close()
for {
var data [1024]byte
n, addr, err := listen.ReadFromUDP(data[:])
if err != nil {
fmt.Printf("read from udp failed, err: %v\n", err)
continue
}
fmt.Println("接收到的数据:", string(data[:n]))
_, err = listen.WriteToUDP(data[:n], addr)
if err != nil {
fmt.Printf("write to %v failed, err: %v\n", addr, err)
return
}
}
}

客户端代码:

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
package main

import (
"bufio"
"fmt"
"net"
"os"
)

func main() {
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 50080,
})
if err != nil {
fmt.Println("连接服务端失败,err:", err)
return
}
defer socket.Close()
input := bufio.NewReader(os.Stdin)
for {
s, _ := input.ReadString('\n')
_, err = socket.Write([]byte(s))
if err != nil {
fmt.Println("发送数据失败,err:", err)
return
}
// 接收数据
var buf [1024]byte
n, addr, err := socket.ReadFromUDP(buf[:])
if err != nil {
fmt.Println("接收数据失败,err:", err)
return
}
fmt.Printf("read from %v, msg: %v\n", addr, string(buf[:n]))
}

}

go websocket

server:

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
package main

import (
"net/http"

"github.com/gorilla/websocket"
)

var (
upgrader = websocket.Upgrader{
// 允许跨域
CheckOrigin: func(r *http.Request) bool {
return true
},
}
)

func wsHandler(w http.ResponseWriter, r *http.Request) {
var (
conn *websocket.Conn
err error
data []byte
)

if conn, err = upgrader.Upgrade(w, r, nil); err != nil {
return
}

for {
// 类型:Text,Binary
if _, data, err = conn.ReadMessage(); err != nil {
goto ERR
}
if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {
goto ERR
}
}

ERR:
conn.Close()

}

func main() {
http.HandleFunc("/ws", wsHandler)
http.ListenAndServe("0.0.0.0:8080", nil)
}

html页面:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
window.addEventListener("load", function (evt) {
var output = document.getElementById("output");
var input = document.getElementById("input");
var ws;
var print = function (message) {
var d = document.createElement("div");
d.innerHTML = message;
output.appendChild(d);
};
document.getElementById("open").onclick = function (evt) {
if (ws) {
return false;
}
ws = new WebSocket("ws://localhost:8080/ws");
ws.onopen = function (evt) {
print("OPEN");
}
ws.onclose = function (evt) {
print("CLOSE");
ws = null;
}
ws.onmessage = function (evt) {
print("RESPONSE: " + evt.data);
}
ws.onerror = function (evt) {
print("ERROR: " + evt.data);
}
return false;
};
document.getElementById("send").onclick = function (evt) {
if (!ws) {
return false;
}
print("SEND: " + input.value);
ws.send(input.value);
return false;
};
document.getElementById("close").onclick = function (evt) {
if (!ws) {
return false;
}
ws.close();
return false;
};
});
</script>
</head>
<body>
<table>
<tr>
<td valign="top" width="50%">
<p>Click "Open" to create a connection to the server,
"Send" to send a message to the server and "Close" to close the connection.
You can change the message and send multiple times.
</p>
<form>
<button id="open">Open</button>
<button id="close">Close</button>
<input id="input" type="text" value="Hello world!">
<button id="send">Send</button>
</form>
</td>
<td valign="top" width="50%">
<div id="output"></div>
</td>
</tr>
</table>
</body>
</html>

go语言的单元测试

go test工具

Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。

go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。

类型 格式 作用
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为Benchmark 测试函数的性能
示例函数 函数名前缀为Example 为文档提供示例文档

go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

测试函数

示例:

定义一个split包:

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

import "strings"

// 将s按照sep进行切割,返回一个字符串的切片
func Split(s, sep string) (ret []string) {
idx := strings.Index(s, sep)
for idx > -1 {
ret = append(ret, s[:idx])
s = s[idx+len(sep):]
idx = strings.Index(s, sep)
}
ret = append(ret, s)
return
}

在当前目录下,我们创建一个split_test.go的测试文件,并定义一个测试函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package split

import (
"reflect"
"testing"
)

// 测试
func TestSplit(t *testing.T) {
got := Split("我是机器人", "是")
want := []string{"我", "机器人"}
//got := Split("a:b:c", ":")
//want := []string{"a", "b", "c"}
if !reflect.DeepEqual(got, want) {
t.Errorf("want:%v go:%v", want, got)
}
}

split包路径下,执行go test命令

多语言通信的基础

RPC基础

什么是RPC

  • RPC(Remote Procedure Call)远程过程调用,简单的理解就是一个节点请求另一个节点提供的服务
  • 对应RPC的 是本地过程调用,函数调用是最常见的本地调用过程
  • 将本地过程调用编程远程过程调用会面临各种问题

本地过程调用

一个简单的本地过程调用的例子:

1
2
3
4
5
6
def add(a, b):
total = a + b
return total

total = add(1, 2)
print(total)

这个函数的调用过程:

  • 将1和2压入add函数的栈中
  • 进入add函数,从栈中取出1和2,分别赋值给a和b
  • 执行a + b 将结果赋值给局部的total并压栈
  • 将栈中的值取出来赋值给全局的total

远程过程调用带来的问题

在远程调用时,我们需要执行的函数体是在远程的机器上的,也就是说,add是在另一个进程中执行的。这就带来了几个新问题:

  • call ID 映射 我们怎么告诉远程机器我们要调用add,而不是sub或者Foo呢?
    • 在本地调用中,函数体是直接通过函数指针来指定的,我们调用add,编译器就自动帮我们调用它相应的函数指针。
    • 但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。
    • 客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <—> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。
    • 当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
  • 序列化和反序列化 客户端怎么把参数值传给远程的函数呢?
    • 在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)
    • 这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程
  • 网络传输 远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。
    • 网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。
    • 因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。Java的Netty也属于这层的东西。

如何解决

解决了上面三个机制,就能实现RPC了,具体过程如下:

client端解决的问题:

  • 将这个调用映射为Call ID。这里假设用最简单的字符串当Call ID的方法
  • 将Call ID,a和b序列化。可以直接将它们的值以二进制形式打包
  • 把2中得到的数据包发送给ServerAddr,这需要使用网络传输层
  • 等待服务器返回结果
  • 如果服务器调用成功,那么就将结果反序列化,并赋给total

server端解决的问题:

  • 在本地维护一个Call ID到函数指针的映射call_id_map,可以用dict完成
  • 等待请求,包括多线程的并发处理能力
  • 得到一个请求后,将其数据包反序列化,得到Call ID
  • 通过在call_id_map中查找,得到相应的函数指针
  • 将a和b反序列化后,在本地调用add函数,得到结果
  • 将结果序列化后通过网络返回给Client

其中:

  • Call ID映射可以直接使用函数字符串,也可以使用整数ID。映射表一般就是一个哈希表。
  • 序列化反序列化可以自己写,也可以使用Protobuf或者FlatBuffers之类的。
  • 网络传输库可以自己写socket,或者用asio,ZeroMQ,Netty之类。

rpc、http以及restful 之间的区别

RPC 和 REST 区别是什么

REST,是Representational State Transfer 的简写,中文描述表述性状态传递(是指某个瞬间状态的资源数据的快照,包括资源数据的内容、表述格式(XML、JSON)等信息)。

REST 是一种软件架构风格。这种风格的典型应用,就是HTTP。其因为简单、扩展性强的特点而广受开发者的青睐。

而RPC 呢,是 Remote Procedure Call Protocol 的简写,中文描述是远程过程调用,它可以实现客户端像调用本地服务(方法)一样调用服务器的服务(方法)。

RPC 可以基于 TCP/UDP,也可以基于 HTTP 协议进行传输的,按理说它和REST不是一个层面意义上的东西,不应该放在一起讨论,但是谁让REST这么流行呢,它是目前最流行的一套互联网应用程序的API设计标准,某种意义下,我们说 REST 可以其实就是指代 HTTP 协议。

使用方式不同

从使用上来看,HTTP 接口只关注服务提供方,对于客户端怎么调用并不关心。接口只要保证有客户端调用时,返回对应的数据就行了。而RPC则要求客户端接口保持和服务端的一致。

  • REST 是服务端把方法写好,客户端并不知道具体方法。客户端只想获取资源,所以发起HTTP请求,而服务端接收到请求后根据URI经过一系列的路由才定位到方法上面去
  • RPC是服务端提供好方法给客户端调用,客户端需要知道服务端的具体类、具体方法,然后像调用本地方法一样直接调用它。

面向对象不同

  • 从设计上来看,RPC,所谓的远程过程调用 ,是面向方法的
  • REST:所谓的 Representational state transfer ,是面向资源的

序列化协议不同

接口调用通常包含两个部分,序列化和通信协议。

通信协议,上面已经提及了,REST 是 基于 HTTP 协议,而 RPC 可以基于 TCP/UDP,也可以基于 HTTP 协议进行传输的。

常见的序列化协议,有:json、xml、hession、protobuf、thrift、text、bytes等,REST 通常使用的是 JSON或者XML,而 RPC 使用的是 JSON-RPC,或者 XML-RPC。

通过httpserver实现rpc

把远程的函数变成一个http请求

自己实现一个简易RPC:

server

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
import json
from urllib.parse import urlparse, parse_qsl
from http.server import HTTPServer, BaseHTTPRequestHandler

host = ('', 8003)


class AddHandler(BaseHTTPRequestHandler):
def do_GET(self):
parsed_url = urlparse(self.path)
qs = dict(parse_qsl(parsed_url.query))
a = int(qs.get("a", 0))
b = int(qs.get("b", 0))
self.send_response(200)
self.send_header("content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(
{
"result": a + b
}
).encode("utf-8"))


if __name__ == '__main__':
server = HTTPServer(host, AddHandler)
print("启动服务器")
server.serve_forever()

client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
import json


class client:
def __init__(self, url):
self.url = url

def add(self, a, b):
resp = requests.get(f"{self.url}/?a={a}&b={b}")
return json.loads(resp.text).get("result")


client = client("http://127.0.0.1:8003")
print(client.add(1, 4))

总的来说,本地程序调用的过程大致可以分为几个步骤和阶段:

  • 开发者开发好的程序,并进行编译,编译成机器认可的可执行文件
  • 运行可执行文件,调用对应的功能方法,期间会读取可执行文件中的机器指令,进行入栈,出栈赋值等操作。此时,计算机由可执行程序所在的进程控制
  • 调用结束,所有的内存数据出栈,程序执行结束。计算机继续由操作系统进行控制

远程过程调用是在两台或者多台不同的物理机器上实现的调用,其间要跨越网络进行调用。因此,我们再想通过前文本地方法调用的形式完成功能调用,就无法实现了,因为编译器无法通过编译的可执行文件来调用远程机器上的程序方法。因此需要采用RPC的方式来实现远端服务器上的程序方法的调用。

RPC技术内部原理是通过两种技术的组合来实现的:本地方法调用 和 网络通信技术

RPC开发的要素分析

RPC技术在架构设计上有四部分组成,分别是:客户端、客户端存根、服务端、服务端存根

  • 客户端(Client):服务调用发起方,也称为服务消费者
  • 客户端存根(Client Stub):该程序运行在客户端所在的计算机机器上,主要用来存储要调用的服务器的地址,另外,该程序还负责将客户端请求远端服务器程序的数据信息打包成数据包,通过网络发送给服务端Stub程序;其次,还要接收服务端Stub程序发送的调用结果数据包,并解析返回给客户端
  • 服务端(Server):远端的计算机机器上运行的程序,其中有客户端要调用的方法
  • 服务端存根(Server Stub):接收客户Stub程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务端执行调用的结果进行数据处理打包发送给客户端Stub程序

实际上,如果我们想要在网络中的任意两台计算机上实现远程调用过程,要解决很多问题,比如:

  • 两台物理机器在网络中要建立稳定可靠的通信连接
  • 两台服务器的通信协议的定义问题,即两台服务器上的程序如何识别对方的请求和返回结果。也就是说两台计算机必须都能够识别对方发来的信息,并且能够识别出其中的请求含义和返回含义,然后才能进行处理。这其实就是通信协议所要完成的工作

在上述图中,通过1-10的步骤图解的形式,说明了RPC每一步的调用过程。具体描述为:

  • 1、客户端想要发起一个远程过程调用,首先通过调用本地客户端Stub程序的方式调用想要使用的功能方法名;
  • 2、客户端Stub程序接收到了客户端的功能调用请求,将客户端请求调用的方法名,携带的参数等信息做序列化操作,并打包成数据包。
  • 3、客户端Stub查找到远程服务器程序的IP地址,调用Socket通信协议,通过网络发送给服务端。
  • 4、服务端Stub程序接收到客户端发送的数据包信息,并通过约定好的协议将数据进行反序列化,得到请求的方法名和请求参数等信息。
  • 5、服务端Stub程序准备相关数据,调用本地Server对应的功能方法进行,并传入相应的参数,进行业务处理。
  • 6、服务端程序根据已有业务逻辑执行调用过程,待业务执行结束,将执行结果返回给服务端Stub程序。
  • 7、服务端Stub程序将程序调用结果按照约定的协议进行序列化,并通过网络发送回客户端Stub程序。
  • 8、客户端Stub程序接收到服务端Stub发送的返回数据,对数据进行反序列化操作,并将调用返回的数据传递给客户端请求发起者。
  • 9、客户端请求发起者得到调用结果,整个RPC调用过程结束。

基于XML的RPC调用

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from xmlrpc.server import SimpleXMLRPCServer


class Calculater:
def add(self, x, y):
return x + y

def multiply(self, x, y):
return x * y

def subtract(self, x, y):
return abs(x - y)

def divide(self, x, y):
return x / y


obj = Calculater()
server = SimpleXMLRPCServer(("localhost", 8088))
# 将实例注册给rpc server
server.register_instance(obj)
print("Listening on port 8088")
server.serve_forever()

客户端:

1
2
3
4
from xmlrpc import client

server = client.ServerProxy("http://localhost:8088")
print(server.multiply(2, 3))

通过 server_proxy 对象就可以远程调用 RPC server的函数了

基于json实现RPC调用

SimpleXMLRPCServer 是基于 xml-rpc 实现的远程调用。

上面我们也提到 除了 xml-rpc 之外,还有 json-rpc 协议。

那 python 如何实现基于 json-rpc 协议呢?

答案有很多,很多web框架其自身都自己实现了json-rpc,但我们要独立这些框架之外,要寻求一种较为干净的解决方案,我们使用 jsonrpclib

https://github.com/tcalmant/jsonrpclib/

安装:

1
pip install jsonrpclib-pelix

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer


def add(a, b):
return a + b

# 1、实例化server
server = SimpleJSONRPCServer(('localhost', 8080))
# 2、将函数注册到server中
server.register_function(add)
# 3、启动server
server.serve_forever()

客户端:

1
2
3
4
import jsonrpclib

server = jsonrpclib.ServerProxy('http://localhost:8080')
print(server.add(2, 3))

zerorpc实现RPC调用

zerorpc 是利用 zeroMQ消息队列 + msgpack 消息序列化(二进制) 来实现类似 grpc 的功能,跨语言远程调用。

主要使用到 zeroMQ 的通信模式是 ROUTER–DEALER,模拟 grpc 的 请求-响应式 和 应答流式 RPC :zerorpc 还支持 PUB-SUB 通信模式的远程调用。

zerorpc实际上会依赖msgpack-python, pyzmq, future, greenlet, gevent

https://github.com/0rpc/zerorpc-python

一元调用

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import zerorpc


class HelloRPC(object):
def hello(self, name):
return "Hello, %s" % name


# 1、实例化一个server
# 2、绑定业务代码到server中
# 3、启动server
s = zerorpc.Server(HelloRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()

客户端:

1
2
3
4
5
import zerorpc

c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")
print(c.hello("RPC"))

流式响应

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
import zerorpc


class StreamingRPC(object):
@zerorpc.stream # @zerorpc.stream这里的函数修饰是必须的,否则会有异常,如TypeError: can’t serialize
def streaming_range(self, fr, to, step):
return range(fr, to, step)


s = zerorpc.Server(StreamingRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()

客户端:

1
2
3
4
5
6
7
import zerorpc

c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")

for item in c.streaming_range(10, 20, 2):
print(item)

传入多个参数

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
import zerorpc

class myRPC(object):
def listinfo(self,message):
return "get info : %s"%message

def getpow(self,n,m):
return n**m

s = zerorpc.Server(myRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()

客户端:

1
2
3
4
5
6
import zerorpc

c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")
print(c.listinfo("this is test string"))
print(c.getpow(2,5))

go语言的RPC调用

简单使用

Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。

服务端:

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 (
"net"
"net/rpc"
)

type HelloService struct{}

func (s *HelloService) Hello(request string, reply *string) error {
*reply = "hello " + request
return nil
}

func main() {
_ = rpc.RegisterName("HelloService", &HelloService{})
listener, err := net.Listen("tcp", ":1234")
if err != nil {
panic("监听端口失败")
}
conn, err := listener.Accept()
if err != nil {
panic("建立链接失败")
}
rpc.ServeConn(conn)

// go语言的rpc序列化协议是什么(Gob)?能否替换成常见的序列化

}

其中Hello方法必须满足Go语言的RPC规则:

  • 方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法

然后就可以将HelloService类型的对象注册为一个RPC服务:(TCP RPC服务)

其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”服务空间之下。然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"net/rpc"
)

func main() {
// 1、建立连接
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
panic("连接失败")
}
var reply *string = new(string)
err = client.Call("HelloService.Hello", "cwz", reply)
if err != nil {
panic("调用失败")
}
fmt.Println(*reply)
}

首先是通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法。在调用client.Call时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别我们定义RPC方法的两个参数。

RPC支持JSON

标准库的RPC默认采用Go语言特有的gob编码,因此从其它语言调用Go语言实现的RPC服务将比较困难。

在互联网的微服务时代,每个RPC以及服务的使用者都可能采用不同的编程语言,因此跨语言是互联网时代RPC的一个首要条件。

得益于RPC的框架设计,Go语言的RPC其实也是很容易实现跨语言支持的。

Go语言的RPC框架有两个比较有特色的设计:

  • 一个是RPC数据打包时可以通过插件实现自定义的编码和解码
  • 另一个是RPC建立在抽象的io.ReadWriteCloser接口之上的,我们可以将RPC架设在不同的通讯协议之上

首先是基于json编码重新实现RPC服务:

服务端

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
package main

import (
"net"
"net/rpc"
"net/rpc/jsonrpc"
)

type HelloService struct {
}

func (s *HelloService) Hello(request string, reply *string) error {
*reply = "hello, " + request
return nil
}

func main() {
listener, _ := net.Listen("tcp", "localhost:1234")
_ = rpc.RegisterName("HelloService", &HelloService{})
for {
conn, _ := listener.Accept()
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}

}

代码中最大的变化是用rpc.ServeCodec函数替代了rpc.ServeConn函数,传入的参数是针对服务端的json编解码器

客户端

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
package main

import (
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)

func main() {
// 1、建立连接
conn, err := net.Dial("tcp", "localhost:1234")
if err != nil {
panic("连接失败")
}
var reply string
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
err = client.Call("HelloService.Hello", "cwz", &reply)
if err != nil {
panic("调用失败")
}
fmt.Println(reply)
}

// {"method": "HelloService.Hello", "params": ["hello"], "id": 0}

python客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import json
import socket

req = {
"id": 0,
"params": ["cwz"],
"method": "HelloService.Hello",
}

client = socket.create_connection(("localhost", 1234))
client.sendall(json.dumps(req).encode())

# 获取服务器返回的数据
rsp = client.recv(1024)
rsp = json.loads(rsp.decode())
print(rsp) # {'id': 0, 'result': 'hello, cwz', 'error': None}

基于http的RPC

前面我们使用了支持json传输的RPC,但是是基于TCP协议的,不是很好用。

现在使用http协议改造这个代码

服务端

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
package main

import (
"io"
"net/http"
"net/rpc"
"net/rpc/jsonrpc"
)

type HelloService struct {
}

func (s *HelloService) Hello(request string, reply *string) error {
*reply = "hello, " + request
return nil
}

func main() {
_ = rpc.RegisterName("HelloService", &HelloService{})

http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
var conn io.ReadWriteCloser = struct {
io.Writer
io.ReadCloser
}{
ReadCloser: r.Body,
Writer: w,
}
_ = rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
})
_ = http.ListenAndServe(":1234", nil)

}

python客户端

1
2
3
4
5
6
7
8
9
10
import requests

req = {
"id": 0,
"params": ["cwz"],
"method": "HelloService.Hello",
}

rsp = requests.post("http://localhost:1234/jsonrpc", json=req)
print(rsp.text) # {"id":0,"result":"hello, cwz","error":null}

进一步改进RPC调用过程

前面的rpc调用虽然简单,但是和普通的http的调用差异不大,需要做出改进

serviceName统一和名称冲突的问题

  • server端和client端如何统一serviceName
  • 多个server的包中serviceName同名的问题

新建handler/handler.go文件内容如下:

1
2
3
package handler

const HelloServiceName = "handler/HelloService"

服务端:

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 (
"net"
"net/rpc"
"start/rpc_ch01/handler"
)



type HelloService struct {}
func (s *HelloService) Hello(request string, reply *string) error {
*reply = "hello "+ request
return nil
}

func main(){
_ = rpc.RegisterName(handler.HelloServiceName, &HelloService{})
listener, err := net.Listen("tcp", ":1234")
if err != nil {
panic("监听端口失败")
}
conn, err := listener.Accept()
if err != nil {
panic("建立链接失败")
}
rpc.ServeConn(conn)

}

客户端:

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

import (
"fmt"
"net/rpc"
"start/rpc_ch01/handler"
)

func main() {
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
panic("连接到服务器失败")
}

var reply string
err = client.Call(handler.HelloServiceName+".Hello", "imooc", &reply)
if err != nil {
panic("服务调用失败")
}

fmt.Println(reply)
}

继续屏蔽HelloServiceName和Hello函数名称

handler代码:

1
2
3
4
5
6
7
8
package handler

type HelloService struct{}

func (s *HelloService) Hello(request string, reply *string) error {
*reply = "hello " + request
return nil
}

服务端代理:

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

import "net/rpc"

const HelloServiceName = "handler/HelloService"

type HelloServiceInterface interface {
Hello(request string, reply *string) error
}

func RegisterHelloService(srv HelloServiceInterface) error {
return rpc.RegisterName(HelloServiceName, srv)
}

服务端:

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

import (
"net"
"net/rpc"
"start/rpc_ch01/handler"
"start/rpc_ch01/server_proxy"
)

func main(){
hellohandler := &handler.HelloService{}
_ = server_proxy.RegisterHelloService(hellohandler)
listener, err := net.Listen("tcp", ":1234")
if err != nil {
panic("监听端口失败")
}
conn, err := listener.Accept()
if err != nil {
panic("建立链接失败")
}
rpc.ServeConn(conn)

}

客户端代理:

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

import "net/rpc"

const HelloServiceName = "handler/HelloService"

type HelloServiceClient struct{
*rpc.Client
}
func NewClient(address string) HelloServiceClient {
conn, err := rpc.Dial("tcp", address)
if err != nil {
panic("连接服务器错误")
}
return HelloServiceClient{conn}
}

func (c *HelloServiceClient) Hello(request string, reply *string) error {
err := c.Call(HelloServiceName+".Hello", request, reply)
if err != nil {
return err
}
return nil
}

客户端:

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

import (
"fmt"
"start/rpc_ch01/client_proxy"
)

func main(){
client := client_proxy.NewClient("localhost:1234")
var reply string
err := client.Hello("cwz",&reply)
if err != nil {
panic("调用失败")
}
fmt.Println(reply)
}

gRPC入门

gRPC和protobuf

gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHPC# 支持

  • 习惯用 Json、XML 数据存储格式的你们,相信大多都没听过Protocol Buffer
  • Protocol Buffer 其实 是 Google出品的一种轻量 & 高效的结构化数据存储格式,性能好
  • protobuf经历了protobuf2和protobuf3,pb3比pb2简化了很多,目前主流的版本是pb3

protobuf的优点:

  • 性能
    • 压缩性好
    • 序列化和反序列化快,比json和xml快2~100倍
    • 传输速度快
  • 便捷性
    • 使用简单,自动生成序列化和反序列化代码
    • 维护成本低,只维护proto文件
    • 向后兼容,不必破快旧格式
    • 加密性好
  • 跨语言
    • 跨平台
    • 支持各种主流语言

protobuf的缺点:

  • 通用性差,json可以任何语言都支持,但是protobuf需要专门的解析库
  • 自解释性差,只有通过proto文件才能了解数据结构

python下protobuf体验

安装

1
2
python -m pip install grpcio #安装grpc
python -m pip install grpcio-tools #安装grpc tools

体验protobuf3

新建hello.proto文件

1
2
3
4
5
syntax = "proto3";

message HelloRequest {
string name = 1; // name表示名称,name的编号为1,1不是name的值
}

生成proto的python文件

1
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. hello.proto

查看protobuf生成的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from protobuf3_test.proto import hello_pb2

# 生成的pb文件不要去改
request = hello_pb2.HelloRequest()
request.name = "cwz"
res_str = request.SerializeToString()
print(res_str) # b'\n\x03cwz'
print(len(res_str)) # 5

res_json = {
"name": "cwz"
}
import json

print(len(json.dumps(res_json))) # 15

# 如何通过字符串反向生成字符串
request2 = hello_pb2.HelloRequest()
request2.ParseFromString(res_str)
print(request2) # name: "cwz"

# 和json对比一下

python体验grpc开发

helloworld.proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto3";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

生成proto的python文件:

1
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. helloworld.proto

python下解决grpc import路径出错的bug

https://github.com/protocolbuffers/protobuf/issues/1491

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from concurrent.futures import ThreadPoolExecutor
import grpc

from grpc_hello.proto import helloworld_pb2, helloworld_pb2_grpc


class Greeter(helloworld_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
return helloworld_pb2.HelloReply(message=f"你好, {request.name}")


if __name__ == '__main__':
# 1、实例化server
server = grpc.server(ThreadPoolExecutor(max_workers=10))
# 2、注册逻辑到server中
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 3、启动server
server.add_insecure_port('127.0.0.1:50051')
server.start()
server.wait_for_termination()

客户端:

1
2
3
4
5
6
7
8
import grpc
from grpc_hello.proto import helloworld_pb2_grpc, helloworld_pb2

if __name__ == '__main__':
with grpc.insecure_channel("localhost:50051") as channel:
stub = helloworld_pb2_grpc.GreeterStub(channel)
rsp: helloworld_pb2.HelloReply = stub.SayHello(helloworld_pb2.HelloRequest(name="cwz"))
print(rsp.message) # 你好, cwz

go体验grpc开发

下载工具:https://github.com/protocolbuffers/protobuf/releases

注意:protoc的版本需要和golang/protobuf保持一致

下载完成后解压后记得将路径添加到环境变量中

下载go的依赖包:

1
go get github.com/golang/protobuf/protoc-gen-go

proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

生成go文件:

1
protoc -I . goods.proto --go_out=plugins=grpc:.

服务端代码:

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 (
"context"
"google.golang.org/grpc"
"net"

"grpc_test_demo/grpc_test/proto"
)

type Server struct{}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
return &proto.HelloReply{
Message: "hello, " + request.Name,
}, nil
}

func main() {
server := grpc.NewServer()
proto.RegisterGreeterServer(server, &Server{})
listen, err := net.Listen("tcp", "0.0.0.0:8080")
if err != nil {
panic("failed to listen: " + err.Error())
}
err = server.Serve(listen)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}

客户端代码:

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

import (
"context"
"fmt"
"google.golang.org/grpc"

"grpc_test_demo/grpc_test/proto"
)

func main() {
conn, err := grpc.Dial("127.0.0.1:8000", grpc.WithInsecure())
if err != nil {
panic(nil)
}
defer conn.Close()

c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "cwz"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}

gRPC的流模式定义

grpc 中的 stream,srteam 顾名思义就是 一种流,可以源源不断的推送数据,很适合传输一些大数据,或者 服务端 和 客户端 长时间 数据交互,比如 客户端 可以向 服务端 订阅 一个数据,服务端 就 可以利用 stream ,源源不断地推送数据。

grpc四种模式:

  • 简单模式。客户端发起一次请求,服务端响应一个数据
  • 服务端数据流。客户端发起一次请求,服务端返回一段连续的数据流
  • 客户端数据流。客户端源源不断的向服务器发送数据流,发送结束后,由服务端返回一个响应
  • 双向数据量。客户端和服务端都可以向对方发送数据流,这个时候双方的数据可以同时互相发送,实时交互。

proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
syntax = "proto3";

option go_package = "./;proto";

service Greeter {
rpc GetStream(StreamReqData) returns (stream StreamResData); // 服务端流模式
rpc PutStream(stream StreamReqData) returns (StreamResData); // 客户端流模式
rpc AllStream(stream StreamReqData) returns (stream StreamResData); // 双向流模式
}

message StreamReqData{
string data = 1;
}

message StreamResData{
string data = 1;
}

服务端:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main

import (
"fmt"
"net"
"sync"
"time"

"google.golang.org/grpc"

"grpc_test_demo/stream_rpc_test/proto"
)

const PORT = ":50052"

type server struct{}

func (s *server) GetStream(req *proto.StreamReqData, res proto.Greeter_GetStreamServer) error {
i := 0
for {
i++
_ = res.Send(&proto.StreamResData{
Data: fmt.Sprintf("%v", time.Now().Unix()),
})
time.Sleep(time.Second)
if i > 10 {
break
}
}
return nil
}

func (s *server) PutStream(cliStr proto.Greeter_PutStreamServer) error {
for {
recv, err := cliStr.Recv()
if err != nil {
fmt.Println(err)
break
}
fmt.Println(recv)
}

return nil
}

func (s *server) AllStream(allStr proto.Greeter_AllStreamServer) error {
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for {
data, _ := allStr.Recv()
fmt.Println("收到客户端消息:" + data.Data)
}
}()
go func() {
defer wg.Done()
for {
_ = allStr.Send(&proto.StreamResData{Data: "我是服务器"})
time.Sleep(time.Second)
}
}()
wg.Wait()
return nil
}

func main() {
s := grpc.NewServer()
proto.RegisterGreeterServer(s, &server{})
listen, err := net.Listen("tcp", PORT)
if err != nil {
panic(err)
}
err = s.Serve(listen)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}

客户端:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
"context"
"fmt"
"sync"
"time"

"google.golang.org/grpc"

"grpc_test_demo/stream_rpc_test/proto"
)

func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithInsecure())
if err != nil {
panic(err)
}

defer conn.Close()

// 服务端流模式
c := proto.NewGreeterClient(conn)
res, _ := c.GetStream(context.Background(), &proto.StreamReqData{Data: "cwz"})
for {
recv, err := res.Recv()
if err != nil {
fmt.Println(err)
break
}
fmt.Println(recv)
}

// 客户端流模式
putStream, _ := c.PutStream(context.Background())
i := 0
for {
i++
_ = putStream.Send(&proto.StreamReqData{
Data: fmt.Sprintf("cwz%d", i),
})
time.Sleep(time.Second)
if i > 10 {
break
}
}

// 双向流模式
allStr, _ := c.AllStream(context.Background())
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for {
data, _ := allStr.Recv()
fmt.Println("收到服务端消息:" + data.Data)
}
}()
go func() {
defer wg.Done()
for {
_ = allStr.Send(&proto.StreamReqData{Data: "我是客户端"})
time.Sleep(time.Second)
}
}()
wg.Wait()
}

深入protobuf

protobuf的官方文档 https://developers.google.com/protocol-buffers/docs/proto3

option go_package的作用

指明生成的这个文件放在哪个地方

1
option go_package = "./;proto";

go_package不会对python、java产生影响

proto文件不同步出现的问题

golang的hello.proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
syntax = "proto3";
option go_package = "./;proto";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
string url = 2;
}

message HelloReply {
string message = 1;
}

client端:

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 (
"context"
"fmt"
"log"

"google.golang.org/grpc"

"grpc_test_demo/grpc_proto_test/proto"
)

func main() {
conn, err := grpc.Dial("127.0.0.1:50053", grpc.WithInsecure())
if err != nil {
log.Printf("连接失败:[%v]\n", err)
return
}
defer conn.Close()

client := proto.NewGreeterClient(conn)
rsp, _ := client.SayHello(context.Background(), &proto.HelloRequest{
Name: "cwz",
Url: "https://www.baidu.com",
})
fmt.Println(rsp.Message)
}

python的server端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from concurrent.futures import ThreadPoolExecutor
import grpc

from grpc_proto_test.proto import hello_pb2, hello_pb2_grpc


class Greeter(hello_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
return hello_pb2.HelloReply(message=f"你好, {request.name}, url: {request.url}")


if __name__ == '__main__':
# 1、实例化server
server = grpc.server(ThreadPoolExecutor(max_workers=10))
# 2、注册逻辑到server中
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 3、启动server
server.add_insecure_port('0.0.0.0:50053')
server.start()
server.wait_for_termination()

如果python中的proto有个地方写的和golang中的不一致,如:

1
2
3
4
message HelloRequest {
string url = 1;
string name = 2;
}

url和name写反了。这样重新生成proto文件,重新运行server和client端,并没有报错,只是获取的数据顺序不对了。

上面的url和name的编号是绑定的。

proto引用其他的proto文件

base.proto:

1
2
3
4
5
6
7
syntax = "proto3";

message Empty{}

message Pong{
string id = 1;
}

hello.proto引用base.proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";

import "base.proto";

option go_package = "./;proto";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
rpc Ping(Empty) returns (Pong);
}

message HelloRequest {
string name = 1;
string url = 2;
}

message HelloReply {
string message = 1;
}

嵌套的message对象

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
syntax = "proto3";

import "google/protobuf/empty.proto";
import "base.proto";

option go_package = "./;proto";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
rpc Ping(google.protobuf.Empty) returns (Pong);
}

message HelloRequest {
string name = 1;
string url = 2;
}


message HelloReply {
string message = 1;

message Result {
string name = 1;
string url = 2;

}
repeated Result data = 2;
}

protobuf中的其他类型

enum枚举类型

proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
syntax = "proto3";
option go_package = "./;proto";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
string url = 2;
Gender g = 3;
}

enum Gender {
MALE = 0;
FEMALE = 1;
}

message HelloReply {
string message = 1;
}

使用:

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
package main

import (
"context"
"fmt"
"log"

"google.golang.org/grpc"

"grpc_test_demo/grpc_proto_test/proto"
)

func main() {
conn, err := grpc.Dial("127.0.0.1:50053", grpc.WithInsecure())
if err != nil {
log.Printf("连接失败:[%v]\n", err)
return
}
defer conn.Close()

client := proto.NewGreeterClient(conn)
rsp, _ := client.SayHello(context.Background(), &proto.HelloRequest{
Name: "cwz",
Url: "https://www.baidu.com",
G: proto.Gender_MALE,
})
fmt.Println(rsp.Message)
}

map类型

proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";
option go_package = "./;proto";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
string url = 2;
Gender g = 3;
map<string, string> mp = 4;
}

enum Gender {
MALE = 0;
FEMALE = 1;
}

message HelloReply {
string message = 1;
}

使用:

1
2
3
4
5
6
7
8
9
rsp, _ := client.SayHello(context.Background(), &proto.HelloRequest{
Name: "cwz",
Url: "https://www.baidu.com",
G: proto.Gender_MALE,
Mp: map[string]string{
"username": "zhangsan",
"address": "SH",
},
})

timestamp类型

proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
syntax = "proto3";
import "google/protobuf/timestamp.proto";
option go_package = "./;proto";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
string url = 2;
Gender g = 3;
map<string, string> mp = 4;
google.protobuf.Timestamp addTime = 5;
}

enum Gender {
MALE = 0;
FEMALE = 1;
}

message HelloReply {
string message = 1;
}

使用:

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
package main

import (
"context"
"fmt"
"log"
"time"

"google.golang.org/grpc"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"

"grpc_test_demo/grpc_proto_test/proto"
)

func main() {
conn, err := grpc.Dial("127.0.0.1:50053", grpc.WithInsecure())
if err != nil {
log.Printf("连接失败:[%v]\n", err)
return
}
defer conn.Close()

client := proto.NewGreeterClient(conn)
rsp, _ := client.SayHello(context.Background(), &proto.HelloRequest{
Name: "cwz",
Url: "https://www.baidu.com",
G: proto.Gender_MALE,
Mp: map[string]string{
"username": "zhangsan",
"address": "SH",
},
AddTime: timestamppb.New(time.Now()),
})
fmt.Println(rsp.Message)
}

gRPC深入

python的grpc结合asyncio

官方地址:https://grpc.github.io/grpc/python/index.html

使用grpclib

proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

生成python源码:

1
2
3
pip install grpclib #安装依赖包
#生成对应的源码
python -m grpc_tools.protoc -I. --python_out=. --grpclib_python_out=. helloworld.proto

服务端:

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
import asyncio

from grpclib.utils import graceful_exit
from grpclib.server import Server

# generated by protoc
from .helloworld_pb2 import HelloReply
from .helloworld_grpc import GreeterBase


class Greeter(GreeterBase):

async def SayHello(self, stream):
request = await stream.recv_message()
message = f'Hello, {request.name}!'
await stream.send_message(HelloReply(message=message))


async def main(*, host='127.0.0.1', port=50051):
server = Server([Greeter()])
# Note: graceful_exit isn't supported in Windows
await server.start(host, port)
print(f'Serving on {host}:{port}')
await server.wait_closed()


if __name__ == '__main__':
asyncio.run(main())

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import asyncio

from grpclib.client import Channel

# generated by protoc
from .helloworld_pb2 import HelloRequest, HelloReply
from .helloworld_grpc import GreeterStub


async def main():
async with Channel('127.0.0.1', 50051) as channel:
greeter = GreeterStub(channel)

reply = await greeter.SayHello(HelloRequest(name='Dr. Strange'))
print(reply.message)


if __name__ == '__main__':
asyncio.run(main())

gRPC的metadata机制

gRPC让我们可以像本地调用一样实现远程调用,对于每一次的RPC调用中,都可能会有一些有用的数据,而这些数据就可以通过metadata来传递

metadata是以key-value的形式存储数据的,其中key是string类型,而value是[]string,即一个字符串数组类型。metadata使得client和server能够为对方提供关于本次调用的一些信息,就像一次http请求的RequestHeader和ResponseHeader一样。http中header的生命周周期是一次http请求,那么metadata的生命周期就是一次RPC调用。

go控制grpc的metadata

go中使用metadata

go中使用metadata比较麻烦。

官方源码:源码 项目文档:文档

新建metadata

MD 类型实际上是map,key是string,value是string类型的slice

1
type MD map[string][]string

创建的时候可以像创建普通的map类型一样使用new关键字进行创建:

1
2
3
4
5
6
7
8
//第一种方式
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
//第二种方式 key不区分大小写,会被统一转成小写。
md := metadata.Pairs(
"key1", "val1",
"key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
"key2", "val2",
)

发送metadata

1
2
3
4
5
6
7
md := metadata.Pairs("key", "val")

// 新建一个有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)

// 单向 RPC
response, err := client.SomeRPC(ctx, someRequest)

接收metadata

1
2
3
4
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
md, ok := metadata.FromIncomingContext(ctx)
// do something with metadata
}
grpc中使用metadata

proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto3";
option go_package = "./;proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

server:

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
package main

import (
"context"
"fmt"
"net"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"

"grpc_test_demo/grpc_test/proto"
)

type Server struct{}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
md, ok := metadata.FromIncomingContext(ctx)
if ok {
fmt.Println("get metadata error")
}
for key, val := range md {
fmt.Println(key, val)
}
return &proto.HelloReply{
Message: "hello, " + request.Name,
}, nil
}

func main() {
server := grpc.NewServer()
proto.RegisterGreeterServer(server, &Server{})
listen, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil {
panic("failed to listen: " + err.Error())
}
err = server.Serve(listen)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}

client:

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
package main

import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"grpc_test_demo/grpc_test/proto"
)

func main() {
conn, err := grpc.Dial("127.0.0.1:50051", grpc.WithInsecure())
if err != nil {
panic(nil)
}
defer conn.Close()

c := proto.NewGreeterClient(conn)

//md := metadata.Pairs("timestamp", time.Now().Format(timestampFormat))
md := metadata.New(map[string]string{
"name": "reese",
"password": "reese123",
})
ctx := metadata.NewOutgoingContext(context.Background(), md)

r, err := c.SayHello(ctx, &proto.HelloRequest{Name: "cwz"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}

运行:

python中使用metadata

可以看官方的例子:https://github.com/grpc/grpc/tree/master/examples/python/metadata

proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto3";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

client端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import grpc
from grpc_metadata_test.proto import helloworld_pb2_grpc, helloworld_pb2

if __name__ == '__main__':
with grpc.insecure_channel("localhost:50052") as channel:
stub = helloworld_pb2_grpc.GreeterStub(channel)
hello_request = helloworld_pb2.HelloRequest()
hello_request.name = "cwz"
response, call = stub.SayHello.with_call(
hello_request,
metadata=(
('name', 'cwz'),
('password', 'cwz321')
))
# rsp: helloworld_pb2.HelloReply = stub.SayHello(helloworld_pb2.HelloRequest(name="cwz"))
print(response.message) # 你好, cwz

server端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from concurrent.futures import ThreadPoolExecutor
import grpc

from grpc_metadata_test.proto import helloworld_pb2, helloworld_pb2_grpc


class Greeter(helloworld_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
for key, value in context.invocation_metadata():
print('Received initial metadata: key=%s value=%s' % (key, value))

return helloworld_pb2.HelloReply(message=f"你好, {request.name}")


if __name__ == '__main__':
# 1、实例化server
server = grpc.server(ThreadPoolExecutor(max_workers=10))
# 2、注册逻辑到server中
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 3、启动server
server.add_insecure_port('0.0.0.0:50052')
server.start()
server.wait_for_termination()

运行:

gRPC的拦截器

go实现

proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
//将 sessionid放入 放入cookie中 http协议
message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

client:

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
package main

import (
"context"
"fmt"
"time"

"google.golang.org/grpc"

"grpc_test_demo/grpc_test/proto"
)

func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Printf("耗时:%s\n", time.Since(start))
return err
}

func main() {

opt := grpc.WithUnaryInterceptor(interceptor)
conn, err := grpc.Dial("127.0.0.1:50051", grpc.WithInsecure(), opt)
if err != nil {
panic(nil)
}
defer conn.Close()

c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "cwz"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}

server:

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
package main

import (
"context"
"fmt"
"net"

"google.golang.org/grpc"

"grpc_test_demo/grpc_test/proto"
)

type Server struct{}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
return &proto.HelloReply{
Message: "hello, " + request.Name,
}, nil
}

func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
fmt.Println("接收到了一个新的请求")
res, err := handler(ctx, req)
fmt.Println("请求完成")
return res, err
}

func main() {

opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt)
proto.RegisterGreeterServer(g, &Server{})
listen, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil {
panic("failed to listen: " + err.Error())
}
err = g.Serve(listen)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}

拦截器的应用场景go-grpc-middleware

python实现

proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

client:

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
import grpc
from datetime import datetime

from grpc_interceptor.proto import helloworld_pb2
from grpc_interceptor.proto import helloworld_pb2_grpc


class DefaultValueClientInterceptor(grpc.UnaryUnaryClientInterceptor):

def intercept_unary_unary(self, continuation, client_call_details, request):
start = datetime.now()
rsp = continuation(client_call_details, request)
last = datetime.now()-start
print(last)
print(last.microseconds/1000)
# print(f"用时: {datetime.now()-start}")
return rsp


def run():
default_value_interceptor = DefaultValueClientInterceptor()
with grpc.insecure_channel('localhost:50051') as channel:
intercept_channel = grpc.intercept_channel(channel,
default_value_interceptor)
stub = helloworld_pb2_grpc.GreeterStub(intercept_channel)
response = stub.SayHello(helloworld_pb2.HelloRequest(name='you'))
print("Greeter client received: " + response.message)


if __name__ == '__main__':
run()

server:

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
from concurrent import futures
import logging

import grpc

from grpc_interceptor.proto import helloworld_pb2
from grpc_interceptor.proto import helloworld_pb2_grpc



class LogInterceptor(grpc.ServerInterceptor):

def intercept_service(self, continuation, handler_call_details):
print("请求开始")
rsp = continuation(handler_call_details)
print("请求结束")
return rsp

class Greeter(helloworld_pb2_grpc.GreeterServicer):

def SayHello(self, request, context):
return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name)


def serve():
header_validator = LogInterceptor()
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10),
interceptors=(header_validator,))
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

通过拦截器和metadata实现grpc的auth认证

对于server来说,client连接过来的时候提供id和密码,才能访问server

proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

server:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main

import (
"context"
"fmt"
"net"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"

"grpc_test_demo/grpc_test/proto"
)

type Server struct{}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
return &proto.HelloReply{
Message: "hello, " + request.Name,
}, nil
}

func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
fmt.Println("接收到了一个新的请求")

md, ok := metadata.FromIncomingContext(ctx)
fmt.Println(md)
if !ok {
// gprc的错误处理
return resp, status.Error(codes.Unauthenticated, "无token认证信息")
}

var (
appid string
appkey string
)

if va1, ok := md["appid"]; ok {
appid = va1[0]
}

if va1, ok := md["appkey"]; ok {
appkey = va1[0]
}

if appid != "010001" || appkey != "i am key" {
return resp, status.Error(codes.Unauthenticated, "无token认证信息")
}
// 继续处理请求
res, err := handler(ctx, req)
fmt.Println("请求完成")
return res, err
}

func main() {

opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt)
proto.RegisterGreeterServer(g, &Server{})
listen, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil {
panic("failed to listen: " + err.Error())
}
err = g.Serve(listen)
if err != nil {
panic("failed to start grpc: " + err.Error())
}
}

client:

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
49
50
51
52
53
54
55
56
57
58
package main

import (
"context"
"fmt"
"google.golang.org/grpc"

"grpc_test_demo/grpc_test/proto"
)

type customCredential struct{}

func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "010001",
"appkey": "i am key",
}, nil
}

func (c customCredential) RequireTransportSecurity() bool {
return false
}

//func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// start := time.Now()
//
// md := metadata.New(map[string]string{
// "appid": "010001",
// "appkey": "i am key",
// })
// ctx = metadata.NewOutgoingContext(context.Background(), md)
//
// err := invoker(ctx, method, req, reply, cc, opts...)
// fmt.Printf("耗时:%s\n", time.Since(start))
// return err
//}

func main() {

grpc.WithPerRPCCredentials(customCredential{})

var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithPerRPCCredentials(customCredential{}))
conn, err := grpc.Dial("127.0.0.1:50051", opts...)

if err != nil {
panic(nil)
}
defer conn.Close()

c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "cwz"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}

grpc的验证器

https://github.com/envoyproxy/protoc-gen-validate

安装和配置

linux:

1
2
3
4
5
# fetches this repo into $GOPATH
go get -d github.com/envoyproxy/protoc-gen-validate

# installs PGV into $GOPATH/bin
make build

windows:

exe下载地址:https://oss.sonatype.org/content/repositories/snapshots/io/envoyproxy/protoc-gen-validate/protoc-gen-validate/

将解压后的exe文件拷贝到go的根目录的bin下

生成go源码:

1
protoc -I .  --go_out=plugins=grpc:. --validate_out="lang=go:." helloworld.proto

proto文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
syntax = "proto3";

import "validate.proto";
option go_package=".;proto";

service Greeter {
rpc SayHello (Person) returns (Person);
}

message Person {
uint64 id = 1 [(validate.rules).uint64.gt = 999];

string email = 2 [(validate.rules).string.email = true];
string name = 3 [(validate.rules).string = {
pattern: "^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$",max_bytes: 256,}];

}

服务端

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
49
50
51
52
53
package main

import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net"

"google.golang.org/grpc"

"start/pgv_test/proto"
)


type Server struct{}

func (s *Server) SayHello(ctx context.Context, request *proto.Person) (*proto.Person,
error){
return &proto.Person{
Id: 32,
}, nil
}

type Validator interface {
Validate() error
}

func main(){
var interceptor grpc.UnaryServerInterceptor
interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
// 继续处理请求
if r, ok := req.(Validator); ok {
if err := r.Validate(); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
}

return handler(ctx, req)
}
var opts []grpc.ServerOption
opts = append(opts, grpc.UnaryInterceptor(interceptor))

g := grpc.NewServer(opts...)
proto.RegisterGreeterServer(g, &Server{})
lis, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil{
panic("failed to listen:"+err.Error())
}
err = g.Serve(lis)
if err != nil{
panic("failed to start grpc:"+err.Error())
}
}

客户端

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
package  main

import (
"context"
"fmt"
"google.golang.org/grpc"
"start/pgv_test/proto"
)

type customCredential struct{}


func main() {
var opts []grpc.DialOption

//opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
opts = append(opts, grpc.WithInsecure())

conn, err := grpc.Dial("localhost:50051", opts...)
if err != nil {
panic(err)
}
defer conn.Close()

c := proto.NewGreeterClient(conn)
//rsp, _ := c.Search(context.Background(), &empty.Empty{})
rsp, err := c.SayHello(context.Background(), &proto.Person{
Email: "cwz",
})
if err != nil {
panic(err)
}
fmt.Println(rsp.Id)
}

gRPC中的异常处理

gRPC的状态码:

https://github.com/grpc/grpc/blob/master/doc/statuscodes.md

python中的异常处理

服务端:

1
2
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details('记录不存在')

客户端:

1
2
3
4
5
6
7
8
9
try:
stub.SayHello(Request())
print(rsp.status)
except grpc.RpcError as e:
d = e.details()
print(d)
status_code = e.code()
print(status_code.name)
print(status_code.value)

go中的异常处理

服务端:

1
st := status.New(codes.InvalidArgument, "invalid username")

客户端:

1
2
3
4
5
6
st, ok := status.FromError(err)
if !ok {
// Error was not a status error
}
st.Message()
st.Code()

gRPC的超时机制

python

1
response = stub.SayHello(helloworld_pb2.HelloRequest(name='you'), timeout=30)

go

1
2
3
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*3)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})

python的orm:peewee

简单使用

安装

1
2
pip install peewee
pip install pymysql

定义表结构

官网文档:http://docs.peewee-orm.com/en/latest/peewee/models.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from peewee import *
import datetime


db = MySQLDatabase('peewee',host ='127.0.0.1',user='root',passwd='root');


class User(Model):
username = CharField(unique=True)

class Meta:
database = db

class Tweet(Model):
user = ForeignKeyField(User, backref='tweets')
message = TextField()
created_date = DateTimeField(default=datetime.datetime.now)
is_published = BooleanField(default=True)

class Meta:
database = db

生成数据表

1
2
db.connect()
db.create_tables([User, Tweet])

增删改查

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
# 1、添加
charlie = User.create(username='charlie')
charlie.save() # 这里会失败。 save方法既可以完成新建,也可以完成更新的操作(你的对象中主键值是否有设置,你是一个更新的操作)
# 可以这么改:
charlie.save(force_insert=True)

# 一般这么添加:
huey = User.create(username='huey')



# 2、查询
# 2.1 get,返回的是直接的User对象,如果查询不到就会排除异常
User.get(User.username == 'charlie')
query = User.get_by_id(User.username == "chali")

# 2.2 查询所有
users = User.select() # 1、没有看到sql查询语句,用来组装sql 2、对象是ModelSelect
print(users, type(users))
for user in User.select():
print(user.username)

usernames = ["chali", "huey", "micky"]
users = User.select().where(User.username.in_(usernames))
for user in users:
print(user.username)




# 更新
Counter.update(count=Counter.count + 1).where(Counter.url == request.url)



# 删除
user = User.get(User.id == 1)
user.delete_instance()

query = Tweet.delete().where(Tweet.creation_date < one_year_ago)
query.execute()

peewee更多功能

表继承

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
import datetime
from peewee import *
import logging

logger = logging.getLogger("peewee")
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler())

db = MySQLDatabase('peewee', host='127.0.0.1', user='root', passwd='123456')


class BaseModel(Model):
add_time = DateTimeField(default=datetime.datetime.now(), verbose_name="添加时间")

class Meta:
database = db


class Person(BaseModel):
name = CharField(verbose_name='姓名', max_length=10, null=False, index=True)
passwd = CharField(verbose_name='密码', max_length=20, null=False, default='123456')
email = CharField(verbose_name='邮件', max_length=50, null=True, unique=True)
gender = IntegerField(verbose_name='姓别', null=False, default=1)
birthday = DateField(verbose_name='生日', null=True, default=None)
is_admin = BooleanField(verbose_name='是否是管理员', default=True)

class Meta:
database = db # 这里是数据库链接,为了方便建立多个表,可以把这个部分提炼出来形成一个新的类
table_name = 'persons' # 这里可以自定义表名


if __name__ == '__main__':
db.connection()
db.create_tables([Person])

主键和约束

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
# 主键和约束
class Person(Model):
first = CharField()
last = CharField()

class Meta:
primary_key = CompositeKey('first', 'last')

class Pet(Model):
owner_first = CharField()
owner_last = CharField()
pet_name = CharField()

class Meta:
constraints = [SQL('FOREIGN KEY(owner_first, owner_last) REFERENCES person(first, last)')]


# 复合主键
class BlogToTag(Model):
"""A simple "through" table for many-to-many relationship."""
blog = ForeignKeyField(Blog)
tag = ForeignKeyField(Tag)

class Meta:
primary_key = CompositeKey('blog', 'tag')

数据插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
p_id = Person.insert({'name': 'bobby'}).execute() # 插入一条数据,返回主键
print(p_id) # 打印出新插入数据的id

data_source = [
{'field1': 'val1-1', 'field2': 'val1-2'},
{'field1': 'val2-1', 'field2': 'val2-2'},
# ...
]

for data_dict in data_source:
Model.create(**data_dict)

#若是有很多数据需要插入,例如几万条数据,为了性能,这时就需要使用insert_many(),如下:

data = [
{'facid': 9, 'name': 'Spa', 'membercost': 20, 'guestcost': 30,'initialoutlay': 100000, 'monthlymaintenance': 800},
{'facid': 10, 'name': 'Squash Court 2', 'membercost': 3.5,'guestcost': 17.5, 'initialoutlay': 5000, 'monthlymaintenance': 80}]
query = Facility.insert_many(data) # 插入了多个

各种查询

单条查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
User.get(User.id == 1)

User.get_by_id(1) # Same as above.

User[1] # Also same as above.
g = Person.select().where(Person.name == 'Grandma L.').get() # where是查询一个集合, select是查询字段

g = Person.get(Person.name == 'fff.') # get是得到第一个

g = Person.select().where(Person.age > 23).get()
# select 代表sql语句中select后面的语句表示要展示的字段 # where 代表where条件语句 得到一个数据集合,用for循环遍历 # get()代表找第一个


person, created = Person.get_or_create(
first_name=first_name,
last_name=last_name,
defaults={'dob': dob, 'favorite_color': 'green'}
)

复合条件查询

1
2
3
query1 = Person.select().where((Person.name == "fff0") | (Person.name == "sss1"))

query2 = Person.select().where((Person.name == "fff") & (Person.is_relative == True))

模糊查询

1
query = Facility.select().where(Facility.name.contains('tennis'))

in查询

1
query = Facility.select().where(Facility.facid.in_([1, 5]))

字典展示

1
2
3
query = User.select().dicts()
for row in query:
print(row)

排序、limit、去重

1
2
3
4
5
6
7
8
query = (Person.select(Person.name).order_by(Person.name).limit(10).distinct())  # 几乎和sql一模一样
Person.select().order_by(Person.birthday.desc()) # 日期排序


Tweet.select().order_by(-Tweet.created_date)

# 查询整张表的数据条数
total_num = Person.select().count()

聚合函数

1
2
3
4
5
6
7
8
9
# SELECT MAX(birthday) FROM person;
query = Person.select(fn.MAX(Person.birthday))

# SELECT name, is_relative FROM person WHERE birthday = (SELECT MAX(birthday) FROM person);
MemberAlias = Member.alias() # 如果一个查询中用了两个表,需要这个Alias作为影子

subq = MemberAlias.select(fn.MAX(MemberAlias.joindate))

query = (Member.select(Person.is_relative, Person.name, ).where(Person.birthday == subq))

分页和计数

1
2
3
4
5
6
# paginate两个参数:page_number 和 items_per_page
for tweet in Tweet.select().order_by(Tweet.id).paginate(2, 10):
print(tweet.message)

# 返回查到了多少条记录
Tweet.select().where(Tweet.id > 50).count()

执行原生SQL

1
2
3
query = MyModel.raw('SELECT * FROM my_table WHERE data = %s', user_data)

query = MyModel.select().where(SQL('Some SQL expression %s' % user_data))

查询多条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查询Person整张表的数据
persons = Person.select()
# 遍历数据
for p in persons:
print(p.name, p.birthday, p.is_relative)

# 获取is_relative为True的数据
# 我们可以在select()后面添加where()当做查询条件
persons = Person.select().where(Person.is_relative == True)
for p in persons:
print(p.name, p.birthday, p.is_relative)

#打印sql
persons = Person.select().where(Person.is_relative == True)
# 打印出的结果为:('SELECT `t1`.`id`, `t1`.`name`, `t1`.`is_relative` FROM `Person` AS `t1` WHERE (`t1`.`is_relative` = %s)', [True])
print(persons.sql())

limit和offset

1
2
3
4
5
# 相当于sql语句: select * from person order by create_time desc limit 5
persons = Person.select().order_by(Person.create_time.asc()).limit(5)

# 相当于sql语句中:select * from person order by create_time desc limit 2, 5
persons = Person.select().order_by(Person.create_time.asc()).limit(5).offset(2)

表连接

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
49
50
51
import datetime
from peewee import *


class BaseModel(Model):
class Meta:
database = db

class User(BaseModel):
username = TextField()

class Tweet(BaseModel):
content = TextField()
timestamp = DateTimeField(default=datetime.datetime.now)
user = ForeignKeyField(User, backref='tweets')

class Favorite(BaseModel):
user = ForeignKeyField(User, backref='favorites')
tweet = ForeignKeyField(Tweet, backref='favorites')

#数据准备
def populate_test_data():
db.create_tables([User, Tweet, Favorite])

data = (
('huey', ('meow', 'hiss', 'purr')),
('mickey', ('woof', 'whine')),
('zaizee', ()))
for username, tweets in data:
user = User.create(username=username)
for tweet in tweets:
Tweet.create(user=user, content=tweet)

# Populate a few favorites for our users, such that:
favorite_data = (
('huey', ['whine']),
('mickey', ['purr']),
('zaizee', ['meow', 'purr']))
for username, favorites in favorite_data:
user = User.get(User.username == username)
for content in favorites:
tweet = Tweet.get(Tweet.content == content)
Favorite.create(user=user, tweet=tweet)

query = Tweet.select().join(User).where(User.username == 'huey')
#等价于
query = (Tweet
.select()
.join(User, on=(Tweet.user == User.id))
.where(User.username == 'huey'))
huey.tweets.sql()

外键

1
2
3
4
5
6
7
8
9
10
11
#正查
dog1 = Pet.get(name="dog1")
dog1.owner.name

# 反查
tweets = User.get(User.username=="huey").tweets
for tweet in tweets:
print(tweet.content)
favs = User.get(User.username=="huey").favorites
for fav in favs:
print(fav.user.username, fav.tweet.content)

避免N+1查询

N+1查询指的是当应用提交一次查询获取结果,然后在取得结果数据集的每一行时,应用至少再次查询一次(也可以看做是嵌套循环)

大多数情况下,n 查询可以通过使用SQL join或子查询来避免。数据库本身可能做了嵌套循环,但是它比在你的应用代码本身里做这些n查询更高效,后者通常会导致与数据库再次潜在通讯,没有利用数据库本身关联和执行子查询时会进行切片等优化工作。

1
2
3
4
5
6
7
8
query = (Tweet
.select(Tweet, User) # Note that we are selecting both models.
.join(User) # Use an INNER join because every tweet has an author.
.order_by(Tweet.id.desc()) # Get the most recent tweets.
.limit(10))

for tweet in query:
print tweet.user.username, '-', tweet.message

没有用join时,得到tweet.user.username会触发一次查询去解析外键tweet.user从而得到相关联的user。

由于我们在User上关联并选择,peewee自动为我们解析外键

go的web框架

gin

gin的helloworld体验

官网地址:https://gin-gonic.com/docs/quickstart/

安装:

1
go get -u github.com/gin-gonic/gin

实现:

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

import (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
// 实例化gin的server对象
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}

使用get、post、put等http方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
// 使用默认中间件创建一个gin路由器
// logger and recovery (crash-free) 中间件
router := gin.Default()

router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)

// 默认启动的是 8080端口,也可以自己定义启动端口
router.Run()
// router.Run(":3000") for a hard coded port
}

使用New和Default初始化路由器的区别:

创建一个路由器实例:

1
2
3
func main() {
router := gin.Default()
}

还可以使用:

1
2
3
func main() {
router := gin.New()
}

区别就是Default会默认开启两个中间件:logger and recovery (crash-free) 中间件

gin的路由分组和url

路由分组

安装服务的前缀分组

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 (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
r := gin.Default()
goodsGroup := r.Group("/goods")
{
goodsGroup.GET("/list", goodsList)
goodsGroup.GET("/1", goodsDetail)
}
r.Run()
}

func goodsDetail(context *gin.Context) {

}


func goodsList(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"name": "goodsList",
})
}

带参数的url

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
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
r := gin.Default()
goodsGroup := r.Group("/goods")
{
goodsGroup.GET("/list", goodsList)
//goodsGroup.GET("/1", goodsDetail)
goodsGroup.GET("/:id/:action", goodsDetail)
}
r.Run()
}

func goodsDetail(c *gin.Context) {

id := c.Param("id")
action := c.Param("action")
c.JSON(http.StatusOK, gin.H{
"id": id,
"action": action,
})
// http://127.0.0.1:8080/goods/1/detail

}

func goodsList(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"name": "goodsList",
})
}

获取路由分组的参数

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
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

type Person struct {
ID int `uri:"id" binding:"required"`
Name string `uri:"name" binding:"required"`
}

func main() {
router := gin.Default()
router.GET("/:name/:id", func(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
c.Status(404)
return
}
c.JSON(http.StatusOK, gin.H{
"name": person.Name,
"id": person.ID,
})
// http://127.0.0.1:8080/cwz/12
})
router.Run()
}

获取get和post表单信息

获取get参数

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

import (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
router := gin.Default()
router.GET("/welcome", welcome)
router.Run()
}

func welcome(c *gin.Context) {
first := c.DefaultQuery("first", "cwz")
last := c.DefaultQuery("last", "jack")
c.JSON(http.StatusOK, gin.H{
"first_name": first,
"last_name": last,
})

}

访问:http://127.0.0.1:8080/welcome,显示默认值:

1
2
3
4
{
first_name: "cwz",
last_name: "jack"
}

访问:http://127.0.0.1:8080/welcome?first=zs&last=lisi,获取get请求的值:

1
2
3
4
{
first_name: "zs",
last_name: "lisi"
}

获取post参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
router := gin.Default()

router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous") // 此方法可以设置默认值

c.JSON(200, gin.H{
"status": "posted",
"message": message,
"nick": nick,
})
})
router.Run(":8080")
}

get post混合

POST /post?id=1234&page=1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded

name=manu&message=this_is_great

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
router := gin.Default()

router.POST("/post", func(c *gin.Context) {

id := c.Query("id")
page := c.DefaultQuery("page", "0")
name := c.PostForm("name")
message := c.PostForm("message")

fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
})
router.Run(":8080")
}

JSON、ProtoBuf 渲染

输出JSON

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
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
router := gin.Default()
router.GET("/moreJSON", moreJSON)
router.Run()
}

func moreJSON(c *gin.Context) {
var msg struct {
Name string `json:"user"` // 利用结构体的标签特性
Message string
Number int
}
msg.Name = "cwz"
msg.Message = "这是一个测试json"
msg.Number = 20

c.JSON(http.StatusOK, msg)
}

访问http://127.0.0.1:8080/moreJSON,输出:

1
2
3
4
5
{
user: "cwz",
Message: "这是一个测试json",
Number: 20
}

输出ProtoBuf

新建user.proto文件:

1
2
3
4
5
6
7
syntax = "proto3";
option go_package = ".;proto";

message Teacher {
string name = 1;
repeated string course = 2;
}
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
package main

import (
"github.com/gin-gonic/gin"
"net/http"
"start/gin_t/proto"
)

func main() {
r := gin.Default()

r.GET("/someProtoBuf", func(c *gin.Context) {
courses := []string{"python", "django", "go"}
// The specific definition of protobuf is written in the testdata/protoexample file.
data := &proto.Teacher{
Name: "justgo",
Course: courses,
}
// Note that data becomes binary data in the response
// Will output protoexample.Test protobuf serialized data
c.ProtoBuf(http.StatusOK, data)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8083")
}

PureJSON

通常情况下,JSON会将特殊的HTML字符替换为对应的unicode字符,比如<替换为\u003c,如果想原样输出html,则使用PureJSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
r := gin.Default()

// Serves unicode entities
r.GET("/json", func(c *gin.Context) {
c.JSON(200, gin.H{
"html": "<b>Hello, world!</b>",
})
})

// Serves literal characters
r.GET("/purejson", func(c *gin.Context) {
c.PureJSON(200, gin.H{
"html": "<b>Hello, world!</b>",
})
})

// listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

表单验证

validator库参数校验若干实用技巧

表单的基本验证

若要将请求主体绑定到结构体中,请使用模型绑定,目前支持JSON、XML、YAML和标准表单值(foo=bar&boo=baz)的绑定。

Gin使用 go-playground/validator 验证参数,查看完整文档

需要在绑定的字段上设置tag,比如,绑定格式为json,需要这样设置 json:"fieldname"

此外,Gin还提供了两套绑定方法:

  • Must bind
    • 这些方法底层使用 MustBindWith,如果存在绑定错误,请求将被以下指令中止,响应状态代码会被设置为400,请求头Content-Type被设置为text/plain; charset=utf-8
  • Should bind
    • 这些方法底层使用 ShouldBindWith,如果存在绑定错误,则返回错误,开发人员可以正确处理请求和错误

当我们使用绑定方法时,Gin会根据Content-Type推断出使用哪种绑定器,如果你确定你绑定的是什么,你可以使用MustBindWith或者BindingWith

你还可以给字段指定特定规则的修饰符,如果一个字段用binding:"required"修饰,并且在绑定时该字段的值为空,那么将返回一个错误。

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
49
50
51
52
53
54
55
56
57
package main

import (
"fmt"
"net/http"

"github.com/gin-gonic/gin"
)

type LoginForm struct {
User string `json:"user" binding:"required,min=3,max=10"`
Password string `json:"password" binding:"required"`
}

type SignUpForm struct {
Age uint8 `json:"age" binding:"gte=1,lte=150"`
Name string `json:"name" binding:"required,min=2"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

func main() {

router := gin.Default()
router.POST("/loginJSON", func(c *gin.Context) {
var loginForm LoginForm
if err := c.ShouldBind(&loginForm); err != nil {
fmt.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "登录成功",
})

})

router.POST("/signup", func(c *gin.Context) {
var signUpForm SignUpForm
if err := c.ShouldBind(&signUpForm); err != nil {
fmt.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "注册成功",
})
})

router.Run()

}

表单验证错误翻译成中文

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package main

import (
"fmt"
"net/http"

"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTranslations "github.com/go-playground/validator/v10/translations/en"
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
)

// 定义一个全局翻译器T
var trans ut.Translator

// InitTrans 初始化翻译器
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎属性,实现自定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {

zhT := zh.New() // 中文翻译器
enT := en.New() // 英文翻译器

// 第一个参数是备用(fallback)的语言环境
// 后面的参数是应该支持的语言环境(支持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)

// locale 通常取决于 http 请求头的 'Accept-Language'
var ok bool
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
}

// 注册翻译器
switch locale {
case "en":
err = enTranslations.RegisterDefaultTranslations(v, trans)
case "zh":
err = zhTranslations.RegisterDefaultTranslations(v, trans)
default:
err = enTranslations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}

type SignUpParam struct {
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

func main() {
if err := InitTrans("zh"); err != nil {
fmt.Printf("init trans failed, err:%v\n", err)
return
}

r := gin.Default()

r.POST("/signup", func(c *gin.Context) {
var u SignUpParam
if err := c.ShouldBind(&u); err != nil {
// 获取validator.ValidationErrors类型的errors
errs, ok := err.(validator.ValidationErrors)
if !ok {
// 非validator.ValidationErrors类型错误直接返回
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
return
}
// validator.ValidationErrors类型错误则进行翻译
c.JSON(http.StatusOK, gin.H{
"msg":errs.Translate(trans),
})
return
}
// 保存入库等具体业务逻辑代码...

c.JSON(http.StatusOK, "success")
})

_ = r.Run(":8999")
}

进一步改进校验

上面的错误提示看起来是可以了,但是还是差点意思,首先是错误提示中的字段并不是请求中使用的字段,例如:RePassword是我们后端定义的结构体中的字段名,而请求中使用的是re_password字段。如何是错误提示中的字段使用自定义的名称,例如jsontag指定的值呢?

只需要在初始化翻译器的时候像下面一样添加一个获取json tag的自定义方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// InitTrans 初始化翻译器
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎属性,实现自定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {

// 注册一个获取json tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})

zhT := zh.New() // 中文翻译器
enT := en.New() // 英文翻译器

// 第一个参数是备用(fallback)的语言环境
// 后面的参数是应该支持的语言环境(支持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)

}
}

但是还是有点瑕疵,那就是最终的错误提示信息中心还是有我们后端定义的结构体名称——SignUpParam,这个名称其实是不需要随错误提示返回给前端的,前端并不需要这个值。我们需要想办法把它去掉。

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
func removeTopStruct(fields map[string]string) map[string]string {
res := map[string]string{}
for field, err := range fields {
res[field[strings.Index(field, ".")+1:]] = err
}
return res
}
...

if err := c.ShouldBind(&u); err != nil {
// 获取validator.ValidationErrors类型的errors
errs, ok := err.(validator.ValidationErrors)
if !ok {
// 非validator.ValidationErrors类型错误直接返回
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
return
}
// validator.ValidationErrors类型错误则进行翻译
// 并使用removeTopStruct函数去除字段名中的结构体名称标识
c.JSON(http.StatusOK, gin.H{
"msg": removeTopStruct(errs.Translate(trans)),
})
return
}

gin中间件

自定义中间件

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
package main

import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
)

// 自定义中间件
func MyLogger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()

c.Set("example", "123")
// 让原本该执行的逻辑继续执行
c.Next()

end := time.Since(t)
fmt.Printf("耗时:%v\n", end)
status := c.Writer.Status()
fmt.Println("状态:", status)
}
}

func main() {
router := gin.New()
// 使用logger和recovery中间件,全局使用
router.Use(gin.Logger(), gin.Recovery())

router.Use(MyLogger())

router.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})

router.Run()
}

通过abort终止中间件后续逻辑的执行

比如我加一个token的验证逻辑,只有通过token验证,才能执行后续操作:

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
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
)

// 自定义中间件
func MyLogger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()

c.Set("example", "123")
// 让原本该执行的逻辑继续执行
c.Next()

end := time.Since(t)
fmt.Printf("耗时:%v\n", end)
status := c.Writer.Status()
fmt.Println("状态:", status)
}
}

func TokenRequired() gin.HandlerFunc {
return func(c *gin.Context) {
var token string
for k, v := range c.Request.Header {
if k == "X-Token" {
token = v[0]
} else {
fmt.Println(k, v)
}
}
if token != "cwz" {
c.JSON(http.StatusUnauthorized, gin.H{
"msg": "未登录",
})
return // 这里的return阻止不了 中间件后续逻辑的执行
}
c.Next()
}
}

func main() {
router := gin.Default()

// 查看token
router.Use(TokenRequired())

router.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})

router.Run()
}

中间件那边的return不能停止后面逻辑的执行,需要使用c.Abort()

gin的中间件原理源码分析

点进Use源码:

调用RouterGroup的User方法,点进去:

会给group加一个Handlers,这个handlers是一个切片

所以当你调用Use这个方法把你的中间件注册进来的时候,就是把中间件的函数放到Handlers这个切片的后面,追加到尾部。

所以当使用如router.GET()时,点击GET源码查看,查看里面的handler:

查看combineHandlers:

把原来的grouphandlers拷贝到更大的空间,再把你传递的handler放到切片尾部。

当调用中间件时,执行的是c.Next(),看一下Next源码:

在Auth中间件中如果使用了return,只会在Auth中结束,往后的中间件函数不会停止的。即使Auth结束了,index仍然可以++,会依次调用后面的中间件。

设置静态文件路径和html

官方地址:https://golang.org/pkg/html/template/

翻译: https://colobu.com/2019/11/05/Golang-Templates-Cheatsheet/

gin返回html

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 (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"os"
"path/filepath"
)

func main() {

router := gin.Default()

dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
fmt.Println("路径:", dir) // 路径: C:\Users\cwz\AppData\Local\Temp, 是一个临时路径

// LoadHTMLFiles会将指定的目录下的文件加载好
router.LoadHTMLFiles("templates/index.tmpl")

router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"name": "cwz",
})
})
router.Run(":8083")
}

templates/index.tmpl

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>{{.name}}</h1>
</body>
</html>

加载多个html

router.LoadHTMLFiles可以传多个html文件路径:

1
router.LoadHTMLFiles("templates/index.tmpl", "templates/goods.tmpl")

如果文件过多可以使用 LoadHTMLGlob

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
package main

import (
"net/http"
"github.com/gin-gonic/gin"
)

func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// 配置模板
r.LoadHTMLGlob("templates/**/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
// 配置静态文件夹路径 第一个参数是api,第二个是文件夹路径
r.StaticFS("/static", http.Dir("./static"))
// GET:请求方式;/hello:请求的路径
// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
r.GET("/posts/index", func(c *gin.Context) {
// c.JSON:返回JSON格式的数据
c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
"title": "posts/index",
})
})

r.GET("gets/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/login.tmpl", gin.H{
"title": "gets/login",
})
})

// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run()
}

index.html

1
2
3
4
5
<html>
<h1>
{{ .title }}
</h1>
</html>

templates/posts/index.html

1
2
3
4
5
6
7
{{ define "posts/index.tmpl" }}
<html><h1>
{{ .title }}
</h1>
<p>Using posts/index.tmpl</p>
</html>
{{ end }}

templates/users/index.html

1
2
3
4
5
6
7
{{ define "users/index.tmpl" }}
<html><h1>
{{ .title }}
</h1>
<p>Using users/index.tmpl</p>
</html>
{{ end }}

gin的优雅退出

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
package main

import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"os"
"os/signal"
"syscall"
)

func main() {
// 优雅退出,当我们关闭程序的时候应该做的后续处理
// 微服务启动之前 或者 启动之后会做一件事:将当前的服务ip和端口注册到注册中心
// 我们当前的服务停止了以后并没有告知注册中心

router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "pong",
})
})

go func() {
router.Run(":8083")
}()

// 如果想接收到信号
quit := make(chan os.Signal)
// SIGINT和SIGTERM任意一个信号有了之后会向channel发送一个消息
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

// 退出之后,处理后续的逻辑
fmt.Println("关闭server中。。。")
fmt.Println("注销服务。。。")
}

微服务开发

第一个微服务:用户服务

用户服务准备

项目目录

定义用户模型:

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
from peewee import *
from user_srv.settings import settings


class BaseModel(Model):
class Meta:
database = settings.DB


class User(BaseModel):
# 用户模型
GENDER_CHOICES = (
("male", "男"),
("female", "女")
)

ROLE_CHOICES = (
(1, "普通用户"),
(2, "管理员")
)

mobile = CharField(max_length=11, index=True, unique=True, verbose_name="手机号码")
password = CharField(max_length=100, verbose_name="密码")
nick_name = CharField(max_length=20, null=True, verbose_name="昵称")
head_url = CharField(max_length=200, null=True, verbose_name="头像")
birthday = DateField(null=True, verbose_name="生日")
address = CharField(max_length=200, null=True, verbose_name="地址")
desc = TextField(null=True, verbose_name="个人简介")
gender = CharField(max_length=6, choices=GENDER_CHOICES, null=True, verbose_name="性别")
role = IntegerField(default=1, choices=ROLE_CHOICES, verbose_name="用户角色")


if __name__ == '__main__':
settings.DB.create_tables([User])

settings/settings.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from playhouse.pool import PooledMySQLDatabase
from playhouse.shortcuts import ReconnectMixin


# 使用peewee的连接池,使用ReconnectMixin来防止连接断开查询失败

class ReconnectMysqlDatabase(ReconnectMixin, PooledMySQLDatabase):
pass


MYSQL_DB = "mxshop_user_srv"
MYSQL_HOST = "192.168.33.27"
MYSQL_PORT = 3306
MYSQL_USER = "root"
MYSQL_PASSWORD = "root"

DB = ReconnectMysqlDatabase(MYSQL_DB, host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER, password=MYSQL_PASSWORD)

MD5盐值加密解决用户密码安全问题

MD5信息摘要

Message Digest Algorithm 5,信息摘要算法

  • 压缩性:任意长度的数据,算出MD5值长度都是固定的
  • 容易计算:从原数据计算MD5值很容易
  • 抗修改性:对原数据进行任何修改,哪怕1个字节,MD5值差异都很大
  • 强碰撞:想找到两个不太的数据,使他们具有相同的MD5值很困难
  • 不可逆性:不可反解
1
2
3
4
5
6
7
import hashlib
m = hashlib.md5()
salt = "gwoow"
password = "123456"
m.update((salt+password).encode("utf8"))
#暴力破解 彩虹表
print(m.hexdigest())

MD5盐值加密

  • 通过生成随机数和MD5生成字符串进行组合
  • 数据库同时存储MD5值和salt值,验证正确性使用salt值进行MD5即可

passlib文档 https://passlib.readthedocs.io/en/stable/

1
2
3
4
5
6
7
#首先pip install passlib

from passlib.hash import pbkdf2_sha256
hash = pbkdf2_sha256.hash("123456")
print(hash)
print(pbkdf2_sha256.verify("123456", hash))
print(pbkdf2_sha256.verify("122121", hash))

go语言下的加密:https://gowebexamples.com/password-hashing/

proto接口定义和生成

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
49
50
51
52
syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";

service User {
rpc GetUserList(PageInfo) returns (UserListResponse); // 用户列表
rpc GetUserByMobile(MobileRequest) returns (UserListResponse); // 通过mobile查询用户
rpc GetUserById(IdRequest) returns (UserInfoResponse); // 通过id查询用户
rpc CreateUser(CreateUserInfo) returns (UserInfoResponse); // 添加用户
rpc UpdateUser(UpdateUserInfo) returns (google.protobuf.Empty); //更新用户
}

message PageInfo {
uint32 pn = 1;
uint32 pSize = 2;
}

message MobileRequest {
string mobile = 1;
}

message IdRequest {
int32 id = 1;
}

message CreateUserInfo {
string nickName = 1;
string password = 2;
string mobile = 3;
}

message UpdateUserInfo {
int32 id = 1;
string nickName = 2;
string gender = 3;
uint64 birthDay = 4;
}

message UserInfoResponse {
int32 id = 1;
string password = 2;
string mobile = 3;
string nickName = 4;
uint64 birthDay = 5;
string gender = 6;
int32 role = 7;
}

message UserListResponse {
int32 total = 1;
repeated UserInfoResponse data = 2;
}
1
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I . user.proto

用户列表接口

handler/user.py

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
import time
from datetime import date
import grpc

from user_srv.model.models import User
from user_srv.proto import user_pb2, user_pb2_grpc


class UserServicer(user_pb2_grpc.UserServicer):
def GetUserList(self, request: user_pb2.PageInfo, context):
# 获取用户的列表
rsp = user_pb2.UserListResponse()

users = User.select()
rsp.total = users.count()

# 分页
start = 0
page = 1
per_page_nums = 10 # 每页的数量
if request.pSize:
per_page_nums = request.pSize
if request.pn:
start = per_page_nums * (request.pn - 1)

users = users.limit(per_page_nums).offset(start)

for user in users:
user_info_rsp = user_pb2.UserInfoResponse()

user_info_rsp.id = user.id
user_info_rsp.password = user.password
user_info_rsp.mobile = user.mobile
user_info_rsp.role = user.role

if user.nick_name:
user_info_rsp.nickName = user.nick_name
if user.gender:
user_info_rsp.gender = user.gender
if user.birthday:
user_info_rsp.birthDay = int(time.mktime(user.birthday.timetuple()))

rsp.data.append(user_info_rsp)

return rsp

启动grpc服务

server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import grpc
import logging
from concurrent import futures

from user_srv.proto import user_pb2, user_pb2_grpc
from user_srv.handler.user import UserServicer


def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

启动客户端,测试服务

client.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import grpc

from user_srv.proto import user_pb2_grpc, user_pb2


class UserTest:
def __init__(self):
# 连接grpc服务器
channel = grpc.insecure_channel("127.0.0.1:50051")
self.stub = user_pb2_grpc.UserStub(channel)

def user_list(self):
rsp: user_pb2.UserListResponse = self.stub.GetUserList(user_pb2.PageInfo(pn=2, pSize=2))
print(rsp.total)
for user in rsp.data:
print(user.mobile, user.birthDay)


if __name__ == '__main__':
user = UserTest()
user.user_list()

日志库loguru

https://loguru.readthedocs.io/en/stable/overview.html

grpc服务加入日志

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
import logging
from concurrent import futures

import grpc
from loguru import logger

from user_srv.proto import user_pb2_grpc
from user_srv.handler.user import UserServicer


def serve():
# 日志打入文件
logger.add("logs/user_srv_{time}.log")

server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server)
server.add_insecure_port("[::]:50051")
logger.info("启动服务:127.0.0.1:50051")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

不同颜色的信息:

优雅退出

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
import sys
import os
import logging
import signal
from concurrent import futures

import grpc
from loguru import logger

from user_srv.proto import user_pb2_grpc
from user_srv.handler.user import UserServicer

# 项目路径
BASE_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__name__)))
print(BASE_DIR)
sys.path.insert(0, BASE_DIR)


def on_exit(signo, frame):
logger.info("进程中断")
sys.exit(0)


def serve():
logger.add("logs/user_srv_{time}.log")

server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server)
server.add_insecure_port("[::]:50051")

# 主进程退出信号监听
signal.signal(signal.SIGINT, on_exit)
signal.signal(signal.SIGTERM, on_exit)

logger.info("启动服务:127.0.0.1:50051")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

通过argparse解析传递进入的参数

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
49
50
51
52
53
54
55
56
57
58
import sys
import os
import logging
import signal
import argparse
from concurrent import futures

import grpc
from loguru import logger

from user_srv.proto import user_pb2_grpc
from user_srv.handler.user import UserServicer

# 项目路径
BASE_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__name__)))
print(BASE_DIR)
sys.path.insert(0, BASE_DIR)


def on_exit(signo, frame):
logger.info("进程中断")
sys.exit(0)


def serve():
parser = argparse.ArgumentParser()
parser.add_argument("--ip",
nargs="?",
type=str,
default="127.0.0.1",
help="binding ip"
)
parser.add_argument("--port",
nargs="?",
type=int,
default=50051,
help="the listening port"
)
args = parser.parse_args()

logger.add("logs/user_srv_{time}.log")

server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server)
server.add_insecure_port(f"{args.ip}:{args.port}")

# 主进程退出信号监听
signal.signal(signal.SIGINT, on_exit)
signal.signal(signal.SIGTERM, on_exit)

logger.info(f"启动服务:{args.ip}:{args.port}")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

根据id和mobile查询用户是否存在

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import time
from datetime import date

import grpc
from loguru import logger
from peewee import DoesNotExist

from user_srv.model.models import User
from user_srv.proto import user_pb2, user_pb2_grpc


class UserServicer(user_pb2_grpc.UserServicer):

def convert_user_to_rsp(self, user):
# 将user的model对象转换成message对象
user_info_rsp = user_pb2.UserInfoResponse()
user_info_rsp.id = user.id
user_info_rsp.password = user.password
user_info_rsp.mobile = user.mobile
user_info_rsp.role = user.role

if user.nick_name:
user_info_rsp.nickName = user.nick_name
if user.gender:
user_info_rsp.gender = user.gender
if user.birthday:
user_info_rsp.birthDay = int(time.mktime(user.birthday.timetuple()))

return user_info_rsp

@logger.catch
def GetUserList(self, request: user_pb2.PageInfo, context):
# 获取用户的列表
rsp = user_pb2.UserListResponse()

users = User.select()
rsp.total = users.count()
# 分页
start = 0
per_page_nums = 10 # 每页的数量
if request.pSize:
per_page_nums = request.pSize
if request.pn:
start = per_page_nums * (request.pn - 1)

users = users.limit(per_page_nums).offset(start)

for user in users:
rsp.data.append(self.convert_user_to_rsp(user))

return rsp

@logger.catch
def GetUserByid(self, request: user_pb2.IdRequest, context):
# 通过id查询用户
try:
user = User.get(User.id == request.id)

return self.convert_user_to_rsp(user)

except DoesNotExist:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("用户不存在")
return user_pb2.UserInfoResponse()

@logger.catch
def GetUserByMobile(self, request: user_pb2.MobileRequest, context):
# 通过mobile查询用户
try:
user = User.get(User.mobile == request.mobile)

return self.convert_user_to_rsp(user)

except DoesNotExist:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("用户不存在")
return user_pb2.UserInfoResponse()

新建用户接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from passlib.hash import pbkdf2_sha256


@logger.catch
def CreateUser(self, request: user_pb2.CreateUserInfo, context):
# 新建用户
try:
User.get(User.mobile == request.mobile)

context.set_code(grpc.StatusCode.ALREADY_EXISTS)
context.set_details("用户已存在")
return user_pb2.UserInfoResponse()

except DoesNotExist:
pass

user = User()
user.nick_name = request.nickName
user.mobile = request.mobile
user.password = pbkdf2_sha256.hash(request.password)
user.save()

return self.convert_user_to_rsp(user)

更新用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from google.protobuf import empty_pb2

@logger.catch
def UpdateUser(self, request: user_pb2.UpdateUserInfo, context):
# 更新用户
try:
user = User.get(User.id == request.id)

user.nick_name = request.nickName
user.gender = request.gender
user.birthday = date.fromtimestamp(request.birthDay)
user.save()
return empty_pb2.Empty()

except DoesNotExist:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("用户不存在")
return user_pb2.UserInfoResponse()

用户服务的web层开发

项目目录:

go高性能日志库-zap

github地址:https://github.com/uber-go/zap

安装

1
go get -u go.uber.org/zap

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"go.uber.org/zap"
)

func main() {
logger, _ := zap.NewProduction() // 生产环境
//logger, _ = zap.NewDevelopment() // 开发环境
defer logger.Sync() // flushes buffer, if any
url := "https://www.baidu.com"
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
// Structured context as loosely typed key-value pairs.
"url", url,
"attempt", 3,
)
sugar.Infof("Failed to fetch URL: %s", url)
}

Zap提供了两种类型的日志记录器—Sugared LoggerLogger

在性能很好但不是很关键的上下文中,使用SugaredLogger。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。

在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。它甚至比SugaredLogger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录

zap的文件输出

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
package main

import (
"go.uber.org/zap"
"time"
)


func NewLogger() (*zap.Logger, error) {
cfg := zap.NewProductionConfig()
cfg.OutputPaths = []string{
"./myproject.log",
}
return cfg.Build()
}

func main() {
//logger, _ := zap.NewProduction()
logger, err := NewLogger()
if err != nil {
panic(err)
//panic("初始化logger失败")
}
su := logger.Sugar()
defer su.Sync()
url := "https://www.baidu.com"
su.Info("failed to fetch URL",
// Structured context as strongly typed Field values.
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
}

go的配置文件管理-viper

Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。它支持以下特性:

  • 设置默认值

  • JSONTOMLYAMLHCLenvfileJava properties格式的配置文件读取配置信息

  • 实时监控和重新读取配置文件(可选)

  • 从环境变量中读取

  • 从远程配置系统(etcd或Consul)读取并监控配置变化

  • 从命令行参数读取配置

  • 从buffer读取配置

  • 显式配置值

安装:https://github.com/spf13/viper

1
go get github.com/spf13/viper

将配置文件映射成struct

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 (
"fmt"
"github.com/spf13/viper"
)


type ServerConfig struct{
ServiceName string `mapstructure:"name"`
Port int `mapstructure:"port"`
}

func main(){
v := viper.New()
//文件的路径如何设置
v.SetConfigFile("viper_test/ch01/config.yaml")
if err := v.ReadInConfig(); err != nil{
panic(err)
}
serverConfig := ServerConfig{}
if err := v.Unmarshal(&serverConfig); err != nil{
panic(err)
}
fmt.Println(serverConfig)
fmt.Printf("%V", v.Get("name"))
}

配置文件config.yaml

1
2
name: 'user-web'
port: 8021

开发环境与生产环境隔离:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"time"
)

type MysqlConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type ServerConfig struct{
Name string `mapstructure:"name"`
MysqlInfo MysqlConfig `mapstructure:"mysql"`
}

// 读取环境变量
func GetEnvInfo(env string) string {
viper.AutomaticEnv()
return viper.GetString(env)
}

func main(){
data := GetEnvInfo("Debug")
var configFileName string
configFileNamePrefix := "config"
if data == "true" {
configFileName = fmt.Sprintf("viper_test/%s-debug.yaml", configFileNamePrefix)
}else{
configFileName = fmt.Sprintf("viper_test/%s-pro.yaml", configFileNamePrefix)
}

serverConfig := ServerConfig{}

fmt.Println(data)

v := viper.New()
v.SetConfigFile(configFileName)
err := v.ReadInConfig()
if err != nil {
panic(err)
}
if err := v.Unmarshal(&serverConfig); err != nil {
panic(err)
}
fmt.Println(serverConfig)

go func() {
// viper的功能-动态监控变化
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
_ = v.ReadInConfig() // 读取配置数据
_ = v.Unmarshal(&serverConfig)
fmt.Println(serverConfig)
})
}()

time.Sleep(time.Second*3000)

}

配置文件:

1
2
3
4
name: 'user-web'
mysql:
host: '127.0.0.1'
port: 3306