This commit is contained in:
Estom
2021-09-03 05:41:27 +08:00
parent 1bad082e49
commit 2d9a1a3b53
47 changed files with 8262 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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<len(address);i++{ // byte
// 将他们编码值,转换成单个字符
// byte能查找字符在ASCII中英文数字已经少量符号
fmt.Printf("%c",address[i])
}
fmt.Println()
// 处理中文,日文,其他复合文字
for _,c:=range address{ // rune
// 将他们编码值,转换成单个字符
fmt.Printf("%c",c)
}
}
```
#### 3.10 字符串的修改和类型转换
参见如下的实例-- 字符串不可更改:
```go
var address string = "嗨! bei jing"
// 字符串是不可以更改的
// address[0]:='A'  会报错
// 可以通过切片是形式,将字符串转换成切片,然后修改, 切片里面保存的就是字符
s3:=[]rune(address)
s3[0] = 'A'
fmt.Print(string(s3)) // A bei jing
```
参见如下示例,整明白 int32, byte, rune 本质上都是在表示一个字符
```go
c1:='A' // int32
c2:="a" // string
c3:='嗨'// int32 == rune
c4:="嗨"// string
fmt.Printf("c1: %T , c2:%T",c1,c2)
fmt.Printf("c3: %T , c4:%T",c3,c4)
// todo 实际总结一下就是: Go里面的字符串中每一个单位都是一个字符 具体是中文的字符还是英文的字符Go给出了两个别命
// todo 如果是中文的字符,用Utf8编码占3位 叫做 rune
// todo 如果是英文的字符,用ACSII编码占一位, 叫做 byte
// todo 但是不管是中文字符还是英文字符都是int32类型的
```
参见如下示例进行类型的转换
* 整形可以和float类型转换
* 字符串可以和切片之间进行类型的转换
```go
n:=1
f:=float32(n)
fmt.Print(f) // 1
```

View File

@@ -0,0 +1,979 @@
[TOC]
### 一、流程控制
#### 1.1 if else 分支
```go
if 表达式1 {
}else if 表示2{
}else{
}
```
语法上和java相似 更为简洁的是它的条件不需要括号
#### 1.2 if 条件判断特殊写法
```go
if score:=64; score>=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中可以通过breakgotoreturnpanic 控制退出
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 intfloat默认为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<len(arr2);i++{
fmt.Print(arr2[i])
}
fmt.Println()
for i,v :=range arr2{
fmt.Printf("i:%d v:%d \n",i,v)
}
```
#### 3.04 多维数组
```go
var arr [3][3]int
// 初始化方式1
arr= [3][3]int{
[3]int{1,2,3},
[3]int{1,2,3},
[3]int{1,2,3},
}
fmt.Print(arr)
// 初始化方式2
// arr[0] = [3]int{1,2,3}
// arr[1] = [3]int{1,2,3}
// arr[2] = [3]int{1,2,3}
```
#### 3.05 切片 (selice)
* 数组的局限性:
如下面的函数, 因为数组的长度是数组的一部分所以下面的函数仅仅能计算长度为3的数组之和
```go
func arraySum([3]int) int{
...
return sum
}
```
* Go中提供了切片去解决这个问题
切片是一个**拥有相同类型元素的可变长度的序列**
相当于是对数组的有一层封装,**支持自动扩容**
**切片是引用类型,包含`地址``长度``容量`, 用于快速的操作数据集合**
* 切片的声明
```go
var name []T
// 例定义了一个存放int类型数据的切片
// 切片的定义和数组的定义很像, 但是它不再需要我们指定初始容量
var s [] int
var a []string
var b []int
//fmt.Println(nil == a) //true nil 相当于java中的null nil表示该切片并没有开辟内存空间
//声明并初始化, 不再为nil
a =[]string{"tom","jerry"}
b =[]int{1,2,3}
fmt.Println("len(a)",len(a))//2
fmt.Println("cap(a)",cap(a))//2
fmt.Println("len(b)",len(b))// 3
fmt.Println("cap(b)",cap(b))// 3
```
* 切片的长度和容量
* 通过`len()`求长度, 切片的长度就是它返回值的长度
* 通过`cap()`求容量, 切片的容量是底层数组的容量
```go
// 通过如下例子深刻理解cap()函数
func main() {
arr1:=[...]int{1,2,3,4,5,6,7,8}
s1 := arr1[3:6] // [4 5 6]
fmt.Println(s1) // [4 5 6]
fmt.Println(cap(s1)) // 5 之所以是5, 是因为cap统计的切片底层数组的长度底层数组是[4 5 6 7 8]
}
```
* 基于数组定义切片
```go
// 如下进行切片,左闭右开
// 对数组arr1从0到4进行切割
s3:=arr1[0:4]
fmt.Print(s3) //[1 2 3 4]
// 从0切到指定位置 , [1 2 3 4]
s4:=arr1[:4]
fmt.Println(s4)
// 从开始,切到最后,实际上就是将数组拷贝成切片
s5:=arr1[:]
fmt.Println(s5) //[1 2 3 4 5]
// 从开始,切到最后,实际上就是将数组拷贝成切片
s6:=arr1[:len(arr1)]
fmt.Println(s6) //[1 2 3 4 5]
```
* 切片可以再切片
```go
func main() {
arr1:=[...]int{1,2,3,4,5,6,7,8}
s1 := arr1[1:4] // [2 3 4]
s2 := arr1[5:8] // [6 7 8]
fmt.Println(s1) // [2 3 4]
fmt.Println(s2) // [6 7 8]
}
```
#### 3.06 `make()` 函数构建切片
指定了长度, 但是不指定默认值, Go会为我们添加上如下的默认值
```go
func main(){
// 第二个参数是可变参数, 分别是len,和cap, 如果写一个的话, 默认这两个参数使用一个值
//func make(t Type, size ...IntegerType) Type
//s1:= make([]int,3,3)
s1:= make([]int,3)
s2:= make([]int,0,4)
fmt.Printf("value:%v len:%d cap:%d \n",s1,len(s1),cap(s1)) // value:[0 0 0] len:3 cap:3
fmt.Printf("value:%v len:%d cap:%d \n",s2,len(s2),cap(s2)) // value:[] len:0 cap:4
}
```
#### 3.07 切片的本质
1. 把切片理解成一个窗口, 他可以**框住一块连续的内存**, 这块内存中**只能存放相同类型的元素**
2. 切片属于引用类型, 所以彼此之间不能比较, 只能和`nil`比较
3. 切片中真正的数据保存在底层的数组中
#### 3.08 切片之间不能直接比较
因为**切片是引用类型的**, 他们**只能和nil比较**, 也就是在判断它是否被分配了内存
如果我们想判断一个切片是否为空,我们可以使用 `len(切片) == 0` 来实现
**如果这个切片为nil, 那么它的长度和容量都是0**
如果**这个切片的长度,容量为0, 但是不一定为nil**, 如下
```go
s:=[]int
```
#### 3.09 切片的赋值拷贝
**切片之间的拷贝是浅拷贝, 他们会共用一个底层数组**, 对一个切片的 修改会彼此影响
```go
s1:=make([]int,3,3)
s2:=s1
s2[0] = 999
fmt.Println(s1) // [999,0,0]
fmt.Println(s2) // [999,0,0]
```
#### 3.10 切片的遍历
如下, **通过for 或者 通过range 进行遍历**
```go
s1:=make([]int,3,3)
for i:=0;i<len(s1);i++{
fmt.Println(s1[i])
}
for i,v:=range s1{
fmt.Println(i,v)
}
```
#### 3.11 `append()`方法的妙用
切片下标越界异常
```go
var mySlice []int
mySlice[1] = 2 // runtime error: index out of range [1] with length 0
```
使用**内置函数`append(切片,追加的内容)` 自动完成切片的扩容**
Go中提供了`append()`方法为切片提供动态的添加元素的操作, 切片会指向一个底层的数组, 当随着元素的增多, 切片底层的数组承载不了更多的数据时, 切片就会按照一定的扩容机制对数组进行扩容, **此时, 原来的切片指向的数组会被更换掉**
如下例:
```go
var mySlice []int
for i:=0;i<10;i++{
// Go强制我们使用一个变量去接收append()的返回值
mySlice = append(mySlice,i)
fmt.Printf("value: %v , len:%d , cap:%d , ptr:%p \n",mySlice,len(mySlice),cap(mySlice),mySlice)
}
结果:
value: [0] , len:1 , cap:1 , ptr:0xc00000a0c8
value: [0 1] , len:2 , cap:2 , ptr:0xc00000a120
value: [0 1 2] , len:3 , cap:4 , ptr:0xc00000e3c0
value: [0 1 2 3] , len:4 , cap:4 , ptr:0xc00000e3c0
value: [0 1 2 3 4] , len:5 , cap:8 , ptr:0xc000010240
value: [0 1 2 3 4 5] , len:6 , cap:8 , ptr:0xc000010240
value: [0 1 2 3 4 5 6] , len:7 , cap:8 , ptr:0xc000010240
value: [0 1 2 3 4 5 6 7] , len:8 , cap:8 , ptr:0xc000010240
value: [0 1 2 3 4 5 6 7 8] , len:9 , cap:16 , ptr:0xc00007a080
value: [0 1 2 3 4 5 6 7 8 9] , len:10 , cap:16 , ptr:0xc00007a080
```
#### 3.12 数组的扩容策略
扩容的逻辑可以在slice.go中的 `growslice()`方法中查看到
```go
newcap := old.cap
doublecap := newcap + newcap
if cap > 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) // <nil>
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)
```

View File

@@ -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)) //f31
}
```
####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 | 求长度, stringarray, 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
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -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**

View File

@@ -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()`方法
不可以

View File

@@ -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 stringvalue 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")
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -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")
}
```

View File

@@ -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))
}
```

View File

@@ -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
```

View File

@@ -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
}
}
```

View File

@@ -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
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

File diff suppressed because it is too large Load Diff

View File

@@ -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")
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -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<t.N;i++{
Split("a:b:c",":")
}
}
```
测试结果: 如下图
有包信息, 系统信息, 测试次数, 平均多少ns测试完成一次
![](Snipaste_2020-04-07_07-15-26.png)
在命令行中完成测试 `go test -bench=XXX`
![](Snipaste_2020-04-07_07-17-30.png)
使用-benchmem 参数, 获取内存分配的统计数据
![](Snipaste_2020-04-07_07-20-07.png)
通过上图可以看出, 我们每次测试都会分配三次内存, 每次分配的内存大小是 112B , 接着我们就能根据这个报告去针对性的提升代码的性能
```go
// 提前分配内存, 减少内存的分配次数
result = make([]string,0,strings.Count(s,sep)+1)
```
![](Snipaste_2020-04-07_07-30-40.png)
结果如上, 每次测试只申请一次内存, 每次分配的内存大小为 48B, 完成了1千万次测试, 性能提升了1倍
#### 6.2 性能比较函数
基准测试只能得到给定操作的绝对耗时,但是在**很多性能问题是发生在两个不同操作之间的相对耗时**
* 比如同一个函数处理1000个元素的耗时与处理1万甚至100万个元素的耗时的差别是多少
* 再或者对于同一个任务究竟使用哪种算法性能最佳?我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。
例如下:
```go
func benchmark(b *testing.B, size int){/* ... */}
func Benchmark10(b *testing.B){ benchmark(b, 10) }
func Benchmark100(b *testing.B){ benchmark(b, 100) }
func Benchmark1000(b *testing.B){ benchmark(b, 1000) }
```
示例:
```go
// 注意下面这个函数
func benchmarkFib(b *testing.B, n int) {
// 依然是从 0 ~ b.N
for i:=0;i<b.N;i++{
// 每次循环都是这个指定的fib值 n
Fib(n)
}
}
func BenchmarkFib1(b *testing.B) {
benchmarkFib(b,1)
}
func BenchmarkFib5(b *testing.B) {
benchmarkFib(b,5)
}
func BenchmarkFib10(b *testing.B) {
benchmarkFib(b,10)
}
func BenchmarkFib20(b *testing.B) {
benchmarkFib(b,20)
}
```
测试命令
```go
// 测试全部
go test -bench=.
// 测试指定的函数
go test -bench="Fib"
```
运行结果如下:
![](Snipaste_2020-04-07_07-47-12.png)
默认情况下: 每个基准测试都至少要运行1秒, 如果在Benchmark函数返回值还没有到1秒, 那么 b.N 会按照1,2,5,10,20,50 ... 增加, 并且重新运行函数
我们也可以使用 -benchtime=xs 设置基准测试的时间 , 以 产生更准确的结果
#### 6.3 重置时间
如果我们确定, 在真正执行性能测试之前会有一段和业务代码无关紧要的代码需要执行, 而且还会耗时影响整个测试的时间准确度, 可以考虑使用`b.ResetTimer` 重置时间
```go
func BenchMarkXXX(b *testing.B){
// 耗时操作
b.ResetTimer()
// 开始测试
xxx
}
```
#### 6.4 并行测试
```go
func (b *B) RunParallel(body func(*PB))
```
RunParallel 会创建出多个 `goroutine` , 并将`b.N` 分配给这些`goroutine`中执行
其中的`goroutine`的默认数量是GOMAXPROCS (默认为CPU数)
如果用户想添加非CPU的受限(non-CPU-bound) , 可以在`RunParallel`调用 `SetParallelism` , `SetParallelism`通常和 `-cpu` 一起使用
```go
func BenchmarkSplitParallel(b *testing.B) {
// 在代码中,可以像下面这样设置CPU数量, 也可以在命令行中设置
// b.SetParallelism(4)
b.RunParallel(func(pb *testing.PB) {
for pb.Next(){
Split("a,b,c",",")
}
})
}
```
在命令行中设置CPU的数量
```go
go test -bench=Spilt -cpu 4
```
![](Snipaste_2020-04-07_08-18-20.png)
### 七、Setup 和 TearDown
* Setup : 用来在测试之前添加额外的配置
* TearDown: 用来在测试结束之后, 卸载额外的配置
#### 7.1 TestMain
如果 `*test.go`中 包含`func TestMain(m *testing.M)` 函数时, 生成的测试会先调用TestMain(m) , 然后再运行具体的测试
`TestMain` 运行在`主goroutine`中 , 于是我们就可以像下面这样, 在`m.Run`前后做一些setup和tearDown的操作, 退出测试的时候, 使用 `os.Exit`
```go
func TestMain(m *testing.M) {
fmt.Println("setup ... ")
retCode := m.Run()
fmt.Println("teardown ... ")
fmt.Println("retCode=", retCode)
os.Exit(retCode)
}
func TestFib(t *testing.T) {
Fib(4)
}
```
结果如图所示:
![](Snipaste_2020-04-07_08-58-15.png)
#### 7.2 子测试的Setup 和 TearDown
如下Demo, 我们可以为子测试设置Setup和TearDown
```go
func TestMain(m *testing.M) {
fmt.Println("setup ... ")
retCode := m.Run()
fmt.Println("teardown ... ")
fmt.Println("retCode=", retCode)
os.Exit(retCode)
}
// 测试集的Setup 与 TearDown
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("测试执行前执行")
return func(t *testing.T) {
t.Log("测试执行后执行")
}
}
// 子测试的Serup和TearDown
func setupSubTest(t *testing.T)func(t *testing.T){
t.Log("子测试之前执行")
return func(t *testing.T) {
t.Log("子测试之后执行")
}
}
func TestSplit(t *testing.T) {
type test struct{
input string
sep string
want []string
}
tests:=map[string]test{
"t1":{input:"a,b,c",sep:",",want:[]string{"a","b","c"}},
"t2":{input:"a:b:c",sep:":",want:[]string{"a","b","c"}},
"t3":{input:"a,b,c",sep:",",want:[]string{"a","b","c"}},
}
// 测试之前执行 setup
testCase := setupTestCase(t)
// 测试之后执行tearDown
defer testCase(t)
for name,tc := range tests{
t.Run(name, func(t *testing.T) {
// 子测试之前执行setup
subTest := setupSubTest(t)
// 子测试之后, 执行teardown
defer subTest(t)
got:=Split(tc.input,tc.sep)
if !reflect.DeepEqual(got,tc.want) {
t.Errorf("got:%v but want:%v",got,tc.want)
}
})
}
}
```
执行结果如下图示
![](Snipaste_2020-04-07_09-21-01.png)

View File

@@ -0,0 +1,531 @@
### 一、Context简介:
context是Go在1.7版本中加入的新的标准库, 它定义了Context的类型, **专门用来简化处理单个请求的多个goroutine之间的请求域的数据和取消信号, 截止时间等相关的操作**
### 二、Context接口
```go
type Context interface {
// 返回context被取消的时间, 也就是完成工作时,调用cancel的时间
Deadline() (deadline time.Time, ok bool)
// 调用Done()方法会返回channel, 这个channel会在当前工作完成,或者被cannel()之后关闭
// 多次调用Done() 返回同一个channel
Done() <-chan struct{}
// 如果channel被取消了, 返回canceled错误
// 如果channel超时了, 返回DeadlineExceeded 错误
Err() error
// 用于传递跨越API和进程间请求域的数据
// Value方法会在Context中返回键值对的值
// 对于Context来说, 多次使用相同的key调用Value, 得到的结果也相同
Value(key interface{}) interface{}
}
```
### 三、重要API之 Background() 和 TODO()
在正式看Context接口前我们先看Go的这两个内置函数 `Background() 和 TODO()`
因为通常我们在构建Context时会使用这个内置函数
```go
// 传入一个父Context, 返回父节点的副本
ctx, cancel := context.WithCancel(context.Background())
```
* `Background()`主要用于main函数、初始化以及测试代码中作为Context这个树结构的最顶层的Context也就是根Context。
* `TODO()`它目前还不知道具体的使用场景如果我们不知道该使用什么Context的时候可以使用这个。
* `background``todo`本质上都是`emptyCtx`结构体类型是一个不可取消没有设置截止时间没有携带任何值的Context。
### 四、goroutine之间的通信
Go 提供Context包目的是实现不同goroutine之间的通信
那么让不同的goroutine之间配合工作我们有哪些实现方式呢
#### 4.1 使用`sync.WaitGroup`实现
```go
import (
"fmt"
"sync"
)
var wait sync.WaitGroup
func SayHi() {
fmt.Println("hi!")
wait.Done()
}
func main() {
wait.Add(1)
go SayHi()
wait.Wait()
fmt.Println("main end")
}
```
#### 4.2 使用一个全局变量当作标志位
```go
var wait sync.WaitGroup
var exit bool
func SayHi() {
for {
fmt.Println("hi")
time.Sleep(time.Second)
if exit {
break
}
}
wait.Done()
}
func main() {
wait.Add(1)
go SayHi()
time.Sleep(time.Second*5)
exit = true
wait.Wait()
fmt.Println("main end")
}
```
#### 4.3 手动使用channel控制不同goroutine之间的通信
```go
var wait sync.WaitGroup
var exit bool
func SayHi(exitChan chan struct{}) {
LOOP:
for {
fmt.Println("hi")
time.Sleep(time.Second)
select {
case <-exitChan:
break LOOP
default:
}
}
wait.Done()
}
func main() {
var exitChan = make(chan struct{})
wait.Add(1)
go SayHi(exitChan)
time.Sleep(time.Second*5)
exitChan <- struct{}{}
close(exitChan)
wait.Wait()
fmt.Println("main end")
}
```
#### 4.4 使用context 实现
使用context控制goroutine之间的关系, 明显要更加优雅一些
```go
var wait sync.WaitGroup
func SayHi(ctx context.Context) {
LOOP:
for {
fmt.Println("hi")
time.Sleep(time.Second)
select {
case <-ctx.Done():
break LOOP
default:
}
}
wait.Done()
}
func main() {
ctx,cancel := context.WithCancel(context.Background())
wait.Add(1)
go SayHi(ctx)
time.Sleep(time.Second*5)
cancel()
wait.Wait()
fmt.Println("main end")
}
```
### 五、With系列函数
#### 5.1 WithCancel
```go
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
```
接收一个父Context , 返回一个子Context的副本, 当调用cancel()函数 , 或者是关闭父context的Done通道是, 将关闭副本的Done()通道, 从而实现在 main goroutine中实现对其子goroutine的控制
示例: 在main goroutine中, 控制当达到一定的条件时, 所有的子go rountine退出
```go
func gen(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // return结束该goroutine防止泄露
case dst <- n:
n++
}
}
}()
return dst
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 当我们取完需要的整数后调用cancel
// 循环从channel中往外读值
for n := range gen(ctx) {
fmt.Println(n)
// 当n==5时, 调用cancel
if n == 5 {
break
}
}
}
```
#### 5.2 WithDeadline 和 WithTimeout
```go
// 指定一个明确的时间
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDealline(context.Background(),d)
// 指定一个相对的时间, 一般在设置超时时间使用
ctx, cancel := context.WithDealline(context.Background(),time.Millisecond*50)
```
* WithDeadline
当时间过期, 或者是cancel()函数被调用了, 或者是父Context的上下文的Done通道被关闭了, 以最先发生的情况为准, 控制ctx.Done()的下一步行为
```go
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// 尽管ctx会过期但在任何情况下调用它的cancel函数都是很好的实践。
// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
defer cancel()
// 使用select让主程序等待
// 等待1s后退出, 或者第二种情况, 超时后设置ctx.Done()返回空的结构体, 表示完成
select {
case <-time.After(1 * time.Second):
fmt.Println("oversleep")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
// 输出结果:
context deadline exceeded
```
* WithTimeout
使用场景: 比如正常情况下, 我们在一个无限循环中去获取一个数据库的连接需要10毫秒 , 但是也不排出有超时的可能, 如果一直获取失败, 我们也得退出循环, 节省资源, 现在就能使用 WithTimeout去控制超时的动作
```go
var wg sync.WaitGroup
func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("db connecting ... ")
time.Sleep(time.Millisecond * 10)
select {
case <-ctx.Done():
break LOOP
default:
}
}
fmt.Println("done")
wg.Done()
}
func main() {
// 创建context, 并设置50毫秒的超时世
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel()
wg.Wait()
fmt.Println("over")
}
```
#### 5.3 WithValue
使用场景, 比如一个请求来了我们将它放入goroutine中处理, Server在 main goroutine, 如果想传递给处理请求的goroutine值的话, 我们可以使用WithValue
```go
type TraceCode string
var wg sync.WaitGroup
func worker(ctx context.Context) {
// 构建相应的key
key:=TraceCode("trance_code")
// 根据指定的key,从Context里面取出数据
tranceCode,ok :=ctx.Value(key).(string)
if !ok{
fmt.Println("invalid trace code")
}
LOOP:
for {
fmt.Println("trance_code: ",tranceCode)
time.Sleep(time.Millisecond * 10)
select {
case <-ctx.Done():
break LOOP
default:
}
}
fmt.Println("done")
wg.Done()
}
func main() {
// 创建context, 并设置50毫秒的超时世
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
ctx = context.WithValue(ctx,TraceCode("trance_code"),"123456")
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel()
wg.Wait()
fmt.Println("over")
}
```
### 六、Context使用的注意事项
* 推荐用参数的形式, 显示的传递Context
* 以Context作为参数的函数, 应该将Context放置在第一位
* 如果一个函数需要接收一个Context, 我们还不知道传递什么Context, 不要传递nil , 传递context.TODO()
* Context的**Value相关方法应该传递请求域的必要数据**, 不应该用于传递可选参数
* Context是线程安全的, 可以放心的再多个goroutine中使用
### 七、客户端超时取消案例
Server端, 随机产生 慢响应和快响应
```go
func indexHandler(w http.ResponseWriter , r *http.Request){
// number:= rand.Intn(2)
number:= 0
if number == 0 {
time.Sleep(time.Second*5)
fmt.Fprintf(w,"slow response")
return
}
fmt.Fprintf(w,"quick response")
}
func main() {
http.HandleFunc("/", indexHandler)
err:=http.ListenAndServe(":8888", nil)
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
}
```
客户端,
```go
// 为了方便传递参数, 用结构体封装 响应 和 err
type respData struct {
resp *http.Response
err error
}
var wg sync.WaitGroup
func doCall(ctx context.Context) {
transport := http.Transport{
DisableKeepAlives: true,
}
// 频繁的请求可以自定义全局的client, 使用自定义的协议, 开启长连接
client := http.Client{
Transport: &transport,
}
// 创建chan , 用于当前goroutine和发送请求的goroutine通信
respchan := make(chan *respData, 1)
// 构造请求对象
req, err := http.NewRequest("GET", "http://localhost:8888/", nil)
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
wg.Add(1)
defer wg.Wait()
// 在新的goroutine中 发送请求,获取响应
go func() {
resp, err := client.Do(req)
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
fmt.Println("response: ", resp)
// 为了方便传递参数, 用结构体封装 响应 和 err , 返回时为了高效,而使用指针
rd := &respData{
resp: resp,
err: err,
}
// 把结果写出去
respchan <- rd
wg.Done()
}()
// 添加超时的机制
select {
case <-ctx.Done():
fmt.Println("请求超时了")
case result := <-respchan:
fmt.Println("成功访问服务端")
if result.err != nil {
fmt.Println("出现错误,", result.err)
return
}
defer result.resp.Body.Close()
bytes, _ := ioutil.ReadAll(result.resp.Body)
fmt.Println("resp: ", string(bytes))
}
}
func main() {
// 定义一个100ms的超时请求
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel()
doCall(ctx)
}
```

View File

@@ -0,0 +1,147 @@
### flag标准库简介
flag标准库是Go为我们提供的原生的命令行解析用它开发命令行工具将更为简单
文档: <https://studygolang.com/pkgdoc>
### 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文件时输入的所有参数 **第一个参数是可执行文件本身**

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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` 下载指定的依赖

View File

@@ -0,0 +1,455 @@
### 一. mysql
go-sql-driver : <https://github.com/go-sql-driver/mysql/wiki/Examples>
#### 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() {
```