GO语言学习笔记 一、Go的安装 gopath下的3个目录:
src:里面的每一个子目录,就是一个包。包内是Go的源文件
pkg:编译后生成的,包的目标文件
bin:生成的可执行文件
1.1 Windows系统 安装方法:
前往Go官网 下载安装包(后缀为msi)
或者去Go中文社区 下载
下载完运行,一直下一步就可以
在配置Go的一些环境
1.2 Linux系统 同上下载安装包**(tar.gz)**
将安装包移动到/usr/local下
1 $ sudo mv 安装包名称 /usr/local
使用命令解压安装包:
配置环境:
在环境变量配置文件/etc/profile中添加以下内容:
打开文件命令:
1 2 export PATH=$PATH :/usr/local/go/bin
输入—–>按i进行编辑,完成后按esc退出编辑,输入:wq退出
让配置文件生效:
查看Go版本
若显示Go版本就表示成功了
二、基础语法 第一个go程序 在控制台打印输出hello world
1 2 3 4 5 package mainimport "fmt" func main () { fmt.Println("hello world" ) }
2.1.1 将go程序编译为可执行文件
Windows 系统编译为.exe文件
Linux 系统为二进制文件
2.1.1.1 编译单个文件 1 2 3 4 $ cd 目录地址 $ go build XXX.go
2.1.1.2 编译整个项目
变量
在go 里面 定义的变量必须要用,如果你不想用可以使用_作为变量名,也就是匿名变量。
在go中,默认不赋值的变量,默认值都为0
2.2.1 匿名变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" func foo () (int , int ) { return 1 , 2 } func main () { var a, _ = foo() fmt.Println(a) }
2.2.2 变量定义 声明变量的一般形式是使用 var 关键字
第一种,指定变量类型,声明后若不赋值,使用默认值0。
第二种,根据值自行判定变量类型。
第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import "fmt" func main() { //第一种 使用默认值 var a int fmt.Printf("a = %d\n", a) //第二种 var b int = 10 fmt.Printf("b = %d\n", b) //第三种 省略后面的数据类型,自动匹配类型 var c = 20 fmt.Printf("c = %d\n", c) //第四种 省略var关键字 d := 3.14 fmt.Printf("d = %f\n", d) }
第四种,多变量声明,如下:
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" var x, y int var ( a int b bool ) var c, d int = 1 , 2 var e, f = 123 , "liudanbing" func main () { g, h := 123 , "需要在func函数体内实现" fmt.Println(x, y, a, b, c, d, e, f, g, h) _, value := 7 , 5 fmt.Println(value) }
常量 常量是一个简单值的标识符,在程序运行时,不会被修改的量。
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
定义格式:const identifier [type] = value
可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。
显式类型定义:const b string = “abc”
隐式类型定义:const b = “abc”
常量可以用len(), cap(), unsafe.Sizeof()常量计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过
1 2 3 4 5 6 常量还可以用作枚举 const ( Unknown = 0 Female = 1 Male = 2 )
优雅的常量 iota 在 golang 中,一个方便的习惯就是使用iota标示符,它简化了常量用于增长数字的定义,给以上相同的值以准确的分类。
1 2 3 4 5 const ( CategoryBooks = iota // 0 CategoryHealth // 1 CategoryClothing // 2 )
iota可以做更多事情,而不仅仅是 increment。更精确地说,iota总是用于 increment,但是它可以用于表达式,在常量中的存储结果值。
1 2 3 4 5 6 7 8 type Allergen int const ( IgEggs Allergen = 1 << iota // 1 << 0 which is 00000001 IgChocolate // 1 << 1 which is 00000010 IgNuts // 1 << 2 which is 00000100 IgStrawberries // 1 << 3 which is 00001000 IgShellfish // 1 << 4 which is 00010000 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fmt.Println(IgEggs | IgChocolate | IgShellfish) // output: // 19 type ByteSize float64 const ( _ = iota // ignore first value by assigning to blank identifier KB ByteSize = 1 << (10 * iota) // 1 << (10*1) MB // 1 << (10*2) GB // 1 << (10*3) TB // 1 << (10*4) PB // 1 << (10*5) EB // 1 << (10*6) ZB // 1 << (10*7) YB // 1 << (10*8) )
基本数据类型
在go语言中数据类型分为了**基本数据和 复杂数据类型**
数据类型
取值范围
bool
true 或 false
string
任何UTF-8编码字符串
int
有符号整数,取值范围为 -2147483648 至 2147483647
int8
有符号8位整数,取值范围为 -128 至 127
int16
有符号16位整数,取值范围为 -32768 至 32767
int32
有符号32位整数,取值范围为 -2147483648 至 2147483647
int64
有符号64位整数,取值范围为 -9223372036854775808 至 9223372036854775807
uint
无符号整数,取值范围为 0 至 4294967295
uint8
无符号8位整数,取值范围为 0 至 255
uint16
无符号16位整数,取值范围为 0 至 65535
uint32
无符号32位整数,取值范围为 0 至 4294967295
uint64
无符号64位整数,取值范围为 0 至 18446744073709551615
uintptr
无符号整数,用于存储一个指针
float32
32位浮点数
float64
64位浮点数
complex64
由32位实数和32位虚数组成的复数
complex128
由64位实数和64位虚数组成的复数
2.4.1 转义符
\
反斜杠用于转义下一个字符
’
单引号用于在单引号括起的字符字面值中表示单引号字符
“
双引号用于在双引号括起的字符串字面值中表示双引号字符
\a
蜂鸣声符
\b
退格符,将当前位置移到前一列
\f
换页符,将当前位置移到下页开头
\n
换行符,将当前位置移到下一行开头
\r
回车符,将当前位置移到本行开头
\t
水平制表符,将当前位置移到下一个制表符位置
\v
垂直制表符,将当前位置移到下一垂直制表符位置
\xhh
十六进制转义符,其中 hh 代表一个或多个表示十六进制值的数字
2.4.2 占位符
占位符
用途
%v
根据变量的值来输出
%+v
在 %v 基础上,对结构体、切片等类型,将字段名与值一起输出
%#v
输出值的 Go 语法表示方式
%T
输出变量的类型
%%
输出百分号本身,不进行任何格式化操作
%t
输出 bool 类型变量
%d or %i
输出十进制整数
%o
输出八进制数
%x or %X
输出十六进制数
%U
输出 Unicode 字符集格式
%e or %E
输出浮点型数据的科学计数法表示
%f
输出浮点型数据的标准计数法表示
%q
输出双引号引起来的字符串
%s
输出字符串
2.4.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 package mainimport ( "fmt" "reflect" ) func main () { var s int var i int8 .... }
2.4.4 浮点型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "reflect" ) func main () { var f1 float32 f1 = 3.15487464 fmt.Println(f1, reflect.TypeOf(f1)) }
2.4.5 布尔型 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" "reflect" ) func main () { var b bool b = true b = false fmt.Println(b, reflect.TypeOf(b)) fmt.Println(1 == 1 ) fmt.Println(3 > 1 ) var name = "yuan" var b2 = name == "rain" fmt.Println(b2) }
2.4.6 字符类型 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" ) func main () { var s = "hello zhang" fmt.Println(s) a := s[2 ] fmt.Println(string (a)) b1 := s[2 :5 ] fmt.Println(b1) b2 := s[0 :] fmt.Println(b2) b3 := s[:8 ] fmt.Println(b3) var s1 = "hello" var s2 = "zhang" var s3 = s1 + s2 fmt.Println(s3) }
2.4.7 进制转换 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 n int = 10 fmt.Printf("%d \n" , n) fmt.Printf("%o \n" , n) fmt.Printf("%b \n" , n) fmt.Printf("%x \n" , n) var b int = 020 fmt.Printf("%o \n" , b) fmt.Printf("%d \n" , b) fmt.Printf("%x \n" , b) fmt.Printf("%b \n" , b) var c = 0x12 fmt.Printf("%d \n" , c) fmt.Printf("%o \n" , c) fmt.Printf("%x \n" , c) fmt.Printf("%b \n" , c) }
字符串的常用方法 字符串类型在 go 里是个结构, 包含指向底层数组的指针和长度,这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节。
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 package mainimport ( "fmt" "reflect" "strings" ) func main () { action() } func allstring () { var a string a = "hello,Zhang,tian,hao" fmt.Println(len (a)) fmt.Println(strings.ToUpper(a)) fmt.Println(strings.ToLower(a)) fmt.Println(strings.Contains(a, "tianhao" )) fmt.Println(strings.HasPrefix(a, "zth" )) fmt.Println(strings.HasSuffix(a, "hao" )) fmt.Println(strings.Contains(a, "tian" )) s := strings.Trim(a, "o" ) fmt.Println(s) fmt.Println(strings.Index(a, "Z" )) fmt.Println(strings.LastIndex(a, "o" )) a2 := strings.Split(a, "," ) fmt.Println(a2) var set = strings.Join(a2, "-" ) fmt.Println(set, "\n" , reflect.TypeOf(set)) } func action () { s := "mysql … -u root -p 123" uindex := strings.Index(s, "-u" ) pindex := strings.Index(s, "-p" ) user := s[uindex+3 : pindex-1 ] psd := s[pindex+3 :] fmt.Println("用户名为" , user, "密码为" , psd) }
数据类型转换 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" "reflect" "strconv" ) func main () { var x int8 = 10 var y int16 = 20 fmt.Println(x + int8 (y)) var agestr = "32" var age, _ = strconv.Atoi(agestr) fmt.Println(age) price := 100 price_str := strconv.Itoa(price) fmt.Println(price_str, reflect.TypeOf(price_str)) ret, _ := strconv.ParseInt("28" , 10 , 8 ) fmt.Println(ret, reflect.TypeOf(ret)) floats, _ := strconv.ParseFloat("3.1415926" , 64 ) fmt.Println(floats, reflect.TypeOf(floats)) b, _ := strconv.ParseBool("0" ) b1, _ := strconv.ParseBool("-1" ) b2, _ := strconv.ParseBool("true" ) b3, _ := strconv.ParseBool("T" ) fmt.Println(b, b1, b2, b3) }
运算符
运算符
描述
算数运算符
用于执行基本的数学运算,如加法、减法、乘法和除法
+
加法运算符,将两个数相加
-
减法运算符,从第一个数中减去第二个数
*
乘法运算符,将两个数相乘
/
除法运算符,将第一个数除以第二个数
%
取模运算符,返回第一个数除以第二个数的余数
自增和自减运算符
自增或自减运算符用于递增或递减变量的值
++
自增运算符,将变量的值加一
–
自减运算符,将变量的值减一
比较运算符
用于比较两个值之间的关系,返回一个布尔值
==
等于运算符,如果两个值相等则返回 true,否则返回 false
!=
不等于运算符,如果两个值不相等则返回 true,否则返回 false
>
大于运算符,如果左边的值大于右边的值则返回 true,否则返回 false
<
小于运算符,如果左边的值小于右边的值则返回 true,否则返回 false
>=
大于等于运算符,如果左边的值大于等于右边的值则返回 true,否则返回 false
<=
小于等于运算符,如果左边的值小于等于右边的值则返回 true,否则返回 false
逻辑运算符
用于执行逻辑运算,如AND、OR、和NOT
&&
逻辑 AND 运算符,两个操作数都为 true 时才返回 true,否则返回 false
||
逻辑 OR 运算符,两个操作数有一个为 true 时返回 true,否则返回 false
!
逻辑 NOT 运算符,如果条件为 true 则返回 false,否则返回 true
位运算符
用于执行二进制位运算,例如 按位与、按位或、按位异或等
&
按位 AND 运算符,参与运算的两个值都为 1 时才返回 1,否则返回 0
|
按位 OR 运算符,参与运算的两个值有一个为 1 时就返回 1,否则返回 0
^
按位异或运算符,参与运算的两个值只有一个为 1 时才返回 1,否则返回 0
<<
左移运算符,将第一个操作数的二进制位向左移动第二个操作数指定的位数
>>
右移运算符,将第一个操作数的二进制位向右移动第二个操作数指定的位数
赋值运算符
用于将一个值赋给一个变量或表达式
=
简单赋值运算符,将右边表达式的值赋给左边的变量
+=
加法赋值运算符,等价于 a = a + b
-=
减法赋值运算符,等价于 a = a - b
*=
乘法赋值运算符,等价于 a = a * b
/=
除法赋值运算符,等价于 a = a / b
%=
取余赋值运算符,等价于 a = a % b
<<=
左移赋值运算符,等价于 a = a << b
>>=
右移赋值运算符,等价于 a = a >> b
&=
按位 AND 赋值运算符,等价于 a = a & b
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" func main () { x, y := 10 , 20 fmt.Println(x%2 == 0 , y) fmt.Println(x >= y) fmt.Println(true && false ) fmt.Println(true || false ) fmt.Println(!true || false ) username := "zhang" password := 123 fmt.Println(username == "root" && password == 123 ) var a = 12 a += 1 fmt.Println(a) var b = 10 b++ fmt.Println(b) var q, w, z = 1 , 2 , 3 fmt.Println(q, w, z) var t = q + w fmt.Println(t) }
函数 Go 函数可以返回多个值,例如:
1 2 3 4 5 6 7 8 9 10 11 package main import "fmt" func swap(x, y string) (string, string) { return y, x } func main() { a, b := swap("Mahesh", "Kumar") fmt.Println(a, b) }
init函数与import init 函数可在package main中,可在其他package中,可在同一个package中出现多次。
main 函数只能在package main中。
建议用户在一个package中每个文件只写一个init函数。
执行顺序
golang里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。
go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。
程序的初始化和执行都起始于main包。
如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。
当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。
等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。
说明如果一个包会被多个包同时导入,那么它只会被导入一次,而先输出lib2是因为main包中导入Lib1时,Lib1又导入了Lib2,会首先初始化Lib2包的东西
函数参数 函数如果使用参数,该变量可称为函数的形参。
形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数:
值拷贝 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
这是go语言的一个特性,与python等动态语言不一样
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 main () { var x = 10 var y = x x = 20 fmt.Println("x=" , x, "y=" , y) sum() } func sum () { var a = 1 + 1 var b = a var c = a * b fmt.Println(a, b, c) }
引用传递(指针传递) 变量是一种使用方便的占位符,用于引用计算机内存地址。
Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
函数中引用传递*
变量指向地址&
1 2 3 4 5 6 7 package main import "fmt" func main() { var a int = 10 fmt.Printf("变量的地址: %x\n", &a ) }
输入输出函数 2.8.1 输出函数
go中常用的输入输出函数,在fmt包中
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 () { var name, age = "yuan" , 32 fmt.Println("hello world" ) fmt.Println(name) fmt.Println(age) fmt.Println("姓名:" , name, "年龄" , age) var isMarried = false fmt.Printf("姓名:%s,年龄:%d,婚否:%t\n" , name, age, isMarried) s := fmt.Sprintf("姓名:%s,年龄:%d,婚否:%t" , name, age, isMarried) fmt.Println(s) }
2.8.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 package mainimport ( "fmt" "strings" ) func main () { scan() } func action () { var ( birth string ) fmt.Println("输入生日格式如:1988-3-16" ) fmt.Scan(&birth) birthslice := strings.Split(birth, "-" ) fmt.Printf("您的生日是%s年-%s月-%s日" , birthslice[0 ], birthslice[1 ], birthslice[2 ]) } func scan () { var a, b int fmt.Scanf("%d+%d" , &a, &b) fmt.Println(a + b) } func action2 () { var name string fmt.Println("请输入一个英文名: " ) fmt.Scan(&name) var b = (strings.HasPrefix(name, "a" )) || (strings.HasPrefix(name, "A" )) fmt.Println(b) }
流程控制语句 2.9.1 if语句 1 2 3 4 5 6 7 8 9 10 11 12 graph TD; start[开始]-->ifElse{是否满足条件?}; ifElse--满足条件-->statement1[执行语句1]; ifElse--不满足条件-->ifElseNext{是否满足另一条件?}; ifElseNext--满足条件-->statement2[执行语句2]; ifElseNext--不满足条件-->statement3[执行语句3]; statement1[执行语句1]-->结束; statement2[执行语句2]-->结束; statement3[执行语句3]-->结束;
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" func main () { twoIf() } func twoIf () { var ( passworld any input string ) passworld = "123" fmt.Println("输入密码:" ) fmt.Scan(&input) if passworld == input { fmt.Println("密码正确" ) } else { 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package mainimport "fmt" func main () { weeks() } func scanres () { var scanre int fmt.Println("输入你的成绩" ) fmt.Scan(&scanre) if scanre > 100 || scanre < 0 { fmt.Println("输入数字不合法" ) } else if scanre >= 90 { fmt.Println("优秀" ) } else if scanre >= 70 { fmt.Println("良好" ) } else if scanre >= 60 { fmt.Println("及格" ) } else { fmt.Println("不及格" ) } } func weeks () { var week int fmt.Println("输入1-7的数字" ) fmt.Scan(&week) if week == 1 { fmt.Printf("星期%d" , week) } else if week == 2 { fmt.Printf("星期%d" , week) } else if week == 3 { fmt.Printf("星期%d" , week) } else if week == 4 { fmt.Printf("星期%d" , week) } else if week == 5 { fmt.Printf("星期%d" , week) } else if week == 6 { fmt.Printf("星期%d" , week) } else if week == 7 { fmt.Printf("星期%d" , week) } else { fmt.Println("非法输入" ) } }
2.9.2 switch语句 1 2 3 4 5 6 7 8 9 10 graph TD; start[开始]-->switchCond{选择变量值}; switchCond--case 1-->statement1[执行语句1]; switchCond--case 2-->statement2[执行语句2]; switchCond--case 3-->statement3[执行语句3]; switchCond--default-->defaultState[执行默认语句]; statement1-->结束; statement2-->结束; statement3-->结束; defaultState-->结束;
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 () { weeks() } func weeks () { switch week { case 1 : fmt.Printf("星期%d" , week) case 2 : fmt.Printf("星期%d" , week) case 3 : fmt.Printf("星期%d" , week) case 4 : fmt.Printf("星期%d" , week) case 5 : fmt.Printf("星期%d" , week) case 6 : fmt.Printf("星期%d" , week) case 7 : fmt.Println("星期日" ) default : fmt.Println("非法输入" ) } }
2.9.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 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 96 97 98 99 100 101 102 103 104 105 106 107 package mainimport "fmt" func main () { var ( month int day int xingZuo string ) fmt.Println("输入月和日" ) fmt.Scan(&month, &day) if month > 12 && day > 31 && day < 1 { fmt.Println("输入有误" ) } else { switch month { case 1 : if day >= 1 && day <= 19 { xingZuo = "摩羯座" } else { xingZuo = "水瓶座" } case 2 : if day >= 1 && day <= 18 { xingZuo = "水瓶座" } else { xingZuo = "双鱼座" } case 3 : if day >= 1 && day <= 20 { xingZuo = "双鱼座" } else { xingZuo = "白羊座" } case 4 : if day >= 1 && day <= 19 { xingZuo = "白羊座" } else { xingZuo = "金牛座" } case 5 : if day >= 1 && day <= 20 { xingZuo = "金牛座" } else { xingZuo = "双子座" } case 6 : if day >= 1 && day <= 21 { xingZuo = "双子座" } else { xingZuo = "巨蟹座" } case 7 : if day >= 1 && day <= 22 { xingZuo = "巨蟹座" } else { xingZuo = "狮子座" } case 8 : if day >= 1 && day <= 22 { xingZuo = "狮子座" } else { xingZuo = "处女座" } case 9 : if day >= 1 && day <= 22 { xingZuo = "处女座" } else { xingZuo = "天秤座" } case 10 : if day >= 1 && day <= 23 { xingZuo = "天秤座" } else { xingZuo = "天蝎座" } case 11 : if day >= 1 && day <= 22 { xingZuo = "天蝎座" } else { xingZuo = "射手座" } case 12 : if day >= 1 && day <= 21 { xingZuo = "射手座" } else { xingZuo = "摩羯座" } default : fmt.Println("输入的月份有问题" ) } fmt.Println("您的星座是:" , xingZuo) } }
循环语句
在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句。一组被重复执行的语句称之为循环体, 能否继续重复,决定循环的终止条件。
与其它主流编程语言不同的的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构。
1 2 3 4 5 6 7 8 graph TD; start[开始]-->forLoop; forLoop-->condCheck{检查条件}; condCheck--条件成立-->statement[执行语句]; statement-->endCheck{检查是否为最后一次循环}; endCheck--不是最后一次-->updateLoop[更新循环变量]; updateLoop-->condCheck; endCheck--是最后一次-->结束;
2.10.1 for循环 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 () { var s = 0 for i := 1 ; i <= 100 ; i++ { s += i } fmt.Println(s) }
2.10.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 package mainimport "fmt" func main () { } func for_if () { for count := 1 ; count <= 100 ; count++ { if count%2 == 0 { fmt.Println("偶数有:" , count) } } } func if_for () { var num int fmt.Println("(1)从大到小,(2)从小到大" ) fmt.Scan(&num) if num == 1 { for count := 1 ; count <= 100 ; count++ { fmt.Println(count) } } else if num == 2 { for count := 100 ; count <= 1 ; count-- { fmt.Println(count) } } else { fmt.Println("输入不规范" ) } }
2.10.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 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" func main () { } func breaks () { for i := 0 ; i < 10 ; i++ { if i == 6 { break } fmt.Println(i) } fmt.Println("退出了循环" ) } func for_switch () { var num int fmt.Println(` 1,普通攻击 2,技能攻击 3,逃跑 4,求助 5,退出 ` ) for true { fmt.Scan(&num) if num == 5 { break } switch num { case 1 : fmt.Println("普通攻击" ) case 2 : fmt.Println("技能攻击" ) case 3 : fmt.Println("逃跑" ) case 4 : fmt.Println("求助" ) default : fmt.Println("输入有误" ) } } } func continues () { for count := 0 ; count < 10 ; count++ { if count == 6 { continue } fmt.Println("这是continue" , count) } } func action () { for i := 1 ; i <= 100 ; i++ { } }
复杂数据类型
以下为go语言常见复杂数据类型
数据类型
描述
数组
存储具有相同数据类型的固定尺寸元素的连续集合
指针
通过数据地址取值,或者操作地址
切片
动态大小的、可变长度的序列,可以看作是对数组的抽象封装
映射(Map)
一种无序的键值对的集合,其中所有的键都是唯一的
结构体
用于表示一组不同类型的数据的集合,通过一个唯一的名字来区分不同的字段
接口
一种类型,指定了一组方法的集合,可以实现多态的效果
通道(Channel)
一种数据类型,用于在不同goroutine之间传递数据,类似于管道(pipe)
2.11.1 指针类型
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package mainimport ( "fmt" "reflect" ) func main () { var x = 10 fmt.Println(&x, reflect.TypeOf(&x)) var p *int p = &x fmt.Println(p, &p) fmt.Println(*p, reflect.TypeOf(*p)) *p = 100 fmt.Println(x) var a = 100 var b = &a var c = &b **c = 200 fmt.Println(a) } func swap (a *int , b *int ) { var temp int temp = *a *a = *b *b = temp } swap(&变量1 ,&变量2 )
2.11.1.1 new函数 1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { var a = new (int ) *a = 10 fmt.Println(*a) }
2.11.2 数组 2.11.2.1 数组的声明 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var arr [5 ]int fmt.Println(arr) var arr [5 ]int {1 ,2 ,3 } arr := []int {1 ,2 ,3 } fmt.Println(arr) for i:=0 ; i<len (arr); i++{ fmt.Println(i) } for index,value:=range arr{ fmt.Println("index=" ,index , "value=" ,value) } fmt.Printf("type= %T\n" ,arr)
2.11.2.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 package mainimport "fmt" func main () { var arr [5 ]int fmt.Println(arr) fmt.Println(arr[0 ]) fmt.Println(arr[1 ]) fmt.Println(arr[2 ]) arr[0 ] = 25 fmt.Println(arr) arr[1 ] = 26 arr[2 ] = 27 arr[3 ] = 28 fmt.Println(arr) var names = [3 ]string {"ran" , "zhang" , "shi" } fmt.Println(names) var age = [3 ]int {1 , 2 , 3 } fmt.Println(age) var name = [...]string {"ran" , "zhang" , "shi" , "jjj" , "aaa" } fmt.Println(name) var name1 = [...]string {0 : "zhang" , 1 : "shi" } fmt.Println(name1) fmt.Println(len (name)) }
2.11.2.3数组操作 2.11.2.3.1索引操作 1 2 3 4 5 var names = [3 ]string {"yuan" , "rain" , "alvin" }fmt.Println(names[2 ]) names[2 ] = "Rain" fmt.Println(names)
2.11.2.3.2切片操作 1 2 3 4 5 6 7 8 9 10 var arr = [...]int {11 , 12 , 13 , 45 , 75 , 64 , 78 , 66 , 54 }s := arr[0 :3 ] fmt.Println(s, reflect.TypeOf(s)) s1 := arr[1 :] fmt.Println(s, reflect.TypeOf(s)) s2 := arr[:3 ] fmt.Println(s, reflect.TypeOf(s)) s3 := arr[2 :4 ] fmt.Println(s, reflect.TypeOf(s)) fmt.Println(s1, s2, s3)
2.11.2.3.3 遍历数组 1 2 3 4 5 6 7 8 9 10 11 var arr2 = [...]int {1 , 2 , 3 , 4 , 5 }for i, i2 := range arr2 { fmt.Println(i, i2) } fmt.Println("======================================================================" ) for i := 0 ; i < len (arr2); i++ { fmt.Println(i, arr2[i]) }
2.11.2.3 切片
切片是对数组的引用,也就是动态数组
1 2 3 4 5 6 7 8 9 10 11 12 var a = [5 ]int {1 , 2 , 3 , 4 , 5 }var slice = a[:] fmt.Println(len (slice), cap (slice)) var slice2 = a[:2 ]fmt.Println(len (slice2), cap (slice2)) newSlicec := slice[1 :3 ] newSlicec[1 ] = 1000 fmt.Println(a) fmt.Println(slice) fmt.Println(slice2)
2.11.2.3.1 声明切片 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 arr := []int {1 ,2 ,3 } fmt.Printf("len=%d,slice=&v\n" ,len (slice1),slice1) var slice1 []int slice1 = make ([]int , 3 ) slice1 := make ([]int , 3 ) slice1 = append (slice1,2 ) var s = []int {10 , 11 , 12 , 13 , 14 }s1 := s[1 :4 ] fmt.Println(len (s1), cap (s1)) s2 := s[3 :] fmt.Println(len (s2), cap (s2)) s3 := s1[1 :2 ] fmt.Println(len (s3), cap (s3))
示例:
1 2 3 4 5 6 7 8 9 func action () { var a = [...]int {1 , 2 , 3 , 4 , 5 , 6 } a1 := a[0 :3 ] a2 := a[0 :5 ] a3 := a[1 :5 ] fmt.Println(a1) fmt.Println(a2) fmt.Println(a3) }
2.11.2.3.2 make函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "fmt" ) func main () { var s = make ([]int , 5 , 10 ) fmt.Println(len (s), cap (s)) fmt.Println(s) s[0 ] = 100 fmt.Println(s) }
2.11.2.4 map类型
相当于python的字典类型 在go中叫做map类型
作为函数参数是引用传递
先声明后赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var myMap map [string ]string myMap = make (map [string ]string , 10 ) myMap := make (map [string ]string ) myMap["name" ] = "zhang" myMap["age" ] = "18" fmt.Println(myMap) myMap := make (map [string ]string ){ "name" : "zhang" "age" : "18" }
直接声明赋值,
1 info := map [string ]string {"name" : "yuan" , "age" : "20" , "city" : "China" }
map增删改查
1 2 3 4 5 6 7 8 9 10 11 info := map [string ]string {"name" : "yuan" , "age" : "20" , "city" : "China" } fmt.Println(info) Map() info2 := map [string ]string {"name" : "yuan" , "age" : "18" , "gender" : "male" } m2 := Map02(info2) fmt.Println(m2) m3 := Map03(info2) fmt.Println(m3)
map容量
1 2 3 4 m := make (map [string ]float32 , 100 ) m4 := Map04(m) fmt.Println(m4)
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 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 func Map () { info := map [string ]string {"name" : "yuan" , "age" : "18" , "gender" : "male" } val := info["name" ] val, is_exist := info["name" ] if is_exist { fmt.Println(val) fmt.Println(is_exist) } else { fmt.Println("键不存在!" ) } for s, s2 := range info { fmt.Println(s, s2) } noSortMap := map [int ]int { 1 : 1 , 2 : 2 , 3 : 3 , 4 : 4 , 5 : 5 , 6 : 6 , } for i, i2 := range noSortMap { fmt.Println(i, i2) } } func Map02 (s map [string ]string ) map [string ]string { s["height" ] = "180cm" s["age" ] = "22" return s } func Map03 (s map [string ]string ) any { delete (s, "name" ) return s } func Map04 (s map [string ]float32 ) any { return len (s) } func Map05 () { data := map [string ][]string {"hebei" : []string {"廊坊市" , "石家庄" , "邯郸" }, "beijing" : []string {"朝阳" , "丰台" , "海淀" }} s := data["hebei" ][1 ] fmt.Println("河北的第二个城市" , s) for s2, _ := range data { for _, s3 := range data[s2] { fmt.Println(s2, len (data[s2]), s3) } } var zz = []string {"眉山" , "成都" } data["chengdu" ] = zz fmt.Println(data) delete (data, "beijing" ) fmt.Println(data) }
2.11.2.5 map底层原理
Go语言中的Map基本数据结构是一张哈希表,其中每个哈希表的元素是一个桶(Bucket)
1 2 3 4 5 6 7 8 graph TD; Map[Map]; Bucket1[Bucket1]; Bucket2[Bucket2]; Bucket3[Bucket3]; Map-->Bucket1; Map-->Bucket2; Map-->Bucket3;
桶(Bucket)是一个包含一些键值对的可拓展数组(slicemap),key和value都占用8个字节的空间。
1 2 3 4 5 6 7 8 9 10 11 12 graph TD; Bucket[Bucket]; KeyValue1[KeyValue]; KeyValue2[KeyValue]; KeyValue3[KeyValue]; KeyValue4[KeyValue]; KeyValue5[KeyValue]; Bucket-->KeyValue1; Bucket-->KeyValue2; Bucket-->KeyValue3; Bucket-->KeyValue4; Bucket-->KeyValue5;
当我们使用Key在Map中存储或获取值时,Go语言中的Map会自动进行哈希计算以获取桶索引,然后在对应的桶中查找对应的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 graph TD; Map[Map]; Key1(Key1); Key2(Key2); Index1(Index1); Index2(Index2); Bucket1(Bucket1); KeyValue(KeyValue); Value(Value); Key1-->Hash(Hash); Hash-->Index1; Map-->Bucket1; Index1-->Bucket1; Bucket1-->KeyValue; KeyValue-->Value; Value-->GetResult(Get Result); Key2-->Hash; Hash-->Index2; Index2-->Result(Result不存在);
需要注意的是,Go语言中的Map底层实现可以随着版本、编译器和运行环境等因素的不同而略有差异。但是在任何情况下,Map都会使用基于哈希表和桶的底层实现机制来实现其存储和查找功能。
2.11.2.6 append函数
append 追加值,给切片追加,在后面追加数据
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" func main () { var s []int s1 := append (s, 1 ) fmt.Println(s1) s2 := append (s1, 2 , 3 , 4 ) fmt.Println(s2) var t = []int {2 , 3 , 4 } t1 := append (t, 1 ) fmt.Println(t1) var s4 = make ([]int , 3 , 10 ) s5 := append (s4, 100 ) fmt.Println(s5) var c = 255 fmt.Println(c >> 10 ) }
append 底层原理(面试题)
1 2 3 4 5 6 7 8 9 10 11 func Append () { var a1 = make ([]int , 2 , 10 ) a2 := append (a1, 1 , 23 , 4 ) fmt.Println(a1) fmt.Println(a2) }
defer语句
defer类似于java中的finally,在函数内的语句全部执行完之后执行。即使在发生错误或提前返回的情况下也能保证执行。
defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。
如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。
defer在return之后执行
defer作用:
1 2 3 4 5 6 7 8 func main(){ defer fmt.Println("main end1") // 多个defer语句,会按照顺序依次压入栈 defer fmt.Println("main end2") fmt.Println("hello") } //输出hello、end2、end1
recover错误拦截 运行时panic异常一旦被引发就会导致程序崩溃。
Go语言提供了专用于“拦截”运行时panic的内建函数“recover”。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。
注意: recover只有在defer调用的函数中有效。
1 func recover interface{}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package main import "fmt" func Demo(i int) { //定义10个元素的数组 var arr [10]int //错误拦截要在产生错误前设置 defer func() { //设置recover拦截错误信息 err := recover() //产生panic异常 打印错误信息 if err != nil { fmt.Println(err) } }() //根据函数参数为数组元素赋值 //如果i的值超过数组下标 会报错误:数组下标越界 arr[i] = 10 } func main() { Demo(10) //产生错误后 程序继续 fmt.Println("程序继续执行...") }
slice Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
定义切片 声明一个未指定大小的数组来定义切片:var identifier []type
切片不需要说明长度。
或使用make()函数来创建切片:
1 2 3 4 5 6 7 var slice1 []type = make([]type, len) 也可以简写为 slice1 := make([]type, len) 也可以指定容量,其中capacity为可选参数。 make([]T, length, capacity) 这里 len 是数组的长度并且也是切片的初始长度。
切片初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 s :=[] int {1,2,3 } 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3 s := arr[:] 初始化切片s,是数组arr的引用 s := arr[startIndex:endIndex] 将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片 s := arr[startIndex:] 缺省endIndex时将表示一直到arr的最后一个元素 s := arr[:endIndex] 缺省startIndex时将表示从arr的第一个元素开始 s1 := s[startIndex:endIndex] 通过切片s初始化切片s1 s :=make([]int,len,cap) 通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
len() 和 cap() 函数 切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
1 2 3 4 5 6 7 8 9 10 11 package main import "fmt" func main() { var numbers = make([]int,3,5) printSlice(numbers) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
空(nil)切片 一个切片在未初始化之前默认为 nil,长度为 0,实例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package main import "fmt" func main() { var numbers []int printSlice(numbers) if(numbers == nil){ fmt.Printf("切片是空的") } } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
切片截取 可以通过设置下限及上限来设置截取切片*[lower-bound:upper-bound]*,实例如下:
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 package main import "fmt" func main() { /* 创建切片 */ numbers := []int{0,1,2,3,4,5,6,7,8} printSlice(numbers) /* 打印原始切片 */ fmt.Println("numbers ==", numbers) /* 打印子切片从索引1(包含) 到索引4(不包含)*/ fmt.Println("numbers[1:4] ==", numbers[1:4]) /* 默认下限为 0*/ fmt.Println("numbers[:3] ==", numbers[:3]) /* 默认上限为 len(s)*/ fmt.Println("numbers[4:] ==", numbers[4:]) numbers1 := make([]int,0,5) printSlice(numbers1) /* 打印子切片从索引 0(包含) 到索引 2(不包含) */ number2 := numbers[:2] printSlice(number2) /* 打印子切片从索引 2(包含) 到索引 5(不包含) */ number3 := numbers[2:5] printSlice(number3) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
append() 和 copy() 函数 如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package main import "fmt" func main() { var numbers []int printSlice(numbers) /* 允许追加空切片 */ numbers = append(numbers, 0) printSlice(numbers) /* 向切片添加一个元素 */ numbers = append(numbers, 1) printSlice(numbers) /* 同时添加多个元素 */ numbers = append(numbers, 2,3,4) printSlice(numbers) /* 创建切片 numbers1 是之前切片的两倍容量*/ numbers1 := make([]int, len(numbers), (cap(numbers))*2) /* 拷贝 numbers 的内容到 numbers1 */ copy(numbers1,numbers) printSlice(numbers1) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
map map和slice类似,只不过是数据结构不同,下面是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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package main import ( "fmt" ) func main() { //第一种声明 var test1 map[string]string //在使用map前,需要先make,make的作用就是给map分配数据空间 test1 = make(map[string]string, 10) test1["one"] = "php" test1["two"] = "golang" test1["three"] = "java" fmt.Println(test1) //map[two:golang three:java one:php] //第二种声明 test2 := make(map[string]string) test2["one"] = "php" test2["two"] = "golang" test2["three"] = "java" fmt.Println(test2) //map[one:php two:golang three:java] //第三种声明 test3 := map[string]string{ "one" : "php", "two" : "golang", "three" : "java", } fmt.Println(test3) //map[one:php two:golang three:java] language := make(map[string]map[string]string) language["php"] = make(map[string]string, 2) language["php"]["id"] = "1" language["php"]["desc"] = "php是世界上最美的语言" language["golang"] = make(map[string]string, 2) language["golang"]["id"] = "2" language["golang"]["desc"] = "golang抗并发非常good" fmt.Println(language) //map[php:map[id:1 desc:php是世界上最美的语言] golang:map[id:2 desc:golang抗并发非常good]] //增删改查 // val, key := language["php"] //查找是否有php这个子元素 // if key { // fmt.Printf("%v", val) // } else { // fmt.Printf("no"); // } //language["php"]["id"] = "3" //修改了php子元素的id值 //language["php"]["nickname"] = "啪啪啪" //增加php元素里的nickname值 //delete(language, "php") //删除了php子元素 fmt.Println(language) }
结构体
结构体是一种自定义的复合数据类型,它由各种属性或字段(Fields)组成,每个字段都有自己的数据类型。在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 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" func main () { Task() Madd() } type Employee struct { ID int Name string Address string Position string Salary int ManagerID int } var dilbert Employeedilbert := Employee{Name:"zhangsan" } func Task () { dilbert.Name = "张天昊" position := &dilbert.Position *position = "Senir" + *position fmt.Println(*position) var employeeOfTheMonth *Employee = &dilbert (*employeeOfTheMonth).Position += "(pass team player)" fmt.Println(employeeOfTheMonth) } type address struct { hostname string port int } func Madd () { hits := make (map [address]int ) hits[address{"golang.org" , 403 }]++ fmt.Println(hits) } type Point struct { x, y int } type Cir struct { Center Point Rauid int } type Whell struct { Cir Cir Spoke int } func Run () { var w Whell w.Cir.Center.x = 8 w.Cir.Center.y = 8 w.Cir.Rauid = 5 w.Spoke = 20 } type Circle struct { Point Rauins int } type wheel struct { Circle Spokes int } func Run2 () { var w wheel w.x = 8 w.y = 8 }
标签 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 package main import{ "fmt" "reflect" } type resume struct{ Name string `info:"name" doc:"我的名字"` Sex string `info:"sex"` } func findTag(str interface{}){ t := reflect.Type0f(str).Elem() for i:=0; i<t.NumField(); i++{ taginfo := t.Field(i).Tag.Get("info") tagdoc := t.Field(i).Tag.Get("doc") fmt.Println("info:",taginfo,"doc:",tagdoc) } } func main(){ var re resume findTag(&re) } 应用 import{ "encoding/json" "fmt" } type Movie struct{ Title string `json:"title"` Year int `json:"year"` Price int `json:"rmb"` Actors []string `json:"actors"` func main(){ movie := Movie{"喜剧之王",2000,10,[]string{"xingye", “zhangbozhi"}} //编码的过程 结构体---> json jsonStr,err := json.Marshal(movie) if err != nil { fmt.Println("json marshal error", err) return } fmt.Printf("jsonstr =%s\n",jsonstr) //解码的过程 jsonstr ---> 结构体 //jsonstr = {"title":"喜剧之","yoor":288,"rmb":10,"arters"': ["xinyye","zhangbozhi"]} myMovie := Movie{} err = json.Unmarshal(jsonstr,&myMovie) if err != nil { fmt.Println("json unmarshal error", err) return } fmt.Printf("%v\n",myMovie) }
三、进阶 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type Student struct { ID int Name string address string } func (this *Student) Show(){ fmt.Println("ID=" ,this.ID) fmt.Println("Name=" ,this.Name) } func (this *Student) GetName(){ return this.Name } func (this *Student) SetName(newName string ){ this.Name = newName } s1 := Student{ID: 1 ,Name:"zhangsan" ,address:"ShangHai" }
继承 1 2 3 4 type Class struct { Student //Class类继承了该类的方法 Name string }
interface与类型断言 Golang的语言中提供了断言的功能。golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface{})的方式传进来的时候,也就意味着这个参数被自动的转为interface{}的类型。
1 2 3 4 5 func funcName(a interface{}) string { return string(a) } 编译器会返回 cannot convert a (type interface{}) to type string: need type assertion
此时,意味着整个转化的过程需要类型断言。类型断言有以下几种形式:
1)直接断言使用
但是如果断言失败一般会导致panic的发生。所以为了防止panic的发生,我们需要在断言前进行一定的判断 但是如果断言失败一般会导致panic的发生。所以为了防止panic的发生,我们需要在断言前进行一定的判断 如果断言失败,那么ok的值将会是false,但是如果断言成功ok的值将会是true,同时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 27 package main import "fmt" /* func funcName(a interface{}) string { return string(a) } */ func funcName(a interface{}) string { value, ok := a.(string) if !ok { fmt.Println("It is not ok for type string") return "" } fmt.Println("The value is ", value) return value } func main() { // str := "123" // funcName(str) //var a interface{} //var a string = "123" var a int = 10 funcName(a) }
2)配合switch使用
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 var t interface{} t = functionOfSomeType() switch t := t.(type) { default: fmt.Printf("unexpected type %T", t) // %T prints whatever type t has case bool: fmt.Printf("boolean %t\n", t) // t has type bool case int: fmt.Printf("integer %d\n", t) // t has type int case *bool: fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool case *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int } 或者如下使用方法 func sqlQuote(x interface{}) string { if x == nil { return "NULL" } else if _, ok := x.(int); ok { return fmt.Sprintf("%d", x) } else if _, ok := x.(uint); ok { return fmt.Sprintf("%d", x) } else if b, ok := x.(bool); ok { if b { return "TRUE" } return "FALSE" } else if s, ok := x.(string); ok { return sqlQuoteString(s) // (not shown) } else { panic(fmt.Sprintf("unexpected type %T: %v", x, x)) } }
接口
接口是一种类型,它定义了一组方法,这些方法不包含实现代码,只是定义了函数名、参数列表和返回值类型。也就是说,接口描述了一个对象可以做什么,但是并不能描述这些功能是如何实现的。具体的实现代码需要由实现接口的类型来完成。
Go语言中,只要一个类型包含了接口中定义的所有方法,那么该类型就是实现了该接口。这种面向接口的编程方式可以实现高度抽象、低耦合的代码,让代码更具可维护性和扩展性。
interface{} 空接口是万能数据类型,比如使用空接口作为函数参数,func get(arg interface{}){ }。可以通过value,ok := arg.(string)判断arg是否是string类型
3.2.1 接口实现与定义
定义接口
1 2 3 4 type XXXX interface { }
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 View_terface interface { Facks() []rune } type runs string func (x runs) Facks() []rune { var View []rune for _, runes := range x { if runes == 's' || runes == 'a' || runes == 'c' { View = append (View, runes) } } return View } func main () { name := runs("My string is name" ) var v View_terface v = name fmt.Printf("Voiews are %c" , v.Facks()) }
在上面程序第8行中,创建了一个名为 VowelsFinder 的接口类型,它有一个方法 FindVowels() []rune。 在下一行中创建一个类型 MyString 它只是 string 的包装类。 在第15行中,我们将方法 FindVowels()[]rune 添加到接收方类型 MyString 中。现在 MyString 被认为实现了 VowelsFinder 接口。 这与 Java 等其他语言非常不同,在 Java 中,类必须使用 implements 关键字显式地声明它实现了接口。如果类型包含接口中声明的所有方法,则 go 和 go 接口将隐式实现。 在第28行,我们将 MyString 类型的 name 赋给 v 类型的 VowelsFinder。这是可能的,因为 MyString 实现了 VowelsFinder。下一行调用 MyString 类型上的 FindVowels 方法, 并打印字符串中所有的元音 My string is name。这个程序输出的 Vowels are [s s a] 这样已经创建并实现了第一个接口。
3.2.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 52 package mainimport ( "fmt" "strings" ) type New_interface interface { Animo() Water() } type Cat struct { New_interface } func (x Cat) Water(i, v int ) (n int ) { n = i + v return } func (z Cat) Animo(i, v string ) (t, x string ) { s := strings.Split(i, "" ) f := strings.Split(v, "" ) for _, n := range s { for _, k := range f { if n == k { t = "" x = "" } else if len (v) >= 2 && len (i) >= 2 { t = i[1 :] x = v[2 :] } else { t = "错误" x = "error" } } } return } func Run_one () { var c Cat fmt.Println(c.Water(10 , 20 )) fmt.Println(c.Animo("run" , "rusn" )) } func main () { Run_one() }
3.2.3 多态 1 2 3 var animal AnimalIF animal = &Cat{"Green"} animal = &Dog{"Red"}
反射 在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。
每种语言的反射模型都不同,并且有些语言根本不支持反射。Golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的,只要包含这个包就可以使用。
Golang的gRPC也是通过反射实现的。
interface 和 反射 在讲反射之前,先来看看Golang关于类型设计的一些原则
变量包括(type, value)两部分
type 包括 static type和concrete type. 简单来说 static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型
类型断言能否成功,取决于变量的concrete type,而不是static type. 因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer.
接下来要讲的反射,就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。
在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:
value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。
例如,创建类型为*os.File的变量,然后将其赋给一个接口变量r:
1 2 3 4 tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) var r io.Reader r = tty
接口变量r的pair中将记录如下信息:(tty, *os.File),这个pair在接口变量的连续赋值过程中是不变的,将接口变量r赋给另一个接口变量w:
1 2 var w io.Writer w = r.(io.Writer)
接口变量w的pair与r的pair相同,都是:(tty, *os.File),即使w是空接口类型,pair也是不变的。
interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package main import ( "fmt" "io" "os" ) func main() { tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { fmt.Println("open file error", err) return } var r io.Reader r = tty var w io.Writer w = r.(io.Writer) w.Write([]byte("HELLO THIS IS A TEST!!!\n")) }
再比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package main import "fmt" type Reader interface { ReadBook() } type Writer interface { WriteBook() } //具体类型 type Book struct { } func (this *Book) ReadBook() { fmt.Println("Read a book.") } func (this *Book) WriteBook() { fmt.Println("Write a book.") } func main() { b := &Book{} var r Reader r = b r.ReadBook() var w Writer w = r.(Writer) w.WriteBook() }
Golang的反射reflect reflect的基本功能TypeOf和ValueOf
既然反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf()。
reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main import ( "fmt" "reflect" ) func main() { var num float64 = 1.2345 fmt.Println("type: ", reflect.TypeOf(num)) fmt.Println("value: ", reflect.ValueOf(num)) } 运行结果: type: float64 value: 1.2345
说明
reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型
reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1 “Allen.Wu” 25} 这样的结构体struct的值
也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种
从relfect.Value中获取接口interface的信息
当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型,因此,下面分两种情况进行说明。
已知类型后转换为其对应的类型的做法如下,直接通过Interface方法然后强制转换,如下:
1 realValue := value.Interface().(已知的类型)
实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package main import ( "fmt" "reflect" ) func main() { var num float64 = 1.2345 pointer := reflect.ValueOf(&num) value := reflect.ValueOf(num) // 可以理解为“强制转换”,但是需要注意的时候,转换的时候,如果转换的类型不完全符合,则直接panic // Golang 对类型要求非常严格,类型一定要完全符合 // 如下两个,一个是*float64,一个是float64,如果弄混,则会panic convertPointer := pointer.Interface().(*float64) convertValue := value.Interface().(float64) fmt.Println(convertPointer) fmt.Println(convertValue) } 运行结果: 0xc42000e238 1.2345
说明
转换的时候,如果转换的类型不完全符合,则直接panic,类型要求非常严格!
转换的时候,要区分是指针还是指
也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
很多情况下,我们可能并不知道其具体类型,那么这个时候,该如何做呢?需要我们进行遍历探测其Filed来得知,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package main import ( "fmt" "reflect" ) type User struct { Id int Name string Age int } func (u User) ReflectCallFunc() { fmt.Println("Allen.Wu ReflectCallFunc") } func main() { user := User{1, "Allen.Wu", 25} DoFiledAndMethod(user) } // 通过接口来获取任意参数,然后一一揭晓 func DoFiledAndMethod(input interface{}) { getType := reflect.TypeOf(input) fmt.Println("get Type is :", getType.Name()) getValue := reflect.ValueOf(input) fmt.Println("get all Fields is:", getValue) // 获取方法字段 // 1. 先获取interface的reflect.Type,然后通过NumField进行遍历 // 2. 再通过reflect.Type的Field获取其Field // 3. 最后通过Field的Interface()得到对应的value for i := 0; i < getType.NumField(); i++ { field := getType.Field(i) value := getValue.Field(i).Interface() fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value) } // 获取方法 // 1. 先获取interface的reflect.Type,然后通过.NumMethod进行遍历 for i := 0; i < getType.NumMethod(); i++ { m := getType.Method(i) fmt.Printf("%s: %v\n", m.Name, m.Type) } } 运行结果: get Type is : User get all Fields is: {1 Allen.Wu 25} Id: int = 1 Name: string = Allen.Wu Age: int = 25 ReflectCallFunc: func(main.User)
说明
通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为:
先获取interface的reflect.Type,然后通过NumField进行遍历
再通过reflect.Type的Field获取其Field
最后通过Field的Interface()得到对应的value
通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:
先获取interface的reflect.Type,然后通过NumMethod进行遍历
再分别通过reflect.Type的Method获取对应的真实的方法(函数)
最后对结果取其Name和Type得知具体的方法名
也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
struct 或者 struct 的嵌套都是一样的判断处理方式
通过reflect.Value设置实际变量的值 reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package main import ( "fmt" "reflect" ) func main() { var num float64 = 1.2345 fmt.Println("old value of pointer:", num) // 通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值 pointer := reflect.ValueOf(&num) newValue := pointer.Elem() fmt.Println("type of pointer:", newValue.Type()) fmt.Println("settability of pointer:", newValue.CanSet()) // 重新赋值 newValue.SetFloat(77) fmt.Println("new value of pointer:", num) //////////////////// // 如果reflect.ValueOf的参数不是指针,会如何? pointer = reflect.ValueOf(num) //newValue = pointer.Elem() // 如果非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value” } 运行结果: old value of pointer: 1.2345 type of pointer: float64 settability of pointer: true new value of pointer: 77
说明
需要传入的参数是* float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意一定要是指针 。
如果传入的参数不是指针,而是变量,那么
通过Elem获取原始值对应的对象则直接panic
通过CanSet方法查询是否可以设置返回false
newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的
也就是说如果要修改反射类型对象,其值必须是“addressable”【对应的要传入的是指针,同时要通过Elem方法获取原始值对应的反射对象】
struct 或者 struct 的嵌套都是一样的判断处理方式
通过reflect.ValueOf来进行方法的调用 这算是一个高级用法了,前面我们只说到对类型、变量的几种反射的用法,包括如何获取其值、其类型、如果重新设置新值。但是在工程应用中,另外一个常用并且属于高级的用法,就是通过reflect来进行方法【函数】的调用。比如我们要做框架工程的时候,需要可以随意扩展方法,或者说用户可以自定义方法,那么我们通过什么手段来扩展让用户能够自定义呢?关键点在于用户的自定义方法是未可知的,因此我们可以通过reflect来搞定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package main import ( "fmt" "reflect" ) type User struct { Id int Name string Age int } func (u User) ReflectCallFuncHasArgs(name string, age int) { fmt.Println("ReflectCallFuncHasArgs name: ", name, ", age:", age, "and origal User.Name:", u.Name) } func (u User) ReflectCallFuncNoArgs() { fmt.Println("ReflectCallFuncNoArgs") } // 如何通过反射来进行方法的调用? // 本来可以用u.ReflectCallFuncXXX直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调动mv.Call func main() { user := User{1, "Allen.Wu", 25} // 1. 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理 getValue := reflect.ValueOf(user) // 一定要指定参数为正确的方法名 // 2. 先看看带有参数的调用方法 methodValue := getValue.MethodByName("ReflectCallFuncHasArgs") args := []reflect.Value{reflect.ValueOf("wudebao"), reflect.ValueOf(30)} methodValue.Call(args) // 一定要指定参数为正确的方法名 // 3. 再看看无参数的调用方法 methodValue = getValue.MethodByName("ReflectCallFuncNoArgs") args = make([]reflect.Value, 0) methodValue.Call(args) } 运行结果: ReflectCallFuncHasArgs name: wudebao , age: 30 and origal User.Name: Allen.Wu ReflectCallFuncNoArgs
说明
要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理
reflect.Value.MethodByName这.MethodByName,需要指定准确真实的方法名字,如果错误将直接panic,MethodByName返回一个函数值对应的reflect.Value方法的名字。
[]reflect.Value,这个是最终需要调用的方法的参数,可以没有或者一个或者多个,根据实际参数来定。
reflect.Value的 Call 这个方法,这个方法将最终调用真实的方法,参数务必保持一致,如果reflect.Value’Kind不是一个方法,那么将直接panic。
本来可以用u.ReflectCallFuncXXX直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调用methodValue.Call
Golang的反射reflect性能 Golang的反射很慢,这个和它的API设计有关。在 java 里面,我们一般使用反射都是这样来弄的。
1 2 3 Field field = clazz.getField("hello"); field.get(obj1); field.get(obj2);
这个取得的反射对象类型是 java.lang.reflect.Field。它是可以复用的。只要传入不同的obj,就可以取得这个obj上对应的 field。
但是Golang的反射不是这样设计的:
1 2 type_ := reflect.TypeOf(obj) field, _ := type_.FieldByName("hello")
这里取出来的 field 对象是 reflect.StructField 类型,但是它没有办法用来取得对应对象上的值。如果要取值,得用另外一套对object,而不是type的反射
1 2 type_ := reflect.ValueOf(obj) fieldValue := type_.FieldByName("hello")
这里取出来的 fieldValue 类型是 reflect.Value,它是一个具体的值,而不是一个可复用的反射对象了,每次反射都需要malloc这个reflect.Value结构体,并且还涉及到GC。
Golang reflect慢主要有两个原因
涉及到内存分配以及后续的GC;
reflect实现里面有大量的枚举,也就是for循环,比如类型之类的.
总结 上述详细说明了Golang的反射reflect的各种功能和用法,都附带有相应的示例,相信能够在工程应用中进行相应实践,总结一下就是:
反射可以大大提高程序的灵活性,使得interface{}有更大的发挥余地
反射必须结合interface才玩得转
变量的type要是concrete type的(也就是interface变量)才有反射一说
反射可以将“接口类型变量”转换为“反射类型对象”
反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息
反射可以将“反射类型对象”转换为“接口类型变量
reflect.value.Interface().(已知的类型)
遍历reflect.Type的Field获取其Field
反射可以修改反射类型对象,但是其值必须是“addressable”
想要利用反射修改对象状态,前提是 interface.data 是 settable,即 pointer-interface
通过反射可以“动态”调用方法
因为Golang本身不支持模板,因此在以往需要使用模板的场景下往往就需要使用反射(reflect)来实现
闭包
闭包函数 每次调用都会增加//重新调用会重置
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 package mainimport "fmt" func add () func (int ) int { var x int return func (y int ) int { x += y return x } } func main () { var f = add() fmt.Println(f(10 )) fmt.Println(f(10 )) fmt.Println(f(10 )) fmt.Println("---------------" ) fmt.Println(f(10 )) fmt.Println(f(10 )) f1 := add() fmt.Println(f1(10 )) }
高阶函数
在go中,如果函数名首字母是大写,则表示为public,若首字母为小写,则只能在当前包内访问
3.4.1 函数作为参数 1 2 3 4 5 6 7 func sayHello (name string ) { fmt.Printf("say,%s" , name) } func test (name string , f func (string ) ) { f(name) }
3.4.2 函数作为返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func add (x, y int ) int { return x + y } func sub (x, y int ) int { return x - y } func cal (operator string ) func (int , int ) int { switch operator { case "+" : return add case "-" : return sub default : return nil } }
3.4.3 匿名函数 1 2 3 4 5 6 7 8 9 10 func Lamda () int { dd := func (a, b int ) int { if a > b { return a - b } else { return b - a } } return dd(5 , 8 ) }
3.4.4 自己调用自己(递归) 1 2 3 4 5 6 7 8 func Run (x uint64 ) (y uint64 ) { if x > 0 { y = x * Run(x-1 ) return y } return 1 }
通道channel
通道是Go的一种数据类型 用chan定义 ch <- v 把 v 发送到通道 ch v := <-ch 从 ch 接收数据 并把值赋给 v
使用 channel <- 语法 发送 一个新的值到通道中。 使用 <-channel 语法从通道中 接收 一个值。
通道基础以及通道缓冲 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 func Sum (n []int , ch chan int ) { sum := 0 for _, v := range n { sum += v } ch <- sum } func main () { s := []int {7 , 2 , 8 , -9 , 4 , 0 } c := make (chan int ) go Sum(s[:len (s)/2 ], c) go Sum(s[len (s)/2 :], c) x, y := <-c, <-c fmt.Println(x, y, x+y) Ch_free() } func Ch_free () { ch := make (chan int , 3 ) ch <- 1 ch <- 2 ch <- 3 fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) close (ch) }
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。 不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。 注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内; 如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
通道关闭和遍历 通道关闭 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 () { jobs := make (chan int , 5 ) done := make (chan bool ) go func () { for { j, more := <-jobs if more { fmt.Println("received job" , j) } else { fmt.Println("received all jobs" ) done <- true return } } }() for j := 1 ; j <= 3 ; j++ { jobs <- j fmt.Println("sent job" , j) } close (jobs) fmt.Println("sent all jobs" ) <-done }
通道遍历 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 () { quemit := make (chan string , 2 ) quemit <- "one" quemit <- "two" close (quemit) for s := range quemit { fmt.Println(s) } New_Queue() } func New_Queue () { quemit := make (chan int , 5 ) for i := 0 ; i < 5 ; i++ { quemit <- i } close (quemit) for s := range quemit { fmt.Println(s) } }
通道关闭与遍历
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 fibonacci (n int , c chan int ) { x, y := 0 , 1 for i := 0 ; i < n; i++ { c <- x x, y = y, x+y } close (c) } func main () { c := make (chan int , 10 ) go fibonacci(cap (c), c) for i := range c { fmt.Println(i) } }
超时处理
Go语言的select语句是用来选择某个channel上的数据,当多个channel都有数据时, select会随机选择其中一个channel来处理数据。它通常与goroutine一起使用, 可以实现非常高效的并发处理。select语句可以用来监听多个channel, 并在其中任意一个channel有数据可读时立即处理该数据,从而避免了阻塞等待的情况。
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 main () { c1 := make (chan string , 1 ) go func () { time.Sleep(2 * time.Second) c1 <- "msg" }() select { case res := <-c1: fmt.Println(res) case <-time.After(1 * time.Second): fmt.Println("time out1" ) } c2 := make (chan string ) go func () { time.Sleep(2 * time.Second) c2 <- "msg" }() select { case res := <-c2: fmt.Println(res) case <-time.After(3 * time.Second): fmt.Println("time out2" ) } }
非阻塞通道操作 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" func main () { messages := make (chan string ) signals := make (chan bool ) select { case msg := <-messages: fmt.Println("received message" , msg) default : fmt.Println("no message received" ) } msg := "hi" select { case messages <- msg: fmt.Println("sent message" , msg) default : fmt.Println("no message sent" ) } select { case msg := <-messages: fmt.Println("received message" , msg) case sig := <-signals: fmt.Println("received signal" , sig) default : fmt.Println("no activity" ) } }
通道同步
其中go xxxx() 请看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 33 34 35 36 37 package mainimport ( "fmt" "time" ) func worker (done chan bool ) { fmt.Print("working..." ) time.Sleep(time.Second) go Adds() time.Sleep(time.Second) fmt.Println("done" ) done <- true } func Adds () { for i := 0 ; i < 10 ; i++ { fmt.Println(i) } } func main () { done := make (chan bool , 1 ) go worker(done) time.Sleep(time.Second) <-done }
通道选择器 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" "time" ) func main () { c1 := make (chan string ) c2 := make (chan string ) go func () { time.Sleep(1 * time.Second) c1 <- "one" }() go func () { time.Sleep(2 * time.Second) c2 <- "two" }() for i := 0 ; i < 2 ; i++ { select { case msg1 := <-c1: fmt.Println(msg1) case msg2 := <-c2: fmt.Println(msg2) } } }
各个通道将在一定时间后接收一个值, 通过这种方式来模拟并行的协程执行(例如,RPC 操作)时造成的阻塞(耗时) 注意,程序总共仅运行了两秒左右。因为 1 秒 和 2 秒的 Sleeps 是并发执行的,
定时器
我们经常需要在未来的某个时间点运行 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 28 29 30 31 package mainimport ( "fmt" "time" ) func main () { timer := time.NewTimer(2 * time.Second) <-timer.C fmt.Println("Timer 1 fired" ) timer2 := time.NewTimer(time.Second) go func () { <-timer2.C fmt.Println("Timer 2 fired" ) }() if timer2.Stop() { fmt.Println("timer 2 Stop" ) } time.Sleep(2 * time.Second) }
打点器
定时器 是当你想要在未来某一刻执行一次时使用的 -打点器 则是为你想要以固定的时间间隔重复执行而准备的。 这里是一个打点器的例子,它将定时的执行,直到我们将它停止。
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 func main () { ticker := time.NewTicker(500 * time.Millisecond) done := make (chan bool ) go func () { for { select { case <-done: return case t := <-ticker.C: fmt.Println("Ticker is" , t) } } }() time.Sleep(1600 * time.Millisecond) ticker.Stop() done <- true fmt.Println("ticker is stop" ) }
工作池 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 ( "fmt" "time" ) func Worker (id int , jobs <-chan int , results chan <- int ) { for j := range jobs { fmt.Println("worker" , id, "started job" , j) time.Sleep(time.Second) fmt.Println("worker" , id, "finished job" , j) results <- j * 2 } } func main () { const Number = 5 jobs := make (chan int , Number) results := make (chan int , Number) for w := 1 ; w <= 3 ; w++ { go Worker(w, jobs, results) } for j := 1 ; j <= Number; j++ { jobs <- j } close (jobs) for a := 1 ; a <= Number; a++ { <-results } }
协程 内核线程就是一般的线程叫thread,用户线程叫协程co-routine,go里面的协程叫做goroutine 。
N个内核线程thread可以通过协程调度器使用M个协程goroutine。
与传统的系统级线程和进程相比,协程最大的优势在于“轻量级”。可以轻松创建上万个而不会导致系统资源衰竭。而线程和进程通常很难超过1万个。这也是协程别称“轻量级线程”的原因。
一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源 。
在协程中,调用一个任务就像调用一个函数一样,消耗的系统资源最少!但能达到进程、线程并发相同的效果。
一个进程可能几GB,一个线程可能几MB,go的协程是几KB。在一次并发任务中,进程、线程、协程均可以实现。从系统资源消耗的角度出发来看,进程相当多,线程次之,协程最少。
GO并发
Go语言为简单地,高效地实现并发编程提供了原生态支持。在Go语言中实现并发编程最常用的方式就是使用goroutine(协程)。Goroutine 是一种轻量级线程,可以由Go的运行时环境调度执行。Go语言还提供了 channel 用来实现 goroutine 之间的通信,这也是Go语言的另一个特色,通过channel可以方便的完成goroutine之间的协作和数据同步
goroutine 使用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" "time" ) func SayHello (s string ) { for i := 0 ; i < 7 ; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main () { go SayHello("hello" ) SayHello("WORD" ) }
Go 在语言级别支持协程,叫goroutine。Go 语言标准库提供的所有系统调用操作(包括所有同步IO操作),都会出让CPU给其他goroutine。这让轻量级线程的切换管理不依赖于系统的线程和进程,也不需要依赖于CPU的核心数量。
Go语言为并发编程而内置的上层API基于顺序通信进程模型CSP(communicating sequential processes)。这就意味着显式锁都是可以避免的,因为Go通过相对安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。
Go语言中的并发程序主要使用两种手段来实现。goroutine和channel。
创建Goroutine
只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。
在并发编程中,我们通常想将一个过程切分成几块,然后让每个goroutine各自负责一块工作,当一个程序启动时,主函数在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。而go语言的并发设计,让我们很轻松就可以达成这一目的。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package main import ( "fmt" "time" ) func newTask () { i := 0 for { i++ fmt.Printf("new goroutine: i = %d\n" , i) time.Sleep(1 *time.Second) } } func main () { go newTask() i := 0 for { i++ fmt.Printf("main goroutine: i = %d\n" , i) time.Sleep(1 * time.Second) } }
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 package main import ( "fmt" "time" ) func newTask() { i := 0 for { i++ fmt.Printf("new goroutine: i = %d\n", i) time.Sleep(1 * time.Second) //延时1s } } func main() { //创建一个 goroutine,启动另外一个任务 go newTask() fmt.Println("main goroutine exit") }
Goexit函数
调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,调度器确保所有已注册 defer 延迟调用被执行。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package main import ( "fmt" "runtime" ) func main() { go func() { defer fmt.Println("A.defer") func() { defer fmt.Println("B.defer") runtime.Goexit() // 终止当前 goroutine, import "runtime" fmt.Println("B") // 不会执行 }() fmt.Println("A") // 不会执行 }() //不要忘记() //死循环,目的不让主goroutine结束 for { } }
channel channel是Go语言中的一个核心类型 ,可以把它看成管道。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。
channel是一个数据类型,主要用来解决go程的同步问题以及go程之间数据共享(数据传递)的问题。两个goroutine之前交流需要通过channel来进行。
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
引⽤类型 channel可用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。
分为带缓冲channel和不带缓冲channel
定义channel变量
和map类似,channel也一个对应make创建的底层数据结构的引用 。
当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
定义一个channel时,也需要定义发送到channel的值的类型。channel可以使用内置的make()函数来创建:
chan 是创建channel所需使用的关键字。Type 代表指定channel收发数据的类型。
1 2 make(chan Type) //等价于make(chan Type, 0) make(chan Type, capacity)
当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
当 参数capacity= 0 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。
channel非常像生活中的管道,一边可以存放东西,另一边可以取出东西。channel通过操作符 <- 来接收和发送数据,发送和接收数据语法:
1 2 3 4 channel <- value //发送value到channel <-channel //接收并将其丢弃 x := <-channel //从channel中接收数据,并赋值给x x, ok := <-channel //功能同上,同时检查通道是否已关闭或者是否为空
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import ( "fmt" ) func main() { c := make(chan int) go func() { defer fmt.Println("子go程结束") fmt.Println("子go程正在运行……") c <- 666 //666发送到c }() num := <-c //从c中接收数据,并赋值给num fmt.Println("num = ", num) fmt.Println("main go程结束") }
无缓冲的channel
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何数据值的通道。
这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。否则,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。
这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
阻塞: 由于某种原因数据没有到达,当前go程(线程)持续处于等待状态,直到条件满足,才解除阻塞。
同步: 在两个或多个go程(线程)间,保持数据内容一致性的机制。
下面步骤展示两个 goroutine 如何利用无缓冲的通道来共享一个值:
在第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执行发送或者接收。
在第 2 步,左侧的 goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在通道中被锁住,直到交换完成。
在第 3 步,右侧的 goroutine 将它的手放入通道,这模拟了从通道里接收数据。这个 goroutine 一样也会在通道中被锁住,直到交换完成。
在第 4 步和第 5 步,进行交换,并最终,在第 6 步,两个 goroutine 都将它们的手从通道里拿出来,这模拟了被锁住的 goroutine 得到释放。两个 goroutine 现在都可以去做其他事情了。
无缓冲的channel创建格式:
1 2 make(chan Type) //等价于make(chan Type, 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 25 26 27 28 29 30 31 package main import ( "fmt" "time" ) func main() { c := make(chan int, 0) //创建无缓冲的通道 c //内置函数 len 返回未被读取的缓冲元素数量,cap 返回缓冲区大小 fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c)) go func() { defer fmt.Println("子go程结束") for i := 0; i < 3; i++ { c <- i fmt.Printf("子go程正在运行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c)) } }() time.Sleep(2 * time.Second) //延时2s for i := 0; i < 3; i++ { num := <-c //从c中接收数据,并赋值给num fmt.Println("num = ", num) } fmt.Println("main进程结束") }
有缓冲的channel
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个数据值的通道。
这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也不同。
只有通道中没有要接收的值时,接收动作才会阻塞。
只有通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
下面步骤展示两个 goroutine 如何利用缓冲的通道来共享一个值:
在第 1 步,右侧的 goroutine 正在从通道接收一个值。
在第 2 步,右侧的这个 goroutine独立完成了接收值的动作,而左侧的 goroutine 正在发送一个新值到通道里。
在第 3 步,左侧的goroutine 还在向通道发送新值,而右侧的 goroutine 正在从通道接收另外一个值。这个步骤里的两个操作既不是同步的,也不会互相阻塞。
最后,在第 4 步,所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存更多的值。
有缓冲的channel创建格式 :
1 make(chan Type, capacity)
如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。
借助函数 len(ch) 求取缓冲区中剩余元素个数, cap(ch) 求取缓冲区元素容量大小。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main() { c := make(chan int, 3) //带缓冲的通道 //内置函数 len 返回未被读取的缓冲元素数量, cap 返回缓冲区大小 fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c)) go func() { defer fmt.Println("子go程结束") for i := 0; i < 3; i++ { c <- i fmt.Printf("子go程正在运行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c)) } }() time.Sleep(2 * time.Second) //延时2s for i := 0; i < 3; i++ { num := <-c //从c中接收数据,并赋值给num fmt.Println("num = ", num) } fmt.Println("main进程结束") }
关闭channel
如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package main import ( "fmt" ) func main() { c := make(chan int) go func() { for i := 0; i < 5; i++ { c <- i } close(c) }() for { //ok为true说明channel没有关闭,为false说明管道已经关闭 if data, ok := <-c; ok { fmt.Println(data) } else { break } } fmt.Println("Finished") }
注意:
channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
关闭channel后,可以继续从channel接收数据;
对于nil channel,无论收发都会被阻塞。
可以使用 range 来迭代不断操作channel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package main import ( "fmt" ) func main() { c := make(chan int) go func() { for i := 0; i < 5; i++ { c <- i } close(c) }() for data := range c { fmt.Println(data) } fmt.Println("Finished") }
单向channel及应用
默认情况下,通道channel是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。
但是,我们经常见一个通道作为参数进行传递而只希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。
单向channel变量的声明非常简单,如下:
1 2 3 var ch1 chan int // ch1是一个正常的channel,是双向的 var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据 var ch3 <-chan int // ch3是单向channel,只用于读int数据
chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
<-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。
可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel:
1 2 3 4 5 6 7 8 9 10 11 c := make(chan int, 3) var send chan<- int = c // send-only var recv <-chan int = c // receive-only send <- 1 //<-send //invalid operation: <-send (receive from send-only type chan<- int) <-recv //recv <- 2 //invalid operation: recv <- 2 (send to receive-only type <-chan int) //不能将单向 channel 转换为普通 channel d1 := (chan int)(send) //cannot convert send (type chan<- int) to type chan int d2 := (chan int)(recv) //cannot convert recv (type <-chan int) to type chan int
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // chan<- //只写 func counter(out chan<- int) { defer close(out) for i := 0; i < 5; i++ { out <- i //如果对方不读 会阻塞 } } // <-chan //只读 func printer(in <-chan int) { for num := range in { fmt.Println(num) } } func main() { c := make(chan int) // chan //读写 go counter(c) //生产者 printer(c) //消费者 fmt.Println("done") }
select Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。
有时候我们希望能够借助channel发送或接收数据,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。
select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。
与switch语句相比,select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作 ,大致的结构如下:
1 2 3 4 5 6 7 8 select { case <- chan1: // 如果chan1成功读到数据,则进行该case处理语句 case chan2 <- 1: // 如果成功向chan2写入数据,则进行该case处理语句 default: // 如果上面都没有成功,则进入default处理流程 }
在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。
如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
l 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。
l 如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package main import ( "fmt" ) func fibonacci(c, quit chan int) { x, y := 1, 1 for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } } } func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i < 6; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit) } 运行结果: 1 1 2 3 5 8 quit
协程池 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 package mainimport ( "fmt" "strconv" "time" ) type Procshop struct { Name string } func Shop (procshop chan <- Procshop) { for { chanshop := Procshop{ Name: "产品" + strconv.Itoa(time.Now().Second()), } procshop <- chanshop fmt.Println("生产者生产了" , chanshop) time.Sleep(1 * time.Second) } } func Vist (procshop <-chan Procshop, vis chan <- int ) { for { pordacket := <-procshop fmt.Println("消费者吃掉了: " , pordacket) vis <- 1 } } func main () { procshop := make (chan Procshop, 5 ) vister := make (chan int , 5 ) go Shop(procshop) go Vist(procshop, vister) for i := 0 ; i < 5 ; i++ { <-vister } fmt.Println("main over" ) }
练习(小项目) 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 package mainimport ( "fmt" "strconv" "time" ) type BookStore struct { BookName string } func BookShop (BookLlist chan <- BookStore) { for { br := BookStore{BookName: "书籍编号" + strconv.Itoa(time.Now().Second())} BookLlist <- br fmt.Println("书店有书" , br) time.Sleep(1 * time.Second) } } func VistShop (BookList <-chan BookStore, VistList chan <- int ) { for { vitsd := <-BookList fmt.Println("借了" , vitsd) VistList <- 1 } } func main () { BookList := make (chan BookStore) VistList := make (chan int ) go BookShop(BookList) go VistShop(BookList, VistList) for i := 0 ; i < 20 ; i++ { <-VistList } 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 package mainimport ( "fmt" "sync" "time" ) func Worker (id int ) { fmt.Printf("Worker %d starting\n" , id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n" , id) } func main () { var wg sync.WaitGroup for i := 1 ; i <= 5 ; i++ { wg.Add(1 ) i := i go func () { defer wg.Done() Worker(i) }() } wg.Wait() }
这段代码主要实现了并发执行多个Worker的过程。 首先,Worker函数模拟了一些需要执行的工作,例如在实际应用中可能是从数据库中获取数据、执行一些计算,或是与外部系统交互等等。 main函数通过调用sync.WaitGroup来等待所有启动的Worker完成后才能结束整个程序。 WaitGroup用于协调多个goroutine的执行,里面包含了一个计数器,计数器的值可以随时增加或减少。 在for循环中,main函数启动了5个Worker,每个Worker都在goroutine中执行。 这个for循环在启动每个Worker之前会先对WaitGroup进行+1的操作,表示有一个Worker进入了工作状态,而每个Worker执行完成之后会调用WaitGroup的Done方法将计数器减1, 表示有一个Worker已经完成工作,该方法通常用于必须等待的操作(如等待所有子goroutine完成执行)的末尾。 值得注意的是,在启动每个Worker的goroutine中,使用了闭包技术将循环索引i的值赋给了局部变量,避免了在Worker函数调用时可能出现的变量共享问题。 这个技巧是在并发编程中经常使用的,可以避免一些潜在的问题出现。
速率限制 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 package mainimport ( "fmt" "time" ) func change_timer (n time.Time) { loc, err := time.LoadLocation("Asia/Shanghai" ) if err != nil { return } now := n.In(loc) fmt.Println(now) } func main () { requests := make (chan int , 5 ) for i := 1 ; i <= 5 ; i++ { requests <- i } close (requests) limiter := time.Tick(200 * time.Millisecond) for req := range requests { <-limiter fmt.Println("requests: " , req, time.Now()) } bus := make (chan time.Time, 3 ) for i := 0 ; i < 3 ; i++ { bus <- time.Now() } go func () { for t := range time.Tick(200 * time.Millisecond) { bus <- t } }() busr := make (chan int , 5 ) for i := 1 ; i <= 5 ; i++ { busr <- i } close (busr) for req := range busr { <-bus fmt.Println("request" , req, time.Now()) } }
原子计数器 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" "sync/atomic" ) var n uint64 func Main_wait () { var wg sync.WaitGroup for i := 0 ; i < 50 ; i++ { wg.Add(1 ) go func () { for i := 0 ; i < 1000 ; i++ { atomic.AddUint64(&n, 1 ) } wg.Done() }() } wg.Wait() fmt.Println("ops: " , n) } func main () { for i := 0 ; i < 10 ; i++ { go Wait_Main(i) wp.Add(1 ) } wp.Wait() fmt.Println("end........" ) } var wp sync.WaitGroupfunc Wait_Main (i int ) { defer wp.Done() fmt.Printf("i:%v\n" , i) }
并发编程之runtime包 实例1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport ( "fmt" "runtime" ) func show (s string ) { for i := 0 ; i < 5 ; i++ { fmt.Println(s) } } func main () { go show("java" ) for i := 0 ; i < 5 ; i++ { runtime.Gosched() fmt.Println("Golang" ) } }
实例2 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" "runtime" "time" ) func shows () { for i := 0 ; i < 15 ; i++ { if i >= 5 { runtime.Goexit() } fmt.Println(i) } } func main () { go shows() time.Sleep(time.Second) }
实例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 28 29 30 31 32 33 34 35 36 37 package mainimport ( "fmt" "runtime" "time" ) func a () { for i := 0 ; i < 10 ; i++ { fmt.Println("a: " , i) } } func b () { for i := 0 ; i < 10 ; i++ { fmt.Println("b: " , i) } } func main () { fmt.Printf("Cpu Number :%v\n" , runtime.NumCPU()) runtime.GOMAXPROCS(8 ) go a() go b() time.Sleep(time.Second) }
互斥锁实现同步 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 package mainimport ( "fmt" "sync" "time" ) var i = 10000 var wg sync.WaitGroupvar lk sync.Mutexfunc add () { defer wg.Done() lk.Lock() i += 1 time.Sleep(time.Millisecond * 10 ) lk.Unlock() } func sub () { defer wg.Done() lk.Lock() i -= 1 time.Sleep(time.Millisecond * 2 ) lk.Unlock() } func main () { for x := 0 ; x < 100 ; x++ { wg.Add(1 ) go add() wg.Add(1 ) go sub() } wg.Wait() fmt.Println("end.... i: " , i) }
Go Modules和代理
Go Modules是Go语言依赖管理解决方案。
Go.mod是Golang1.11版本新引入的官方包管理工具用于解决之前没有地方记录依赖包具体版本的问题,方便依赖包的管理。
Go.mod其实就是一个Modules,Modules是相关Go包的集合,是源代码交换和版本控制的单元。
一个源代码目录甚至是空目录都可以作为Modules,只要其中包含有go.mod文件
Go Modoules的目的之一就是淘汰GOPATH,在使用 GOPATH 模式下,我们需要将应用代码存放在固定的$GOPATH/src目录下,并且如果执行go get来拉取外部依赖会自动下载并安装到$GOPATH目录下。
用Go Modules的方式创建一个项目, 建议为了与GOPATH分开,不要将项目创建在GOPATH/src下.
mod文件字段内容 go.mod是启用了Go moduels的项目所必需的最重要的文件,它描述了当前项目(当前模块)的元信息,每一行都以一个动词开头。目前有以下5个动词
module :用于定义当前项目的模块路径
go :用于设置预期的Go版本。
requir e:用于设置一个特定的模块版本。
exclud e:用于从使用中排除一个特定的模块版本。
replac e:用于将一个模块版本替换为另外一个模块版本。
生成go.mod 1 2 3 4 5 6 7 go env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn,direct
在go文件夹创建一个hello.go的文件。文件内容如下:
1 2 3 4 5 6 7 package mainimport "fmt" func main () { fmt.Println("Hello World!" ) }
在项目根目录生成go.mod文件
1 2 $ cd 文件路径 $ go mod init hello(模板名称)
Go modules常用命令
命令
作用
go mod init
生成 go.mod 文件
go mod download
下载 go.mod 文件中指明的所有依赖
go mod tidy
整理现有的依赖
go mod graph
查看现有的依赖结构
go mod edit
编辑 go.mod 文件
go mod vendor
导出项目所有的依赖到vendor目录
go mod verify
校验一个模块是否被篡改过
go mod why
查看为什么需要依赖某模块
go get
拉取最新版本
go get 地址+版本号
拉取指定版本
go get -u
更新所有依赖
go mod环境变量 可以通过 go env 命令来进行查看
go env -w GO111MODULE=on //开启gomod
GO111MODULE
Go语言提供了 GO111MODULE这个环境变量来作为 Go modules 的开关,其允许设置以下参数:
auto:只要项目包含了 go.mod 文件的话启用 Go modules,目前在 Go1.11 至 Go1.14 中仍然是默认值。
on:启用 Go modules,推荐设置,将会是未来版本中的默认值。
off:禁用 Go modules,不推荐设置。
GOPROXY 这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。
GOPROXY 的默认值是:https://proxy.golang.org,direct
proxy.golang.org国内访问不了,需要设置国内的代理.
go env -w GOPROXY=https://goproxy.cn,direct //设置代理
GOPROXY 的值是一个以英文逗号 “,” 分割的 Go 模块代理列表,允许设置多个模块代理,假设你不想使用,也可以将其设置为 “off” ,这将会禁止 Go 在后续操作中使用任何 Go 模块代理。
如:go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct
direct
实际上 “direct” 是一个特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等),场景如下:当值列表中上一个 Go 模块代理返回 404 或 410 错误时,Go 自动尝试列表中的下一个,遇见 “direct” 时回源,也就是回到源地址去抓取,而遇见 EOF 时终止并抛出类似 “invalid version: unknown revision…” 的错误。
GOSUMDB 它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止。
GOSUMDB 的默认值为:sum.golang.org,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理(详见:Proxying a Checksum Database)。
因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn 就能支持代理 sum.golang.org,所以这一个问题在设置 GOPROXY 后,你可以不需要过度关心。
另外若对 GOSUMDB 的值有自定义需求,其支持如下格式:
格式 1:<SUMDB_NAME>+<PUBLIC_KEY>。
格式 2:<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>。
也可以将其设置为“off”,也就是禁止 Go 在后续操作中校验模块版本。
GONOPROXY/GONOSUMDB/GOPRIVATE
这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。
更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。
而一般建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳姿势是直接使用 GOPRIVATE 。
并且它们的值都是一个以英文逗号 “,” 分割的模块路径前缀,也就是可以设置多个,例如:
1 $ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"
设置后,前缀为 git.xxx.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块。
如果不想每次都重新设置,我们也可以利用通配符,例如:
1 $ go env -w GOPRIVATE="*.example.com"
这样子设置的话,所有模块路径为 example.com 的子域名(例如:git.example.com)都将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括 example.com 本身 。
使用Go Modules初始化项目 (1) 开启Go Modules 1 2 3 4 $ go env -w GO111MODULE=on 又或是可以通过直接设置系统环境变量(写入对应的~/.bash_profile 文件亦可)来实现这个目的: $ export GO111MODULE=on
(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 创建项目目录 $ mkdir -p $HOME/aceld/modules_test $ cd $HOME/aceld/modules_test 执行Go modules 初始化 $ go mod init github.com/aceld/modules_test go: creating new go.mod: module github.com/aceld/modules_test 在执行 go mod init 命令时,我们指定了模块导入路径为 github.com/aceld/modules_test。接下来我们在该项目根目录下创建 main.go 文件,如下: package main import ( "fmt" "github.com/aceld/zinx/znet" "github.com/aceld/zinx/ziface" ) //ping test 自定义路由 type PingRouter struct { znet.BaseRouter } //Ping Handle func (this *PingRouter) Handle(request ziface.IRequest) { //先读取客户端的数据 fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData())) //再回写ping...ping...ping err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping")) if err != nil { fmt.Println(err) } } func main() { //1 创建一个server句柄 s := znet.NewServer() //2 配置路由 s.AddRouter(0, &PingRouter{}) //3 开启服务 s.Serve() } OK, 我们先不要关注代码本身,我们看当前的main.go也就是我们的aceld/modules_test项目,是依赖一个叫github.com/aceld/zinx库的. znet和ziface只是zinx的两个模块. 接下来我们在$HOME/aceld/modules_test,本项目的根目录执行 $ go get github.com/aceld/zinx/znet go: downloading github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 go: found github.com/aceld/zinx/znet in github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 我们会看到 我们的go.mod被修改,同时多了一个go.sum文件.
(3) 查看go.mod文件 1 2 3 4 5 6 7 8 9 10 内容如下: module github.com/aceld/modules_test go 1.14 require github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 // indirect 解释: module: 用于定义当前项目的模块路径 go:标识当前Go版本.即初始化版本 require: 当前项目依赖的一个特定的必须版本 indirect: 示该模块为间接依赖,也就是在当前应用程序中的 import 语句中,并没有发现这个模块的明确引用,有可能是你先手动 go get 拉取下来的,也有可能是你所依赖的模块所依赖的.我们的代码很明显是依赖的"github.com/aceld/zinx/znet"和"github.com/aceld/zinx/ziface",所以就间接的依赖了github.com/aceld/zinx
(4) 查看go.sum文件 在第一次拉取模块依赖后,会发现多出了一个 go.sum 文件,其详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。
四、数据结构与算法 排序算法 4.1.1 冒泡排序
时间复杂度 O(n2 )
1 2 3 4 5 6 7 8 9 10 11 func Bubble_Sorting (c []int ) []int { for i := 0 ; i < len (c); i++ { for j := i; j < len (c); j++ { if c[i] > c[j] { c[i], c[j] = c[j], c[i] } } } return c }
4.1.2 快速排序
时间复杂度 O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func Quick_sort (arr []int ) []int { if len (arr) <= 1 { return arr } pivot := arr[0 ] left, right := 1 , len (arr)-1 for left <= right { if arr[left] <= pivot { left++ } else { arr[left], arr[right] = arr[right], arr[left] right-- } } arr[0 ], arr[right] = arr[right], arr[0 ] leftArr := Quick_sort(arr[:right]) rightArr := Quick_sort(arr[right+1 :]) result := append (leftArr, arr[right]) result = append (result, rightArr...) return result }
4.1.3 归并排序
时间复杂度 O(nlogn)
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 func Merge_Sort (a []int ) []int { merge := func (arr1, arr2 []int ) []int { arr := make ([]int , len (arr1)+len (arr2)) i, j, k := 0 , 0 , 0 for i < len (arr1) && j < len (arr2) { if arr1[i] < arr2[j] { arr[k] = arr1[i] i++ } else { arr[k] = arr2[j] j++ } k++ } for i < len (arr1) { arr[k] = arr1[i] i++ k++ } for j < len (arr2) { arr[k] = arr2[j] j++ k++ } return arr } n := len (a) if n <= 1 { return a } mid := n / 2 left := Merge_Sort(a[:mid]) right := Merge_Sort(a[mid:]) return merge(left, right) }
4.1.4 插入排序
时间复杂度O(n2 )
1 2 3 4 5 6 7 8 9 10 func Inser_sort (c []int ) []int { for i := 1 ; i < len (c); i++ { for j := i; j > 0 ; j-- { if c[j-1 ] > c[j] { c[j], c[j-1 ] = c[j-1 ], c[j] } } } return c }
4.1.3 选择排序
时间复杂度O(n2 )
1 2 3 4 5 6 7 8 9 10 11 func Select_Sort (c []int ) []int { for i := 0 ; i < len (c); i++ { for j := i + 1 ; j < len (c); j++ { if c[i] > c[j] { c[i], c[j] = c[j], c[i] } } } return c }
查找算法 二分查找
时间复杂度 O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func Binary_search (a []int , b int ) int { left, right := 0 , len (a)-1 for left <= right { mid := (left + right) / 2 if a[mid] == b { return mid } else if a[mid] > b { right = mid - 1 } else { right = mid + 1 } } return -1 }
4.2.2线性查找
时间复杂度 O(n)
1 2 3 4 5 6 7 8 func Line_search (a []int , b int ) int { for i := 0 ; i < len (a); i++ { if a[i] == b { return i } } return -1 }
五、Gin框架 Gin是一个用Go语言编写的web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter,速度提高了近40倍。
5.1 Gin框架的安装 1 $ go get -u github.com/gin-gonic/gin
5.2 第一个Gin实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "github.com/gin-gonic/gin" ) func main () { r := gin.Default() r.GET("/hello" , func (c *gin.Context) { c.JSON(200 , gin.H{ "message" : "Hello world!" , }) }) r.Run() }
5.3 Restful API 简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。
GET用来获取资源
POST用来新建资源
PUT用来更新资源
DELETE用来删除资源。
按照Restful API设计
请求方法
URL
含义
GET
/users
获取所有用户数据
GET
/users/{id}
获取特定用户的数据
POST
/users
创建一个新用户
PUT
/users/{id}
更新特定用户的数据
DELETE
/users/{id}
删除特定用户的数据
Gin框架支持restful API设计方式
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 package mainimport "github.com/gin-gonic/gin" type User struct { ID int `json:"id"` Name string `json:"name"` Age int `json:"age"` } var users []*Userfunc main () { r := gin.Default() r.GET("/users" , func (c *gin.Context) { c.JSON(200 , users) }) r.GET("/users/:id" , func (c *gin.Context) { id := c.Param("id" ) for _, u := range users { if strconv.Itoa(u.ID) == id { c.JSON(200 , u) return } } c.JSON(404 , gin.H{"message" : "User not found" }) }) r.POST("/users" , func (c *gin.Context) { var u User if err := c.BindJSON(&u); err != nil { c.JSON(400 , gin.H{"message" : "Invalid request" }) return } u.ID = len (users) + 1 users = append (users, &u) c.JSON(201 , gin.H{"message" : "User created" , "id" : u.ID}) }) r.PUT("/users/:id" , func (c *gin.Context) { id := c.Param("id" ) for _, u := range users { if strconv.Itoa(u.ID) == id { var update struct { Name string `json:"name"` Age int `json:"age"` } if err := c.BindJSON(&update); err != nil { c.JSON(400 , gin.H{"message" : "Invalid request" }) return } u.Name = update.Name u.Age = update.Age c.JSON(200 , gin.H{"message" : "User updated" , "id" : u.ID}) return } } c.JSON(404 , gin.H{"message" : "User not found" }) }) r.DELETE("/users/:id" , func (c *gin.Context) { id := c.Param("id" ) for i, u := range users { if strconv.Itoa(u.ID) == id { users = append (users[:i], users[i+1 :]...) c.JSON(200 , gin.H{"message" : "User deleted" , "id" : id}) return } } c.JSON(404 , gin.H{"message" : "User not found" }) }) r.Run() }
5.4 Gin渲染
现在前后分离,没咋学
5.4.1 HTML渲染 我们首先定义一个存放模板文件的templates文件夹,然后在其内部按照业务分别定义一个posts文件夹和一个users文件夹。
posts/index.html文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 {{define "posts/index.html"}} <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > posts/index</title > </head > <body > {{.title}} </body > </html > {{end}}
users/index.html文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 {{define "users/index.html"}} <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > users/index</title > </head > <body > {{.title}} </body > </html > {{end}}
Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法进行HTML渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func main () { r := gin.Default() r.LoadHTMLGlob("templates/**/*" ) r.GET("/posts/index" , func (c *gin.Context) { c.HTML(http.StatusOK, "posts/index.html" , gin.H{ "title" : "posts/index" , }) }) r.GET("users/index" , func (c *gin.Context) { c.HTML(http.StatusOK, "users/index.html" , gin.H{ "title" : "users/index" , }) }) r.Run(":8080" ) }
5.4.2 自定义模板函数
定义一个不转义相应内容safe模板函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { router := gin.Default() router.SetFuncMap(template.FuncMap{ "safe" : func (str string ) template.HTML{ return template.HTML(str) }, }) router.LoadHTMLFiles("./index.tmpl" ) router.GET("/index" , func (c *gin.Context) { c.HTML(http.StatusOK, "index.tmpl" , "<a href='https://www.baidu.com'>百度</a>" ) }) router.Run(":8080" ) }
在index.tmpl中使用定义好的safe函数
1 2 3 4 5 6 7 8 9 <!DOCTYPE html > <html lang ="zh-CN" > <head > <title > 修改模板引擎的标识符</title > </head > <body > <div > {{ . | safe }}</div > </body > </html >
5.4.3 静态文件处理
当我们渲染的HTML文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用gin.Static方法即可
1 2 3 4 5 6 7 8 func main () { r := gin.Default() r.Static("/static" , "./static" ) r.LoadHTMLGlob("templates/**/*" ) r.Run(":8080" ) }
在html文件中这样导入静态文件
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="zh-CN" > <head > <title > 静态文件导入</title > <link src ="http://locahost:8080/static/xxx.css" > </head > <body > </body > </html >
5.4.4 使用模板继承 Gin框架默认都是使用单模板,如果需要使用block template功能,可以通过"github.com/gin-contrib/multitemplate"库实现,具体示例如下:
首先,假设我们项目目录下的emplates文件夹下有以下模板文件,其中home.tmpl和index.tmpl继承了base.tmpl:
1 2 3 4 5 6 7 templates ├── includes │ ├── home.tmpl │ └── index.tmpl ├── layouts │ └── base.tmpl └── scripts.tmpl
然后我们定义一个loadTemplates函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func loadTemplates (templatesDir string ) multitemplate.Renderer { r := multitemplate.NewRenderer() layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl" ) if err != nil { panic (err.Error()) } includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl" ) if err != nil { panic (err.Error()) } for _, include := range includes { layoutCopy := make ([]string , len (layouts)) copy (layoutCopy, layouts) files := append (layoutCopy, include) r.AddFromFiles(filepath.Base(include), files...) } return r }
我们在main函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func indexFunc (c *gin.Context) { c.HTML(http.StatusOK, "index.tmpl" , nil ) } func homeFunc (c *gin.Context) { c.HTML(http.StatusOK, "home.tmpl" , nil ) } func main () { r := gin.Default() r.HTMLRender = loadTemplates("./templates" ) r.GET("/index" , indexFunc) r.GET("/home" , homeFunc) r.Run() }
5.4.5 补充文件路径处理 关于模板文件和静态文件的路径,我们需要根据公司/项目的要求进行设置。可以使用下面的函数获取当前执行程序的路径。
1 2 3 4 5 6 func getCurrentPath () string { if ex, err := os.Executable(); err == nil { return filepath.Dir(ex) } return "./" }
5.4.6 JSON渲染 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { r := gin.Default() r.GET("/someJSON" , func (c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message" : "Hello world!" }) }) r.GET("/moreJSON" , func (c *gin.Context) { var msg struct { Name string `json:"user"` Message string Age int } msg.Name = "小王子" msg.Message = "Hello world!" msg.Age = 18 c.JSON(http.StatusOK, msg) }) r.Run(":8080" ) }
5.4.7 XML渲染 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main () { r := gin.Default() r.GET("/someXML" , func (c *gin.Context) { c.XML(http.StatusOK, gin.H{"message" : "Hello world!" }) }) r.GET("/moreXML" , func (c *gin.Context) { type MessageRecord struct { Name string Message string Age int } var msg MessageRecord msg.Name = "小王子" msg.Message = "Hello world!" msg.Age = 18 c.XML(http.StatusOK, msg) }) r.Run(":8080" ) }
5.4.8 YMAL渲染 1 2 3 r.GET("/someYAML" , func (c *gin.Context) { c.YAML(http.StatusOK, gin.H{"message" : "ok" , "status" : http.StatusOK}) })
5.4.9 protobuf渲染 1 2 3 4 5 6 7 8 9 10 11 12 13 r.GET("/someProtoBuf" , func (c *gin.Context) { reps := []int64 {int64 (1 ), int64 (2 )} label := "test" data := &protoexample.Test{ Label: &label, Reps: reps, } c.ProtoBuf(http.StatusOK, data) })
5.5 获取参数 5.5.1 获取querystring参数
querystring指的是URL中?后面携带的参数,例如:/user/search?username=小王子&address=沙河。 获取请求的querystring参数的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { r := gin.Default() r.GET("/user/search" , func (c *gin.Context) { username := c.DefaultQuery("username" , "小王子" ) address := c.Query("address" ) c.JSON(http.StatusOK, gin.H{ "message" : "ok" , "username" : username, "address" : address, }) }) r.Run() }
当前端请求的数据通过form表单提交时,例如向/user/search发送一个POST请求,获取请求数据的方式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { r := gin.Default() r.POST("/user/search" , func (c *gin.Context) { username := c.PostForm("username" ) address := c.PostForm("address" ) c.JSON(http.StatusOK, gin.H{ "message" : "ok" , "username" : username, "address" : address, }) }) r.Run(":8080" ) }
5.5.3 获取JSON数据
当前端请求的数据通过JSON提交时,例如向/json发送一个POST请求,则获取请求参数的方式如下:
1 2 3 4 5 6 7 8 9 10 r.POST("/json" , func (c *gin.Context) { b, _ := c.GetRawData() var m map [string ]interface {} _ = json.Unmarshal(b, &m) c.JSON(http.StatusOK, m) })
具体详细在下面**参数绑定 **
5.5.4 获取path 参数
请求的参数通过URL路径传递,例如:/user/search/小王子/沙河。 获取请求URL路径中的参数的方式如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { r := gin.Default() r.GET("/user/search/:username/:address" , func (c *gin.Context) { username := c.Param("username" ) address := c.Param("address" ) c.JSON(http.StatusOK, gin.H{ "message" : "ok" , "username" : username, "address" : address, }) }) r.Run(":8080" ) }
5.5.5 参数绑定
为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。
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 type Login struct { User string `form:"user" json:"user" binding:"required"` Password string `form:"password" json:"password" binding:"required"` } func main () { router := gin.Default() router.POST("/loginJSON" , func (c *gin.Context) { var login Login if err := c.ShouldBind(&login); err == nil { fmt.Printf("login info:%#v\n" , login) c.JSON(http.StatusOK, gin.H{ "user" : login.User, "password" : login.Password, }) } else { c.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) } }) router.POST("/loginForm" , func (c *gin.Context) { var login Login if err := c.ShouldBind(&login); err == nil { c.JSON(http.StatusOK, gin.H{ "user" : login.User, "password" : login.Password, }) } else { c.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) } }) router.GET("/loginForm" , func (c *gin.Context) { var login Login if err := c.ShouldBind(&login); err == nil { c.JSON(http.StatusOK, gin.H{ "user" : login.User, "password" : login.Password, }) } else { c.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) } }) router.Run(":8080" ) }
ShouldBind会按照下面的顺序解析请求中的数据完成绑定:
如果是 GET 请求,只使用 Form 绑定引擎(query)。
如果是 POST 请求,首先检查 content-type 是否为 JSON 或 XML,然后再使用 Form(form-data)。
5.6 文件上传 5.6.1 单文件上传 demo.html内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 文件上传</title > </head > <body > <form action ="http://localhost:8080/value" method ="post" enctype ="multipart/form-data" > <input type ="file" name ="file" id ="file" > 文件上传 <input type ="submit" value ="上传" > </form > </body > </html >
main.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 33 34 35 package mainimport ( "fmt" "github.com/gin-gonic/gin" "log" ) func main () { r := gin.Default() r.LoadHTMLFiles("gin/learn_Gin/文件上传/demo.html" ) r.GET("/value" , txtUp) r.POST("/value" , func (c *gin.Context) { t, err := c.FormFile("file" ) if err != nil { c.JSON(200 , gin.H{ "message" : err, }) return } log.Println(t.Filename) dst := "/usr/local/go/Lear_Project/gin/learn_Gin/文件上传/" + t.Filename c.SaveUploadedFile(t, dst) c.JSON(200 , gin.H{ "message" : fmt.Sprintf("%s,保存成功!!" , t.Filename), }) return }) r.Run(":8080" ) } func txtUp (c *gin.Context) { c.HTML(200 , "demo.html" , nil ) }
5.6.2 多文件上传 demo.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 <!DOCTYPE html> <html> <head> <meta charset="UTF-8" > <title>多文件上传</title> <style> body { font-family: Arial; background-color: #f3f3f3; padding: 20 px; } .container { background-color: #fff; padding: 20 px; border-radius: 5 px; box-shadow: 0 0 5 px rgba(0 ,0 ,0 ,0.3 ); } .form { display: flex; flex-direction: column; align-items: center; } .form label { margin-bottom: 10 px; font-weight: bold; } .form input[type ="file" ] { margin-bottom: 20 px; } .form button { background-color: #4 CAF50; color: #fff; padding: 10 px 20 px; border: none; border-radius: 5 px; cursor: pointer; transition: background-color 0.3 s; } .form button:hover { background-color: #3e8 e41; } .form button:active { background-color: #296626 ; } </style> </head> <body> <div class="container" > <form class="form" action="http://localhost:8080/value" method="post" enctype="multipart/form-data" > <input type ="file" id="file" name="file" multiple> <input type ="submit" >上传文件</input> </form> </div> </body> </html>
mian.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 33 34 35 36 37 38 39 40 41 42 43 44 package mainimport ( "fmt" "github.com/gin-gonic/gin" "log" "net/http" "strconv" ) func main () { r := gin.Default() r.LoadHTMLFiles("/usr/local/go/Lear_Project/gin/learn_Gin/多文件上传/demo.html" ) r.Any("/value" , func (c *gin.Context) { switch c.Request.Method { case http.MethodGet: c.HTML(200 , "demo.html" , nil ) case http.MethodPost: forms, err := c.MultipartForm() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message" : "上传失败" , }) return } form := forms.File["file" ] for index, files := range form { log.Println(files.Filename) sindex := strconv.Itoa(index) dst := "/usr/local/go/Lear_Project/gin/learn_Gin/多文件上传/" + files.Filename + sindex c.SaveUploadedFile(files, dst) } c.JSON(200 , gin.H{ "message" : fmt.Sprintf("%d 个文件上传成功!!!" , len (form)), }) return default : c.String(200 , fmt.Sprintf("请求方式错误%s" , c.Request.Method)) } }) r.Run(":8080" ) }
5.7 重定向 5.7.1 HTTP重定向
HTTP 重定向很容易。 内部、外部重定向均支持。
1 2 3 r.GET("/test" , func (c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/" ) })
5.7.2 路由重定向
路由重定向,使用HandleContext:
1 2 3 4 5 6 7 8 r.GET("/test" , func (c *gin.Context) { c.Request.URL.Path = "/test2" r.HandleContext(c) }) r.GET("/test2" , func (c *gin.Context) { c.JSON(http.StatusOK, gin.H{"hello" : "world" }) })
使用函数实现路由重定向
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 ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.GET("/url" , func (c *gin.Context) { c.Request.URL.Path = "/school" r.HandleContext(c) }) r.GET("/abc" ,func (c *gin.Context) { c.JSON(200 ,gin.H{ "message" :"重定向成功" }) }) r.GET("/urls" , Render) r.Run(":8080" ) } func Render (c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/abc" ) }
5.8 Gin路由 5.8.1 普通路由 1 2 3 4 r.GET("/index" , func (c *gin.Context) {...}) r.GET("/login" , func (c *gin.Context) {...}) r.POST("/login" , func (c *gin.Context) {...})
此外,还有一个可以匹配所有请求方法的Any方法如下:
1 r.Any("/test" , func (c *gin.Context) {...})
顺便写个判断any的
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 ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.Any("/book" , AnyPs) err := r.Run(":8080" ) if err != nil { panic (err) } } func AnyPs (c *gin.Context) { switch c.Request.Method { case http.MethodGet: c.JSON(http.StatusOK, gin.H{ "Method" : http.MethodGet, }) case http.MethodDelete: c.JSON(http.StatusOK, gin.H{ "Method" : http.MethodDelete, }) case http.MethodPost: c.JSON(http.StatusOK, gin.H{ "Method" : http.MethodPost, }) default : c.JSON(http.StatusOK, gin.H{ "Method" : "Hello" , }) } }
为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回404.html页面。
1 2 3 r.NoRoute(func (c *gin.Context) { c.HTML(http.StatusNotFound, "404.html" , nil ) })
5.8.2 路由组 我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { r := gin.Default() userGroup := r.Group("/user" ) { userGroup.GET("/index" , func (c *gin.Context) {...}) userGroup.GET("/login" , func (c *gin.Context) {...}) userGroup.POST("/login" , func (c *gin.Context) {...}) } shopGroup := r.Group("/shop" ) { shopGroup.GET("/index" , func (c *gin.Context) {...}) shopGroup.GET("/cart" , func (c *gin.Context) {...}) shopGroup.POST("/checkout" , func (c *gin.Context) {...}) } r.Run() }
路由组也是支持嵌套的,例如:
1 2 3 4 5 6 7 8 9 10 11 shopGroup := r.Group("/shop" ) { shopGroup.GET("/index" , func (c *gin.Context) {...}) shopGroup.GET("/cart" , func (c *gin.Context) {...}) shopGroup.POST("/checkout" , func (c *gin.Context) {...}) xx := shopGroup.Group("xx" ) xx.GET("/oo" , func (c *gin.Context) {...}) }
5.8.3 路由原理 Gin框架中的路由使用的是httprouter 这个库。
其基本原理就是构造一个路由地址的前缀树。
5.9 中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。 定义中间件
Gin中的中间件必须是一个gin.HandlerFunc类型。
5.9.1记录接口耗时的中间件 例如我们像下面的代码一样定义一个统计请求耗时的中间件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func StatCost () gin.HandlerFunc { return func (c *gin.Context) { start := time.Now() c.Set("name" , "小王子" ) c.Next() cost := time.Since(start) log.Println(cost) } }
5.9.2记录响应体的中间件 我们有时候可能会想要记录下某些情况下返回给客户端的响应数据,这个时候就可以编写一个中间件来搞定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type bodyLogWriter struct { gin.ResponseWriter body *bytes.Buffer } func (w bodyLogWriter) Write(b []byte ) (int , error ) { w.body.Write(b) return w.ResponseWriter.Write(b) } func ginBodyLogMiddleware (c *gin.Context) { blw := &bodyLogWriter{body: bytes.NewBuffer([]byte {}), ResponseWriter: c.Writer} c.Writer = blw c.Next() fmt.Println("Response body: " + blw.body.String()) }
5.9.3跨域中间件cors 推荐使用社区的https://github.com/gin-contrib/cors 库,一行代码解决前后端分离架构下的跨域问题。
注意: 该中间件需要注册在业务处理函数前面。
这个库支持各种常用的配置项,具体使用方法如下。
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 ( "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main () { router := gin.Default() router.Use(cors.New(cors.Config{ AllowOrigins: []string {"https://foo.com" }, AllowMethods: []string {"GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" }, AllowHeaders: []string {"Origin" , "Authorization" , "Content-Type" }, ExposeHeaders: []string {"Content-Length" }, AllowCredentials: true , AllowOriginFunc: func (origin string ) bool { return origin == "https://github.com" }, MaxAge: 12 * time.Hour, })) router.Run() }
当然你可以简单的像下面的示例代码那样使用默认配置,允许所有的跨域请求。
1 2 3 4 5 6 7 8 9 func main () { router := gin.Default() router.Use(cors.Default()) router.Run() }
5.9.4注册中间件 在gin框架中,我们可以为每个路由添加任意数量的中间件。 为全局路由注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { r := gin.New() r.Use(StatCost()) r.GET("/test" , func (c *gin.Context) { name := c.MustGet("name" ).(string ) log.Println(name) c.JSON(http.StatusOK, gin.H{ "message" : "Hello world!" , }) }) r.Run() }
5.9.5 为某个路由单独注册 1 2 3 4 5 6 7 8 r.GET("/test2" , StatCost(), func (c *gin.Context) { name := c.MustGet("name" ).(string ) log.Println(name) c.JSON(http.StatusOK, gin.H{ "message" : "Hello world!" , }) })
5.9.6 为路由组注册中间件 为路由组注册中间件有以下两种写法。
写法1:
1 2 3 4 5 shopGroup := r.Group("/shop" , StatCost()) { shopGroup.GET("/index" , func (c *gin.Context) {...}) ... }
写法2:
1 2 3 4 5 6 shopGroup := r.Group("/shop" ) shopGroup.Use(StatCost()) { shopGroup.GET("/index" , func (c *gin.Context) {...}) ... }
5.9.7 中间件注意事项 gin默认中间件
gin.Default()默认使用了Logger和Recovery中间件,其中:
Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。
Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。gin中间件中使用goroutine
当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。
5.10 运行多个服务 我们可以在多个端口启动服务,例如:
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 package mainimport ( "log" "net/http" "time" "github.com/gin-gonic/gin" "golang.org/x/sync/errgroup" ) var ( g errgroup.Group ) func router01 () http.Handler { e := gin.New() e.Use(gin.Recovery()) e.GET("/" , func (c *gin.Context) { c.JSON( http.StatusOK, gin.H{ "code" : http.StatusOK, "error" : "Welcome server 01" , }, ) }) return e } func router02 () http.Handler { e := gin.New() e.Use(gin.Recovery()) e.GET("/" , func (c *gin.Context) { c.JSON( http.StatusOK, gin.H{ "code" : http.StatusOK, "error" : "Welcome server 02" , }, ) }) return e } func main () { server01 := &http.Server{ Addr: ":8080" , Handler: router01(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } server02 := &http.Server{ Addr: ":8081" , Handler: router02(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } g.Go(func () error { return server01.ListenAndServe() }) g.Go(func () error { return server02.ListenAndServe() }) if err := g.Wait(); err != nil { log.Fatal(err) } }
六、Gorm 入门
Gorm官方中文文档
Gorm安装 1 go get -u github.com/jinzhu/gorm
数据库连接 连接不同的数据库都需要导入对应数据的驱动程序,GORM已经贴心的为我们包装了一些驱动程序,只需要按如下方式导入需要的数据库驱动即可:
1 2 3 4 import _ "github.com/jinzhu/gorm/dialects/mysql"
6.2.1 连接MySQL 1 2 3 4 5 6 7 8 9 10 import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) func main () { db, err := gorm.Open("mysql" , "user:password@(localhost)/dbname?charset=utf8mb4&parseTime=True&loc=Local" ) defer db.Close() }
6.2.2 连接PostgreSQL 基本代码同上,注意引入对应postgres驱动并正确指定gorm.Open()参数。
1 2 3 4 5 6 7 8 9 import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres" ) func main () { db, err := gorm.Open("postgres" , "host=myhost port=myport user=gorm dbname=gorm password=mypassword" ) defer db.Close() }
6.2.3 连接Sqlite3 基本代码同上,注意引入对应sqlite驱动并正确指定gorm.Open()参数。
1 2 3 4 5 6 7 8 9 import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) func main () { db, err := gorm.Open("sqlite3" , "/tmp/gorm.db" ) defer db.Close() }
6.2.4 连接SQL Server 基本代码同上,注意引入对应mssql驱动并正确指定gorm.Open()参数。
1 2 3 4 5 6 7 8 9 import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mssql" ) func main () { db, err := gorm.Open("mssql" , "sqlserver://username:password@localhost:1433?database=dbname" ) defer db.Close() }
GORM基本示例
注意:
本文以MySQL数据库为例,讲解GORM各项功能的主要使用方法。
往下阅读本文前,你需要有一个能够成功连接上的MySQL数据库实例。
6.3 操作数据库 6.3.1 Docker快速创建MySQL实例 如果不会安装MySQL或者懒得安装MySQL,可以使用一下命令快速运行一个MySQL8.0.19实例,当然前提是你要有docker环境…
在本地的13306端口运行一个名为mysql8019,root用户名密码为root1234的MySQL容器环境:
1 docker run --name mysql8019 -p 13306:3306 -e MYSQL_ROOT_PASSWORD=root1234 -d mysql:8.0.19
在另外启动一个MySQL Client连接上面的MySQL环境,密码为上一步指定的密码root1234:
1 docker run -it --network host --rm mysql mysql -h127.0.0.1 -P13306 --default-character-set=utf8mb4 -uroot -p
6.3.2创建数据库 在使用GORM前手动创建数据库db1:
6.3.3GORM操作MySQL 使用GORM连接上面的db1进行创建 、查询 、更新 、删除 操作。
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 ( "fmt" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) type UserInfo struct { ID uint Name string Gender string Hobby string } func main () { db, err := gorm.Open("mysql" , "root:root1234@(127.0.0.1:13306)/db1?charset=utf8mb4&parseTime=True&loc=Local" ) if err!= nil { panic (err) } defer db.Close() db.AutoMigrate(&UserInfo{}) u1 := UserInfo{1 , "七米" , "男" , "篮球" } u2 := UserInfo{2 , "沙河娜扎" , "女" , "足球" } db.Create(&u1) db.Create(&u2) var u = new (UserInfo) db.First(u) fmt.Printf("%#v\n" , u) var uu UserInfo db.Find(&uu, "hobby=?" , "足球" ) fmt.Printf("%#v\n" , uu) db.Model(&u).Update("hobby" , "双色球" ) db.Delete(&u) }
6.3.4 GORM Model定义 在使用ORM工具时,通常我们需要在代码中定义模型(Models)与数据库中的数据表进行映射,在GORM中模型(Models)通常是正常定义的结构体、基本的go类型或它们的指针。 同时也支持sql.Scanner及driver.Valuer接口(interfaces)。gorm.Model
为了方便模型定义,GORM内置了一个gorm.Model结构体。gorm.Model是一个包含了ID, CreatedAt, UpdatedAt, DeletedA四个字段的Golang结构体。
1 2 3 4 5 6 7 8 type Model struct { ID uint `gorm:"primary_key"` CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time }
可以将它嵌入模型中:
1 2 3 4 5 type User struct { gorm.Model Name string }
也可以完全自己定义模型:
1 2 3 4 5 type User struct { ID int Name string }
模型定义示例
1 2 3 4 5 6 7 8 9 10 11 12 type User struct { gorm.Model Name string Age sql.NullInt64 Birthday *time.Time Email string `gorm:"type:varchar(100);unique_index"` Role string `gorm:"size:255"` MemberNumber *string `gorm:"unique;not null"` Num int `gorm:"AUTO_INCREMENT"` Address string `gorm:"index:addr"` IgnoreMe int `gorm:"-"` }
使用结构体声明模型时,标记(tags)是可选项。gorm支持以下标记: 支持的结构体标记(Struct tags)
结构体标记
作用
type
定义结构体类型
table
定义映射的数据库表名
column
定义映射的数据库列名
primary_key
定义主键
auto_increment
自动生成递增主键
unique
定义唯一键
not null
定义不能为空
default
定义默认值
index
定义索引
unique_index
定义唯一索引
size
定义字段类型长度
precision
定义浮点数类型精度
embedded
嵌套结构体
ignored or -
忽略该字段
migrate
定义迁移操作,如创建表、添加列等
comment
定义注释
type:varchar(255)
定义字符串类型长度为255
type:decimal(10,2)
定义十进制数类型,整数部分10位,小数部分2位
type:timestamp(6)
定义时间戳类型精确到毫秒
type:text
定义长文本类型
type:mediumtext
定义中等长度文本类型
type:longtext
定义长文本类型
type:datetime
定义日期时间类型
type:date
定义日期类型
type:time
定义时间类型
6.3.5.2 关联相关标记
结构体标记(Tag)
描述
MANY2MANY
指定连接表
FOREIGNKEY
设置外键
ASSOCIATION_FOREIGNKEY
设置关联外键
POLYMORPHIC
指定多态类型
POLYMORPHIC_VALUE
指定多态值
JOINTABLE_FOREIGNKEY
指定连接表的外键
ASSOCIATION_JOINTABLE_FOREIGNKEY
指定连接表的关联外键
SAVE_ASSOCIATIONS
是否自动完成 save 的相关操作
ASSOCIATION_AUTOUPDATE
是否自动完成 update 的相关操作
ASSOCIATION_AUTOCREATE
是否自动完成 create 的相关操作
ASSOCIATION_SAVE_REFERENCE
是否自动完成引用的 save 的相关操作
PRELOAD
是否自动完成预加载的相关操作
6.3.6 主键、表名、列表的约定 6.3.6.1 主键 GORM 默认会使用名为ID的字段作为表的主键。
1 2 3 4 5 6 7 8 9 10 11 12 type User struct { ID string Name string } type Animal struct { AnimalID int64 `gorm:"primary_key"` Name string Age int64 }
6.3.6.2 表名(Table Name) 表名默认就是结构体名称的复数,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type User struct {} func (User) TableName() string { return "profiles" } func (u User) TableName() string { if u.Role == "admin" { return "admin_users" } else { return "users" } } db.SingularTable(true )
6.3.6.2.1 也可以通过Table()指定表名: 1 2 3 4 5 6 7 8 9 db.Table("deleted_users" ).CreateTable(&User{}) var deleted_users []Userdb.Table("deleted_users" ).Find(&deleted_users) db.Table("deleted_users" ).Where("name = ?" , "jinzhu" ).Delete()
GORM还支持更改默认表名称规则:
1 2 3 gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string ) string { return "prefix_" + defaultTableName; }
6.3.6.3 列名(Column Name) 列名由字段名称进行下划线分割来生成
1 2 3 4 5 6 7 type User struct { ID uint Name string Birthday time.Time CreatedAt time.Time }
6.3.6.3.1可以使用结构体tag指定列名: 1 2 3 4 5 type Animal struct { AnimalId int64 `gorm:"column:beast_id"` Birthday time.Time `gorm:"column:day_of_the_beast"` Age int64 `gorm:"column:age_of_the_beast"` }
6.3.6.4时间戳跟踪 CreatedAt 如果模型有 CreatedAt字段,该字段的值将会是初次创建记录的时间。
1 2 3 4 5 db.Create(&user) db.Model(&user).Update("CreatedAt" , time.Now())
UpdatedAt 如果模型有UpdatedAt字段,该字段的值将会是每次更新记录的时间。
1 2 db.Save(&user) db.Model(&user).Update("name" , "jinzhu" )
DeletedAt
如果模型有DeletedAt字段,调用Delete删除该记录时,将会设置DeletedAt字段为当前时间,而不是直接将记录从数据库中删除。
七、jwt库
这个库是用来进行token生成的
官方文档 —->github文档
导入这个包
1 go get -u github.com/dgrijalva/jwt-go
这个也没找到教程 只有我之前写的项目用过
7.1生成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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 package commonimport ( "Appitems/models" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "github.com/dgrijalva/jwt-go" "time" ) var ( jwtPrivateKey, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) jwtPublicKey = &jwtPrivateKey.PublicKey jwtKey = []byte ("a_secret_crect" ) ) func ReleaseToken (user models.UserDatabase) (string , error ) { expirationTime := time.Now().Add(Week) claims := &models.Claims{ UserId: user.ID, StandardClaims: jwt.StandardClaims{ ExpiresAt: expirationTime.Unix(), IssuedAt: time.Now().Unix(), Issuer: "oceanlearn.tech" , Subject: "user access token" , }, } token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) tokenString, err := token.SignedString(jwtPrivateKey) if err != nil { return "" , err } return tokenString, nil } func ReleaseTokenUser (user models.UserDatabase) (string , error ) { expirationTime := time.Now().Add(Week) claims := &models.Claims{ UserId: user.ID, StandardClaims: jwt.StandardClaims{ ExpiresAt: expirationTime.Unix(), IssuedAt: time.Now().Unix(), Issuer: "oceanlearn.tech" , Subject: "user access token" , }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) Tokenstring, err := token.SignedString(jwtKey) if err != nil { return "" , err } return Tokenstring, nil } func ParseToken (tokenString string ) (*jwt.Token, *models.Claims, error ) { Cliams := &models.Claims{} token, err := jwt.ParseWithClaims(tokenString, Cliams, func (t *jwt.Token) (interface {}, error ) { return jwtKey, nil }) return token, Cliams, err }
7.2 对应的model 1 2 3 4 5 6 type Claims struct { UserId uint jwt.StandardClaims }
八、Cors库
这是我用来解决跨域问题的库
安装这个库
1 go get -u github.com/gin-contrib/cors
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 config := cors.DefaultConfig() config.AllowHeaders = []string { "Authorization" , "Access-Control-Allow-Headers" , "Access-Control-Allow-Origin" , "Access-Control-Allow-Methods" , "Content-Type" , "Content-Length" , "Accept-Encoding" , "X-CSRF-Token" , "Accept-Language" , "Accept-Encoding" , "Cookie" , } config.AllowOrigins = []string {"http://127.0.0.1:8848" , "http://localhost:63343" , "http://localhost:63342" } config.AllowCredentials = true config.AddAllowMethods("OPTIONS" ) r.Use(cors.New(config))
九、Viper库
用于读取配置文件
安装这个库
1 go get -u github.com/spf13/viper
先创建配置文件 —>我用的yml配置文件
这配置的mysql的相关信息
1 2 3 4 5 6 7 8 9 10 server: prot: 8080 datasource: driverName : mysql host : 127.0 .0 .1 prot: 3306 database: learn_gorm username : root password : 17078151675abc charset: utf8
读取配置文件内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func InitConfig () { workDir, err := os.Getwd() if err != nil { panic (err) } viper.SetConfigName("application" ) viper.SetConfigType("yml" ) viper.AddConfigPath(workDir + "/config" ) err = viper.ReadInConfig() if err != nil { } }
其他文件怎么使用
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 func DatabaseConnection (DB *gorm.DB) *gorm.DB { driverName := viper.GetString("datasource.driverName" ) username := viper.GetString("datasource.username" ) password := viper.GetString("datasource.password" ) host := viper.GetString("datasource.host" ) prot := viper.GetString("datasource.prot" ) dbname := viper.GetString("datasource.database" ) charset := viper.GetString("datasource.charset" ) aigis := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%smb4&parseTime=True&loc=Local" , username, password, host, prot, dbname, charset, ) var err error DB, err = gorm.Open(driverName, aigis) if err != nil { panic (err) } DB.AutoMigrate( &models.UserDatabase{}, &models.UserDto{}, &models.IpRecord{}, &models.BlogComment{}, &models.BlogArticle{}, &models.Claims{}, ) return DB } func GetDB () *gorm.DB { return db }