diff --git a/Go/其他教程/02常量、变量、基本数据类型/Snipaste_2020-03-18_11-08-29.png b/Go/其他教程/02常量、变量、基本数据类型/Snipaste_2020-03-18_11-08-29.png new file mode 100644 index 00000000..62f48476 Binary files /dev/null and b/Go/其他教程/02常量、变量、基本数据类型/Snipaste_2020-03-18_11-08-29.png differ diff --git a/Go/其他教程/02常量、变量、基本数据类型/常量、变量、数据类型.md b/Go/其他教程/02常量、变量、基本数据类型/常量、变量、数据类型.md new file mode 100644 index 00000000..33041fbc --- /dev/null +++ b/Go/其他教程/02常量、变量、基本数据类型/常量、变量、数据类型.md @@ -0,0 +1,590 @@ +[TOC] + +### 一、Hello Go + +```go +// 包名为main , 表示当前go将会被编译成二进制可执行文件 +// 当前文件想被编译成二进制的可执行文件,除了main包, 还得出现main()函数 +// 如果是我们写一个工具包, 那么不用取名叫main +package main + +import "fmt" + +// 函数外只能放置变量的声明,不能防止逻辑语句( 如打印一句话 ) + +// main() 很干净,没参数,没返回值 +func main(){ + fmt.Println("hello world") +} +``` + + + +### 二、变量和常量 + +#### 2.1 标识符 + +GO中的标识符,实际上就是程序员提供的变量名,方法名,常量名等等,在Go中这些标识符由字母、数字、下划线组成, 并且只用以字符和下划线开头。 + +```go +abc +_123 +a312 +``` + + + +#### 2.2 关键字 + +* Go中一共有25个关键字。 + +```go +for break continue func interface var +if else switch case import return +range type package const default map +select defer struct goto chan go +fallthrough +``` + + + +* 37个保留字 + +就像java中Goto的是保留字一样,虽然现在好没有开发,但是未来可能会拓展。 + +```go +常量4个 true false iota nil + +类型19个 int int8 int16 int32 int64 + uint uint8 uint16 uint32 uint64 + float32 float64 complex64 complex128 + bool byte rune string error + +函数13个 make len cap new append copy close delete + complex real imag + panic recover +``` + +#### 2.3 变量 + +* 变量的声明 + +```go +var 变量名 变量类型 +``` + +Go的变量名推荐驼峰命令法 如: `studentName`。 + +同一个作用域下不能申明重复的变量。 + +* 标准声明 + +```go +var name string +var age int +var isDone bool + +// 声明变量的同时并赋值 +var name string = "zhangsan" + +// Go同样支持类型推导,所以我们可以直接这样写 +var name = "Jerry" +``` + +> **和Java中顺序有不同,且每行语句后都没有分号结尾。** + + + +* 批量声明全局变量 + +```go +var ( + name string // "" + age int // 0 + isDone bool // false +) + +// 声明时赋值 +var ( + name string // "" + age int // 0 + isDone bool // false +) +``` + + + +* 短变量声明: + +```go +name := "Jerry" // 相当于 var name = "Jerry" 的简写 +``` + +短变量声明,要求只能在函数中使用。 + + + +* 变量一旦声明,必须使用 + +demo如下: + +![](Snipaste_2020-03-18_11-08-29.png) + +**变量先声明,再使用**,变量一旦声明了,就要求我们使用它 + +* 对于全局变量: 爆出黄色的警告说,变量不曾使用到。 +* 对于方法内部的局部变量: 声明了,但是不使用,就会爆红。 + + + +#### 2.4 匿名变量 + +```go +func foo()(int,string){ + return 1,"tom" +} + +func main(){ + x,_ := foo() + _,y := foo() + fmt.Print(x) + fmt.Print(y) +} +``` + +Go中的匿名变量使用 _ 表示, 表示占位与忽略值。 + +如果我们确定不会使用函数的返回值,就将其表示成匿名变量。 + +匿名变量不占用命名空间,不会分配内存,所以匿名变量不存在重复声明。 + + + +#### 2.5 常量 + +相当于js中的const, 数值恒定不变的量, 同样的关键字使用const + +```go +// 声明常量 +const pi = 3.1415926 +const e = 2.71 + +// 批量声明常量 +const ( + name = "tom" + age = 23 +) + +// 声明多个常量同时使用相同值, var变量则不行 +const( + n1= 100 + n2 + n3 +) +``` + +尝试修改常量时,编译器会爆红 + + + +#### 2.6 iota + +iota是Go语言中的**常量计数器** + +在const中出现时重置为0 ,每多一行const变量的声明,计数+1 + +```go +const ( + n1 = iota //0 + n2 //1 + n3 //2 +) + +func main() { + fmt.Print(n1) // 0 + fmt.Print(n2) // 1 + fmt.Print(n3) // 2 +} +``` + +更多示例: + +```go +const ( + n1 = iota //0 + n2 //1 + _ // 什么都没写表示和上面一行 + n3 //3 +) + +const ( + n1 = iota //0 + n2 //1 + n3=100 // 100 + n4 //100和上一行一样 +) + +const ( + n1 = iota //0 + n2 =100 //100 + n3= iota // 1 + n4 //2 +) + +// 多个常量声明在一行, 想知道结果话,得记住下面这句话 +// 当const出现时 iota初始化为1,以后const中每多一行(不算空行),iota+1 +const ( + d1, d2 = iota+1, iota+2 + d3, d4 = iota+1, iota+2 +) + +func main() { + fmt.Println("n1: ", d1) //1 + fmt.Println("n2: ", d2) //2 + fmt.Println("n3: ", d3) //2 + fmt.Println("n4: ", d4) //3 +} + +// 定义数量级常量 +const( + _ = iota + kB = 1 << (10*iota) //1左移10位, 相当于2的10次方 + MB = 1 << (10*iota) + GB = 1 << (10*iota) + TB = 1 << (10*iota) + PB = 1 << (10*iota) +) + kB: 1024 + MB: 1048576 + GB: 1073741824 + TB: 1099511627776 + PB: 1125899906842624 +``` + + + +### 三、Go基本数据类型 + + + +整形、浮点型、布尔型、字符串 + +数组、切片、结构体、函数、map、通道(channel) + +#### 3.01 整形 + +| 类型 | 描述 | +| :-------------------------------------: | :-----------------------------------------------: | +| uint8(能表示的范围实际上就是byte的范围) | 无符号 0-255 | +| uint16 | 无符号0-65535 | +| uint32 | 无符号 0-4294967295 | +| uint64 | 无符号0-18446744073709551615 | +| int8 | 有符号-128 到127 | +| int16 | 有符号 -32768 到 32767 | +| int32 | 有符号 -2147483648 到 2147484647 | +| int64 | 有符号 -9223372037854776808到 9223372037854776807 | + +特殊整型: + +| 类型 | 描述 | +| ------- | --------------------------------------------------- | +| uint | 32位的操作系统就是uint32, 64位的操作系统就是uint64 | +| int | 32位的操作系统就是int32, 64位的操作系统就是int64 | +| uintptr | 无符号整形,用于存放一个指针 | + +#### 3.02 八进制和十六进制 + +Go无法直接定义二进制数,如下是8进制和16进制数的示例 + +```go +func main() { + var a int = 10 + fmt.Printf("%d \n", a) // %d 十进制表示10 + fmt.Printf("%b \n", a) // %b 二进制表示1010 + + // 八进制数,以0开头 + var b int = 077 + fmt.Printf("%o \n", b)// 77 + + // 16进制数,以0x 开头 + var c int = 0xff + // c := 0xff + fmt.Printf("%x \n", c) //ff + fmt.Printf("%X \n", c) //FF +} +``` + + + +#### 3.03 浮点数 + +见如下实例: + +```go + fmt.Printf("%f \n",math.Pi) // 3.141593 + fmt.Printf("%.2f \n",math.Pi)// 3.14 + fmt.Printf("%f \n",math.MaxFloat32) // 340282346638528859811704183484516925440.000000 + fmt.Printf("%f \n",math.MaxFloat64) //179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000 + + f1:=1.23456 + fmt.Printf("%T \n",f1) // float64 + + f2:=float32(1.23456) // 显示声明 + fmt.Printf("%T \n",f2) // float32 + + // 和java一样,Go是静态类型的语言,所以f1与f2之间不能随便相互赋值, + // 需要进行强转,但是强转后会发生精度损失 + f1= float64(f2) +``` + + + +#### 3.04 复数 + +```go + var c1 complex64 + c1 = 1+2i + + var c2 complex128 + c2 = 2+3i + + fmt.Println(c1) // (1+2i) + fmt.Println(c2) // (2+3i) +} +``` + +#### 3.05 布尔值 + +```go + var b1 bool = true + b2:=false + var b3 bool + fmt.Print(b1) // true + fmt.Print(b2) // false + fmt.Print(b3) // false +``` + +Go中, bool 不能参与类型强制转换其他类型 + + + +#### 3.06 字符串 + +Go语言中的字符串不同于java, Go中的字符串以原生的类型出现,string + +Go中的字符串的内部编码实现使用utf-8 + +只能用 “ ” 包裹 + +```go + var s string = "string" + s2:= "string" + fmt.Printf("s= %#v \n", s) // s= "string" + fmt.Printf("s= %#v \n", s2) // s= "string" +``` + +#### 3.07 字符串的常见操作 + +```go + var name string = "tom" + var address string = "beijing" + + //全局函数,求长度 + fmt.Println(len(name)) //3 + + // 合并两个字符串 + result:=fmt.Sprintf("%s %s",name,address) + fmt.Println(result) // tom beijing + + // 和java一样,重载了+号,实现运算符的拼接 + result2:=name+address + fmt.Println(result2) //tombeijing +``` + +其他方法的使用strings工具 + +```go +func main() { + + var address string = "bei jing" + + // 分隔 + result1:=strings.Split(address," ") + fmt.Println(result1) // [bei jing] + + // 收否包含 + result2:=strings.Contains(address,"jing") + fmt.Println(result2) // true + + // 前后缀判断 + result3:=strings.HasPrefix(address,"b") + fmt.Println(result3) // true + + // 后缀判断 + result4:=strings.HasSuffix(address,"g") + fmt.Println(result4) // true + + // 返回字符首次出现的下标 + result5:=strings.Index(address,"i") + fmt.Println(result5) // 2 + + // 返回字符首次出现的下标 + result6:=strings.LastIndex(address,"i") + fmt.Println(result6) // 5 + + // 连接字符串 + fmt.Println(strings.Join(result1,"+")) //bei+jing + +} +``` + + + + + +#### 3.08 转义符号 + +| 转义 | 含义 | +| :--: | :-------------------: | +| \r | 回车 | +| \n | 换行 | +| \t | 制表符,相当于4个空格 | +| `\'` | 单引号 | +| `\"` | 双引号 | +| `\\` | 反斜杠 | + +```go + path1:=`D:\EEText` // 反引号里面的东西会原样输出 + path2:= "D:\\EEText" //通过 \ 转意路径 + fmt.Print(path1) + fmt.Print(path2) +``` + +#### 3.09 byte , rune 和 int32 + +在Go中,字符串中的元素叫做**字符**, 使用 ’ ‘ 表示 + +1. **uint8 或者叫byte类型,代表了ASCII的字符** + + int8 可以表示 0-2^8 即 0-128, uint8可以表示 0-256 , 整好在ascii范围内 + +2. **rune , 代表一个UTF8类型的字符** + +当我们需要处理中文或者其他国家的语言时,我们选择使用`rune`类型, `rune`类型实际上是一个`int32` + +```go +func main() { + + var address string = "嗨! bei jing" + + // 处理英文数字 + for i:=0; i=90{ + fmt.Print("A") + }else{ + fmt.Print("B") + } +``` + +score>=90 , 是判断的条件,看上面的例子,我们能发现**它可以在判断条件前多执行一个语句** + +#### 1.3 for循环 + +Go中所有可循环的类型都可以使用 `for`完成 + +```go +for 初始语句;条件表达式;每次循环修改条件{ + 循环体 +} + +// 变种1 +var i = 5 +for ;i<10;i++{ + ... +} + +// 变种2 +var i = 5 +for i<10{ + ... + i++ +} + +// 死循环 +for{ + ... +} + +// for range(键值循环) +// 可以用来遍历:数组,切片,字符串,map,channel +// 通过for range遍历的返回值有如下的规律 +1. 数组,切片,字符串 返回索引和值 +2. map返回key和value +3. channel只返回通道内的值, 常用这种方式去不断从channel中取值, 当channel被关闭时, for range自动退出, 而不用成员手动判断channel是否关闭了 +如下: + s:="嗨,Go" + for i,v := range s{ + fmt.Printf("i:%d v:%c \n",i,v) + } +结果: + i:0 v:嗨 // 嗨是中文,占有012三个字节 + i:3 v:, + i:6 v:G + i:7 v:o +``` + +和java的for几乎一样,只不过省去了小括号 + +在Go中,可以通过break,goto,return,panic 控制退出 + +continue实现退出本次循环,继续下次循环 + + + +#### 1.4 switch case + +default代码块仅仅有一个 + +写法1: + +```go +func main() { + finger := 2 + switch finger { + case 1: + fmt.Print("111") + case 2: + fmt.Println("222") + default: + fmt.Println("default ... ") + } +} +``` + +写法2: + +```go + // 和上面的if相似, 都将变量的声明和使用写在一行了 + switch finger := 2 ; finger { + case 1: + fmt.Print("111") + case 2: + fmt.Println("222") + default: + fmt.Println("default ... ") + } +``` + +写法3: + +```go + switch finger := 2 ; finger { + case 1,2,3,4,5: + fmt.Print("111") + case 6: + fmt.Println("222") + default: + fmt.Println("default ... ") + } +``` + +`fallthrough`语法可以执行满足的case的下一个case,目的是为了兼容C语言中的case而设计 + +```go + switch n:=1 ; n { + case 1: + fmt.Print("111") + fallthrough + case 2: + fmt.Println("222") + } +// 结果就是 +111 +222 +``` + + + +#### 1.5 goto + +for循环的存在的不足: + +```go +func tryGoto(){ + for i := 0;i<10;i++{ + for j:=0 ; j<10;j++{ + if j==2{ + break + } + fmt.Printf("i:%d j:%d \n",i,j) + } + } +} + +// 输出结果如下,break仅仅跳出了内层的for循环 +i:0 j:0 +i:0 j:1 +i:1 j:0 +i:1 j:1 +i:2 j:0 +i:2 j:1 +i:3 j:0 +i:3 j:1 +i:4 j:0 +i:4 j:1 +i:5 j:0 +i:5 j:1 +i:6 j:0 +i:6 j:1 +i:7 j:0 +i:7 j:1 +i:8 j:0 +i:8 j:1 +i:9 j:0 +i:9 j:1 + +// 如下实现了当内层遇到break时,两层循环全部退出 +// 通过添加一个标记为实现 +func tryGoto(){ + var breakTag bool + for i := 0;i<10;i++{ + for j:=0 ; j<10;j++{ + if j==2{ + breakTag = true + break + } + fmt.Printf("i:%d j:%d \n",i,j) + } + + if breakTag{ + break + } + fmt.Printf("i:%d",i) + } +} +``` + +goto + label 解决 for 循环带来的不足 + +```go +func tryGoto(){ + for i := 0;i<10;i++{ + for j:=0 ; j<10;j++{ + if j==2{ + // 跳转到先的标签所在的位置继续执行,而标签而双重循环的外面 + // 实现了退出循环的动作 + goto BreakTag + } + fmt.Printf("i:%d j:%d \n",i,j) + } + } + BreakTag: + fmt.Println("循环结束") +} +``` + + + +### 二、运算符 + +Go中内置了5种运算符 + +1. 算数运算符 `+ - * / %` +2. 关系运算符 `== != > < >= <=` +3. 逻辑运算符 `&& || !` +4. 位运算符`& | ^ << >>` +5. 赋值运算符 `= += *= /= %= <<= >>= &= |= ^=` + +> **Go中, ++和--同java中一样,是单独的语句,不是运算符** + +上面的运算符和java中的运算符如出一辙 + + + +### 三、复合数据类型 + +#### 3.01 数组的定义: + +```go +var 数组名 [初始容量] T +如: +var arr1 [3]int +var arr2 [3]bool +``` + + + +#### 3.02 Go的数组和其他语言数组的区别 + +Go中,**数组的容量和数组的类型都是数组的一部分,换句话说,即使两个数组都存放bool类型的数据,但是很可能因为他们的容量不同导致他们无法比较。** + +```go + var arr1 [3]bool // 类型: 长度为3的bool + var arr2 [4]bool // 类型: 长度为4的bool + + fmt.Printf("arr1: %T , arr2:%T",arr1,arr2) // arr1: [3]bool , arr2:[4]bool +``` + +#### 3.03 数组的初始化 + +默认值: + +```go + var arr1 [3]bool + var arr2 [4]bool + fmt.Print(arr1,arr2) //[false false false] [false false false false] + // 如果不显示的初始化,bool默认为false, int,float默认为0 string默认"" +``` + + + +初始化: + +```go + // 定义长度为3的bool数组 + var arr1 [3]bool + + // 定义长度为3的bool数组并初始化 + arr1 = [3]bool{true,false,true} //[true false true] + + // 根据初始化值,推断数组的长度 + arr2:=[...]int {1,2,3,4,5,6} //[true false true] + + // 初始化方式3 + arr3:=[5]int{1,2} //[1 2 0 0 0] + + // 根据索引初始化 + arr4:=[5]int{0:1,3:2} //[1 0 0 2 0] +``` + + + +遍历: + +```go +for i:=0;i doublecap { + newcap = cap + } else { + if old.len < 1024 { + newcap = doublecap + } else { + // Check 0 < newcap to detect overflow + // and prevent an infinite loop. + for 0 < newcap && newcap < cap { + newcap += newcap / 4 + } + // Set newcap to the requested cap when + // the newcap calculation overflowed. + if newcap <= 0 { + newcap = cap + } + } + } +``` + +1. 如果新申请的容量大于两倍的旧容量, 最终的容量及就是新申请的cap +2. 如果不满足1, 且切片的旧容量长度小于1024 , 那么切片的容量为两倍的旧容量 +3. 如果不满足1和2, 如果旧容量已经大于等于1024, 那么就从旧容量开始循环, 每次给旧容量增加自己的1/4大小, 直到旧容量它大于新申请的容量 , 然后用累加出来的结果当作新容量 +4. 如果计算newcap时发生了overflow溢出的情况, 那么最终容量就是新申请的容量 + +> **需要注意的是, 切片扩容会根据不同的数据类型做不同的处理, 如int , string类型的处理方式不同,结果不同** + + + +#### 3.13 使用`copy()` 函数进行赋值切片 + +参见如下例子: 注意点写在下面的注释里面 + +```go + s0:= []int{1,2,3} + // var s1 []int 这样声明一个变量,内存为nil, 不能进行拷贝 + // 使用make()创建切片, 注意算好len, 不然他不能接收全拷贝给它的值 + // 新切片的cap也要算好, 不够的话会报错, out of range + s1:=make([]int,len(s0)) // + + // 这仅仅是简单的引用的传递, 实际上他们会共用一个底层的数组 + s2:=s0 + // 使用copy()函数, 会拷贝一份底层的数组给当前的切片使用 + copy(s0,s1) + fmt.Println(s0)// [0,0,0] + fmt.Println(s1)// [0,0,0] + fmt.Println(s2)// [0,0,0] + + s2[0]=666 + s1[0]=888 + fmt.Println(s0)// [666,0,0] + fmt.Println(s1)// [888,0,0] + fmt.Println(s2)// [666,0,0] +``` + + + +#### 3.15 从切片中删除元素 + + Go中没有专门用来删除切片中元素的方法, 只能使用切片的特性去删除元素 + +```go + s0:= []int{1,2,3,4,5,6,7,8,9} + // 删除index = 2的元素 + // 参数1: 在谁身上追加 + // 参数2: 追加的内容 + s0 = append(s0[0:2],s0[3:]...) + fmt.Print(s0) // [1 2 4 5 6 7 8 9] +``` + + + +#### 3.16 练习 + +```go +var a = make([]int,5,10) +for i:=0;i<10;i++{ + // 在a的基础上继续追加, cap不够了会自动扩容 + a = append(a,i) +} +fmt.Print(a) // [0,0,0,0,0,1,2,3,4,5,6,7,8,9] +``` + + + + + +#### 3.17 指针 + +Go语言中的指针相对较为简单 , 因为它没有指针的运算 + +用法和C语言中的指针相当 + +* 通过&取变量的内存地址(16进制) +* 通过*取值 + +```go + s:= 18 + p:=&s // 取地址 + fmt.Printf("type:%T ,value:%v" , p,p) //type:*int ,value:0xc00000a0c8 + fmt.Printf("type:%T ,value:%v",*p,*p) // type:int ,value:18 +``` + + + +#### 3.18 new 和 make + +```go +// 返回指针, 用来给基本的数据类型分配内存 +func new(Type) *Type + +// 返回Type, 一般用来给 channel , map , slice 分配内存 +func make(t Type, size ...IntegerType) Type +``` + + + +如下例: 会发生panic + +**原因是: Go中我们对于引用类型的变量在使用时不仅要声明它, 而且还要为他分配内存空间, 否则我们的值就没办法存储** + +> 像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值 +> +> 更复杂的数据通常会需要使用多个字节,这些数据一般使用引用类型保存。一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。 + + + +而**对于值类型的声明,我们不需要再为他分配内存空间**, 因为我们在声明的时候已经默认为他分配好了内存空间 + +而我们想分配内存空间, 需要使用 `new`和`make` + +```go +func main(){ + var a *int + *a = 100 + fmt.Println(*a) + + var b map[string]int + b["白日梦"] = 100 + fmt.Println(b) +} + +// 报错如下: +panic: runtime error: invalid memory address or nil pointer dereference +[signal 0xc0000005 code=0x1 addr=0x0 pc=0x49dd4a] + +``` + +--- + +**`new()`很少用, 一般用来给string,int等基本类型的指针分配内存, 返回值是*int , *string 这种类型的指针** + +> **使用new , 一般是用来给基本类型的指针分配内存** + +通过`new()` 分配内存地址如下: + +```go +func main(){ + var a *int + fmt.Println(a) // + + var a2 = new(int) + fmt.Println(a2) // 0xc00000a0f0 + fmt.Println(*a2) // 0 + + *a2 = 100 + fmt.Println(*a2) // 100 +} +``` + +--- + +`make()`的详解 + +```go +func make(t Type,size ...IntegerType) Type +``` + +`make()`也可以实现内存地址的分配, 区别于new, 他们**只适用于slice, map, 以及chan**的**内存分配和创建**, 而且我们在使用slice,map,chan时, 也必须使用`make()` 为他们分配内存 + +`make()`的返回值是Type, 而不是他们的指针, 因为这三种类型本身就是引用类型, 所以没有必要返回他们的指针 + +```go +func main(){ + // 声明map变量 + var b map[string]int + + // 为map申请内存 + b = make(map[string]int , 10) + + // 赋值 + b["age"] = 12 + fmt.Print(b) // map[age:12] +} +``` + + + +#### 3.19 map + +Go语言提供的双列容器, map, 存储的是键值对, 底层使用散列表实现 + +map无序, 基于k-v的数据结构, map是引用类型, 必须通过初始化后(分配内存)才能使用 + + + +map定义 + +```go +map[keyType]ValueType + +// 如: +var m map[string]int +定义m是map, key=为字符串类型, 值为数值类型 +``` + + + +例: 不初始化而直接使用 报出panic + +```go + // 声明map变量 + var b map[string]int + b["age"] = 12 + fmt.Print(b) + // 报错如下: panic: assignment to entry in nil map +``` + +需要我们为其申请空间, 再使用 + +```go + var b map[string]int // 未在内存中申请空间 + b = make(map[string]int,5) // 申请内存 + b["age"] = 12 + fmt.Print(b) // map[age:12] + + //取值 + fmt.Println(b["age"]) //12 + + // 取不存在的值, 返回对应类型的默认值 + fmt.Println(b["age2"]) // 0 + + // 一般我们可以这样写 + value,ok :=b["age3"] + if !ok{ + fmt.Println("找不到key") + }else { + fmt.Printf("value: %d",value) + } +``` + + + +#### 3.20 map的遍历 + +```go + +func main() { + var m map[string]int + m = make(map[string]int , 5) + m["age"] = 12 + m["length"] = 13 + for k, v := range m { + fmt.Print(k," ",v," \n") + } + + for k:= range m { + fmt.Print(k,"\n") + } + + for k,_:= range m { + fmt.Print(k, "\n") + } + + for _,v:= range m { + fmt.Print(v," \n") + } +} + +``` + + + +#### 3.21 根据key, 删除元素 + +使用全局函数 `delete()` + +```go +func main() { + var m map[string]int + m = make(map[string]int , 5) + m["age"] = 12 + m["length"] = 13 + // 删除存在的key + delete(m,"age") + // 删除不存在的key, no-op 不报错, 也不做任何操作 + delete(m,"age2") +} +``` + + + +#### 3.22 按照指定的顺序遍历map + +思路, 使用切片将map的值切出来, 然后对切片进行排序等操作, 最后在遍历切片的同时取出map中的值 + + + +#### 3.23 元素为map的切片 + +```go +var s1 = make([]map[string]int,3,5) + +// s1[0]["age"] = 12 报错, 没有初始化内部的map + +// 先为map分配内存,再使用 +s1[0] = make(map[string]int,1) +s1[0]["age"] = 12 +fmt.Println(s1) // [map[age:12] map[] map[]] + +``` + + + +#### 3.24 值为切片类型的map + +```go +var s1 = make(map[string][]int,3) +s1["age"] = []int{1,3,5} +// s1["age"] = make([]int,3) // map[age:[0 0 0]] +fmt.Println(s1) +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Go/其他教程/04函数、defer、闭包/04函数、defer、闭包.md b/Go/其他教程/04函数、defer、闭包/04函数、defer、闭包.md new file mode 100644 index 00000000..498b9cb7 --- /dev/null +++ b/Go/其他教程/04函数、defer、闭包/04函数、defer、闭包.md @@ -0,0 +1,469 @@ +[TOC] + +### 补充: + +语言中有函数和方法, 我们写的做的最多的 func xxx (){} 这被称为Go语言中的函数 + +而指定接受者的函数被称为方法 , 如下面就是专属于user的方法 + +``` + func (u user) SaveUser(name string, age int) (err error){ + + } +``` + +### 一、函数 + +#### 1.1 函数的定义: + +```go +// 无参数,无返回值 +func fun(){ + +} + +// 无参数,有返回值, 并且给返回值取了名字叫 ret +func fun()(ret Type){ + +} + +// 有参数,有返回值, 并且没有给返回值取名字 +func sum(a int,b int) int { + return a+b +} + +// 连续参数类型相同时,可像下面这样简写 +func sun (a,b int) { + +} + +// 有参数,有返回值, 且给返回值取了名字 +func sum(a int,b int) (ret int) { + ret = a+b + return +} + +// 多个返回值 +func fun()(int,int){ + return 1,2 +} +// 使用 +_,v := fun() + +// 可变长度的参数 , b可以接受多个int, 也能接受int类型的切片 +func fun(a int,b...int){ + +} +``` + +#### 1.2 函数的作用域 + +Go中函数的作用域和Java中的作用域相当, 会被{ }限制住作用域, 局部找不到就找全局的 + +```go +package main +import "fmt" + +var x = 100 + +func f1() { + // 删除中变量查找的顺序: + // 1. 现在函数内部找, 找不到找函数的外部,全局的变量x + fmt.Print(x) +} + +func main() { + f1() +} +``` + + + +#### 1.3 函数类型与作用域 + +通过如下例查看函数的返回值类型 + +```go +func f1() { + fmt.Print("hello f1 \n") +} + +func f2() int{ + return 1 +} + +func f3(f func() int)(ret int){ + ret = f() + return +} + + + +func main() { + a := f1 + b := f2 + + // 查看函数的返回值类型 + fmt.Printf("%T \n",a) // 类型:func() + fmt.Printf("%T \n",b) // 类型:func() int + + //调用 + f1() //hello f1 + // 函数也做当作参数传递 + fmt.Printf("f3:%v ",f3(f2)) //f3:1 +} + +``` + + + +####1.4 匿名函数 + +参加如下示例学习匿名函数 + +```go +// 声明匿名函数 +// 但是一般我们都是在函数的内部申明匿名函数 +var f = func () { + +} + +func main() { + // 通常在函数的内部声明一个匿名函数 + var f1 = func() { + + } + f1() + f1() + f1() + + // 如果函数仅执行一次,完全可以这样写 + func(x int){ + fmt.Print("仅会被执行一次的函数, 入参传递进来的值为:",x) + }(1) + +} +``` + + + +#### 1.5 内置函数 + +| 内置函数 | 介绍 | +| -------------- | ------------------------------------------- | +| close | 主要用于关闭channel | +| len | 求长度, string,array, slice, map, channel | +| new | 分配内存, int, struct | +| make | 用来分配内存, chan, map , slice | +| append | 向数组, slice中追加元素 | +| panic和recover | 错误尝试恢复 | + +当出现panic时,程序会崩溃, 通过recover尝试恢复错误 + +**在Go中是没有try, catch这种异常处理机制来处理** + +**在Go的设计哲学中, 对待每一种错误都当做是一种具体的值,应该去具体判断,具体处理, 会比较多的if-else** + + + +参照如下的使用`recover()`的实例: + +* `recover()`必须搭配defer使用 +* defer+recover必须出现在 panic前面执行才有意义 + +```go +package main + +import "fmt" + +func a () { + fmt.Println("a") +} + +func b () { + defer func() { + // recover也不被推荐使用 + err:=recover() + if err!=nil{ + fmt.Printf("尝试恢复错误") + } + }() + panic("error panic \n") +} + +func main() { + a () // a + b () // 尝试恢复错误 +} +``` + + + +关于`fmt.Scan` 获取用户的输入 + +```go +func main() { + var s string + // 通过& 取值 + fmt.Scan(&s) + fmt.Println(s) + + // 输入指定的格式数据 + var ( + name string + age int + ) + fmt.Scanf("%s %d \n",&name,&age) + fmt.Println(name,age) + + // 可以扫描到换行 + fmt.Scanln(&name,&age) + fmt.Println(name,age) +} +``` + + + +### 二、defer语句 + +#### 2.1 初识defer语句 + +Go 语言中的defer语句会将其后面跟随的语句进行延迟处理, + +先被defer修饰的函数最后被执行, 最后被defer修饰的函数先执行 + +一个函数中可以存在多个defer + +defer多用于释放资源,文件句柄,数据库连接,socket连接 + +```go +func main(){ + fmt.Println("1") + defer fmt.Println("2") + defer fmt.Println("3") + defer fmt.Println("4") + fmt.Println("5") +} +运行结果: +1 +5 +4 +3 +2 +``` + + + +#### 2.2 defer的执行时机 + +![](Snipaste_2020-03-21_11-50-46.png) + +Go的底层, return语句并不是原子操作, 分成赋值和执行RET命令两步操作 + +如上图为defer的执行时机 + + + +#### 2.3 defer修饰匿名函数 + +```go + +func f1() int { + x := 5 + defer func() { + x++ + }() + return x +} + +func f2() (x int) { + defer func() { + x++ + }() + return 5 +} + +func f3() (y int) { + x := 5 + defer func() { + x++ // 匿名函数自己没有x, 所以使用的是外部的x + }() + return x +} + +func f4() (x int) { + defer func(x int) { + x++ // 这里面的x使用的就是传递给匿名函数的x + }(x) // 将x当作参数传进去 + return 5 +} + +func main() { + // 结果5, 因为他们的执行顺序为: 1.为返回值赋值 2. 执行defer 3.RET指令 + // 所以在defer之前,返回值就确定了 + + fmt.Println(f1()) // 5 + + /* + 第一步: 对返回值进行赋值 x=5 + 第二步: defer 对x++, x=6 + 第三步: 执行RET 返回6 + */ + fmt.Println(f2()) // 6 + + /* + 第一步对返回值进行赋值, 返回值是y,赋值成5 + 第二步 defer对x++, 但是没有y的事 + 第三步执行RET指令,返回y = 5 + */ + fmt.Println(f3()) //5 + + /* + 第一步对返回值进行赋值, 返回值是x,赋值成5 + 第二步 defer 中将返回值x当成参数传递给defer匿名函数, 是值传递,传递的副本 + 第三步执行RET指令,返回x = 5 + */ + fmt.Println(f4()) //5 +} +``` + + + +### 三、闭包 + +闭包大概率是一个函数(多数为匿名函数),这个函数可以起到适配的作用。 + +根据Go的作用域规定,内层函数能访问到外层函数中的变量。 + +所以: 闭包 = 函数+外部变量的引用 + +参照如下的实例: + +下例中,函数f3里面的匿名函数实际上就是一个闭包, 通过他实现了`main()`中的调用关系 + +```go +package main + +import "fmt" + +func f1(f2 func()) { + fmt.Println("this is f1 will call f2") + f2() + fmt.Println("this is f1 finished call f2") +} + +func f2(x int, y int) { + fmt.Println("this is f2 start") + fmt.Printf("x: %d y: %d \n", x, y) + fmt.Println("this is f2 end") +} + +// 接受一个两个参数的杉树, 返回一个包装函数 +func f3(f func(int,int) ,x,y int) func() { + fun := func() { + f(x,y) + } + return fun +} + +func main() { + // 目标是实现如下的传递与调用 + f1(f3(f2,6,6)) +} + +// 结果 +this is f1 will call f2 +this is f2 start +x: 6 y: 6 +this is f2 end +this is f1 finished call f2 +``` + +实际上底层的原理就是: + +1. 函数可以作为返回值 +2. 函数中变量的查找顺序:现在自己的{}作用域中查找,再从全局范围内查找 + + + +案例: + +```go +package main + +import "fmt" + +func calc(base int) (func(int) int, func(int)int){ + + add:= func(i int) int{ + base+=i + return base + } + + sub:= func(i int)int { + base-=i + return base + } + return add,sub +} + +func main() { + add,sub :=calc(10) + + fmt.Println(add(1)) // 11 + fmt.Println(sub(2)) // 9 , 之所以是9, 是因为他先执行的add 将base修改成了11 +} +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Go/其他教程/04函数、defer、闭包/Snipaste_2020-03-21_11-50-46.png b/Go/其他教程/04函数、defer、闭包/Snipaste_2020-03-21_11-50-46.png new file mode 100644 index 00000000..34fe37b3 Binary files /dev/null and b/Go/其他教程/04函数、defer、闭包/Snipaste_2020-03-21_11-50-46.png differ diff --git a/Go/其他教程/05自定义类型与结构体/自定义类型与结构体.md b/Go/其他教程/05自定义类型与结构体/自定义类型与结构体.md new file mode 100644 index 00000000..8246b05c --- /dev/null +++ b/Go/其他教程/05自定义类型与结构体/自定义类型与结构体.md @@ -0,0 +1,694 @@ +[TOC] + + + +### 一、自定义类型和类型别名 + +* 自定义类型: + +自定义类型指的是我们可以通过Go原生数据类型如 string,整形,浮点型,布尔等 去定义我们自己的数据类型如下: + +```go +type myString string +``` + +如上的myString,就是我们自定义的类型,它具有string的特性 + + + +* 类型别名: + +类型别名其实在学这之前我们就遇到过:如下: + +```go +//表示中文符号的rune +type rune = int32 +type byte = uint8 +``` + + + +* 自定义类型和类型别名的区别 + +```go +func main() { + type myString string + type myInt = int + + var a myString + var b myInt + + fmt.Printf("type of a %T",a)//type of a main.myString + fmt.Printf("type of b %T",b)//type of b int +} +``` + +区别如下: + +1. 自定义类型的类型数据**`main.自定义类型`** +2. 而我们为某一个类型所取的**类型别名一旦经过编译就退化成原生类型** + + + +### 二、结构体 + +#### 2.01 什么是结构体 + +因为之前学过C C++, 所以对Go语言中的struct并不陌生 + +Go语言中使用结构体实现面向对象 + +定义格式如下: + +```go +type 自定义类型名 struct{ + 字段名1 字段类型 + 字段名2 字段类型 + ... +} +``` + +注意点: + +* **同一个包中 自定义类型名 不能重复** +* **同一个结构体中, 字段名不能重复** + +例: + +```go +type person struct{ + name string + age int +} + +// 也可以像下面这样简化书写 +type person struct{ + name,sex string + age int +} +``` + + + +#### 2.02 结构体的实例化 + +```go +// 假设我们有如下的结构体 +type person struct{ + name string + age int +} +``` + +* 方式1: + +```go +// 实例化方式1:如下 +func main() { + var p person + fmt.Print(p) // { 0} + fmt.Printf("p4=%#v\n", p) //p=main.person{name:"", age:0} + + p.name = "tom" + p.age = 23 + fmt.Print(p) // {tom 23} + fmt.Printf("p4=%#v\n", p) //p=main.person{name:"tom", age:23} + +} +``` + +1. 我们打印结构体的值, 可以看到结构体长的样子是 被一对大括号直接包裹 +2. 如果我们不指定值, 结构体中的字段会使用默认值, 比如字符串是`""` +3. **如果想更友好的显示结构体,我们可以使用上面的打印格式化模式 #** + + + +* 方式2: + + 通过**new创建指针类型的结构体, 得到是指向结构体的指针** + +```go +// 实例化方式2:如下, 通过new创建指针类型的结构体, 得到是指向结构体的指针 +func main() { + var p = new(person) + fmt.Printf("a %T \n",p) // a *main.person + + fmt.Printf("a %#v \n",p) // a &{ 0} // 字符串的默认值是"", int的默认值是0 + p.name = "jerry" + fmt.Printf("a %#v \n",p) // a &{jerry 0} // 取值时可以看到为 + (*p).age = 23 + fmt.Printf("a %#v \n",p) // a &{jerry 23} + + fmt.Printf("a %#v \n",*p) // a main.person{name:"jerry", age:23} +} +``` + +注意上面的语法糖 + +1. 我们可以直接通过 指针.属性名使用结构体 +2. 也可以正常使用 (*指针).属性名 使用结构体 +3. 可以通过 (*指针)取出指针的值 +4. 直接格式化打印结构体指针的值可以看到编译器我们添加上了 &符号 + + + +* 实例化方式3: + +使用键值对的方式对指针进行初始化 + +```go +func main() { + p := person{ + name: "tom", + age: 23, + } + + fmt.Printf("%#T \n", p) //main.person + fmt.Printf("%#v \n", p) // main.person{name:"tom", age:23} +} +``` + + + +* 实例化方式4: + +我们**对一个结构体变量进行取地址& , 就会得到这个结构体变量的指针** + +同样我们可以通过键值对的方式去实例化一个结构体 + +```go +func main() { + + p := &person{ + name: "tom", + age: 23, + } + + fmt.Printf("%#T \n", p) //*main.person + fmt.Printf("%#v \n", p) //&main.person{name:"tom", age:23} +} +``` + +* 方式5 + +实际上是方式4的简写 + +```go +func main() { + p := &person{ + "tom", + 23, + } + + fmt.Printf("%#T \n", p) //*main.person + fmt.Printf("%#v \n", p) //&main.person{name:"tom", age:23} +} +``` + +但是这就意味着我们必须按照结构体中字段定义的顺序, 实例化结构体中所有的字段 + + + + + +#### 2.03 匿名结构体 + +匿名结构体和java中的匿名内部类有异曲同工之妙, 如果我们真的是仅仅使用一次这个结构体, 完全可以将其定义成匿名结构体如下: + +```go +var user = struct{ + name string + age int +} +``` + +#### 2.04 结构体的匿名字段 + +```go +type student struct{ + string + age +} +func main(){ + stu:=&student{ + "tom" + 23 + } + fmt.Printf("%#v",stu) +} +``` + +我们可以图省事为结构体添加匿名字段, 但是同样是存在限制的, 就是我们的不能出现重复的类型, 比如, 不能出现两个相同的字段string + +#### 2.05 嵌套结构体 + +如下示例: + +```go +type class struct { + non int + students []student +} + +type student struct { + name string + age int +} + +func main() { + cla:=class{ + non:1, + students:[]student{ + student{name:"jerry",age:23}, + student{name:"tom",age:23}, + }, + } + + // main.class{non:1, students:[]main.student{main.student{name:"jerry", age:23}, main.student{name:"tom", age:23}}} + fmt.Printf("%#v",cla) +} +``` + + + +#### 2.06 嵌套匿名结构体 + +```go +type class struct { + non int + []student // 嵌套匿名结构体 +} + +type student struct { + name string + age int +} +``` + + + +#### 2.07 嵌套结构体的字段冲突 + +```go +type class struct { + non int + []student // 嵌套匿名结构体 +} + +type student struct { + non int +} +``` + +如上, class结构体和student结构体中都存在一个int 类型的non字段, 这就是所谓的字段冲突, 解决方式如下: + +```go +type class struct { + non int + students []student +} + +type student struct { + non int +} + +func main() { + cla:=class{ + non:1, + students:[]student{ + student{non:2}, + student{non:3}, + }, + } + fmt.Printf("%#v",cla.non) + fmt.Printf("%#v",cla.students[0].non) +} +``` + + + + + + + +#### 2.08 结构体的内存布局 + +为结构体分配内存时, 默认会分配给它一整块连续的内存 + +```go +type test struct { + a int8 + b int8 + c int8 + d int8 +} +n := test{ + 1, 2, 3, 4, +} +fmt.Printf("n.a %p\n", &n.a) +fmt.Printf("n.b %p\n", &n.b) +fmt.Printf("n.c %p\n", &n.c) +fmt.Printf("n.d %p\n", &n.d) +``` + +结果: + +```go +n.a 0xc0000a0060 +n.b 0xc0000a0061 +n.c 0xc0000a0062 +n.d 0xc0000a0063 +``` + + + +> **空结构体不占用内存** + + + +#### 2.09 练习题 + +```go +type student struct { + name string + age int +} + +func main() { + m := make(map[string]*student) + stus := []student{ + {name: "小王子", age: 18}, + {name: "娜扎", age: 23}, + {name: "大王八", age: 9000}, + } + + for i:=0;i<3;i++{ + m[stus[i].name] = &stus[i] + } + + for k, v := range m { + fmt.Println(k, "=>", v.name) + } +} +// 输出结果如下 +小王子 => 小王子 +娜扎 => 娜扎 +大王八 => 大王八 + + + +type student struct { + name string + age int +} + +func main() { + m := make(map[string]*student) + stus := []student{ + {name: "小王子", age: 18}, + {name: "娜扎", age: 23}, + {name: "大王八", age: 9000}, + } + + for _, stu := range stus { + // m中存放是结构体.name = 结构体指针本身 + m[stu.name] = &stu + fmt.Printf("value = %#v \n",*m[stu.name]) + } + + for k, v := range m { + fmt.Println(k, "=>", v.name) + } +} +// 输出结果如下: +小王子 => 大王八 +娜扎 => 大王八 +大王八 => 大王八 +``` + +原因是因为: 我们m中的key虽然一直在变, 但是所有的key对应的value都指向了一块内存地址stu, 而stu里面的值一直在变, 所以得到了最终的结果 + + + + + +#### 2.10 构造函数 + +我们可以为结构体创建构造函数, **一般结构体的构造函数会返回结构体指针**, 防止结构体过大而带来的内存开销问题 + +一般结构体指针的命名规范 `new结构体名` + +```go +func newPerson(name string,age int){ + return &person{ + name:name + age:age + } +} + +func main(){ + p:=newPerson("tom",23) +} +``` + + + +#### 2.11 方法的接受者 + +在Go中存在方式接受者机制 + +如果说结构体好比java中的类, 那么这种**方法的接受者就像是类内部的成员函数** + +```go +func (自定义接受者名 接受者类型) 方法名(参数列表)(返回值列表){ + +} +``` + +**自定义接受者名就好比是 java中的this, python中的self** + + + +#### 2.12 值类型接受者 + +示例如下: **值类型的接受者传递的实际上是接受者值的一份拷贝, 所作出的任何修改也不会影响到main中的原始状态** + +```go +type student struct { + name string + age int +} + +func newStudent(name string,age int)(*student){ + return &student{ + name: name, + age: age, + } +} + +//值类型接受者 +func (stu student) saySomething(word string){ + fmt.Print(word) +} + +func main() { + stu:=newStudent("tom",23) + stu.saySomething("hi go") +} +``` + +#### 2.13 指针类型接受者 + +```go +//值类型接受者 +func (stu *student) saySomething(word string){ + fmt.Print(word) +} +``` + +这种写法传递是指针, 就真的**类似this和self的感觉了** + + + +#### 2.14 为任意类型添加接受者 + +```go +//MyInt 将int定义为自定义MyInt类型 +type MyInt int + +//SayHello 为MyInt添加一个SayHello的方法 +func (m MyInt) SayHello() { + fmt.Println("Hello, 我是一个int。") +} + +func main() { + var m1 MyInt + m1.SayHello() //Hello, 我是一个int。 + m1 = 100 + fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt +} +``` + +接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法 + + + +#### 2.15 结构体的继承 + +Go使用结构体实现了面向对象编程的思想 + +同时也提供了结构体的继承 : **在Go中, 通过使用嵌套匿名结构体来实现继承** + +```go +type person struct { + name string +} + +type student struct { + age int + *person +} + +func main() { + stu:=student{age:23,person:&person{name:"jerry"}} + fmt.Printf("%#v",stu.name)// "jerry" +} +``` + +虽然咱看着有点牵强, 但是人家确实算是实现了继承 + + + +#### 2.16 结构体与JSON的序列化 + +这个模块主要是学习一下 下面的函数. 将结构体序列化成json串 + +```go +json.Marshal(结构体) +json.Marshal(结构体指针) +``` + + + +第一个注意点: **如果我们想使用json的序列话和反序列化函数,我们需要将结构体指针的属性首字母开头大写: 表示对外开放** + + + +第二个注意点: **json.Marshal()**`函数如下: 入参位置可以是任意类型 , 就意味着**我们可以将结构体的值赋值给他, 也可以将结构体指针赋值给他, 至于效率嘛. 一眼就看出来了** + +```go +func Marshal(v interface{}) ([]byte, error) {...} +``` + + + +参照下面的例子: + +```go +type Person struct { + Name string +} + +type Student struct { + Age int + *Person +} + +func main() { + + stu:=&Student{Age:23,Person:&Person{Name:"jerry"}} + + // 序列化 + data,err:=json.Marshal(stu) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Printf("json: %s \n",data)//json: {"Age":23,"Name":"jerry"} + + + // 反序列化 + str:="{\"Age\":23,\"Name\":\"jerry\"}" + var Stu Student + err2:= json.Unmarshal([]byte(str),&Stu) + if err2 != nil { + fmt.Printf("error: %v\n", err) + return + } + //obj: main.Student{Age:23, Person:(*main.Person)(0xc000088360)} + fmt.Printf("obj: %#v \n",Stu) +} +``` + + + +#### 2.17 结构体标签 + +结构体标签长下面这样 + +```go +`key1:"value1" key2:"value2"` + +// 示例: +type Person struct { + Name string `tag:"name"` + age int `myTag:"age"` +} +``` + + + +结构体标签主要是用在反射的领域 + +参照如下的例子: 因为json的Marshal方法底层使用的就是反射的技术 + +```go +type Person struct { + Name string `tag:"name"` + Age int `json:"json-age"` + address string `json:"add"` // 能被序列化或者反序列话 +} + +func main() { + p:= &Person{ + Name: "tom", + Age: 23, + address: "shandong", + } + + data,err:=json.Marshal(p) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + + fmt.Printf("%s",data) // {"Name":"tom","json-age":23} +} +``` + +通过上面的例子我们其实就能看出来, **序列化反射时, 得到的结果的key, 其实是可以通过Tag指定的, 默认使用字段名称当作key** + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Go/其他教程/06package/package.md b/Go/其他教程/06package/package.md new file mode 100644 index 00000000..4534eeb0 --- /dev/null +++ b/Go/其他教程/06package/package.md @@ -0,0 +1,101 @@ +### 一. package + +Go语言中,为我们提供了多个内置包, 如fmt , os , io等等 + +我们也可以定义自己的包, 一个package . 可以简单的理解成它是用来存放 `*.go`文件的文件夹 + +所有的`*.go`文件的第一行都要添加如下的代码 + +```go +package 包名 +``` + + + +### 二. 包名的命名规范 + +* **同一个文件夹下直接包含的文件只能属于一个package, 同一个package不能在多个文件夹下** +* 包名可以不和文件夹的名字一样 +* 包名中不能含有`-` 符号 +* 只有 `package main` 才会被编译成可执行的文件 + + + +### 三. 可见性 + +如果包内的内容想对外暴露可见, 要求将变量名 / 函数名 / 结构体名**首字母大写** + + + +### 四. 包的导入 + +```go +// 单行导入 +//import "code.github.com/changwu/2020-03-17/helloworld" + +// 单行导入+自定义包名 + import hw "code.github.com/changwu/2020-03-17/helloworld" + +// 单行导入+匿名导入 + import _ "code.github.com/changwu/2020-03-17/helloworld" + +import ( + // 多行导入 + //"code.github.com/changwu/2020-03-17/helloworld" + + // 多行导入+自定义包名 + //"code.github.com/changwu/2020-03-17/helloworld" + + // 多行导入+匿名导入 + //_ "code.github.com/changwu/2020-03-17/helloworld" +) + +func main(){ + hw.SayHello() +} + +// 补充: +点(.)标识的包导入后,调用该包中函数时可以省略前缀包名。点(.)操作的语法为: +import ( +. "package1" +. "package2" +. "package3" ... ) +``` + +### 五. `init()` + +包内部都会有一个`init()` , 每次导入时都会先触发被导入包的`init()`的调用, 再触发自己的`init()`的调用 + +```go +import ( + hw "code.github.com/changwu/2020-03-17/helloworld" + "fmt" +) + +func init(){ + fmt.Println("this is structToJson init()") +} + + +func main(){ + /** + this is hello world init() + this is structToJson init() + */ + hw.SayHello() +} +``` + + + +#### 5.1 init()函数的执行时机 + +```go +全局变量 --> init() --> main() +``` + + + +#### 5.2 是否可以主动调用`init()`方法 + +不可以 \ No newline at end of file diff --git a/Go/其他教程/07接口/接口.md b/Go/其他教程/07接口/接口.md new file mode 100644 index 00000000..ab4f4391 --- /dev/null +++ b/Go/其他教程/07接口/接口.md @@ -0,0 +1,297 @@ +### 一. 接口 + +Go中的接口的概念和Java中的接口, 或者python中的鸭子类型是相仿的, 都是体现了面象对象的过程, 只不过具体的实现语法, 细节不同 + +如果说goroutine和channel是Go并发的两大基石,那么接口是Go语言编程中数据类型的关键。 + +在Go中, **接口也是一种类型, 一种抽象的类型** + +**接口中一般会定义一组方法**, 用来拓展某个对象的能力 + +Go语言中的接口是一些方法的集合(method set),它指定了对象的行为: + +```go +type 接口类型名 interface{ + 方法名1( 参数列表1 )返回值列表1 + 方法名2( 参数列表2 )返回值列表2 +} +// 一般接口名的命名规范: 在末尾+er , 如 reader Stringer +// 当接口名+方法名的首字母都大写时, 这个方法可以被接口所在包之外的包访问 +// 参数列表, 返回值列表中的变量名同样可以省略不写 +``` + +#### 1.1 如何算作实现了接口 + +在Go中, 只要结构体**实现了接口中的所有方法**, 就算作是实现了接口 + +> **注意, 是实现所有方法** + +### 二. 优雅的接口 + +先看如**下不使用接口**的编程方式 + +如下的say()方法,作用大同小异,首先说代码肯定会出现两份, 这不是代码的冗余,而是具体的struct对say()方法有不同的实现,但是这不够灵活,比如这不能实现父类引用指向子类对象。 + +```go +type Cat struct{} +type Dog struct{} + +func (c Cat)say{ + fmt.Println("喵~~~") +} +func (d Dog)say{ + fmt.Println("汪~~~") +} + +func main(){ + c:=Cat{} + d:=Dog{} + c.say() + d.say() +} +``` + +让Cat和Dog分别实现接口, 我们就可以使用父类引用指向子类对象 , 代码变的更加优雅 + +```go +// 定义接口 +type Animal interface{ + say() +} +type Cat struct{} +type Dog struct{} + +func (c Cat)say(){ + fmt.Println("喵~~~") +} +func (d Dog)say(){ + fmt.Println("汪~~~") +} + +func main() { + var a Animal + a = Cat{} + a.say() + + a = Dog{} + a.say() +} +``` + +### 三. 值接收者&指针接收者 + +在Go中存在对指针变量求值的语法糖, 将像下面这样, Go会将指针p 处理成 *p , 再去求值 + +示例如下: + +```go +type Animal interface { + say() +} +type Cat struct{} +type Dog struct{} + +func (c Cat) say() { + fmt.Println("喵~~~") +} +func (d Dog) say() { + fmt.Println("汪~~~") +} + +func main() { + var a Animal + a = Cat{} + a.say() + fmt.Printf("%T \n",a) // main.Cat + + a = &Dog{} + a.say() // go会自动的将指针处理成 *p , 然后再调用函数 + fmt.Printf("%T \n",a)// *main.Dog +} +``` + +### 测试题: + +下面的代码是否能编译通过? + +```go +type People interface { + Speak(string) string +} + +type Student struct{} + +func (stu *Student) Speak(think string) (talk string) { + // do something ... + return +} + +func main() { + // 能调用Speak函数, 前提是得有一个Student类型的指针, 所以下面的这行不对 + // var peo People = Student{} + + var peo People = &Student{} + think := "~" + fmt.Println(peo.Speak(think)) +} +``` + + + +### 四. 类型和接口的关系 + +* **一个类型可以实现多个接口, 且多个接口之间不会相互影响** +* **多个类型可以实现同一个接口, 而具有这个接口的能力** +* **实现接口, 并不一定需要自己将接口中方法完全实现, 我们可以通过在类型中嵌入其他类型或者是结构体来实现** + + + + + +### 五. 接口的嵌套: + +接口和接口之间通过嵌套可以创建出新的接口 + +```go +type mover interface { + move() +} + +type sayer interface { + say() +} + +type aminal interface { + mover + sayer +} +``` + + + +### 六. 空接口 + +在Go语言中,所有其它数据类型都实现了空接口。 + +在Go中,如果我们想让入参可以接受任意类型的变量, 那么我们可以使用空接口来实现 + +常见的实现的`fmt`之所以能打印输出各种类型, 就是因为他使用了空接口 + +诸如下面的方法 + +```go +// Println formats using the default formats for its operands and writes to standard output. +// Spaces are always added between operands and a newline is appended. +// It returns the number of bytes written and any write error encountered. +func Println(a ...interface{}) (n int, err error) { + return Fprintln(os.Stdout, a...) +} + +// Sprintln formats using the default formats for its operands and returns the resulting string. +// Spaces are always added between operands and a newline is appended. +func Sprintln(a ...interface{}) string { + p := newPrinter() + p.doPrintln(a) + s := string(p.buf) + p.free() + return s +} +``` + + + +此外, 空接口还一可以作为map的值, 从而让map不再局限存放一种特定的类型 + +```go + // 空接口作为map值 + var studentInfo = make(map[string]interface{}) + studentInfo["name"] = "沙河娜扎" + studentInfo["age"] = 18 + studentInfo["married"] = false + fmt.Println(studentInfo) +``` + +###七. 类型断言 + +既然接口可以实现接收任意类型的变量, 那么我们如何去确定接口的类型呢? -- 断言 + +语法 + +```go +// 断言x接口是T类型的值 +// v 表示第一个参数转换成T类型的后的值 +// ok bool值, 若为true, 表示断言成功 +v,ok:=x.(T) +``` + +示例: + +```go +func main() { + var x interface{} + x = 123 + v, ok := x.(string) + if ok { + fmt.Println(v) + } else { + fmt.Println("类型断言失败") + } +} + +// 使用switch 判断多个断言 +func justifyType(x interface{}) { + switch v:=x.(type){ + case string: + fmt.Printf("x is a string,value is %v\n", v) + case int: + fmt.Printf("x is a int is %v\n", v) + case bool: + fmt.Printf("x is a bool is %v\n", v) + default: + fmt.Println("unsupport type!") + } +} +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Go/其他教程/08文件操作/Snipaste_2020-04-02_06-56-32.png b/Go/其他教程/08文件操作/Snipaste_2020-04-02_06-56-32.png new file mode 100644 index 00000000..280062da Binary files /dev/null and b/Go/其他教程/08文件操作/Snipaste_2020-04-02_06-56-32.png differ diff --git a/Go/其他教程/08文件操作/文件.md b/Go/其他教程/08文件操作/文件.md new file mode 100644 index 00000000..960a6364 --- /dev/null +++ b/Go/其他教程/08文件操作/文件.md @@ -0,0 +1,328 @@ +## 一. 文件读取 + + + +### 1.1 用只读的方式打开文件 + +示例如下, 通过`os.Open()`打开文件 + +```go +func main() { + // 打开文件 + file,err:=os.Open("E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\structToJson\\test") + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + // 关闭文件 + defer file.Close() +} +``` + +### 1.2 读取文件 + +读取一次, 将文件中的内容读取到缓冲切片中 + +```go +func main() { + // 打开文件 + file, err := os.Open("E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\structToJson\\test") + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + // 关闭文件 + defer file.Close() + + // 创建缓存 + var tmp = make([]byte, 128) + // 读取文件 + num, err := file.Read(tmp) + + if err == io.EOF { + fmt.Printf("error: %v\n", err) + break + } + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Printf("读取到的字节数:%v \n",num) + fmt.Printf("读取到的内容: %v \n",string(tmp[:num])) +} + +``` + +### 1.3 循环读取大文件 + +通过for循环, 循环读取 , 并将读取到的内容累加到缓存中 + +注意点, 最后一次读取文件时, 会发生读不到的情况, 出现 `io.EOF`的错误, 这是break出去就好 + +```go + +func main() { + // 打开文件 + file, err := os.Open("E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\structToJson\\test") + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + // 关闭文件 + defer file.Close() + + // 创建缓存,每次读取的大小 + var tmp = make([]byte, 128) + // 存储文件的总大小 + var content []byte + for { + // 读取文件 + num, err := file.Read(tmp) + if err == io.EOF{ + fmt.Printf("error: %v\n", err) + break + } + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + + content = append(content,tmp[:num]...) + } + + fmt.Println(string(content)) +} + +``` + + + +### 1.4 通过bufio读取文件 + +Go为我们封装了带缓存的IO类, 叫做bufio + +![Snipaste_2020-04-02_06-56-32.png](Snipaste_2020-04-02_06-56-32.png) + +常用到上面的方法 + +示例如下: + +```go +func main() { + // 打开文件 + file, err := os.Open("E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\structToJson\\test") + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + // 关闭文件 + defer file.Close() + reader := bufio.NewReader(file) + for { + line, err := reader.ReadString('\n') + // 读取文件 + if err == io.EOF { + if len(line) != 0 { + fmt.Println(line) + } + break + } + if err != nil { + fmt.Printf("error: %v", err) + return + } + fmt.Println(line) + } +} + +``` + + + +### 1.5 通过ioutil读取到整个文件 + +```go +func main() { + // 打开文件 + data,err := ioutil.ReadFile("E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\structToJson\\test") + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println(string(data)) +} +``` + + + + + +## 二. 文件写入 + +### 2.1 用指定的模式打开文件 + +`os.OpenFile()` + +```go +// name : 要打开的文件名 +// flag : 用什么模式打开文件 +// perm : 文件的权限: r04 w02 x01 +func OpenFile(name string, flag int, perm FileMode) (*File, error) { + testlog.Open(name) + f, err := openFileNolog(name, flag, perm) + if err != nil { + return nil, err + } + f.appendMode = flag&O_APPEND != 0 + + return f, nil +} +``` + + + +打开的默认以常量的方式定义在 `os` 中 + +```go +const ( + // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified. + O_RDONLY int = syscall.O_RDONLY // open the file read-only. + O_WRONLY int = syscall.O_WRONLY // open the file write-only. + O_RDWR int = syscall.O_RDWR // open the file read-write. + // The remaining values may be or'ed in to control behavior. + O_APPEND int = syscall.O_APPEND // append data to the file when writing. + O_CREATE int = syscall.O_CREAT // create a new file if none exists. + O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist. + O_SYNC int = syscall.O_SYNC // open for synchronous I/O. + O_TRUNC int = syscall.O_TRUNC //会直接覆盖掉源文件中的内容 +) +``` + + + +### 2.2 `Write()` 和 `WriteString()` + +```go +func main() { + filePath := "E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\structToJson\\test" + // 打开文件 + // os.O_TRUNC 截断文件, 会把源文件的内容覆盖, 没办法直接在文件中间插入新内容 + file, err := os.OpenFile(filePath,os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + defer file.Close() + str:="hi openfile\n" + file.Write([]byte(str)) + file.WriteString(str) +} +``` + + + +### 2.3 bufio.NewWriter() + +```go +func main() { + filePath := "E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\structToJson\\test" + // 打开文件 + // os.O_TRUNC 截断文件, 会把源文件的内容覆盖, 没办法直接在文件中间插入新内容 + file,err:=os.OpenFile(filePath,os.O_CREATE|os.O_APPEND,0666); + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + defer file.Close() + + write:=bufio.NewWriter(file) + // 写入缓存 + write.Write([]byte("write slice\n")) + write.WriteString("write string\n") + write.WriteByte('Y') + write.WriteRune('中') + // 注意最后要flush一下, 将缓存中的值刷新到磁盘中 + write.Flush() +} +``` + +###2.4` ioutil.WriteFile()` + +```go +func main() { + filePath := "E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\structToJson\\test2" + // 打开文件 + data:="use ioutil write this data" + err:=ioutil.WriteFile(filePath,[]byte(data),0666) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } +} + +``` + + + +## 三. 练习 + +### 3.1 使用`io.Copy()` 实现文件拷贝 + +```go +// 思路: 使用读取的方式打开文件 +// 使用: 使用创建的方式打开目标文件 +func CopyFile(targetFileName,SourceFileName string)(written int64,err error){ + sourceFile, err := os.Open(SourceFileName) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + defer sourceFile.Close() + + targetFile, err := os.OpenFile(targetFileName, os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + defer targetFile.Close() + + return io.Copy(targetFile,sourceFile) +} + +func main() { + filePath2 := "E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\structToJson\\test2" + filePath3 := "E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\structToJson\\test3" + // 打开文件 + _, err := CopyFile(filePath3, filePath2) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println("done") +} +``` + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Go/其他教程/09time标准库/time标准库.md b/Go/其他教程/09time标准库/time标准库.md new file mode 100644 index 00000000..adbd6c46 --- /dev/null +++ b/Go/其他教程/09time标准库/time标准库.md @@ -0,0 +1,210 @@ + + +### 一. 时间类型 + +获取到年月日, 时分秒 + +```go +func main() { + now := time.Now() + //current time 2020-04-02 08:08:06.8322921 +0800 CST m=+0.006981901 + fmt.Printf("current time %v \n",now) + year := now.Year() + month := now.Month() + day := now.Day() + + hour := now.Hour() + minute := now.Minute() + second := now.Second() + //2020-04-02 08-08-06 + fmt.Printf("%d-%02d-%02d %02d-%02d-%02d \n",year,month,day,hour,minute,second) +} +``` + + + +### 二. 时间戳 + +时间戳指的是 从1970年1月1日(08:00:00GMT)到现在的毫秒数 + +```go +func main() { + now := time.Now() + // 时间戳 + unix := now.Unix() + // 纳秒的时间戳 + nano := now.UnixNano() + fmt.Printf(" %v \n",unix)// 1585786306 + fmt.Printf(" %v \n",nano)// 1585786306678067200 +} +``` + + + +### 三. 时间间隔 + +Go定义了时间间隔类型常量, 我们可以直接使用`time.Duration` + +```go +const ( + Nanosecond Duration = 1 + Microsecond = 1000 * Nanosecond + Millisecond = 1000 * Microsecond + Second = 1000 * Millisecond + Minute = 60 * Second + Hour = 60 * Minute +) +``` + + + +### 四. 时间计算 + +#### 4.1 当前时间加一小时 + +```go +func main() { + now := time.Now() + later := now.Add(time.Hour) // 当前时间加1小时后的时间 + fmt.Println(later) +} +``` + +#### 4.2 两时间之差 + +```go +// 返回一个时间差 t-u +func (t Time) Sub(u Time) Duration +``` + +#### 4.3 判断两时间是否相等 + +本方法不同于简单的 t==u , 内部的实现考虑了时区的影响 + +```go +func (t Time) Equal(u Time) bool +``` + +#### 4.4 判断时间的前后 + +```go +func (t Time) Before(u Time) bool +func (t Time) After(u Time) bool +``` + + + +### 五. 定时器 + +如下是定时器demo, 每秒都会执行一次任务 + +```go +func myTick(){ + ticker := time.Tick(time.Second) + for i:=range ticker{ + fmt.Println(i) + } +} + +``` + + + +### 六. 格式化 + +Go诞生于2009年11月10日 + +Go 语言的格式化和其他语言是不同的, 格式按照美国人为了好记忆的日期书写顺序写的一个1234567。1月2号下午3时4分5秒 06年 7时区 + +```go +func formatDemo() { + now := time.Now() + // 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan + // 24小时制 + fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan")) + // 12小时制 + fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan")) + fmt.Println(now.Format("2006/01/02 15:04")) + fmt.Println(now.Format("15:04 2006/01/02")) + fmt.Println(now.Format("2006/01/02")) +} + +// 解析字符串格式的时间 +func Parse(){ + now := time.Now() + fmt.Println(now) + // 加载时区 + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + fmt.Println(err) + return + } + // 按照指定时区和指定格式解析字符串时间 + timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2019/08/04 15:14:20", loc) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(timeObj) + fmt.Println(timeObj.Sub(now)) +} +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Go/其他教程/10反射/反射.md b/Go/其他教程/10反射/反射.md new file mode 100644 index 00000000..6c1e579a --- /dev/null +++ b/Go/其他教程/10反射/反射.md @@ -0,0 +1,337 @@ +[TOC] + + + +### 一. 反射: + +####1.1 简介: + +前面有接触到Go的鸭子类型, interface, 如果入参是interface类型的变量, 那么很显然我们可以给他传递进去任意类型的值 + +然而问题也来了: + +* 常见的我们这样写`a:=1` 在编译时我们就能确定a是int类型的变量 +* 但是像前面所说的入参的类型是interface, 我们就无法在程序编译时期动态的获取到程序的类型, 这是就得通过反射区实现, **通过反射, 我们可以在程序运行的过程中,实现动态的获取到变量的类型, 变量转换成具体类型后的值** + + + +#### 1.2 入门示例: + +```go +func main() { + a:=3.14 + v := reflect.ValueOf(a) + fmt.Println(v) // 获取变量具体的值, 3.14 + + t := reflect.TypeOf(a) + fmt.Println(t) // 获取变量具体的类型 float64 +} +``` + + + +#### 1.3 Type 的 Name 和 Kind + +这是一个注意点, 我们通过反射可以获取到变量的类型(Type) + +有了Type进而可以获取到变量的TypeName和TypeKind属性 + +* TypeName: 指的是这个变量的类型 +* TypeKind: 指的是这个变量所对应的Go原生类型 + +看下面这个例子: + +```go +type person struct { + name string + age int +} + +var d = person{ + name: "小王子", + age: 18, +} + +t := reflect.TypeOf(d) +fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind()) // TypeName:person TypeKind:struct + + + var a *float32 // 指针 type: kind:ptr + var b myInt // 自定义类型 type:myInt kind:int64 + var c rune // 类型别名 type:int32 kind:int32 + + +``` + +**通过反射获取到的 数组, 切片, Map, 指针, 等类型的变量, 他们的Type.Name()都是返回的空** + + + + + +Kind类型变量如下: + +```go +type Kind uint +const ( + Invalid Kind = iota // 非法类型 + Bool // 布尔型 + Int // 有符号整型 + Int8 // 有符号8位整型 + Int16 // 有符号16位整型 + Int32 // 有符号32位整型 + Int64 // 有符号64位整型 + Uint // 无符号整型 + Uint8 // 无符号8位整型 + Uint16 // 无符号16位整型 + Uint32 // 无符号32位整型 + Uint64 // 无符号64位整型 + Uintptr // 指针 + Float32 // 单精度浮点数 + Float64 // 双精度浮点数 + Complex64 // 64位复数类型 + Complex128 // 128位复数类型 + Array // 数组 + Chan // 通道 + Func // 函数 + Interface // 接口 + Map // 映射 + Ptr // 指针 + Slice // 切片 + String // 字符串 + Struct // 结构体 + UnsafePointer // 底层指针 +) +``` + + + +#### 1.4 ValueOf()方法 + +示例如下: + +```go +a:=3.14 +v := reflect.ValueOf(a) +fmt.Println(v) // 获取变量具体的值, 3.14 +``` + +`Valueof()`方法的返回值是`reflect.Value`类型变量, 如下 + +```go +// ValueOf returns a new Value initialized to the concrete value +// stored in the interface i. ValueOf(nil) returns the zero Value. +func ValueOf(i interface{}) Value { + if i == nil { + return Value{} + } + + // TODO: Maybe allow contents of a Value to live on the stack. + // For now we make the contents always escape to the heap. It + // makes life easier in a few places (see chanrecv/mapassign + // comment below). + escapes(i) + + return unpackEface(i) +} +``` + +拿到这个`reflect.Value`, 我们可以进一步获取到具体的值 + +如: + +```go +Interface() interface {} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 +Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回 +Bool() bool 将值以 bool 类型返回 +... +``` + +通过反射获取到值的常用套路 + +```go +func reflectValue(x interface{}) { + v := reflect.ValueOf(x) + k := v.Kind() + switch k { + case reflect.Int64: + // v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换 + fmt.Printf("type is int64, value is %d\n", int64(v.Int())) + case reflect.Float32: + // v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换 + fmt.Printf("type is float32, value is %f\n", float32(v.Float())) + case reflect.Float64: + // v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换 + fmt.Printf("type is float64, value is %f\n", float64(v.Float())) + } +} +``` + + + +#### 1.5 通过反射修改变量的值 + +注意点, 在Go中方法中参数的传递都是值拷贝, 我们想修改值, 就得传递指针 + +但是问题来了, 通过上面的示例我们可以发现, 指针没有TypeName, 而且它的TypeKind是 ptr类型的, 所以是不是意味着不能获取到指针的具体类型呢? 其实不是的, 如下: + +在反射中, 通过专有的方法`Elem()`去获取指针对应的具体类型 + +```go +func reflectSetValue1(x interface{}) { + v := reflect.ValueOf(x) + if v.Kind() == reflect.Int64 { + fmt.Println(v.Kind()) + v.SetInt(200) //修改的是副本,reflect包会引发panic + } +} + +func reflectSetValue2(x interface{}) { + v := reflect.ValueOf(x) + // 反射中使用 Elem()方法获取指针对应的值 + if v.Elem().Kind() == reflect.Int64 { + fmt.Println(v.Elem()) + fmt.Println(v.Elem().Kind()) + v.Elem().SetInt(200) + } +} +func main() { + var a int64 = 100 + // reflectSetValue1(a)// panic: reflect: reflect.Value.SetInt using unaddressable value + reflectSetValue1(&a) // 即使传递进去指针, 上面的if条件也不会成立 + reflectSetValue2(&a) // 正常修改值 + fmt.Println(a) +} + +``` + + + +#### 1.6 `IsNil()` 和 `IsValid()` + +`IsNil()`常被用于判断指针是否为空; + +`IsValid()`常被用于判定返回值是否有效。 + +```go +func main() { + // *int类型空指针 + var a *int + fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil()) + // nil值 + fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) + // 实例化一个匿名结构体 + b := struct{}{} + // 尝试从结构体中查找"abc"字段 + fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid()) + // 尝试从结构体中查找"abc"方法 + fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid()) + // map + c := map[string]int{} + // 尝试从map中查找一个不存在的键 + fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid()) +} +``` + + + +#### 1.7 结构体反射 + +| 方法 | 说明 | +| ----------------------------------------------------------- | ------------------------------------------------------------ | +| Field(i int) StructField | 根据索引,返回索引对应的结构体字段的信息。 | +| NumField() int | 返回结构体成员字段数量。 | +| FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串对应的结构体字段的信息。 | +| FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。 | +| FieldByNameFunc(match func(string) bool) (StructField,bool) | 根据传入的匹配函数匹配需要的字段。 | +| NumMethod() int | 返回该类型的方法集中方法的数目 | +| Method(int) Method | 返回该类型方法集中的第i个方法 | +| MethodByName(string)(Method, bool) | 根据方法名返回该类型方法集中的方法 | + +```go +type student struct { + Name string `json:"name"` + Score int `json:"score"` +} + +func main() { + stu1 := student{ + Name: "小王子", + Score: 90, + } + + t := reflect.TypeOf(stu1) + fmt.Println(t.Name(), t.Kind()) // student struct + // 通过for循环遍历结构体的所有字段信息 + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json")) + } + + // 通过字段名获取指定结构体字段信息 + if scoreField, ok := t.FieldByName("Score"); ok { + fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json")) + } +} +``` + + + +### 反射第一定律: + +反射是一种检查存储在接口变量中的(类型,值)对的机制 + + + +### 反射第二定律 + +给定一个reflect.Value,我们能用Interface方法把它恢复成一个接口值; + + + +### 反射第三定律 + +为了修改一个反射对象,值必须是settable的(To modify a reflection object, the value must be settable) + + + +```go + var x float64 = 3.4 + // 一定注意, 我们传递进来的是地址 + v := reflect.ValueOf(&x) + fmt.Println("settability of v:", v) + fmt.Println("settability of v:", v.Elem()) // == *v + fmt.Println("settability of v:", v.Elem().CanSet()) + + // 结果: + settability of v: 0xc00000a110 + settability of v: 3.4 + settability of v: true +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Go/其他教程/11conf.ini解析器/conf.ini解析器.md b/Go/其他教程/11conf.ini解析器/conf.ini解析器.md new file mode 100644 index 00000000..7f285e96 --- /dev/null +++ b/Go/其他教程/11conf.ini解析器/conf.ini解析器.md @@ -0,0 +1,156 @@ +### conf.ini 解析器 + +```go +package main + +import ( + "fmt" + "io/ioutil" + "reflect" + "strconv" + "strings" +) + +type MysqlConfig struct { + Address string `ini:"address"` + Port int `ini:"port"` + Username string `ini:"username"` + Password string `ini:"password"` +} + +type Redis struct { + Host string `ini:"host"` + Port int `ini:"port"` + Password string `ini:"password"` + Database string `ini:"database"` +} + +type Config struct { + MysqlConfig `ini:"mysql"` + Redis `ini:"redis"` +} + +func loadIni(filePath string, data interface{}) (err error) { + /* + 1 参数校验 + 1.1 data为指针类型, 因为需要满足 setability + 1.2 data为结构体指针 + 2 读取文件,得到字节切片 + 3 按行读取数据 + 4 如果是注解 ; # 就掉过 + 5 如果是 [ 开头表示遇到了新的节点 + 6 如果不是[开头, 按照 = 分隔取值 + */ + + // Config中单个配置节点的名称 + var structName string + // 判断类型, 使用Typeof + dataType := reflect.TypeOf(data) + if dataType.Kind() != reflect.Ptr && dataType.Elem().Kind() != reflect.Struct { + err = fmt.Errorf("为了能将解析出的值返回给您, 请传入结构体指针") + } + // ioutil.ReadFile(path) 一次性将配置文件读取出来 + bytes, err2 := ioutil.ReadFile(filePath) + if err2 != nil { + fmt.Printf("error: %v\n", err) + return + } + // 使用strings工具, 将读取到的字节流按照 \r\n 回车换行切割(windows) + lines := strings.Split(string(bytes), "\r\n") + + for index, line := range lines { + line = strings.TrimSpace(line) + // 解析注释和空行 + if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") || line == "" { + continue + } else if strings.HasPrefix(line, "[") { // 解析 [ 开头的行 + + // 校验节点语法声明节点 + if line[0] == '[' && line[len(line)-1] != ']' { + err = fmt.Errorf("配置文件第%d行出现语法错误", index+1) + return + } + if line[0] == '[' && line[len(line)-1] == ']' && len(line) < 2 { + err = fmt.Errorf("配置文件第%d行出现语法错误, 节点名不能为空", index+1) + return + } + + // 解析出节点名称 + sectionName := strings.TrimSpace(line[1 : len(line)-1]) + // 通过反射解析 data , 然后将值给他填充上 + for i := 0; i < dataType.Elem().NumField(); i++ { + field := dataType.Elem().Field(i) + // todo 如果当前节点的名字 和 用户传递传递进来的结构体指针中封装的结构体相同, 先暂存一下这个节点的名字, 后续 + // 对比解析出来的配置文件中的[name] name 和 通过反射从结构体的ini部分取出的值, + // 如果相同, 那么就保存下当前结构体的名字, 因为通过反射创建出data对象之后, 需要根据这个名字获取出具体的字段, 再填充上配置文件中的值 + // 比如: 从配置文件中解析出 mysql , 通过反射创建出 Config , 取出Config第一个属性的ini也是mysql , 于是我们就将mysql对应的属性名: MysqlcConfig保存下来 + // 因为后续我们解析出 username=root时, 需要将值赋值给MysqlConfig, 但是它是从Config中通过FieldByName获取出来的, 这个name就是前面存储的 structName + if sectionName == field.Tag.Get("ini") { + // 从config的封装结构体中找出mysql,redis的配置项 + structName = field.Name + } + } + } else { //节点名称没有出先语法错误, 处理 key=value 格式的行 + + // 检查语法, 不包含 = , 报错 + if strings.Index(line, "=") == -1{ + err = fmt.Errorf("配置文件第%d行出现语法错误, 格式应该为 key=value", index+1) + return + } + // 根据=切割行 + splitLine := strings.Split(line, "=") + key:=strings.TrimSpace(splitLine[0]) + value:=strings.TrimSpace(splitLine[1]) + // 检查语法 + if key==""||value==""{ + err = fmt.Errorf("配置文件第%d行出现语法错误, 格式应该为 key=value", index+1) + return + } + + // 反射创建用户传递进来的结构体指针, 准备初始化 + v := reflect.ValueOf(data) + structObjValue := v.Elem().FieldByName(structName) // 嵌套结构体 , 现在是空值, 再往下执行给他初始化 + + // 参数校验: 确定它是结构体类型, 才做后续的初始化 + if structObjValue.Kind() != reflect.Struct{ + err = fmt.Errorf("data中的%s 应该是结构体变量...",structName) + return + } + + // 从结构体中的ini中找到和当前行属性名相同的行 + for i:=0;i < structObjValue.NumField() ;i++{ + field:=structObjValue.Type().Field(i) + if field.Tag.Get("ini") == key{ + // 初始化 + switch structObjValue.FieldByName(field.Name).Kind() { + case reflect.String: + structObjValue.Field(i).SetString(value) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + var parseInt int64 + parseInt, error := strconv.ParseInt(value, 10,64) + if error != nil { + error = fmt.Errorf("第%d行, error: %v\n", err,index+1) + return + } + structObjValue.Field(i).SetInt(parseInt) + } + break + } + } + } + } + return +} + +func main() { + + var config Config + err := loadIni("E:\\GO\\src\\code.github.com\\changwu\\2020-03-17\\parseIni\\conf.ini", &config) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } +} + +``` + diff --git a/Go/其他教程/12strconvc标准库/strconv标准库.md b/Go/其他教程/12strconvc标准库/strconv标准库.md new file mode 100644 index 00000000..acf38cfd --- /dev/null +++ b/Go/其他教程/12strconvc标准库/strconv标准库.md @@ -0,0 +1,110 @@ +### Go是强类型语言 + +Go是强类型的语言, 换句话说, 不同类型的变量之间不能随意转换 + +比如: int8 int16 int32 int64 之间虽然可以进行转换, 但是 int 和 string之间就不能随意转换 + + + +### strconv + +strconv包实现了基本**数据类型与其字符串**表示的转换 + +主要有以下常用函数: `Atoi()`、`Itia()`、parse系列、format系列、append系列 + + + +* string 转int + +```go +func Atoi(s string) (i int, err error) +``` + +* int 转 string + +```go +func Itoa(i int) string +``` + + + +> **无论是 Atoi 还是 Itoa, 其中的 i 表示 int , a代表string, 之所以用a代表string , 是因为在C语言中, 没有string, 所有C语言不得不使用array表示字符串** + + + +* 将字符串转换成指定的值 + +```go +ParseBool() +ParseFloat() +ParseInt() +ParseUint() +``` + + + +* 将其他类型的值, 格式化为string + +```go +func FormatBool(b bool) string +func FormatInt(i int64, base int) string +func FormatUint(i uint64, base int) string +func FormatFloat(f float64, fmt byte, prec, bitSize int) string +``` + + + +* 其他 + +```go +// 返回一个字符是否是可打印的,和unicode.IsPrint一样,r必须是:字母(广义)、数字、标点、符号、ASCII空格。 +func IsPrint(r rune) bool + +// 返回字符串s是否可以不被修改的表示为一个单行的、没有空格和tab之外控制字符的反引号字符串。 +func CanBackquote(s string) bool +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Go/其他教程/13并发编程/1.png b/Go/其他教程/13并发编程/1.png new file mode 100644 index 00000000..e23ba857 Binary files /dev/null and b/Go/其他教程/13并发编程/1.png differ diff --git a/Go/其他教程/13并发编程/并发编程.md b/Go/其他教程/13并发编程/并发编程.md new file mode 100644 index 00000000..524cb88a --- /dev/null +++ b/Go/其他教程/13并发编程/并发编程.md @@ -0,0 +1,1401 @@ +### 一. 并发编程 + +#### 1.1 简介: + +Java对多线程的封装类是Thread, 每一个Thread的实例都会通过JVM调用C++的方法, 和系统内核的线程做唯一的绑定, 因此, java中的多线程, 是真实的多线程, 可以利用多核系统的特性 + +但是Java从1997年诞生, 那时候还没有发展成多核机器, 因此Java在设计之初是没有考虑为多核多线程而设计的, 即使他后续推出了多线程编程, 但是相对来说依然还是挺复杂的 + +此外, Java的多线程中的线程栈空间初始大小为2兆, 那么2兆*1000 = 2G的内容, 所以, Java的多线程很吃内存, 这可不太好, 因为内存资源是很珍贵的 + + + +对于Go来说, 它诞生于09年, 那时多核机器已经问世, 它对适配多核, 在语言设计指出就有所考虑, 通过`goroutine`实现了多个函数之间的并发执行, 我们不再自己去写什么进程, 线程, 协程, 一个goroutine就够了 + +`goroutine`绑在着用户空间的线程, 通常只需要2k的内存就可以, 所以Go可以实现的高并发很容易就到达十万级别 + +#### 1.2 goroutine的使用 + +使用很简单, 只需要在func之前, 添加上go关键字就行 + +```go + +func hello() { + fmt.Println("Hello Goroutine!") +} +func main() { + go hello() + fmt.Println("main goroutine done!") +} +// 只会输出 main goroutine done! , 因为main goroutine执行结束之后, 就会结束, 子任务也被退出 +``` + +* 解决方式1: + +```go +// 可以简单粗暴的让main goroutine 睡一秒, 等待子任务的执行 +func hello() { + fmt.Println("Hello Goroutine!") +} +func main() { + go hello() + fmt.Println("main goroutine done!") + time.Sleep(time.Second) +} +``` + + + +* 解决方式2: 使用 `sync.waitGroup`来实现同步 + +```go +var wait sync.WaitGroup + +func hello() { + // 减少一次计数 + defer wait.Done() + fmt.Println("Hello Goroutine!") +} +func main() { + + for i:=0;i<=5;i++{ + // 每次启动一次, 就增加一次计数 + wait.Add(1) + go hello() + } + // 当计数为0时, 不再wait + wait.Wait() + fmt.Println("main goroutine done!") +} +``` + + + +#### 1.3 goroutine的调度 + +goroutine的调度简称: GPM + +一句话总结就是: P 管理着一组 G, 挂载到M上执行 + +* G: 就是goroutine本身, 还存放着goroutine和GPM之间绑定的信息 +* P: P里面管理着一组goroutine, 存储当前goroutine的运行时环境, 函数指针, 堆栈, 地址边界, 同时 P会去对自己管理的goroutine队列进行调度,( 比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等 , 当自己的队列中**消费完了goroutine就去全局的队列中取, 全局的goroutine队列也被消费完了, 就是其他队列里面抢** +* M:(machine) 是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的; + + + +P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。 + +P的个数是通过`runtime.GOMAXPROCS`设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。 + + + +* 单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,`goroutine`则是由Go运行时(runtime)自己的调度器调度的, + +* 调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。  +* 充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。 + + + +#### 1.4 GOMAXPROCS + +Go运行时的调度器使用`GOMAXPROCS`参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n) + +**Go语言中可以通过`runtime.GOMAXPROCS()`函数设置当前程序并发时占用的CPU逻辑核心数。** + +**Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。** + +我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子 + +```go +func main() { + runtime.GOMAXPROCS(1) + go a() + go a() + go a() +} +``` + + + +### 二 . channel + +#### 2.01 channel 简介: + +在Java中, JMM规范要求, 如果两条线程之间想通信的话, 需要借助于主存中的变量区完成通信 + +**在Go中, 两个并发执行的函数可以通过channel进行数据的交互** + +Go的并发模型是`CSP (communicating sequential processor)` 即, 通过通信来共享内存, 而不是通过内存来实现通信 + +在Go中, 通过channel去实现各个goroutine之间的通信, chanel是一个特殊类型, 遵循先进先出 (FIFO)的规则, 保证收发信息的顺序, 每一个channel都有他具体的类型, 声明通道时, 需要指定这个类型 + + + +#### 2.02 channel的类型 + +```go +var ch1 chan int +var ch2 chan string +var ch3 chan bool +var ch4 chan []int +... +``` + + + +#### 2.03 创建通道 + +通道声明后, **还得为他分配空间才能使用** + +可以通过make函数设置通道缓冲区的大小 + +```go +func main() { + var ch chan int + ch4 := make(chan int) + ch5 := make(chan bool) + ch6 := make(chan []int) + ch7 := make(chan []int,15) + + fmt.Println(ch) // + fmt.Println(ch4) // <0xc00001a120> + fmt.Println(ch5) // <0xc00001a180> + fmt.Println(ch6) // <0xc00001a1e0> + fmt.Println(ch7) // <0xc00003e060> +} +``` + +#### 2.04 channel的操作 + +```go +// 创建 +ch := make(chan int ,15) +// 将值写入通道: +ch <- 10 + +// 取出存储value中 +value := <- ch + +// 取出并忽略 +<- ch + +// 关闭通道, 只有在通知接收方所有的数据都发送完毕的时候, 才需要关闭通道 +// 通道是可以被强制回收的, 所以不一定非得关闭通道 +close(ch) +``` + + + +#### 2.05 有缓冲的channel + +当缓冲信道达到满的状态的时候,就会表现出阻塞了,因为这时再也不能承载更多的数据了,「你们必须把 数据拿走,才可以流入数据」。 + +在声明一个信道的时候,我们给make以第二个参数来指明它的容量(默认为0,即无缓冲): + +```go + // 写入2个元素都不会阻塞当前goroutine, 存储个数达到2的时候会阻塞 + // 如果被阻塞后, 还有没其他的goroutine去消费这个channel, 就会爆出死锁 +var ch chan int = make(chan int, 2) +``` + +如下的例子,缓冲信道ch可以无缓冲的流入3个元素: + +```go +func main() { + ch := make(chan int, 3) + ch <- 1 + ch <- 2 + ch <- 3 +} +``` + +如果你再试图流入一个数据的话,信道ch会阻塞main线, 报死锁。 + +也就是说,缓冲信道会在满容量的时候加锁。 + +其实,缓冲信道是先进先出的,我们可以把缓冲信道看作为一个线程安全的队列: + +```go +func main() { + ch := make(chan int, 3) + ch <- 1 + ch <- 2 + ch <- 3 + + fmt.Println(<-ch) // 1 + fmt.Println(<-ch) // 2 + fmt.Println(<-ch) // 3 +} +``` + + + + + +```go +// 缓冲区大小是15的通道 +ch7 := make(chan []int,15) + +func main() { + ch := make(chan int,3) + for i:=3;i<8 ;i++ { + ch <-i + } +} + +// 向有缓冲区的通道中, 只存入, 当超出缓冲区大小的容量时, 就会报错 死锁 +fatal error: all goroutines are asleep - deadlock! +goroutine 1 [chan send]: +main.main() +``` + + + +#### 2.06 无缓冲区的channel + +如下: + +```go + // 没有缓冲区的通道 + ch6 := make(chan []int) +``` + + + +例: 当向没有缓冲区的channel写入数据, 同时也没有其他的goroutine去尝试获取锁, 就会报错死锁 + +> **因为我们使用`ch := make(chan int)`创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。** + +```go +func main() { + ch := make(chan int) + ch <- 10 +} + +fatal error: all goroutines are asleep - deadlock! +goroutine 1 [chan send]: +main.main() + + +// 可以向下面这样, 找个goroutine去消费它 +func hello(ch <-chan int) { + var value = <- ch + fmt.Println("value: ",value) +} + +func main() { + ch := make(chan int) + go hello(ch) + ch <- 10 +} +``` + +无缓冲的信道永远不会存储数据,只负责数据的流通 + +* 从无缓冲信道取数据,必须要有数据流进来才可以,否则当前线阻塞 +* 数据流入无缓冲信道, 如果没有其他goroutine来拿走这个数据,那么当前线阻塞 + + + +#### 2.07 无缓冲信道的数据进出顺序 + +我们已经知道,无缓冲信道从不存储数据,流入的数据必须要流出才可以 + +```go +var ch chan int = make(chan int) + +func foo(id int) { //id: 这个routine的标号 + ch <- id +} + +func main() { + // 开启5个routine + for i := 0; i < 5; i++ { + go foo(i) + } + + // 取出信道中的数据 + for i := 0; i < 5; i++ { + fmt.Print(<- ch) + } +} +``` + +我们开了5个goroutine,然后又依次取数据。其实整个的执行过程细分的话,5个线的数据 依次流过信道ch, main打印之, 而宏观上我们看到的即 无缓冲信道的数据是先到先出,但是 无缓冲信道并不存储数据,只负责数据的流通 + + + + + + + +#### 2.08 体会从空channel中获取值会被阻塞 + +示例1: + +```go +// 可以向下面这样, 找个goroutine去消费它 +func hello(ch <-chan int) { + fmt.Println("准备获取值 value: ") + var value = <- ch // 被阻塞住,等到main往channel中存入值 + fmt.Println("value: ",value) +} + +func main() { + ch := make(chan int) + go hello(ch) + + time.Sleep(time.Second*5) + ch <- 10 + time.Sleep(time.Second*5) +} +``` + +示例2: + +```go +var complete chan int = make(chan int) + +func loop() { + for i := 0; i < 10; i++ { + fmt.Printf("%d ", i) + } + + complete <- 0 // 执行完毕了,发个消息 +} + + +func main() { + go loop() + <- complete // 直到线程跑完, 取到消息. main在此阻塞住 +} +``` + + + +> 默认的,信道的存消息和取消息都是阻塞的 (叫做无缓冲的信道,不过缓冲这个概念稍后了解,先说阻塞的问题)。 +> +> 也就是说, 无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好。 + +#### 2.09 for range从通道循环取值 + +* 如果通道已经关闭了, 还往里面写入值, 就会爆出 panic +* for range 可以自动判断channel是否关闭了, 当 channel 关闭后, for range也会自动退出, 这是普遍的做法 + + + +```go +func main() { + ch1 := make(chan int) + ch2 := make(chan int) + + go func() { + for i := 0; i < 100; i++ { + ch1 <- i + } + // todo 发送完毕后, 关闭channel + close(ch1) + }() + + go func() { + for { + // todo channel 关闭后, 得到的ok = false + v, ok := <-ch1 + if !ok { + break + } + ch2 <- v * v + } + // 关闭channel + close(ch2) + }() + + // todo 通常使用 for range 去遍历channel, channel关闭后, 会自动退出 + for value := range ch2 { + fmt.Println(value) + } +} +``` + + + +#### 2.10 单向通道 + +首先得知道, 一个通道, 它既可以被写, 也可以被读 + +但是, 我们可以限制什么情况下它只能被写, 什么情况下只能被读 + +就比如下面的例子, 通道 ch , 默认的可以对ch进行读写, 也可以像下面那样, 限制` inChannel`函数, 只能往通道里面写, 限制 outerChannel函数, 只能从通道里面读 + +```go +// 只能往里面写的通道如下: +func inChannel(ch chan<- int){ + ch <- 10 + close(ch) +} + +// 只能往外读 +func outerChannel( ch <-chan int){ + v:= <- ch + close(ch) +} + +``` + +#### 2.11 worker pool + +worker pool 实际上就是 goroutine 池 , + +```go +// 开启指定数量的 goroutine +func worker(number int, jobs <-chan int, results chan<- int) { + for i:=1;i<=number;i++{ + go func(i int) { + for j := range jobs { + fmt.Printf("worker:%d start job:%d\n", i, j) + time.Sleep(time.Second) + fmt.Printf("worker:%d end job:%d\n", i, j) + results <- j * 2 + } + }(i) + } +} + +func main() { + jobs := make(chan int, 100) + results := make(chan int, 100) + // 开启3个goroutine + worker(3, jobs, results) + // 添加5个任务 + for j := 1; j <= 5; j++ { + jobs <- j + } + close(jobs) + time.Sleep(time.Second*6) +} + +``` + + + +#### 2.12 select + +Go内置了select 关键字, 可以同时监听多个channel, 在接收和发送的过程中同样会发生阻塞等待 + + + +```go +select{ + case <-ch1: + ... + case data := <-ch2: + ... + case ch3<-data: + ... + default: + 默认操作 +} + +示例: +func main() { + ch1 := make(chan int) + ch2 := make(chan int) + select{ + case value1:=<-ch1: + fmt.Println("ch1",value1) + case value2 := <-ch2: + fmt.Println("ch2",value2) + default: + fmt.Println("default") + } +} +//结果: +default + + +// 示例2 +func main() { + ch1 := make(chan int,1) + ch2 := make(chan int,1) + ch1 <- 10 + ch2 <- 10 + + select{ + case value1:=<-ch1: + fmt.Println("ch1",value1) + case value2 := <-ch2: + fmt.Println("ch2",value2) + default: + fmt.Println("default") + } +} +结果: +ch1 10 + + + +// 示例3: +func main() { + ch1 := make(chan int,1) + ch2 := make(chan int,1) + ch3 := make(chan int,1) + ch4 := make(chan int,1) + ch5 := make(chan int,1) + ch6 := make(chan int,1) + ch7 := make(chan int,1) + ch1 <- 10 + ch2 <- 10 + ch3 <- 10 + ch4 <- 10 + ch5 <- 10 + ch6 <- 10 + ch7 <- 10 + + for i:=1;i<=8 ;i++ { + select{ + case value1:=<-ch1: + fmt.Println("ch1",value1) + case value2 := <-ch2: + fmt.Println("ch2",value2) + case value2 := <-ch3: + fmt.Println("ch3",value2) + case value2 := <-ch4: + fmt.Println("ch4",value2) + case value2 := <-ch5: + fmt.Println("ch5",value2) + case value2 := <-ch6: + fmt.Println("ch6",value2) + case value2 := <-ch7: + fmt.Println("ch7",value2) + default: + fmt.Println("default") + } + } +} + +// 结果: 表名 select会随机选中case进行处理 +ch2 10 +ch6 10 +ch5 10 +ch7 10 +ch3 10 +ch4 10 +ch1 10 +default + + +// 示例4: +func main() { + ch1 := make(chan int,1) + ch2 := make(chan int,1) + ch1 <- 10 + ch2 <- 10 + + for i:=1;i<=3 ;i++ { + select{ + case value1:=<-ch1: + fmt.Println("ch1",value1) + time.Sleep(time.Second*5) + case value2 := <-ch2: + fmt.Println("ch2",value2) + default: + fmt.Println("default") + } + } +} +// 证明, 任意一个case出现阻塞的情况, 整个select 都被阻塞 +``` + +select的好处: + +* 可处理一个或多个channel的发送/接收操作。 +* 如果多个`case`同时满足,`select`会随机选择一个。 +* 对于没有`case`的`select{}`会一直等待,可用于阻塞main函数 + + + + + +### 三. 并发和并行 + +什么是并发, 什么是并行? + +简单举例来说: + +* 两个队列,一个Coffee机器,那是并发 +* 两个队列,两个Coffee机器,那是并行 + +说白了其实就是如果我们的电脑只有一个CPU, 那最多只能做到并发 , 而 如果我们的电脑本身就是多核的, 多个CPU之间可以做大并行执行 , 那我们的go程序就可以变成并行 + +看这个示例: + +```go +var quit chan int = make(chan int) + +func loop() { + for i := 0; i < 10; i++ { + fmt.Printf("%d ", i) + } + quit <- 0 +} + +func main() { + // 在go1.5之前, 默认是1 + runtime.GOMAXPROCS(1) // 最多使用2个核 + // 开两个goroutine跑函数loop, loop函数负责打印10个数 + go loop() + go loop() + + for i := 0; i < 2; i++ { + <-quit + } +} + +``` + + + +如果将GOMAXPROCES设置值>1 , 则会出现争抢执行的效果 (go 1.5之后, 默认的GOMAXPROCES值才是 机器的最大的核心数) , 且, 只有当GOMAXPROCS设置的值大于1时, 才可能是真正的并行运行 + + + +还可以像这样, 显示的让CPU让出执行的时间 + +```go +func loop() { + for i := 0; i < 10; i++ { + runtime.Gosched() // 显式地让出CPU时间给其他goroutine + fmt.Printf("%d ", i) + } + quit <- 0 +} + + +func main() { + + go loop() + go loop() + + for i := 0; i < 2; i++ { + <- quit + } +} +结果: +0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 +``` + + + +### 四. runtime 调度器 + +* Gosched: 让当前goroutine让出cpu +* NumCPU: 返回当前系统的CPU核心数 +* GOMAXPROCS: 设置当前可同时使用的CPU数 +* Goexit: 退出当前goroutine , 但是 defer 依然会正常执行 + + + + + +### 五. 并发安全和锁 + +多个goroutine 操作同一个数据, 如果不加锁, 很容易出现并发异常, 导致数据不准确, 和Java一样, Go也提供了一些列的锁用来同步各个goroutine的计算过程 + + + +#### 5.1 互斥锁 + +出现线程安全的demo + +```go +var wait sync.WaitGroup + +var x int + +func add() { + for i := 1; i <= 5000; i++ { + x++ + } + wait.Done() +} + +func main() { + + wait.Add(2) + go add() + go add() + wait.Wait() + fmt.Println(x) // 7656 +} +``` + +解决: 互斥锁`sync.Mutex` 它的用法和java中的ReentranLock相仿, 示例如下 + +```go +var wait sync.WaitGroup +var lock sync.Mutex + +var x int + +func add() { + for i := 1; i <= 5000; i++ { + lock.lock() + x++ + lock.unlock() + } + wait.Done() +} + +func main() { + + wait.Add(2) + go add() + go add() + wait.Wait() + fmt.Println(x) // 7656 +} +``` + +> **使用 sync.Mutex 加锁, 被唤醒的几率是随机的** + + + +#### 5.2 读写锁 + +Go提供的读写锁是 `sync.RWMutex` + +遵循: + +* 读读不互斥 +* 读写互斥 +* 写写互斥 + +> 需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。 + + + +#### 5.3 几个死锁的例子 + +1. 只在单一的goroutine里操作无缓冲信道,一定死锁。比如你只在main函数里操作信道: + +```go +func main() { + ch := make(chan int) + ch <- 1 // 1流入信道,堵塞当前线, 没人取走数据信道不会打开 + fmt.Println("This line code wont run") //在此行执行之前Go就会报死锁 +} +``` + +1. 如下也是一个死锁的例子: + +其中主线等ch1中的数据流出,ch1等ch2的数据流出,但是ch2等待数据流入,两个goroutine都在等,也就是死锁。 + +```go +var ch1 chan int = make(chan int) +var ch2 chan int = make(chan int) + +func say(s string) { + fmt.Println(s) + ch1 <- <- ch2 // ch1 等待 ch2流出的数据 +} + +func main() { + go say("hello") + <- ch1 // 堵塞主线 +} +``` + +1. 总结来看,为什么会死锁?非缓冲信道上如果发生了流入无流出,或者流出无流入,也就导致了死锁。或者这样理解 Go启动的所有goroutine里的非缓冲信道一定要一个线里存数据,一个线里取数据,要成对才行 。所以下面的示例一定死锁: + +```go +c, quit := make(chan int), make(chan int) + +go func() { + c <- 1 // c通道的数据没有被其他goroutine读取走,堵塞当前goroutine + quit <- 0 // quit始终没有办法写入数据 +}() + +<- quit // quit 等待数据的写 +``` + + + + + +#### 5.4 sync.WaitGroup + +我们一般都是使用它去防止main goroutine提前终止, 而中断子 goroutine + +他的作用和Java中的CountDownLatch相仿 + +| 方法名 | 功能 | +| ------------------------------- | ------------------- | +| (wg * WaitGroup) Add(delta int) | 计数器+delta | +| (wg *WaitGroup) Done() | 计数器-1 | +| (wg *WaitGroup) Wait() | 阻塞直到计数器变为0 | + +#### 5.5 sync.Once + +编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。 + +`sync.Once`只有一个`Do`方法,其签名如下: + +```go +func (o *Once) Do(f func()) {} +``` + +实例如下: 只会打印一次 hi + +```go +var once sync.Once + +func add() { + for i := 1; i <= 5000; i++ { + once.Do(func() { + fmt.Println("hi") + }) + } + wait.Done() +} + +func main() { + wait.Add(2) + go add() + go add() + go add() + go add() + go add() + wait.Wait() +} +``` + + + +#### 5.6 单例模式 + +借助`sync.Once`的特性去做这件事 + +```go +type singleton struct { + name string +} +var once sync.Once +var instance *singleton + +func getInstance()(*singleton){ + once.Do(func() { + instance = &singleton{name:"tom"} + }) + return instance +} +``` + + + +#### 5.7 sync.Map + +Go语言中提供的原生map是不能保证并发安全的, 在sync包中的Map可以保证并发安全的特性 + +sync.Map 内置了诸如: `Store , Load , LoadOrStore , Delete , Range` 方法, 都可以保证线程安全 + + + +#### 5.8 原子操作 + +Demo + +```go +// 导入原子类相关的包 +import ( + "fmt" + "sync/atomic" +) + +func main() { + // 注意这里不能写 index := 1 , 因为下loadInt32, 编译都过不去 + index := int32(1) + // 传递地址 + loadInt32 := atomic.LoadInt32(&index) + // 添加的动作也是传递地址 + atomic.AddInt32(&loadInt32,1) + fmt.Println(loadInt32) +} + +``` + +* atomic常用的API如下: + +```go +// 加载一个原子类型的数 loadXXX +func LoadInt32(addr *int32) (val int32) +func LoadInt64(addr *int64) (val int64) +func LoadUint32(addr *uint32) (val uint32) +func LoadUint64(addr *uint64) (val uint64) +func LoadUintptr(addr *uintptr) (val uintptr) +func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) + +// 写入操作, 它会对原有的值进行一次覆盖 +func StoreInt32(addr *int32, val int32) +func StoreInt64(addr *int64, val int64) +func StoreUint32(addr *uint32, val uint32) +func StoreUint64(addr *uint64, val uint64) +func StoreUintptr(addr *uintptr, val uintptr) +func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) + +// 累加 +func AddInt32(addr *int32, delta int32) (new int32) +func AddInt64(addr *int64, delta int64) (new int64) +func AddUint32(addr *uint32, delta uint32) (new uint32) +func AddUint64(addr *uint64, delta uint64) (new uint64) +func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) + +// 交换值 +func SwapInt32(addr *int32, new int32) (old int32) +func SwapInt64(addr *int64, new int64) (old int64) +func SwapUint32(addr *uint32, new uint32) (old uint32) +func SwapUint64(addr *uint64, new uint64) (old uint64) +func SwapUintptr(addr *uintptr, new uintptr) (old uintptr) +func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) + +// CAS 比较+交换 +func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) +func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool) +func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool) +func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool) +func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool) +func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) +``` + + + +#### 5.9 Cond + +条件等待Cond + +示例: + +```go +func main() { + condition:=false + + var lock sync.Mutex + cond:=sync.NewCond(&lock) + + go func() { + lock.Lock() + condition=true + // 模拟其他耗时操作 + time.Sleep(time.Second*3) + // 通过signal使得 + cond.Signal() + lock.Unlock() + }() + + + /* + 官方要求的写法 + c.L.Lock() + for !condition() { + c.Wait() + } + // 执行条件满足之后的动作... + c.L.Unlock() + */ + + lock.Lock() + // 为了防止虚假唤醒, wait放置到条件循环中 + for !condition{ + cond.Wait() + } + fmt.Println("条件满足, 开始后续的动作") + lock.Unlock() + +} +``` + +文档: + +```go +type Cond struct { + L Locker // 在“检查条件”或“更改条件”时 L 应该锁定。 +} + +// 创建一个条件等待 +func NewCond(l Locker) *Cond + +// Broadcast 唤醒所有等待的 Wait,建议在“更改条件”时锁定 c.L,更改完毕再解锁。 +func (c *Cond) Broadcast() + +// Signal 唤醒一个等待的 Wait,建议在“更改条件”时锁定 c.L,更改完毕再解锁。 +func (c *Cond) Signal() + +// Wait 会解锁 c.L 并进入等待状态,在被唤醒时,会重新锁定 c.L +func (c *Cond) Wait() +``` + + + +### 六. Pool 临时对象池 + +```go + +type Pool struct { + noCopy noCopy + + local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal + localSize uintptr // size of the local array + + // New optionally specifies a function to generate + // a value when Get would otherwise return nil. + // It may not be changed concurrently with calls to Get. + New func() interface{} +} + + +// Local per-P Pool appendix. +type poolLocalInternal struct { + private interface{} // Can be used only by the respective P. + shared []interface{} // Can be used by any P. + Mutex // Protects shared. +} + +type poolLocal struct { + poolLocalInternal + + // Prevents false sharing on widespread platforms with + // 128 mod (cache line size) = 0 . + pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte +} +``` + +Pool 用于存储临时对象,它将使用完毕的对象存入对象池中,在需要的时候取出来重复使用,目的是为了避免重复创建相同的对象造成 GC 负担过重。其中存放的临时对象随时可能被 GC 回收掉(如果该对象不再被其它变量引用) + + + +从 Pool 中取出对象时,如果 Pool 中没有对象,将返回nil,但是如果给 Pool.New 字段指定了一个函数的话,Pool 将使用该函数创建一个新对象返回。 + + + + Pool 可以安全的在多个例程中并行使用,但 Pool 并不适用于所有空闲对象,Pool 应该用来管理并发的例程共享的临时对象,而不应该管理短寿命对象中的临时对象,因为这种情况下内存不能很好的分配,这些短寿命对象应该自己实现空闲列表。Pool 在开始使用之后,不能再被复制。 + + + +Pool是提供给外部使用的对象。其中的local成员的真实类型是一个poolLocal数组,localSize是数组长度。poolLocal是真正保存数据的地方。priveate保存了一个临时对象,shared是保存临时对象的数组。   + + + +为什么Pool中需要这么多poolLocal对象呢?实际上,Pool是给每个线程分配了一个poolLocal对象。也就是说local数组的长度,就是工作线程的数量(size := runtime.GOMAXPROCS(0))。当多线程在并发读写的时候,通常情况下都是在自己线程的poolLocal中存取数据。当自己线程的poolLocal中没有数据时,才会尝试加锁去其他线程的poolLocal中“偷”数据。 + +```go +func (p *Pool) Get() interface{} { + if race.Enabled { + race.Disable() + } + l := p.pin() //获取当前线程的poolLocal对象,也就是p.local[pid]。 + x := l.private + l.private = nil + runtime_procUnpin() + if x == nil { + l.Lock() + last := len(l.shared) - 1 + if last >= 0 { + x = l.shared[last] + l.shared = l.shared[:last] + } + l.Unlock() + if x == nil { + x = p.getSlow() + } + } + if race.Enabled { + race.Enable() + if x != nil { + race.Acquire(poolRaceAddr(x)) + } + } + if x == nil && p.New != nil { + x = p.New() + } + return x +} +``` + +为什么这里要锁住。答案在getSlow中。因为当shared中没有数据的时候,会尝试去其他的poolLocal的shared中偷数据。Pool.Get的时候,首先会在local数组中获取当前线程对应的poolLocal对象。如果private中有数据,则取出来直接返回。如果没有则先锁住shared,有数据则直接返回。 + + + +Go语言的goroutine虽然可以创建很多,但是真正能物理上并发运行的goroutine数量是有限的,是由runtime.GOMAXPROCS(0)设置的。所以这个Pool高效的设计的地方就在于将数据分散在了各个真正并发的线程中,每个线程优先从自己的poolLocal中获取数据,很大程度上降低了锁竞争。 + + + +### 七. 正确使用锁的八个注意点 + +1. 尽量减少锁的持有时间,毕竟使用锁是有代价的,通过减少锁的持有时间来减轻这个代价: + +* 细化锁的粒度。通过细化锁的粒度来减少锁的持有时间以及避免在持有锁操作的时候做各种耗时的操作。 +* 不要在持有锁的时候做 IO 操作。尽量只通过持有锁来保护 IO 操作需要的资源而不是 IO 操作本身: + +```go + +func doSomething() { + m.Lock() + item := ... + http.Get() // 各种耗时的 IO 操作 + m.Unlock() +} + +// 改为 +func doSomething() { + m.Lock() + item := ... + m.Unlock() + + http.Get() +} +``` + + + + + +2. 善用 defer 来确保在函数内正确释放了锁 + +尤其是在那些内部有好几个通过 if err != nil 判断来提前返回的函数中,通过 defer 可以确保不会遗漏释放锁操作,避免出现死锁问题,以及避免函数内非预期的 panic 导致死锁的问题: + +```go +func doSomething() { + m.Lock() + defer m.Unlock() + + err := ... + if err != nil { + return + } + + err = ... + if err != nil { + return + } + + ... + return +} +``` + +3. 不过使用 defer 的时候也要注意别因为习惯性的 defer m.Unlock() 导致无意中在持有锁的时候做了 IO 操作,出现了非预期的持有锁时间太长的问题。 + +```go +// 非预期的在持有锁期间做 IO 操作 +func doSomething() { + m.Lock() + defer m.Unlock() + + item := ... + http.Get() // 各种耗时的 IO 操作 +} +``` + + + +4. copy 结构体操作可能导致非预期的死锁 + +copy 结构体时,如果结构体中有锁的话,记得重新初始化一个锁对象,否则会出现非预期的死锁: + +```go +type User struct { + sync.Mutex + + name string +} + +func main() { + u1 := &User{name: "test"} + u1.Lock() + defer u1.Unlock() + + tmp := *u1 // 取出u1的值 + u2 := &tmp // 将指向u1的指针赋值给u2 , 实现结构体的拷贝 + // u2.Mutex = sync.Mutex{} // 没有这一行就会死锁 + + fmt.Printf("%#p\n", u1) + fmt.Printf("%#p\n", u2) + + u2.Lock() + defer u2.Unlock() +} +``` + + + +5. build/test 时使用 -race 参数以便运行时检测数据竞争问题 + +可以在执行 go build 或 go test 时增加一个 -race 参数来开启数据竞争检测功能,通过这种方式来实现在本地开发环境/CI/测试环境阶段发现程序中可能存在的数据竞争问题: + +```go +type Todo struct { + sync.Mutex + + tasks []string +} + +func (t *Todo) do() { + for _, task := range t.tasks { + fmt.Println(task) + } +} + +func (t *Todo) Add(task string) { + t.Lock() + defer t.Unlock() + + t.tasks = append(t.tasks, task) +} + +func main() { + t := &Todo{} + + for i := 0; i < 2; i++ { + go t.Add(fmt.Sprintf("%d", i)) + } + for i := 0; i < 2; i++ { + t.do() + } +} + + +///////// +$ go build -race -o main . +$ +$ ./main +================== +WARNING: DATA RACE +Read at 0x00c0000a0048 by main goroutine: + main.(*Todo).do() + /Users/xxx/tmp/golang/race/main.go:15 +0x42 + main.main() + /Users/xxx/tmp/golang/race/main.go:34 +0x154 + +Previous write at 0x00c0000a0048 by goroutine 6: + main.(*Todo).Add() + /Users/xxx/tmp/golang/race/main.go:24 +0x11d + +Goroutine 6 (finished) created at: + main.main() + /Users/xxx/tmp/golang/race/main.go:31 +0x127 +================== +0 +================== +WARNING: DATA RACE +Read at 0x00c0000b0010 by main goroutine: + main.(*Todo).do() + /Users/xxx/tmp/golang/race/main.go:15 +0x85 + main.main() + /Users/xxx/tmp/golang/race/main.go:34 +0x154 + +Previous write at 0x00c0000b0010 by goroutine 7: + main.(*Todo).Add() + /Users/xxx/tmp/golang/race/main.go:24 +0xe3 + +Goroutine 7 (finished) created at: + main.main() + /Users/xxx/tmp/golang/race/main.go:31 +0x127 +================== +1 +0 +1 +Found 2 data race(s) +``` + + + +6. 使用 go-deadlock 检测死锁或锁等待问题 + +上面说的在持有锁的时候做 IO 操作或其他非预期的耗时超时的问题,一方面需要在写程序的时候注意一下,另一方面也有可能是无意中代入进去的(比如上面提到的习惯性 defer 导致的)。对于那些无意中代入进去的锁等待的问题人为的去 review 的话通常很难发现,此时就需要用工具来检测了。恰好有一个叫 [go-deadlock](https://github.com/sasha-s/go-deadlock) 的工具可以实现这个功能。 ' + +```go +import ( + "net/http" + "time" + + sync "github.com/sasha-s/go-deadlock" +) + +var mu sync.Mutex +var url = "http://baidu.com:90" + +func do() { + mu.Lock() + defer mu.Unlock() + + u := url + http.Get(u) // 非预期的在持有锁期间做 IO 操作,导致锁等待时间变长 +} + +func main() { + // 检测超过 100 ms 的锁等待 + sync.Opts.DeadlockTimeout = time.Millisecond * 100 + + var wg sync.WaitGroup + for i := 0; i < 3; i++ { + wg.Add(1) + go func() { + defer wg.Done() + do() + }() + } + + wg.Wait() +} + +///////////////////////// +$ go run main.go +POTENTIAL DEADLOCK: +Previous place where the lock was grabbed +goroutine 36 lock 0x1483b90 +main.go:14 main.do { mu.Lock() } <<<<< +main.go:30 main.main.func1 { do() } + +Have been trying to lock it again for more than 100ms +goroutine 35 lock 0x1483b90 +main.go:14 main.do { mu.Lock() } <<<<< +main.go:30 main.main.func1 { do() } + +Here is what goroutine 36 doing now +goroutine 36 [select]: +net/http.(*Transport).getConn(0x14616c0, 0xc00015e150, 0x0, 0x128adb3, 0x4, 0xc000014100, 0xc, 0x0, 0x0, 0xc0000559e8) + /usr/local/Cellar/go/1.11/libexec/src/net/http/transport.go:1004 +0x58e +net/http.(*Transport).roundTrip(0x14616c0, 0xc000160000, 0x203000, 0xc000055c90, 0x11d823a) + /usr/local/Cellar/go/1.11/libexec/src/net/http/transport.go:451 +0x690 +net/http.(*Transport).RoundTrip(0x14616c0, 0xc000160000, 0x14616c0, 0x0, 0x0) + /usr/local/Cellar/go/1.11/libexec/src/net/http/roundtrip.go:17 +0x35 +net/http.send(0xc000160000, 0x12c78a0, 0x14616c0, 0x0, 0x0, 0x0, 0xc00000e030, 0x1708000, 0xc000055d20, 0x1) + /usr/local/Cellar/go/1.11/libexec/src/net/http/client.go:250 +0x14b +net/http.(*Client).send(0x1466200, 0xc000160000, 0x0, 0x0, 0x0, 0xc00000e030, 0x0, 0x1, 0x0) + /usr/local/Cellar/go/1.11/libexec/src/net/http/client.go:174 +0xfa +net/http.(*Client).do(0x1466200, 0xc000160000, 0x0, 0x0, 0x0) + /usr/local/Cellar/go/1.11/libexec/src/net/http/client.go:641 +0x2a8 +net/http.(*Client).Do(0x1466200, 0xc000160000, 0x128adb3, 0x13, 0x0) + /usr/local/Cellar/go/1.11/libexec/src/net/http/client.go:509 +0x35 +net/http.(*Client).Get(0x1466200, 0x128adb3, 0x13, 0xc0000220c0, 0x12412c0, 0xc000055f80) + /usr/local/Cellar/go/1.11/libexec/src/net/http/client.go:398 +0x9d +net/http.Get(0x128adb3, 0x13, 0x1483b90, 0x0, 0xc000114fb8) + /usr/local/Cellar/go/1.11/libexec/src/net/http/client.go:370 +0x41 +main.do() + /Users/xxx/tmp/golang/deadlock/main.go:18 +0x75 +main.main.func1(0xc00009c3f4) + /Users/xxx/tmp/golang/deadlock/main.go:30 +0x48 +created by main.main + /Users/xxx/tmp/golang/deadlock/main.go:28 +0x83 + +exit status 2 +``` + + + +7. 使用go mutux实现 tryLock功能: + + + +8. 改为使用 channel + +有些时候可能使用 channel 会更符合需求,对于这些更适合 channel 的场景可以改为使用 channel 而不是 lock \ No newline at end of file diff --git a/Go/其他教程/14网络编程/网络编程.md b/Go/其他教程/14网络编程/网络编程.md new file mode 100644 index 00000000..ea8df3d3 --- /dev/null +++ b/Go/其他教程/14网络编程/网络编程.md @@ -0,0 +1,631 @@ +## 一、net 包 + +### 1.1 Go-Tcp-Server + +思路, 通过Go提供的API创建出Server端, 然后做如下事 + +1. 创建Server服务端, 绑定监听ip +2. 监听端口 +3. 接受客户端请求, 建立连接 +4. 创建goroutine处理链接 + +```go +package main +import ( + "bufio" + "fmt" + "net" +) + +func process(conn net.Conn) { + defer conn.Close() + for { + reader := bufio.NewReader(conn) + var buf [128]byte + num, err := reader.Read(buf[:]) + if err != nil { + fmt.Printf("read from client error: %v\n", err) + return + } + reciveStr := string(buf[:num]) + fmt.Println("收到客户端的消息: ", reciveStr) + conn.Write([]byte(reciveStr)) + } +} + + +func RunServer(){ + + // 绑定ip , 监听端口 + listen, err := net.Listen("tcp", "127.0.0.1:20000") + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println("server has been start") + // 接受连接 + for { + conn, err := listen.Accept() + if err != nil { + fmt.Printf("error: %v\n", err) + continue + } + // 开启goroutine处理连接 + go process(conn) + } +} + +func main() { + RunServer() +} + + +``` + +### 1.2 Go-Tcp-Client + +```go +package main + +import ( + "bufio" + "fmt" + "net" + "os" + "strings" +) + +func RunClient() { + conn, err := net.Dial("tcp", "127.0.0.1:20000") + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + defer conn.Close() + fmt.Println("client has been start") + inputReader := bufio.NewReader(os.Stdin) + for { + // 处理用户输入 + line, err := inputReader.ReadString('\n') + if err != nil { + fmt.Printf("error: %v\n", err) + break + } + line = strings.TrimSpace(line) + + // 添加退出逻辑 + if strings.ToUpper(line) == "EXIT" { + break + } + // 发送消息 + _, err2 := conn.Write([]byte(line)) + if err2 != nil { + fmt.Printf("error: %v\n", err) + break + } + + // 从服务端读取消息 + arr := [512]byte{} + n ,err:=conn.Read(arr[:]) + if err != nil { + fmt.Printf("error: %v\n", err) + break + } + + // 输出消息 + fmt.Println("从服务端收到: ",string(arr[:n])) + } +} + +func main() { + RunClient() +} + +``` + +### 1.3 Tcp的黏包问题 + + + +#### 1.3.1为什么会出现TCP黏包的问题 + +* **原因之一: ** + + Nagle算法, 它是为了改善网络传输效率, 简单来说就是当我们想提交一部分数据发送出去, Nagle算法不会立刻就把数据发送走, 而是等待一小段时间, 看看还有没有其他的数据需要发送, 如果有的话就将这两段数据一起发送, 改善了网络传输的效率, 但是每次发送数据也变的不确定起来 + + + +* **原因之二:** + + 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据 + + + +#### 1.3.2 如何解决 + +出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。 + +所以可以自定义协议去实现 + +```go +// 比如client 和 server 约定, 协议的前4个字节规定数据的长度 +// server先解析出数据的长度, 然后再去根据指定的长度读取数据就拿到完整的, 不粘连的数据 + + +// socket_stick/proto/proto.go +package proto + +import ( +"bufio" +"bytes" +"encoding/binary" +) + +// Encode 将消息编码 +func Encode(message string) ([]byte, error) { + // 读取消息的长度,转换成int32类型(占4个字节) + var length = int32(len(message)) + // pkg是指针, 指向带有缓存的buffer内存地址 + var pkg = new(bytes.Buffer) + // 写入消息头 , Write是将 最后一个参数写入 第一个参数的缓存中 + err := binary.Write(pkg, binary.LittleEndian, length) + if err != nil { + return nil, err + } + // 写入消息实体 + err = binary.Write(pkg, binary.LittleEndian, []byte(message)) + if err != nil { + return nil, err + } + return pkg.Bytes(), nil +} + +// Decode 解码消息 +func Decode(reader *bufio.Reader) (string, error) { + // 读取消息的长度 + lengthByte, _ := reader.Peek(4) + // 创建和消息长度相同的缓冲区 + lengthBuff := bytes.NewBuffer(lengthByte) + var length int32 + // todo 注意这里的read方法, 将缓冲区的长度读取入length中, 需要给他传递进一个指针进去 + err := binary.Read(lengthBuff, binary.LittleEndian, &length) + if err != nil { + return "", err + } + // Buffered返回缓冲中现有的可读取的字节数。 + if int32(reader.Buffered()) < length+4 { + return "", err + } + + // 创建 (4+数据长度)字节的切片, 临时存储 + pack := make([]byte, int(4+length)) + // 从reader里面读取当前大小的数据 4+下一条完整的信息的长度 大小的数据 + _, err = reader.Read(pack) + if err != nil { + return "", err + } + // 将开头的4字节切到, 剩下的就是完整的一条数据 + return string(pack[4:]), nil +} + +``` + + + +### 1.4 Go-UDP-Server + +```go +import ( + "fmt" + "net" +) +func main() { + listen, err := net.ListenUDP("udp", &net.UDPAddr{ + IP: net.IPv4(0, 0, 0, 0), + Port: 20000, + }) + + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + + defer listen.Close() + + for { + // 读取 + arr := [1024]byte{} + n,addr,err:=listen.ReadFromUDP(arr[:]) + if err != nil { + fmt.Printf("error: %v\n", err) + continue + } + // 打印 + fmt.Printf("data:%v addr:%v count:%v",string(arr[:n]),addr,n) + + // 发送 + _,err=listen.WriteToUDP(arr[:n],addr) + if err != nil { + fmt.Printf("error: %v\n", err) + continue + } + } +} +``` + + + + + +### 1.5 Go-UDP-Client + + + +```go +import ( + "fmt" + "net" +) + +func main() { + listen,err:=net.DialUDP("udp",nil,&net.UDPAddr{ + IP: net.IPv4(0,0,0,0), + Port: 20000, + }) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + defer listen.Close() + + sendData:="hi server" + _,err=listen.Write([]byte(sendData)) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + + data:=make([]byte,4096) + n,addr,err:=listen.ReadFromUDP(data) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Printf("data:%v addr:%v count:%v",string(data[:n]),addr,n) +} +``` + + + +## 二、http 包 + +### 2.1 Http、Https请求总览 + +Get、Head、Post和PostForm函数发出HTTP/HTTPS请求。 + +```go +resp, err := http.Get("http://example.com/") +... +resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf) +... +resp, err := http.PostForm("http://example.com/form", + url.Values{"key": {"Value"}, "id": {"123"}}) +``` + +**程序在使用完response之后必须关闭回复的主体** + +```go +resp, err := http.Get("http://example.com/") +if err != nil { + // handle error +} +// do +defer resp.Body.Close() +body, err := ioutil.ReadAll(resp.Body) +// ... +``` + +### 2.2 默认的Server + +通过http包下面的 `ListenAndServe`方法可以启动一个HTTP服务器,处理器的参数通常默认为nil, 表示要使用默认的`DefaultServeMux`作为处理器 + +```go +http.Handle("/foo", fooHandler) +http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) +}) +log.Fatal(http.ListenAndServe(":8080", nil)) +``` + + + +### 2.3 自定义Server + +```go +func main() { + myServer:=&http.Server{ + Addr: ":8080", + Handler: nil, + TLSConfig: nil, + ReadTimeout: 0, + ReadHeaderTimeout: 0, + WriteTimeout: 0, + IdleTimeout: 0, + MaxHeaderBytes: 0, + TLSNextProto: nil, + ConnState: nil, + ErrorLog: nil, + BaseContext: nil, + ConnContext: nil, + } + myServer.ListenAndServe() +} + +``` + +### 2.4 Get请求 + +#### 2.4.1 不带参数的Get请求 + + 发送http请求的Client端 + +```go +func main(){ + + res,err:=http.Get("http://www.baidu.com") + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + + // 别忘记关闭响应体 + defer res.Body.Close() + + fmt.Println("res . status code",res.StatusCode) + fmt.Println("res . status proto",res.Proto) + data,err:=ioutil.ReadAll(res.Body) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println(string(data)) +} + +``` + +#### 2.4.2 带参数的Get请求 + +请求需要携带的参数, 使用内置的 net/url 标准库来实现 + +```go +func main(){ + + // 使用Go提供的url包添加参数 + apiUrl:="http://127.0.0.1:8888/get/info" + data:=url.Values{} + data.Set("name","tom") + data.Set("age","23") + u,err:=url.ParseRequestURI(apiUrl) + + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + u.RawQuery = data.Encode() + + // 发送请求 + res,err:=http.Get(u.String()) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + defer res.Body.Close() + + bytes,err2:=ioutil.ReadAll(res.Body) + if err2 != nil { + fmt.Printf("error: %v\n", err) + return + } + + fmt.Println(string(bytes)) +} +``` + +编写server端验证 + +```go +func handler(writer http.ResponseWriter,request *http.Request) { + defer request.Body.Close() + data:=request.URL.Query() + name:=data.Get("name") + age:=data.Get("age") + fmt.Println(name,age) + str:=name+" : "+age + writer.Write([]byte(str)) +} + +func main(){ + // 添加路径与回调函数 + http.HandleFunc("/get/info/",handler) + // 开启服务器,绑定端口 + err:=http.ListenAndServe("127.0.0.1:8888",nil) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } +} + +``` + + + +**注意点: 如果报错说 `**[Http: panic serving 127.0.0.1:34804; runtime error: invalid memory address](https://forum.golangbridge.org/t/http-panic-serving-127-0-0-1-34804-runtime-error-invalid-memory-address-or-nil-pointer-dereference/16922) ` 非法的内存地址, 那很可能我们在代码中的某一处, 没有做验证,直接使用一个nil类型的值** + + + +### 2.5 Post请求 + +Http-Server 端、接收client的POST请求 + +```go +func main() { + // 使用Go的http包, 模拟客户端发送Post请求 + url:="http://127.0.0.1:20000/addUserInfo" + + // 表单类型的数据 + contentType := "application/x-www-form-urlencoded" + // json对象类型 + //contentType:="application/json" + data:=`{"name":"tom","age":23}` + + res,err:=http.Post(url,contentType,strings.NewReader(data)) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + // 关闭Body + defer res.Body.Close() + + // + bytes,err:=ioutil.ReadAll(res.Body) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println("收到响应: ",string(bytes)) +} +``` + + + +如下Demo, 使用http发送 post请求 ,模拟提交表单数据 + +发送json类型的数据, 还是发送from表单类型的数据,使用contentType控制 + +```go +func handler(writer http.ResponseWriter, request *http.Request) { + // 不要忘记关闭Body + defer request.Body.Close() + err := request.ParseForm() + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + + fmt.Println("请求表单为:", request.PostForm) + fmt.Println("表单中的参数为:", request.PostForm.Get("name"), " : ", request.PostForm.Get("age")) + + // 读取数据 + bytes, err := ioutil.ReadAll(request.Body) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println("接受到请求体为:", string(bytes)) + + // 写回响应数据 + res := `{"status":200}` + writer.Write([]byte(res)) +} + +func main() { + http.HandleFunc("/addUserInfo", handler) + err := http.ListenAndServe("127.0.0.1:20000", nil) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } +} +``` + + + +### 2.6 自定义Client + +要管理Http客户端的头部域,重定向策略,和其他设置,创建一个Client + +```go +func main() { + client := &http.Client{ + Transport: nil, + CheckRedirect: nil, + Jar: nil, + Timeout: 0, + } + + url := "http://www.baidu.com" + client.Get(url) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + req.Header.Add("If-None-Match",`W/"wyzzy"`) + + client.Do(req) +} + +``` + + + +### 2.7 自定义Transport + +自定义协议: 管理代理,TLS(也叫SSL,用于Https)、keep-alive、压缩和其他设置 + +```go + +func main() { + tr:= &http.Transport{ + Proxy: nil, + DialContext: nil, + Dial: nil, + DialTLSContext: nil, + DialTLS: nil, + TLSClientConfig: nil, + TLSHandshakeTimeout: 0, + DisableKeepAlives: false, + DisableCompression: false, + MaxIdleConns: 0, + MaxIdleConnsPerHost: 0, + MaxConnsPerHost: 0, + IdleConnTimeout: 0, + ResponseHeaderTimeout: 0, + ExpectContinueTimeout: 0, + TLSNextProto: nil, + ProxyConnectHeader: nil, + MaxResponseHeaderBytes: 0, + WriteBufferSize: 0, + ReadBufferSize: 0, + ForceAttemptHTTP2: false, + } + client := &http.Client{Transport: tr} + resp, err := client.Get("https://example.com") + +} + +``` + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-26-10.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-26-10.png new file mode 100644 index 00000000..55230d37 Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-26-10.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-29-13.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-29-13.png new file mode 100644 index 00000000..597815ea Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-29-13.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-31-59.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-31-59.png new file mode 100644 index 00000000..a189b02c Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-31-59.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-36-32.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-36-32.png new file mode 100644 index 00000000..b2059d87 Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-36-32.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-37-22.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-37-22.png new file mode 100644 index 00000000..e0f4a6cf Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-37-22.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-52-02.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-52-02.png new file mode 100644 index 00000000..dc8497fd Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_06-52-02.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-00-55.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-00-55.png new file mode 100644 index 00000000..07ef1096 Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-00-55.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-03-11.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-03-11.png new file mode 100644 index 00000000..e64796af Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-03-11.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-06-09.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-06-09.png new file mode 100644 index 00000000..28433f6f Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-06-09.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-15-26.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-15-26.png new file mode 100644 index 00000000..c7f18cd0 Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-15-26.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-17-30.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-17-30.png new file mode 100644 index 00000000..20732e90 Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-17-30.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-20-07.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-20-07.png new file mode 100644 index 00000000..f7a11b13 Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-20-07.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-30-40.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-30-40.png new file mode 100644 index 00000000..859e4153 Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-30-40.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-47-12.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-47-12.png new file mode 100644 index 00000000..81da4d57 Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_07-47-12.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_08-18-20.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_08-18-20.png new file mode 100644 index 00000000..624360ba Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_08-18-20.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_08-58-15.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_08-58-15.png new file mode 100644 index 00000000..03f692a1 Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_08-58-15.png differ diff --git a/Go/其他教程/15测试模块/Snipaste_2020-04-07_09-21-01.png b/Go/其他教程/15测试模块/Snipaste_2020-04-07_09-21-01.png new file mode 100644 index 00000000..ff201657 Binary files /dev/null and b/Go/其他教程/15测试模块/Snipaste_2020-04-07_09-21-01.png differ diff --git a/Go/其他教程/15测试模块/测试模块.md b/Go/其他教程/15测试模块/测试模块.md new file mode 100644 index 00000000..f7248d85 --- /dev/null +++ b/Go/其他教程/15测试模块/测试模块.md @@ -0,0 +1,639 @@ +### 一、go test 工具 + +Go为了我们提供了成体系内置的测试工具 + +测试依赖命令 `go test` + +按照约定,对于`go test`来说,所有以`_test.go` 为后缀名的源代码文件都是需要测试的一部分,而不会被`go build`命令编译进最终的可执行文件中 + +在`*test.go`中有三种类型的函数,单元测试函数,基准测试函数和示例函数 + +| 类型 | 格式 | 作用 | +| -------- | --------------------- | ---------------------- | +| 测试函数 | 函数名前缀为Test | 测试程序的逻辑是否正确 | +| 基准函数 | 函数名前缀为Benchmark | 测试函数性能 | +| 示例函数 | 前缀名为Example | 提供实例文档 | + +**当我们执行 `go test` 命令时, 遍历项目中所有的 `*_test.go` 文件中符合上述命名规则的函数,然后生成一个临时main包去调用相关的测试函数,然后运行 -> 构建测试结果 -> 清理测试生成的临时文件** + + + +### 二、测试函数 + +> **如果使用编译器为Golang , 可以不使用下面的命令...** , 但是使用命令行,有丰富的参数供我们选择 + +**使用测试函数主要是看看我们的代码能不能跑通** + +#### 2.1 格式: + +所有的测试函数都需要如下要求 + +* 导入 `testing`包 +* 函数名以Test开头 + +基本格式如下: + +```go +func TestAdd(t *testing.T){ + // ... +} +``` + +其中的参数t用来**报告测试失败和附加的日志信息**, testing t 还有如下的方法 + +```go +func (c *T) Error(args ...interface{}) +func (c *T) Errorf(format string, args ...interface{}) +func (c *T) Fail() +func (c *T) FailNow() +func (c *T) Failed() bool +func (c *T) Fatal(args ...interface{}) +func (c *T) Fatalf(format string, args ...interface{}) +func (c *T) Log(args ...interface{}) +func (c *T) Logf(format string, args ...interface{}) +func (c *T) Name() string +func (t *T) Parallel() +func (t *T) Run(name string, f func(t *T)) bool +func (c *T) Skip(args ...interface{}) +func (c *T) SkipNow() +func (c *T) Skipf(format string, args ...interface{}) +func (c *T) Skipped() bool +``` + +#### 2.2 示例: + +在测试之前, 我们得先有一个需要被测试的模块, 如下随便写个模块待会就测试它 + +```go +/*按照给定的sep, 完成str的切割*/ +func Split(str,sep string)(result []string){ + i:=strings.Index(str,sep) + for i>-1{ + result = append(result,str[:i]) + str = str[i+1:] + i=strings.Index(str,sep) + } + result = append(result,str) + return +} + +``` + +测试函数: + +```go +func TestSplit(t *testing.T) { + // 运行函数, 得到结果 + got:=Split("a:b:c",":") + // 期望的结果 + want:=[]string{"a","b","c"} + //slice不能直接进行比较, 借助反射包里卖弄的DeepEquals比较 + if !reflect.DeepEqual(got,want){ + // 使用T, 输出错误日志 + t.Errorf("except:%v , got:%v",want,got) + } +} +``` + + + +测试命令 : go test + +结果如下: + +![](Snipaste_2020-04-07_06-26-10.png) + + + +测试命令: go test -v + +**可以查看到测试函数是谁, 耗费了多少时间** + +![](Snipaste_2020-04-07_06-29-13.png) + + + +测试未通过, 会按我们预先的定义, 打印错误日志 + +![](Snipaste_2020-04-07_06-31-59.png) + + + +如果我们已经确定了未通过测试的函数的函数名, 我们通过`-run="FuncName"` 参数, 明确指定仅测试函数名为FuncName 的方法 , 他是一个正则表达式, 当函数名匹配上时, 才会被 go test 运行 + +![](Snipaste_2020-04-07_06-36-32.png) + + + +如果我们通过-run="不存在的方法名" , go test 会提示我们没有可以测试的函数 + +![](Snipaste_2020-04-07_06-37-22.png) + + + +### 三、测试组 + +使用测试组一次性测试多组数据 + +```go +func TestSplit(t *testing.T) { + // 定义测试类型 + type test struct { + input string + seq string + want []string + } + // 定义存储测试用例的切片 + tests:=[]test{ + {input:"a:b:c",seq:":",want:[]string{"a","b","c"}}, + {input:"a:b:c",seq:",",want:[]string{"a:b:c"}}, + {input:"abcd",seq:"bc",want:[]string{"a","d"}}, + } + + // 遍历切片,测试 + for _,tc:=range tests{ + got:=Split(tc.input,tc.seq) + if !reflect.DeepEqual(got,tc.want){ + t.Errorf("got:%#v , want:%#v",got,tc.want) + } + } +} +``` + +测试: + +![](Snipaste_2020-04-07_06-52-02.png) + + + +### 四、子测试 + +上面的测试组是存在问题的, 测试组中可能有多个测试, 但是我们没办法一眼就判断出测试组中那个测试出现问题了 , 这时可以考虑子测试 + + + +* 我们可以使用map去解决这个事, 将上面的切片换成map + +```go + +/* + 1. 方法名以Test开头 + 2. 参数位置传入参数 *testing.T +*/ +func TestSplit(t *testing.T) { + // 定义测试类型 + type test struct { + input string + seq string + want []string + } + // 定义存储测试用例的切片 + tests:=map[string]test{ + "t1":{input:"a:b:c",seq:":",want:[]string{"a","b","c"}}, + "t2":{input:"a:b:c",seq:",",want:[]string{"a:b:c"}}, + "t3":{input:"abcd",seq:"bc",want:[]string{"a","d"}}, + } + + // 遍历切片,测试 + for _,tc:=range tests{ + got:=Split(tc.input,tc.seq) + if !reflect.DeepEqual(got,tc.want){ + t.Errorf("got:%#v , want:%#v",got,tc.want) + } + } +} + +``` + + + +* Go1.7中也添加了测试组 , 我们可以按照如下的方式执行 t.Run 执行子测试 + +```go +/* + 1. 方法名以Test开头 + 2. 参数位置传入参数 *testing.T +*/ +func TestSplit(t *testing.T) { + // 定义测试类型 + type test struct { + input string + seq string + want []string + } + // 定义存储测试用例的切片 + tests:=map[string]test{ + "t1":{input:"a:b:c",seq:":",want:[]string{"a","b","c"}}, + "t2":{input:"a:b:c",seq:",",want:[]string{"a:b:c"}}, + "t3":{input:"abcd",seq:"bc",want:[]string{"a","d"}}, + } + + // 遍历切片,测试 + for key,tc:=range tests{ + // 使用,t.Run(name,func) 完成子测试 + t.Run(key, func(t *testing.T) { + got:=Split(tc.input,tc.seq) + if !reflect.DeepEqual(got,tc.want){ + t.Errorf("got:%#v , want:%#v",got,tc.want) + } + }) + } +} +``` + + + +测试结果: + +![](Snipaste_2020-04-07_07-00-55.png) + + + +### 五、测试覆盖率 + +测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。 + + + +Go提供内置功能来检查你的代码覆盖率。我们可以使用`go test -cover`来查看测试覆盖率。例如 + +![](Snipaste_2020-04-07_07-03-11.png) + + + +也可以使用 -coverprofile 参数, 将覆盖率输出到每一个具体的文件 + +```bash +go test -v -coverprofile=./c.out +``` + +查看生成结果 + +![](Snipaste_2020-04-07_07-06-09.png) + + + +使用如下命令, 将c.out 文件以html的形式打开 + +```go +go tool cover -html=c.out +``` + +![](C:\Users\acer\AppData\Local\Temp\1586214486688.png) + +红色部分是未被覆盖的部分 + + + +### 六、基准测试 + +基准测试目的是检测程序的性能 + +命名格式要求 + +* 测试函数以 `benchMarkXXX` 为前缀 +* 入参位置需要参数 `*testing.B` + +```go +func BenchmarkName(b *testing.B){ + // ... +} +``` + + + +`testing.B`有如下的方法 + +```go +func (c *B) Error(args ...interface{}) +func (c *B) Errorf(format string, args ...interface{}) +func (c *B) Fail() +func (c *B) FailNow() +func (c *B) Failed() bool +func (c *B) Fatal(args ...interface{}) +func (c *B) Fatalf(format string, args ...interface{}) +func (c *B) Log(args ...interface{}) +func (c *B) Logf(format string, args ...interface{}) +func (c *B) Name() string +func (b *B) ReportAllocs() +func (b *B) ResetTimer() +func (b *B) Run(name string, f func(b *B)) bool +func (b *B) RunParallel(body func(*PB)) +func (b *B) SetBytes(n int64) +func (b *B) SetParallelism(p int) +func (c *B) Skip(args ...interface{}) +func (c *B) SkipNow() +func (c *B) Skipf(format string, args ...interface{}) +func (c *B) Skipped() bool +func (b *B) StartTimer() +func (b *B) StopTimer() +``` + + + +#### 6.1 基准测试示例 + +```go +func BenchmarkSplit(t *testing.B) { + for i:=0;i + + + +### flag的参数类型 + +flag包支持的命令行参数类型有`bool`、`int`、`int64`、`uint`、`uint64`、`float` `float64`、`string`、`duration`。 + +| flag参数 | 有效值 | +| ------------ | ------------------------------------------------------------ | +| 字符串flag | 合法字符串 | +| 整数flag | 1234、0664、0x1234等类型,也可以是负数。 | +| 浮点数flag | 合法浮点数 | +| bool类型flag | 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。 | +| 时间段flag | 任何合法的时间段字符串。如”300ms”、”-1.5h”、”2h45m”。 合法的单位有”ns”、”us” /“µs”、”ms”、”s”、”m”、”h”。 | + +###注册flag参数的方式 + +#### 将结果保存在 `*type`指针中 + +```go + import "flag" + + // flag.type 的返回值是指针 + var ip = flag.Int("flagname", 1234, "help message for flagname") + ip := flag.Int("flagname", 1234, "help message for flagname") +``` + +#### 将结果保存在变量中 + +```go + var flagvar int + flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname") +``` + +#### 自定义flag类型 + +```go +// 默认值就是该变量的初始值。 +flag.Var(&flagVal, "name", "help message for flagname") +``` + + + +### `flagParse()` + +**所有的flag完成注册之后, 使用函数`flag.Parse()` 进行解析** + +解析后,注册进flag的参数可以通过`flag.Args() ` 或者`flag.Args(i)`取出 + +- `-flag xxx` (使用空格,一个`-`符号) +- `--flag xxx` (使用空格,两个`-`符号) +- `-flag=xxx` (使用等号,一个`-`符号) +- `--flag=xxx` (使用等号,两个`-`符号) + + + +其他flag函数 + +```go +flag.Args() ////返回命令行参数后的其他参数,以[]string类型 +flag.NArg() //返回命令行参数后的其他参数个数 +flag.NFlag() //返回使用的命令行参数个数 +``` + + + +### 示例 + +```go +func main() { + //定义命令行参数方式1 + var name string + var age int + var married bool + var delay time.Duration + flag.StringVar(&name, "name", "张三", "姓名") + flag.IntVar(&age, "age", 18, "年龄") + flag.BoolVar(&married, "married", false, "婚否") + flag.DurationVar(&delay, "d", 0, "延迟的时间间隔") + + //解析命令行参数 + flag.Parse() + fmt.Println(name, age, married, delay) + //返回命令行参数后的其他参数 + fmt.Println(flag.Args()) + //返回命令行参数后的其他参数个数 + fmt.Println(flag.NArg()) + //返回使用的命令行参数个数 + fmt.Println(flag.NFlag()) +} +``` + +命令行参数提示: + +```go +$ ./flag_demo -help +Usage of ./flag_demo: + -age int + 年龄 (default 18) + -d duration + 时间间隔 + -married + 婚否 + -name string + 姓名 (default "张三") +``` + +正常使用命令行flag参数 + +```go +$ ./flag_demo -name 沙河娜扎 --age 28 -married=false -d=1h30m +沙河娜扎 28 false 1h30m0s +[] +0 +4 +``` + + + + + + + + + + + +### os.Args + +如果不想使用flag标准库可以使用os.Args简单的获取到命令行参数 + +```go +func main() { + if len(os.Args) > 0{ + for index,args :=range os.Args{ + fmt.Printf("index: %v value:%v",index,args) + } + } +} +``` + +os.Args中,存储启动编译好的go文件时输入的所有参数, **第一个参数是可执行文件本身** \ No newline at end of file diff --git a/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-08-25.png b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-08-25.png new file mode 100644 index 00000000..e97e5eba Binary files /dev/null and b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-08-25.png differ diff --git a/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-09-56.png b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-09-56.png new file mode 100644 index 00000000..a53373b8 Binary files /dev/null and b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-09-56.png differ diff --git a/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-11-08.png b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-11-08.png new file mode 100644 index 00000000..3c11f709 Binary files /dev/null and b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-11-08.png differ diff --git a/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-18-47.png b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-18-47.png new file mode 100644 index 00000000..e1c24c11 Binary files /dev/null and b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-18-47.png differ diff --git a/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-20-16.png b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-20-16.png new file mode 100644 index 00000000..6d5d006c Binary files /dev/null and b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-20-16.png differ diff --git a/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-21-14.png b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-21-14.png new file mode 100644 index 00000000..7cf81b0d Binary files /dev/null and b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-21-14.png differ diff --git a/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-22-03.png b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-22-03.png new file mode 100644 index 00000000..7417e963 Binary files /dev/null and b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-22-03.png differ diff --git a/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-27-34.png b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-27-34.png new file mode 100644 index 00000000..51299cbd Binary files /dev/null and b/Go/其他教程/18gomodule/Snipaste_2020-04-10_08-27-34.png differ diff --git a/Go/其他教程/18gomodule/go-module.md b/Go/其他教程/18gomodule/go-module.md new file mode 100644 index 00000000..472bbae3 --- /dev/null +++ b/Go/其他教程/18gomodule/go-module.md @@ -0,0 +1,187 @@ +### 一. go-module + +在Go的1.11版本之后, 官方推出了版本管理工具,并且从G1.13版本之后, go module 将成为Go默认的版本依赖工具 + + + +### 二. GO111MODULE + +要启用`go module` 支持首先需要配置环境变量 , `GO111MODULE` 通过他可以开启或者关闭模块支持 , 它有如下三个可选值 + +* `GO111MODULE=off` 禁用模块支持, 编译时会从 GOPATH和Vendor文件夹中查找依赖 +* `GO111MODULE=on` 启用模块支持,编译时会忽略GOPATH和Vendor , 直接从 `go.mod`中查找配置 +* `GO111MODULE=auto` 当前项目在 $Path/src 外, 且根路径有`go.mod`时开启模块支持 + + + +**一般我们使用 on 就可以了 , 这也就意味着, 以后写代码,不用必须在goPath中编写了, 任何地方都可以写** + +当我们启用模块支持后, go module将在我们项目的根路径下生成两个文件,` go.mod 和 go.sum` + +通过`go get`命令可以查找生成当前项目中的依赖关系, 同时生成一个`go.sum`记录每一个依赖库的版本和hash + + + +### 三. 设置代理 + +很多库都是谷歌公司自己出的, 但是在国内, 谷歌被强了, 故我们设置代理, 间接访问 + +Go1.11之后的版本设置 + +```bash +# 在windowsshang +set GOPROXY=https://goproxy.cn +# mac +export GOPROXY=https://goproxy.cn +``` + +Go1.13之后设置 + +``` +go env -w GOPROXY=https://goproxy.cn,direct +``` + + + +### 四. 常见命令 + +```bash +go mod init # 初始化,创建go.mod +go mod download # 下载依赖的module到本地的cache中, 默认是 $GOPATH/pkg/mod + +go mod edit # 编辑go.mod文件 +go mod edit -fmt # 快捷格式化go.mod文件 + +go mod graph # 打印模块依赖图 +go mod tidy # 添加缺失的mod, 删除没有的module +go mod vendor # 将依赖复制到vendor中 +go mod verify # 校验依赖 +go mod why # 解释为什么需要依赖 +``` + + + +### 五. 示例: + +* 开启 + +```bash +# go1.13版本后, go mod默认是开启的 +# 通过如下命令 go env 可以看到 GO111MODULE +# 如果没有开启的话, 我们使用 如下命令开启 +# windows +set GO111MODULE=on +# mac +export GO111MODULE=on +``` + +![](Snipaste_2020-04-10_08-08-25.png) + + + +* 初始化 + +```go +go mod init +``` + +![](Snipaste_2020-04-10_08-09-56.png) + + + +查看文件内容 + +![](Snipaste_2020-04-10_08-11-08.png) + + + + + +* 设置代理 + +```go +go env -w GOPROXY=https://goproxy.cn,direct +``` + + + +比如 , 我们依赖了 github上的库, 那go原生没有的代码, 我们只能去github上下载 + +![](Snipaste_2020-04-10_08-20-16.png) + +结果如下: + +![](Snipaste_2020-04-10_08-18-47.png) + + + +下载的位置: 默认会下载到 $GOPATH / pkg 目录下面 + +![](Snipaste_2020-04-10_08-21-14.png) + + + + + +再次查看go.mod文件可以发现已经将我们下载的文件加入进去了 + +![](Snipaste_2020-04-10_08-22-03.png) + + + +默认`go get`下载的都是最新的版本, 我们可以通过去github上找一个自己需要的版本, 然后明确下载它在 + +github上的某一个分支, 因为每一个分支都有tag , 下载时, 指定tag就ok + + + +第一步: 改`go.mod` 将其换成我们希望的版本 + +![](Snipaste_2020-04-10_08-27-34.png) + + + +第二步: 校验是否存在: `go mod verify` + +如果存在, 返回 all modules verified + +如果不存在,返回 Not Found + + + +第三步: 使用 `go mod download` 下载指定的依赖 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Go/其他教程/19mysqlx/mysqlx.md b/Go/其他教程/19mysqlx/mysqlx.md new file mode 100644 index 00000000..9f79f2bc --- /dev/null +++ b/Go/其他教程/19mysqlx/mysqlx.md @@ -0,0 +1,455 @@ +### 一. mysql + +go-sql-driver : + +#### 1.1 下载 + +``` +go get -u github.com/go-sql-driver/mysql +``` + +但是手动下载时, golang导入时很不好用, 基本上是导入不了, 一直爆红 + +如果用golang开发, 不如是用快捷键下载 + +### 二. 编码 + +#### 2.1 实体的封装结构体 + +```go +// 实体的结构体 +type user struct { + id int + name string +} +``` + + + +#### 2.2 初始化连接 + +```go +// 初始化连接 +func initDB() (err error) { + db, err = sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test") + if err != nil { + panic(err) + } + // todo 不要在这里关闭它, 函数一结束,defer就执行了 + // defer db.Close() + err = db.Ping() + if err != nil { + return err + } + return nil +} +``` + + + +#### 2.3 设置最大连接数, 最大空闲数 + +通过如下两个函数, 设置数据库连接池的最大连接数和连接池中的最大空闲数 + +```go +// 默认n是0, 表示没有设置最大上限 , 如果n<0 , 表示不设置最大连接 +func (db *DB) SetMaxOpenConns(n int) + +// 如果最大空闲数比最大连接数还大, 那么最大空闲数 会被设置成 最大连接数 +// 如果n<0 , 表示不设置 +func (db *DB) SetMaxIdleConns(n int) +``` + + + +#### 2.4 单行查询 + +```go +func queryRow(sqlstr string) { + var u user + // 通过Scan , 将查询出的结果映射到 + err := db.QueryRow(sqlstr).Scan(&u.id, &u.name) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Printf("user.id:%v user:name:%v\n", u.id, u.name) +} + +``` + + + +#### 2.5 多行查询 + +```go +func queryRows() { + sqlStr := "select * from user where id >?" + rows,err:=db.Query(sqlStr,0) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + // 关闭rows 保持的数据库连接 + defer rows.Close() + // 这里有点像迭代器模式 + for rows.Next(){ + var u user + err= rows.Scan(&u.id,&u.name) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Printf("id: %v name:%v",u.id,u.name) + } +} +``` + + + + + +#### 2.6 插入数据 + +```go +func insertData(){ + // strSql:="insert into user values(?,?)" + // result,err:=db.Exec(strSql,nil,"tom") + + //strSql1:="insert into user(id,name) values(?,?)" + //result,err:=db.Exec(strSql1,nil,"tom") + + strSql2:="insert into user(name) values(?)" + result,err:=db.Exec(strSql2,"tom") + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + // 获取插入数据的id + id,err:=result.LastInsertId() + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println("last insert id : ",id) + + // 响应行数 + rows,err:=result.RowsAffected() + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println("row affected : ",rows) +} +``` + + + +#### 2.7 更新 + +```go +func updateUser (){ + sqlStr := "update user set user.name = ? where id =?" + result,err:=db.Exec(sqlStr,"jerry",5) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + id,err:=result.LastInsertId() + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println("last insert id : ",id) + + affectRow,err:=result.RowsAffected() + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println("row affected : ",affectRow) + +} + +``` + + + +#### 2.8 刪除 + +```go +func deleteUserById(targetId int) { + sqlStr := "delete from user where id = ?" + result,err:=db.Exec(sqlStr,targetId) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + + id, err := result.LastInsertId() + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println("last insert id : ", id) + + affectRow, err := result.RowsAffected() + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println("row affected : ", affectRow) +} + +``` + + + +### 三. Sql预处理 + +#### 3.1 什么是sql预处理? + +* 普通sql执行过程 + * 从客户端对sql中的占位符进行替换,得到完整的sql + * 客户端将完整的sql , 发送到 MysqlServer + * MysqlServer执行sql , 返回给客户端结果 +* 预处理过程 + * Sql被分成两部分 , 命令部分, 预处理部分 + * 先把命令部分发送给Mysql服务端, Mysql服务端进行Sql的预处理 + * 把数据部分发送给Mysql服务端, 让MysqlServer用起替换命令部分的占位符 + * MysqlServer 执行sql , 返回给客户端结果 + + + + + +#### 3.2 为什么要预处理? + +* 优化Mysql服务器重复执行sql的流程, 一次编译, 下次就不用重新编译sql了, 节省编译成本 + +* 避免sql注入问题 + + ```go + // sql注入示例 + func sqlInjectDemo(name string) { + // 像这样自己拼接sql, 很容易造成sql注入 + sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name) + fmt.Printf("SQL:%s\n", sqlStr) + + var users []user + err := db.Select(&users, sqlStr) + if err != nil { + fmt.Printf("exec failed, err:%v\n", err) + return + } + for _, u := range users { + fmt.Printf("user:%#v\n", u) + } + } + + sqlInjectDemo("xxx' or 1=1#") + sqlInjectDemo("xxx' union select * from user #") + sqlInjectDemo("xxx' and (select count(*) from user) <10 #") + ``` + +#### 3.3 Go的Sql预处理 + +```go +func (db *DB) Prepare(query string) (*Stmt, error) +``` + +```go +func prepareQuery(){ + sqlStr:= "select * from user where id > ?" + + // ******************************** + stat,err:=db.Prepare(sqlStr) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + // ******************************** + defer stat.Close() + // *************多行, Query , 单行QueryRow()******************* + rows,err:=stat.Query(0) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + defer rows.Close() + + for rows.Next() { + var u user + err:=rows.Scan(&u.id,&u.name) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Printf("id:%v name:%v",u.id,u.name) + } +} +``` + + + +### 四. 事务 + +Go语言中使用如下三个方法实现Mysql中的事务操作 + +```go +// 开启事务, 方法返回一个事物 +func (db *DB) Begin() (*Tx, error) + +// 由事务执行sql +sqlStr1 := "Update user set age=30 where id=?" +_, err = tx.Exec(sqlStr1, 2) + +// 由tx发起提交事务的操作 +func (tx *Tx) Commit() error + +// 在err不为空, 或者是其他的情况下, 由tx发起回滚 +func (tx *Tx) Rollback() error +``` + + + +### 五. sqlx + +第三方库`sqlx`能够简化操作,提高开发效率。 + +#### 5.1 安装 + +```go +go get github.com/jmoiron/sqlx +``` + +推荐将这个依赖收到写入import块中, 然后通过goland的快捷键导入 + + + +#### 5.2 连接数据库 + +```go + +var db *sqlx.DB + +func initDb() (er error) { + db, er = sqlx.Connect("mysql", "root:root@tcp(127.0.0.1:3306)/test") + if er != nil { + fmt.Printf("error: %v\n", er) + return er + } + db.SetMaxOpenConns(200) + db.SetMaxIdleConns(100) + return nil +} +``` + + + +#### 5.3 踩坑 + +报错: `scannable dest type struct with >1 columns (2) in result` + +其实原因看看下面的代码就大概能猜出来 + +```go + +type User struct { + id int `db:"id"` + name string `db:"name"` +} + +func GetUserById(id int) { + sqlStr := "select id, name from user where id =?" + var u User + // 问题就出在这里 , sqlx底层通过v := reflect.ValueOf(dest) ... 为user赋值的 + // 所以上面的我们的结构体首字母需要大写才行, 不然怎么反射的了呢? + err:=db.Get(&u, sqlStr, id) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + fmt.Println(u.id , ":", u.name) +} +``` + + + +#### 5.4 查找 + +```go + // 单行 + sqlStr := "select id, name from user where id =?" + err:=db.Get(&u, sqlStr, id) + + // 多行 + sqlStr := "select id, name, age from user where id > ?" + var users []user + err := db.Select(&users, sqlStr, 0) +``` + + + +#### 5.5 删除,更新,修改 + +删除,更新,修改和原生的sql 用法基本一直 + + + +#### 5.6 事务 + +sqlx相对原生的sql提供了 db.Beginx() , db.MustExec() , 用来简化错误处理的过程 + +示例: + +```go +func transactionDemo() { + tx, err := db.Beginx() + if err != nil { + fmt.Printf("error: %v\n", err) + tx.Rollback() + return + } + + sql1 := "" + tx.MustExec(sql1, 1) + sql2 := "" + tx.MustExec(sql2, 1) + + err = tx.Commit() + if err != nil { + tx.Rollback() + fmt.Println("commit error:",err) + return + } + fmt.Println("success") +} +``` + + + +### 六. sql中的占位符 + +| 数据库 | 占位符语法 | +| ---------- | ------------ | +| MySQL | `?` | +| PostgreSQL | `$1`, `$2`等 | +| SQLite | `?` 和`$1` | +| Oracle | `:name` | + + + + + + + + + + + +``` +defer rows.Close() ???? 不确定 + // 循环读取结果集中的数据 + for rows.Next() { +``` \ No newline at end of file