golang基础 变量和常量 如何定义变量 单声明变量 var名称类型是声明单个变量的语法
1 2 var name string name ="cwz"
第二种,根据值自行判定变量类型(类型推断Type inference)
如果一个变量有一个初始值,Go将自动能够使用初始值来推断该变量的类型。因此,如果变量具有初始值,则可以省略变量声明中的类型
第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的(多个变量同时声明时,至少保证一个是新变量),否则会导致编译错误(简短声明)
1 2 3 var a int = 10 var b = 10 c := 10
这种方式只能被用在函数体内,而不可以用于全局变量的声明与赋值
1 2 3 4 5 6 7 8 package mainvar 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 mainimport ( "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 mainimport "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 mainimport ( "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 mainimport "fmt" func main () { const ( a = iota b c d = "ha" e f = 100 g h = iota i ) fmt.Println(a,b,c,d,e,f,g,h,i) }
注意:
如果中断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))
浮点型
float32 32位浮点型数
float64 64位浮点型数
1 2 3 4 fmt.Println(math.MaxFloat32) fmt.Println(math.MaxFloat64)
其他类型
byte 等于 uint8,主要用来处理ASCII码的处理
rune 等于 int32,主要处理中文字符
1 2 3 4 5 6 7 8 type byte = uint8 type rune = int32
字符 Golang中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" ) func main () { var a byte a = 'a' 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 mainimport ( "fmt" "reflect" ) func main () { a := 'a' fmt.Println(reflect.TypeOf(a+1 )) fmt.Printf("a+1=%c" , a+1 ) }
字符串 字符串就是一串固定长度的字符连接起来的字符序列。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 mainimport "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) var d int = c fmt.Println(d)
说明:
Go允许在底层结构相同的两个类型之间互转
不是所有数据类型都能转换的,例如字母格式的string类型”abcd”转换为int肯定会失败
低精度转换为高精度时是安全的,高精度的值转换为低精度时会丢失精度。例如int32转换为int16,float32转换为int,这样会丢失精度
这种简单的转换方式不能对int(float)和string进行互转,要跨大类型转换,可以使用strconv包提供的函数
strconv转换 Itoa和Atoi
1 println ("a" + strconv.Itoa(32 ))
1 2 3 4 data, _:=strconv.Atoi("12" ) fmt.Println(data)
Parse类函数
Parse类函数用于转换字符串为给定类型的值 :ParseBool()、ParseFloat()、ParseInt()、ParseUint()
1 2 3 4 b, err := strconv.ParseBool("true" ) f, err := strconv.ParseFloat("3.1415" , 64 ) 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 ) 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 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 mainimport "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 = 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))
字符串,返回的是字节的长度,中文一个字符占3个字节,所以长度为13
所以使用rune来存储中文字符:
1 2 3 var name string = "cwz:中国人" name_arr := []rune (name) fmt.Println(len (name_arr))
转义符
转义字符
意义
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 mainimport ( "fmt" "strings" ) func main () { var name string = "cwz:你好啊aac" var date string = "2021\\01\\12" fmt.Println(name, date) fmt.Println(strings.Contains(name, "c" )) fmt.Println(strings.Index(name, "你" )) fmt.Println(strings.Count(name, "c" )) fmt.Println(strings.HasPrefix(name, "c" )) fmt.Println(strings.HasSuffix(name, "h" )) fmt.Println(strings.ToUpper("hello" )) fmt.Println(strings.ToLower("AAAA" )) fmt.Println(strings.Compare("he" , "ha" )) fmt.Println(strings.TrimSpace(" hhh " )) fmt.Println(strings.Split("192.189.211.0" , "." )) arrs := strings.Split("192.189.211.0" , "." ) fmt.Println(strings.Join(arrs, "-" )) fmt.Println(strings.Replace("cwz:18" , "18" , "20" , 1 )) }
字符串的输入输出格式化 缺省格式和类型
格式化后的效果
动词
描述
[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 mainimport ( "fmt" "strconv" ) func main () { name := "cwz" age := 18 fmt.Println("name:" + name + ",age:" + strconv.Itoa(age)) fmt.Printf("name:%v, age:%v\n" , name, age) fmt.Printf("name:%#v, age:%#v\n" , name, age) fmt.Printf("name:%T, age:%T\n" , name, age) }
整型(缩进, 进制类型, 正负符号)
格式化后的效果
动词
描述
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)
浮点(缩进, 精度, 科学计数)
格式化后的效果
动词
描述
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 布尔表达式 { } if 布尔表达式 { } else { } if 布尔表达式1 { } else if 布尔表达式2 { } else { }
for Go 语言的 For 循环有 3 种形式,只有其中的一种使用分号
1 2 3 4 5 6 7 8 for init; condition; post { }for condition { }for { }
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { sum := 0 for i := 1 ; i < 10 ; i++ { sum += 1 } fmt.Println(sum) }
for循环的range格式:
1 2 3 4 5 6 7 8 9 10 package mainimport "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 mainimport "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 mainimport "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 mainimport "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 fmt.Println(a) var b = [5 ] string {"ruby" , "王二狗" , "rose" }fmt.Println(b) var c = [5 ] int {'A' , 'B' , 'C' , 'D' , 'E' } fmt.Println(c) d := [...] int {1 ,2 ,3 ,4 ,5 } fmt.Println(d) e := [5 ] int {4 : 100 } fmt.Println(e) f := [...] int {0 :1 , 4 :1 , 9 :1 } fmt.Println(f)
取值 1 2 3 4 5 6 7 8 package mainimport "fmt" func main () { course := [5 ]string {"golang" , "mysql" } fmt.Println(course[0 ]) }
for遍历数组 1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { var a = [3 ]int {1 , 3 , 5 } for i := 0 ; i < len (a); i++ { fmt.Println(a[i]) } }
for range 遍历数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { var a = [3 ]int {1 , 3 , 5 } for i, v := range a { fmt.Println(i,v) } }
数组是值类型 Go中的数组是值类型,而不是引用类型。这意味着当它们被分配给一个新变量时,将把原始数组的副本分配给新变量。如果对新变量进行了更改,则不会在原始数组中反映。
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { a := [...]string {"USA" , "China" , "India" , "Germany" , "France" } b := a b[0 ] = "Singapore" fmt.Println("a is " , a) fmt.Println("b is " , b) }
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 mainimport "fmt" func main () { var courses []string fmt.Printf("%T\n" , courses) courses2 := []string {"django" , "scrapy" , "flask" } fmt.Printf("%T" , courses2) 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) 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 mainimport "fmt" func main () { var course4 = [5 ]string {"python" ,"flask" ,"django" ,"golang" ,"beego" } subCourse := course4[1 :4 ] subCourse3 := subCourse[1 :3 ] subCourse3 = append (subCourse3, "abc" ) fmt.Println(subCourse3) appendCourses := []string {"a" , "b" , "c" } subCourse3 = append (subCourse3, appendCourses...) fmt.Println(subCourse3) subCourse4 := make ([]string , 2 ) fmt.Println(len (subCourse4)) copy (subCourse4, subCourse3) fmt.Println(subCourse4) 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 mainimport "fmt" func main () { m1 := map [string ]string { "m1" : "v1" , } fmt.Printf("%v\n" , m1) m2 := make (map [string ]string ) m2["m2" ] = "v2" fmt.Printf("%v\n" , m2) m3 := map [string ]string {} fmt.Printf("%v\n" , m3) }
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 mainimport "fmt" func main () { m := map [string ]string { "a" : "va" , "b" : "vb" , } m["c" ] = "vc" m["b" ] = "vb1" fmt.Printf("%v\n" , m) v, ok := m["d" ] fmt.Println(v, ok) 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 mainimport ( "fmt" "sort" ) func main () { fruits := map [string ]int { "oranges" : 100 , "apples" : 200 , "banans" : 300 , } 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]) } }
go语言的指针 什么是指针 抛砖引玉:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" func swap (a int , b int ) { c := a a = b b = c } func main () { a := 10 b := 20 swap(a, b) fmt.Println(a, b) }
上述代码并没有将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)
&
操作符用于获取变量的地址。如果在类型前面加*
,表示指向这个类型的指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" func main () { a := 10 var ip *int ip = &a fmt.Println(ip) *ip = 50 fmt.Println(a) fmt.Printf("ip所指向的内存空间地址是:%p,内存中的值:%d" , ip, *ip) }
数组指针和指针数组 数组指针:指向数组导入指针
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { var a [3 ]int = [3 ]int {1 , 2 , 3 } var b *[3 ]int = &a fmt.Println(b) }
指针数组:数组里面放入指针
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { var x, y, z = 1 , 2 , 3 var b [3 ]*int = [3 ]*int {&x, &y, &z} fmt.Println(b) }
指针的默认值是nil,一般判断:
交换a和b:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" func swap (a *int , b *int ) { c := *a *a = *b *b = c } func main () { a := 10 b := 20 swap(&a, &b) fmt.Println(a, b) }
指向指针的指针 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { var a int = 123 var b *int = &a var c **int = &b var d ***int = &c fmt.Println(d) fmt.Println(*d) fmt.Println(**d) fmt.Println(***d) }
go的nil 先看一个现象:
上述代码会报错。这是因为初始的时候没有分配空间。当然这是对于默认值为nil的类型来说的。
像int byte rune float bool string 都有默认值,所以初始化的时候一开始申明就会分配内存;但是像指针,切片,map,接口这些默认值为nil,没有一开始就分配内存。
new函数和make函数 对于指针这些默认值为nil的类型来说,如何一开始申明的时候就分配内存?
可以使用new函数:
1 2 var p *int = new (int ) *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 2 3 func add (a, b int ) int { return a + b }
带参数的函数,两个参数类型相同,可以省略
1 2 3 func add2 (a, b int ) int { return a + b }
有返回值
1 2 3 4 func add2 (a, b int ) (sum int ) { sum = a + b return }
以给返回值命名,return 可以不用带返回值
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" func test (a string , b int ) (string , int ) { return a, b } func main () { fmt.Println(test("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 mainimport ( "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 mainimport "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 mainimport "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...)) }
创建长度不定的数组 1 2 arr := [...]int {1 ,2 ,3 } fmt.Printf("%T" , arr)
匿名函数 go 中我们也可以使用匿名函数,经常用在一些临时的小函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func test () { func () { fmt.Println("我是匿名函数" ) }() } func main () { test() }
函数类型 go 里边函数其实也是一等公民 ,函数本身也是一种类型,所以我们可以定义一个函数然后赋值给一个变量:
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" func main () { res := func (a string ) {fmt.Println(a)} res("hello golang" ) fmt.Printf("%T" , res) }
函数这个类型,它的参数,返回值,都是类型的一部分
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 mainimport "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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" func test () { su:="golang" addSu := func (name string ) string { return name + su } fmt.Println(addSu("hello, " )) } func main () { test() }
go中没有装饰器语法糖,所以要利用闭包实现装饰器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "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 mainimport ( "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 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 mainimport ( "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 () { a, b := 1 , 0 res, err := Divide(a, b) if err != nil { fmt.Println(err) } fmt.Println(res) }
defer语句 defer语句的使用 go 中提供了一个 defer 语句用来延迟一个函数(匿名函数)或者方法的执行
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "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 mainimport "fmt" func testDefer () string { defer fmt.Println("defer 1" ) defer fmt.Println("defer 2" ) fmt.Println("函数体" ) return "test" } func main () { fmt.Println(testDefer()) }
defer语句的细节 defer语句执行时的拷贝机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 test := func () { fmt.Println("test1" ) } defer test()test = func () { fmt.Println("test2" ) } fmt.Println("test3" )
值传递:
1 2 3 4 5 6 x := 10 defer func (a int ) { fmt.Println(a) }(x) x++
这个defer把函数的逻辑和变量值都压入栈中,函数和值都有了,就与外面的x++无关了。
引用传递:
1 2 3 4 5 6 x := 10 defer func (a *int ) { fmt.Println(*a) }(&x) x++
压栈的时候压的是函数的代码和函数里的参数值,这里参数值是一个指针,指向x的值,外部改了值之后,函数里是顺着指针指向那块内存取值的。x已经变成11了,所以打印的也是11。
1 2 3 4 5 6 7 x := 10 defer func () { fmt.Println(x) }() x++
这里压栈是把函数压入是没有参数的,函数里的逻辑是指向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 mainimport "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()) fmt.Println(*handle2()) }
在 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 mainimport "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)) }
使用panic的坑:
panic会引起主线程的挂掉,同时会导致其他协程都挂掉
在父协程中无法捕获子协程中出现的异常
go语言中的结构体 type的几种使用常见
1 2 3 type myByte = byte var b myBytefmt.Printf("%T\n" , b)
1 2 3 type myInt int var i myIntfmt.Printf("%T\n" , i)
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 mainimport "fmt" type Person struct { name string age int } func main () { p := Person{"张三" , 30 } fmt.Println(p, p.name, p.age) }
这里面有一些细节,值得注意:
在其他语言中,比如java是通过关键字public、private等来限制成员变量的访问权限的。在go语言中,它是通过结构体内部变量的大小写来决定访问权限的,首字母大写是公开变量,首字母小写是内部变量(只有同属于一个包下的代码才能直接访问)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" type Person struct { Name string Age int } func main () { var p Person = Person{ Name: "张三" , Age: 30 , } fmt.Println(p.Name, p.Age) }
结构体的实例化
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 mainimport ( "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) }
结构体的零值 结构体是值类型,零值是属性的零值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" type Person struct { name string age int } func main () { var p Person fmt.Println(p) p.name = "cwz" p.age = 23 fmt.Println(p, p.name, p.age) }
多种方式零值初始化:
1 2 3 4 5 6 var c5 Course = Course{}var c6 Coursevar c7 *Course = new (Course)fmt.Println(c5.Price) fmt.Println(c6.Price) fmt.Println(c7.Price)
go语言中结构体无处不在 先来看一下这个现象:
1 2 fmt.Println(unsafe.Sizeof("" )) fmt.Println(unsafe.Sizeof("asddddddddddddddddd" ))
空字符串和有值的字符串占用空间是一样的。
通过观察 Go 语言的底层源码,可以发现所有的 Go 语言内置的高级数据结构都是由结构体来完成的。
字符串头的结构体 在 64 位机器上将会占用 16 个字节
1 2 3 4 type string struct { Data uintptr Len int }
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))
map的结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 type hmap struct { count int ... buckets unsafe.Pointer ... } m1 := map [string ]string { "k1" : "v1" , "k2" : "v2" , "k3" : "v3" , } fmt.Println(unsafe.Sizeof(m1))
数组只有「体」,切片除了「体」之外,还有「头」部。切片的头部和内容体是分离的,使用指针关联起来。看一下以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "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)) }
结构体绑定方法 基本语法:
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 mainimport "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 mainimport "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 mainimport "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 ) 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 mainimport "fmt" type Person struct { Name string Age int Hobby Hobby } 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() }
匿名内嵌结构体 还有一种特殊的内嵌结构体形式,内嵌的结构体不提供名称。
这时外面的结构体将直接继承内嵌结构体所有的内部字段和方法,就好像把子结构体的一切全部都揉进了父结构体一样。
匿名的结构体字段将会自动获得以结构体类型的名字命名的字段名称
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 mainimport "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{ "Shanghai" , "Jingan" , }, } fmt.Println(p) }
如果嵌入结构的字段和外部结构的字段相同,那么想要修改嵌入结构的字段值需要加上外部结构中声明的嵌入结构名称
结构体的标签 结构体的字段除了名字和类型外,还可以有一个可选的标签(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 mainimport ( "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)) }
自定义标签,对字段进行处理
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 mainimport ( "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)) 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) } }
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 mainimport "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 () { 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 mainimport ( "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() }
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 mainimport "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 mainimport "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() }
结构体组合实现了所有接口方法
空接口 没有包含方法的接口称为空接口。控接口表示为 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 mainimport "fmt" func printAnything (i interface {}) { fmt.Printf("%v\n" , i) } func main () { var i interface {} i = 10 printAnything(i) i = "cwz" printAnything(i) i = []string {"aa" , "bb" } printAnything(i) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { var personInfo = make (map [string ]interface {}) personInfo["name" ] = "cwz" personInfo["age" ] = 19 personInfo["hobby" ] = [3 ]string {"跑步" , "游泳" } fmt.Printf("%v" , personInfo) }
接口的类型断言 类型断言的语法格式如下:
1 2 instance, ok := interfaceVal.(RealType)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "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) }
判断传的值是什么类型的
使用if判断写起来代码比较不工整,可以使用switch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "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 mainimport "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 mainimport ( "fmt" "sort" ) type Person struct { Name string Age int } type PersonA []Personfunc (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(pp) for _, v := range pp { fmt.Println(v) } }
go语言的反射 反射介绍 反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
Go程序在运行期使用reflect包访问程序的反射信息。
reflect包 在Go语言的反射机制中,任何接口值都由是一个具体类型 和具体类型的值 两部分组成的。
在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type
和reflect.Value
两部分组成,并且reflect包提供了reflect.TypeOf
和reflect.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 mainimport ( "fmt" "reflect" ) func reflectType (x interface {}) { typeOf := reflect.TypeOf(x) fmt.Printf("type:%v\n" , typeOf) } func main () { var a int32 = 20 reflectType(a) var b float64 = 3.1415 reflectType(b) }
获取类别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 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 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 mainimport ( "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) reflectType(b) reflectType(c) type person struct { name string age int } p := person{ name: "cwz" , age: 18 , } reflectType(p) }
上述代码中,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 mainimport ( "fmt" "reflect" ) func reflectValue (x interface {}) { valueOf := reflect.ValueOf(x) k := valueOf.Kind() switch k { case reflect.Int64: fmt.Printf("type is int64, value is %d\n" , int64 (valueOf.Int())) case reflect.Float32: fmt.Printf("type is float32, value is %f\n" , float32 (valueOf.Float())) case reflect.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) reflectValue(b) c := reflect.ValueOf(10 ) fmt.Printf("type c :%T\n" , c) }
通过反射设置值 想要在函数中通过反射修改变量的值,需要注意的是:
函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的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 mainimport ( "fmt" "reflect" ) func reflectSetValue1 (x interface {}) { v := reflect.ValueOf(x) if v.Kind() == reflect.Int64 { v.SetInt(200 ) } } func reflectSetValue2 (x interface {}) { v := reflect.ValueOf(x) if v.Elem().Kind() == reflect.Int64 { v.Elem().SetInt(200 ) } } func main () { var a int64 = 100 reflectSetValue2(&a) fmt.Println(a) }
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 mainimport ( "fmt" "reflect" ) func main () { var a *int fmt.Println("var a *int IsNil:" , reflect.ValueOf(a).IsNil()) fmt.Println("nil IsValid:" , reflect.ValueOf(nil ).IsValid()) b := struct {}{} fmt.Println("不存在的结构体成员:" , reflect.ValueOf(b).FieldByName("abc" ).IsValid()) fmt.Println("不存在的结构体方法:" , reflect.ValueOf(b).MethodByName("abc" ).IsValid()) c := map [string ]int {} fmt.Println("map中不存在的键:" , reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎" )).IsValid()) }
结构体反射 与结构体相关的方法 任意值通过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 string PkgPath string Type Type Tag StructTag Offset uintptr Index []int 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 mainimport ( "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()) 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" )) } }
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" )
包的初始化 每个包都允许有一个 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 mainimport "fmt" var a int = 10 const pi = 3.14 func init () { fmt.Println("init:" , a) } func main () { fmt.Println("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
禁用模块支持,编译时会从GOPATH
和vendor
文件夹中查找包。
GO111MODULE=on
启用模块支持,编译时会忽略GOPATH
和vendor
文件夹,只根据 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 modelpackage main
文件名 尽量采取有意义的文件名,简短,有意义,应该为小写 单词,使用下划线 分隔各个单词
结构体命名
采用驼峰命名法,首字母根据访问控制大写或者小写
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 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 4 5 type User struct { Username string Email string }
函数注释 每个函数都应该有注释说明,函数的注释应该包括三个方面(严格按照此顺序撰写):
简要说明,格式说明:以函数名开头,“,”分隔说明部分
参数列表:每行一个参数,参数名开头,“,”分隔说明部分
返回值: 每行一个返回值
1 2 3 4 5 6 7 func NewAttrModel (ctx *common.Context) *AttrModel {}
代码逻辑注释 对于一些关键位置的代码逻辑,或者局部较为复杂的逻辑,需要有相应的逻辑说明,方便其他开发者阅读该段代码
注释风格 统一使用中文注释,对于中英文字符之间严格使用空格分隔, 这个不仅仅是中文和英文之间,英文和中文标点之间也都要使用空格分隔,例如:
上面 Redis 、 id 、 DB 和其他中文字符之间都是用了空格分隔
建议全部使用单行注释
和代码的规范一样,单行注释不要过长,禁止超过 120 字符
import规范 import在多行的情况下,goimports会自动帮你格式化,但是我们这里还是规范一下import的一些规范,如果你在一个文件里面引入了一个package,还是建议采用如下格式:
如果你的包引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包:
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 { } else { } if err != nil { return }
go的并发编程 Go语言的并发通过goroutine
实现。goroutine
类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine
并发工作。goroutine
是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。
Go语言还提供channel
在多个goroutine
间进行通信。goroutine
和channel
是 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() 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() 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 mainimport ( "fmt" "sync" ) var wg sync.WaitGroupfunc hello (i int ) { defer wg.Done() fmt.Println("Hello Goroutine!" , i) } func main () { for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go hello(i) } wg.Wait() }
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的关系:
一个操作系统线程对应用户态多个goroutine。
go程序可以同时使用多个操作系统线程。
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 2 3 var ch1 chan int var ch2 chan bool var ch3 chan []int
初始化channel 通道是引用类型,通道类型的空值是 nil 。
1 2 var ch chan int fmt.Println(ch)
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 2 3 4 5 6 ch <- 10 x := <- ch <-ch close (ch)
关闭后的通道有以下特点:
对一个关闭的通道再发送值就会导致panic
对一个关闭的通道进行接收,会一直获取值直到通道为空
对一个关闭的并且没有值的通道,执行接收操作会得到对应类型的零值
关闭一个已经关闭的通道会导致panic
无缓冲的通道 无缓冲的通道又称为阻塞的通道
1 2 3 4 5 6 7 8 9 package mainimport "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 mainimport "fmt" func recv (c chan int ) { ret := <-c fmt.Println("接收成功" , ret) } func main () { ch := make (chan int ) go recv(ch) ch <- 10 fmt.Println("发送成功" ) }
无缓冲通道上的发送操作会阻塞,直到另一个goroutine
在该通道上执行接收操作,这时值才能发送成功,两个goroutine
将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine
在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine
同步化。因此,无缓冲通道也被称为同步通道
。
有缓冲的通道 我们可以在使用make函数初始化通道的时候为其指定通道的容量:
1 2 3 4 5 func main () { ch := make (chan int , 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 mainimport ( "fmt" ) func main () { ch1 := make (chan int ) ch2 := make (chan int ) go func () { for i := 0 ; i < 100 ; i++ { ch1 <- i } close (ch1) }() go func () { for { i, ok := <-ch1 if !ok { break } ch2 <- i * i } close (ch2) }() for i := range ch2 { 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 mainimport ( "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 ) for w := 1 ; w <= 3 ; w++ { go worker(w, jobs, results) } 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 { data, ok := <-ch1 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
会随机选择一个。
对于没有case
的select{}
会一直等待,可用于阻塞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 mainimport ( "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 mainimport ( "fmt" "sync" ) var x int64 var wg sync.WaitGroupfunc 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 mainimport ( "fmt" "sync" ) var x int64 var wg sync.WaitGroupvar lock sync.Mutexfunc 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 mainimport ( "fmt" "sync" "time" ) var ( x int64 wg sync.WaitGroup lock sync.Mutex rwlock sync.RWMutex ) func write () { rwlock.Lock() x = x + 1 time.Sleep(10 * time.Millisecond) rwlock.Unlock() wg.Done() } func read () { rwlock.RLock() time.Sleep(time.Millisecond) rwlock.RUnlock() 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.WaitGroupfunc hello () { defer wg.Done() fmt.Println("Hello Goroutine!" ) } func main () { wg.Add(1 ) go 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 mainimport ( "context" "fmt" "time" ) func monitor (ctx context.Context, number int ) { for { select { 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) cancel() 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 并退出了。
第三行:当你想到取消 context 的时候,只要调用一下 cancel 方法即可。这个 cancel 就是我们在创建 ctx 的时候返回的第二个值。
运行结果输出如下:
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 mainimport ( "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 mainimport ( "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.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 mainimport ( "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 () { 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 { conn, err := listen.Accept() if err != nil { fmt.Printf("accept failed, err: %v\n" , err) continue } 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 mainimport ( "bufio" "fmt" "net" "os" "strings" ) func main () { conn, err := net.Dial("tcp" , "127.0.0.1:50070" ) if err != nil { fmt.Printf("dail failed, err: %v\n" , err) return } 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 mainimport ( "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 mainimport ( "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 mainimport ( "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 { 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 splitimport "strings" 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 splitimport ( "reflect" "testing" ) func TestSplit (t *testing.T) { got := Split("我是机器人" , "是" ) want := []string {"我" , "机器人" } 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 jsonfrom urllib.parse import urlparse, parse_qslfrom http.server import HTTPServer, BaseHTTPRequestHandlerhost = ('' , 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 requestsimport jsonclass 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 SimpleXMLRPCServerclass 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 )) server.register_instance(obj) print ("Listening on port 8088" )server.serve_forever()
客户端:
1 2 3 4 from xmlrpc import clientserver = 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 SimpleJSONRPCServerdef add (a, b ): return a + b server = SimpleJSONRPCServer(('localhost' , 8080 )) server.register_function(add) server.serve_forever()
客户端:
1 2 3 4 import jsonrpclibserver = 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 zerorpcclass HelloRPC (object ): def hello (self, name ): return "Hello, %s" % name s = zerorpc.Server(HelloRPC()) s.bind("tcp://0.0.0.0:4242" ) s.run()
客户端:
1 2 3 4 5 import zerorpcc = 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 zerorpcclass StreamingRPC (object ): @zerorpc.stream 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 zerorpcc = 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 zerorpcclass 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 zerorpcc = 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 mainimport ( "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) }
其中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 mainimport ( "fmt" "net/rpc" ) func main () { 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 mainimport ( "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 mainimport ( "fmt" "net" "net/rpc" "net/rpc/jsonrpc" ) func main () { 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) }
python客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import jsonimport socketreq = { "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)
基于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 mainimport ( "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 requestsreq = { "id" : 0 , "params" : ["cwz" ], "method" : "HelloService.Hello" , } rsp = requests.post("http://localhost:1234/jsonrpc" , json=req) print (rsp.text)
进一步改进RPC调用过程 前面的rpc调用虽然简单,但是和普通的http的调用差异不大,需要做出改进
serviceName统一和名称冲突的问题
server端和client端如何统一serviceName
多个server的包中serviceName同名的问题
新建handler/handler.go文件内容如下:
1 2 3 package handlerconst 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 mainimport ( "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 mainimport ( "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 handlertype 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_proxyimport "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 mainimport ( "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_proxyimport "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 mainimport ( "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 , PHP 和 C# 支持
习惯用 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 ; }
生成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_pb2request = hello_pb2.HelloRequest() request.name = "cwz" res_str = request.SerializeToString() print (res_str) print (len (res_str)) res_json = { "name" : "cwz" } import jsonprint (len (json.dumps(res_json))) request2 = hello_pb2.HelloRequest() request2.ParseFromString(res_str) print (request2)
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 ThreadPoolExecutorimport grpcfrom grpc_hello.proto import helloworld_pb2, helloworld_pb2_grpcclass Greeter (helloworld_pb2_grpc.GreeterServicer): def SayHello (self, request, context ): return helloworld_pb2.HelloReply(message=f"你好, {request.name} " ) if __name__ == '__main__' : server = grpc.server(ThreadPoolExecutor(max_workers=10 )) helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), 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 grpcfrom grpc_hello.proto import helloworld_pb2_grpc, helloworld_pb2if __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)
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 mainimport ( "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 mainimport ( "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 mainimport ( "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 mainimport ( "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 mainimport ( "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 ThreadPoolExecutorimport grpcfrom grpc_proto_test.proto import hello_pb2, hello_pb2_grpcclass Greeter (hello_pb2_grpc.GreeterServicer): def SayHello (self, request, context ): return hello_pb2.HelloReply(message=f"你好, {request.name} , url: {request.url} " ) if __name__ == '__main__' : server = grpc.server(ThreadPoolExecutor(max_workers=10 )) hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), 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 mainimport ( "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 mainimport ( "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 asynciofrom grpclib.utils import graceful_exitfrom grpclib.server import Serverfrom .helloworld_pb2 import HelloReplyfrom .helloworld_grpc import GreeterBaseclass 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()]) 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 asynciofrom grpclib.client import Channelfrom .helloworld_pb2 import HelloRequest, HelloReplyfrom .helloworld_grpc import GreeterStubasync 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让我们可以像本地调用一样实现远程调用,对于每一次的RPC调用中,都可能会有一些有用的数据,而这些数据就可以通过metadata来传递
metadata是以key-value的形式存储数据的,其中key是string
类型,而value是[]string
,即一个字符串数组类型。metadata使得client和server能够为对方提供关于本次调用的一些信息,就像一次http请求的RequestHeader和ResponseHeader一样。http中header的生命周周期是一次http请求,那么metadata的生命周期就是一次RPC调用。
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" }) md := metadata.Pairs( "key1" , "val1" , "key1" , "val1-2" , "key2" , "val2" , )
发送metadata
1 2 3 4 5 6 7 md := metadata.Pairs("key" , "val" ) ctx := metadata.NewOutgoingContext(context.Background(), md) 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) }
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 mainimport ( "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 mainimport ( "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.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) }
运行:
可以看官方的例子: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 grpcfrom grpc_metadata_test.proto import helloworld_pb2_grpc, helloworld_pb2if __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' ) )) print (response.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 from concurrent.futures import ThreadPoolExecutorimport grpcfrom grpc_metadata_test.proto import helloworld_pb2, helloworld_pb2_grpcclass 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__' : server = grpc.server(ThreadPoolExecutor(max_workers=10 )) helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), 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) ; } 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 mainimport ( "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 mainimport ( "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 grpcfrom datetime import datetimefrom grpc_interceptor.proto import helloworld_pb2from grpc_interceptor.proto import helloworld_pb2_grpcclass 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 ) 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 loggingimport grpcfrom 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()
对于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 mainimport ( "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 { 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 mainimport ( "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 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 mainimport ( "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 mainimport ( "context" "fmt" "google.golang.org/grpc" "start/pgv_test/proto" ) type customCredential struct {}func main () { var opts []grpc.DialOption 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, 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 { } 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 datetimedb = 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 charlie = User.create(username='charlie' ) charlie.save() charlie.save(force_insert=True ) huey = User.create(username='huey' ) User.get(User.username == 'charlie' ) query = User.get_by_id(User.username == "chali" ) users = User.select() 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 datetimefrom peewee import *import logginglogger = 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) data_source = [ {'field1' : 'val1-1' , 'field2' : 'val1-2' }, {'field1' : 'val2-1' , 'field2' : 'val2-2' }, ] for data_dict in data_source: Model.create(**data_dict) 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 ) User[1 ] g = Person.select().where(Person.name == 'Grandma L.' ).get() g = Person.get(Person.name == 'fff.' ) g = Person.select().where(Person.age > 23 ).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()) 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 query = Person.select(fn.MAX(Person.birthday)) MemberAlias = Member.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 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 persons = Person.select() for p in persons: print (p.name, p.birthday, p.is_relative) persons = Person.select().where(Person.is_relative == True ) for p in persons: print (p.name, p.birthday, p.is_relative) persons = Person.select().where(Person.is_relative == True ) print (persons.sql())
limit和offset 1 2 3 4 5 persons = Person.select().order_by(Person.create_time.asc()).limit(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 datetimefrom 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) 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) .join(User) .order_by(Tweet.id .desc()) .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 mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.GET("/ping" , func (c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message" : "pong" , }) }) r.Run() }
使用get、post、put等http方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { 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) router.Run() }
使用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 mainimport ( "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 mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() goodsGroup := r.Group("/goods" ) { goodsGroup.GET("/list" , goodsList) 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, }) } 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 mainimport ( "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, }) }) 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 mainimport ( "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 mainimport ( "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 mainimport ( "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" } data := &proto.Teacher{ Name: "justgo" , Course: courses, } c.ProtoBuf(http.StatusOK, data) }) 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() r.GET("/json" , func (c *gin.Context) { c.JSON(200 , gin.H{ "html" : "<b>Hello, world!</b>" , }) }) r.GET("/purejson" , func (c *gin.Context) { c.PureJSON(200 , gin.H{ "html" : "<b>Hello, world!</b>" , }) }) 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 mainimport ( "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 mainimport ( "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" ) var trans ut.Translatorfunc InitTrans (locale string ) (err error ) { if v, ok := binding.Validator.Engine().(*validator.Validate); ok { zhT := zh.New() enT := en.New() uni := ut.New(enT, zhT, enT) var ok bool 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 { errs, ok := err.(validator.ValidationErrors) if !ok { c.JSON(http.StatusOK, gin.H{ "msg" : err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "msg" :errs.Translate(trans), }) return } c.JSON(http.StatusOK, "success" ) }) _ = r.Run(":8999" ) }
进一步改进校验 上面的错误提示看起来是可以了,但是还是差点意思,首先是错误提示中的字段并不是请求中使用的字段,例如:RePassword
是我们后端定义的结构体中的字段名,而请求中使用的是re_password
字段。如何是错误提示中的字段使用自定义的名称,例如json
tag指定的值呢?
只需要在初始化翻译器的时候像下面一样添加一个获取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 func InitTrans (locale string ) (err error ) { if v, ok := binding.Validator.Engine().(*validator.Validate); ok { 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() 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 { errs, ok := err.(validator.ValidationErrors) if !ok { c.JSON(http.StatusOK, gin.H{ "msg" : err.Error(), }) return } 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 mainimport ( "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() 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 mainimport ( "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 } c.Next() } } func main () { router := gin.Default() 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 mainimport ( "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) 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 mainimport ( "net/http" "github.com/gin-gonic/gin" ) func main () { r := gin.Default() r.LoadHTMLGlob("templates/**/*" ) r.StaticFS("/static" , http.Dir("./static" )) r.GET("/posts/index" , func (c *gin.Context) { 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" , }) }) 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 mainimport ( "fmt" "github.com/gin-gonic/gin" "net/http" "os" "os/signal" "syscall" ) func main () { 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) 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 settingsclass 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 PooledMySQLDatabasefrom playhouse.shortcuts import ReconnectMixinclass 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 hashlibm = 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 from passlib.hash import pbkdf2_sha256hash = 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) ; rpc GetUserById(IdRequest) returns (UserInfoResponse) ; 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 timefrom datetime import dateimport grpcfrom user_srv.model.models import Userfrom user_srv.proto import user_pb2, user_pb2_grpcclass 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 grpcimport loggingfrom concurrent import futuresfrom user_srv.proto import user_pb2, user_pb2_grpcfrom user_srv.handler.user import UserServicerdef 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 grpcfrom user_srv.proto import user_pb2_grpc, user_pb2class UserTest : def __init__ (self ): 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 loggingfrom concurrent import futuresimport grpcfrom loguru import loggerfrom user_srv.proto import user_pb2_grpcfrom user_srv.handler.user import UserServicerdef 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 sysimport osimport loggingimport signalfrom concurrent import futuresimport grpcfrom loguru import loggerfrom user_srv.proto import user_pb2_grpcfrom user_srv.handler.user import UserServicerBASE_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 sysimport osimport loggingimport signalimport argparsefrom concurrent import futuresimport grpcfrom loguru import loggerfrom user_srv.proto import user_pb2_grpcfrom user_srv.handler.user import UserServicerBASE_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 timefrom datetime import dateimport grpcfrom loguru import loggerfrom peewee import DoesNotExistfrom user_srv.model.models import Userfrom user_srv.proto import user_pb2, user_pb2_grpcclass UserServicer (user_pb2_grpc.UserServicer): def convert_user_to_rsp (self, user ): 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 ): 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 ): 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 mainimport ( "go.uber.org/zap" ) func main () { logger, _ := zap.NewProduction() defer logger.Sync() url := "https://www.baidu.com" sugar := logger.Sugar() sugar.Infow("failed to fetch URL" , "url" , url, "attempt" , 3 , ) sugar.Infof("Failed to fetch URL: %s" , url) }
Zap提供了两种类型的日志记录器—Sugared Logger
和Logger
。
在性能很好但不是很关键的上下文中,使用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 mainimport ( "go.uber.org/zap" "time" ) func NewLogger () (*zap.Logger, error ) { cfg := zap.NewProductionConfig() cfg.OutputPaths = []string { "./myproject.log" , } return cfg.Build() } func main () { logger, err := NewLogger() if err != nil { panic (err) } su := logger.Sugar() defer su.Sync() url := "https://www.baidu.com" su.Info("failed to fetch URL" , zap.String("url" , url), zap.Int("attempt" , 3 ), zap.Duration("backoff" , time.Second), ) }
go的配置文件管理-viper Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。它支持以下特性:
安装: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 mainimport ( "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 mainimport ( "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 () { 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