AI智能
改变未来

从0开始学Go(二)

文章目录

  • 23. map
  • 23.1 map的声明
  • 23.2 make给map分配内存空间
  • 23.3 map声明的时候初始化
  • 23.4 map的增删改查
  • 23.5 map的遍历
  • 23.6 map数据查询
  • 24 对于 slice、map等需要分配内存空间的,使用之前一定要判断一下是否为空
  • 25 线程同步的包sync
    • 25.1 锁的初步使用
  • 26 原子操作
  • 27 结构体
    • 27.1 通过结构体指针访问结构体成员
    • 27.2 结构体中的所有字段在内存中都是连续的
    • 27.3 结构体中的成员的访问权限控制
    • 27.4 二叉树
    • 27.5 工厂模式
    • 27.5 struct中的tag
    • 27.6 匿名字段
    • 27.7 方法
    • 27.8 go里面通过匿名字段实现继承
    • 27.9 多重继承
    • 27.10 接口
    • 27.11 interface关键字
    • 27.12 接口嵌套
    • 27.13 类型断言
    • 27.14 反射
    • 27.15 获取 `ValueOf` 返回值的具体值
    • 27.16 对 ValueOf() 返回值使用 SetXxx() 方法
    • 27.16.1 Example1:
    • 27.16.2 Example2:
  • 27.17 反射操作结构体
  • 27.18 模仿 `encoding/json` 获取结构体tag
    • 27.18.1 下面是官方文档的示例:
    • 27.18.2 我们自己写一个
  • 27.19 均衡负载案例
  • 28 go的类型很严格
  • 29 读写
    • 29.1 终端读写
    • 29.2 格式化读写
    • 29.3 带缓冲区的终端读写
    • 29.4 带缓冲区的文件读写
    • 29.5 读取压缩文件
    • 29.6 文件拷贝
  • 30 命令行参数
    • 30.1 原始用法
    • 30.2 进阶用法
  • 31 json协议
    • 31.1 序列化
    • 31.2 反序列化
  • 32 错误处理
    • 32.1 自定义错误类型
    • 32.2 panic&Recover
  • 33 设置goroute运行在多少个核上
    • 33.1 goroute通信之全局变量与锁同步
    • 33.2 goroute通信之 `channel`
    • 33.2.1 读空channel会阻塞
    • 33.2.2 读有数据的已关闭channel
    • 33.2.3 读已关闭的空channel
    • 33.2.4 写一个满的channel
    • 33.2.5 写一个尚有容量已关闭的channel会panic
    • 33.2.6 使用channel进行goroute间通信并保持同步
    • 33.2.7 使用无缓冲的 channel 保持协程同步
  • 33.3 声明只能读/写的channel
  • 33.4 对 channel 进行 select 操作
    • 33.4.1 select 的分支执行机制
  • 33.5 go中捕获panic
  • 34 定时器
    • 34.1 一次性定时器 time.NewTimer
    • 34.2 一次性定时器之 time.After
    • 34.3 多次定时器
    • 34.4 time.NewTicker 的用法探究
    • 34.4.1 time.NewTicker 所引起的阻塞
    • 34.4.2 非阻塞版 time.NewTicker
  • 35 单元测试
    • 35.1 目录结构:
    • 35. 2 go.mod:
    • 35.3 calc.go
    • 35.4 calc_test.go:
    • 35.5 add函数有bug时的执行结果:
    • 35.6 add函数OK时的执行结果:

    23. map

    key-value 的数据结构,又叫字典或关联数组

    23.1 map的声明

    声明格式如下,声明的map需要使用make分配内存空间,也可以声明的时候初始化。

    package mainimport (\"fmt\")func main() {//var mapName map[keyType]valueTypevar map1 map[string]stringvar map2 map[string]intvar map3 map[int]stringvar map4 map[string]map[string]stringfmt.Println(map1)fmt.Println(map2)fmt.Println(map3)fmt.Println(map4)}/*output:API server listening at: 127.0.0.1:47828map[]map[]map[]map[]Process exiting with code: 0*/

    23.2 make给map分配内存空间

    package mainimport (\"fmt\")func main() {//var mapName map[keyType]valueTypevar map2 map[string]intmap2 = make(map[string]int, 3)map2[\"xiaoming\"] = 22map2[\"xiaowang\"] = 23map2[\"xiaoyao\"] = 25fmt.Println(map2)}/*output:API server listening at: 127.0.0.1:18955map[xiaoming:22 xiaowang:23 xiaoyao:25]Process exiting with code: 0*/

    23.3 map声明的时候初始化

    package mainimport (\"fmt\")func main() {//var mapName map[keyType]valueTypevar map2 map[string]int = map[string]int{\"xiaoming\": 22, \"xiaowang\": 23, \"xiaoyao\": 25}fmt.Println(map2)}/*output:API server listening at: 127.0.0.1:16196map[xiaoming:22 xiaowang:23 xiaoyao:25]Process exiting with code: 0*/

    23.4 map的增删改查

    备注:想清空 map 只能使用 23.5 中的 for 循环,或者重新 make 一下

    package mainimport (\"fmt\")func main() {//var mapName map[keyType]valueTypevar map2 map[string]intmap2 = make(map[string]int, 3)fmt.Println(map2)map2[\"xiaoyao\"] = 25 		//增fmt.Println(map2)fmt.Println(map2[\"xiaoyao\"])//查map2[\"xiaoyao\"] = 23        //改 在key\"xiaoyao\"已经存在的情况下,重新赋值完成了改操作fmt.Println(map2[\"xiaoyao\"])delete(map2, \"xiaoyao\")     //删fmt.Println(map2)fmt.Println(len(map2)) 		//求长度}/*output:API server listening at: 127.0.0.1:38566map[]map[xiaoyao:25]2523map[]0Process exiting with code: 0*/

    23.5 map的遍历

    package mainimport \"fmt\"func main() {//var mapName map[keyType]valueTypevar map2 = map[string]int{\"xiaoyao\": 25, \"xiaowang\": 27, \"xiaolei\": 30}fmt.Println(map2)for key, value := range map2 {fmt.Println(key, \" = \", value)}}/*output:API server listening at: 127.0.0.1:29281map[xiaolei:30 xiaowang:27 xiaoyao:25]xiaoyao  =  25xiaowang  =  27xiaolei  =  30Process exiting with code: 0*/

    23.6 map数据查询

    根据key查询value是否存在,不能直接使用

    var value = mapName[key]

    的形式,因为假如

    mapName[key]

    不存在,它会发回给你一个初始值,而初始值并不能证明该值存在与否。

    package mainimport \"fmt\"func main() {//var mapName map[keyType]valueTypevar map2 = map[string]int{\"xiaoyao\": 25, \"xiaowang\": 27, \"xiaolei\": 30}fmt.Println(map2)var value = map2[\"xiaojia\"]fmt.Println(\"map2[\\\"xiaojia\\\"] = \", value)}/*output:API server listening at: 127.0.0.1:12698map[xiaolei:30 xiaowang:27 xiaoyao:25]map2[\"xiaojia\"] =  0Process exiting with code: 0*/

    查询map数据的正确姿势:

    package mainimport \"fmt\"func main() {//var mapName map[keyType]valueTypevar map2 = map[string]int{\"xiaoyao\": 0, \"xiaowang\": 27, \"xiaolei\": 30}fmt.Println(map2)value1, ok1 := map2[\"xiaojia\"]if ok1 {fmt.Println(\"map2[\\\"xiaojia\\\"] = \", value1)} else {fmt.Println(\"map2[\\\"xiaojia\\\"] doesn\'t exist.\")}value2, ok2 := map2[\"xiaoyao\"]if ok2 {fmt.Println(\"map2[\\\"xiaoyao\\\"] = \", value2)} else {fmt.Println(\"map2[\\\"xiaoyao\\\"] doesn\'t exist.\")}}/*output:API server listening at: 127.0.0.1:49476map[xiaolei:30 xiaowang:27 xiaoyao:0]map2[\"xiaojia\"] doesn\'t exist.map2[\"xiaoyao\"] =  0Process exiting with code: 0*/

    24 对于 slice、map等需要分配内存空间的,使用之前一定要判断一下是否为空

    package mainimport \"fmt\"func main() {//var mapName map[keyType]valueTypevar map2 map[string]intif map2 == nil {map2 = make(map[string]int, 3)}fmt.Println(map2)}/*output:API server listening at: 127.0.0.1:19465map[]Process exiting with code: 0*/

    25 线程同步的包sync

    25.1 锁的初步使用

    锁分为读锁和写锁,这方面的资料有待补充,写好之后会填充一个链接在这里
    使用go build编译时,使用

    --race

    选项,可以使程序在编译完成后,执行时,如果存在资源竞争,则会报出来。

    package mainimport (\"math/rand\"\"sync\")var lock sync.Mutexfunc testMap() {var a map[int]inta = make(map[int]int, 10)for i := 0; i < 10; i++ {a[i] = i}for i := 0; i < 3; i++ {go func(b map[int]int) {lock.Lock()b[0] = rand.Intn(100)lock.Unlock()}(a)}}func main() {testMap()}

    26 原子操作

    go提供了原子操作相关的包:sync/atomic

    package mainimport (\"fmt\"\"sync/atomic\"\"time\")var count int32func testAtomic() {for i := 0; i < 10000; i++ {go func() {atomic.AddInt32(&count, 1)//count++}()}}func main() {testAtomic()time.Sleep(3 * time.Second)fmt.Println(count)}/*output:此时无论执行多少次,输出结果都是10000但是如果不使用原子操作,而是直接count++多次执行,几乎每次输出结果都小于10000*/

    27 结构体

    • 用来自定义复杂数据结构
    • struct 里面可以包含多个字段(属性)
    • struct 类型可以定义方法,注意和函数区分
    • struct 可以嵌套
    • Go语言没有class,只有struct
      定义语法:
      type structName struct {attr1 type1attr2 type2...}

    27.1 通过结构体指针访问结构体成员

    pname.attr1 或者 (*pname).attr1,标准访问形式应该是第二种,但是go做了优化,第一种也可以。

    27.2 结构体中的所有字段在内存中都是连续的

    27.3 结构体中的成员的访问权限控制

    和包的权限控制一样,结构体首字母大写的可以在包外部直接访问,首字母小写的无法在包外部访问。

    27.4 二叉树

    //TODO:此处资料待补充

    27.5 工厂模式

    go语言中struct没有构造函数,如果想要解决这个问题,可以使用工厂模式,专门为这个结构体创建一个初始化函数。

    27.5 struct中的tag

    我们可以为struct中的每一个字段写上一个tag,这个tag可以通过反射的机制来获取,最常用的场景就是json的序列化和反序列化。

    package mainimport (\"encoding/json\"\"fmt\")type Student struct {Name string //`json:\"name\"`Age  uint   //`json:\"age\"`}func main() {var stu Student = Student{\"xiaoyao\",18,}data, err := json.Marshal(stu)if err != nil {fmt.Println(err)return}fmt.Println(string(data))}/*output:API server listening at: 127.0.0.1:32963{\"name\":\"xiaoyao\",\"age\":18}Process exiting with code: 0*/

    27.6 匿名字段

    同类型的匿名字段只能出现一个
    访问匿名结构体字段时,可以直接越过结构体字段,直接使用点访问结构体成员内部的字段
    访问匿名的基础类型字段时,可以通过

    structName.type

    的方式访问
    下面是示例:

    package mainimport (\"fmt\")type Score struct {math    uintEnglish uint}type Student struct {Name stringAge  uintintScore}func main() {var stu Studentstu.Name = \"xiaoyao\"//stu.Score.math = 100stu.math = 100	//实际是上一种写法的简化stu.int = 175fmt.Println(stu)}/*output:API server listening at: 127.0.0.1:13300{xiaoyao 0 175 {100 0}}Process exiting with code: 0*/

    27.7 方法

    定义格式:

    func (receiver type) methodName (paramsList)(returnValuesType) {}	//method1func (receiver *type) methodName (paramsList)(returnValuesType) {}	//method2

    上面两种写法中,method1的修改不会生效,method2的修改才会生效
    下面是示例代码:

    package mainimport (\"fmt\")type Student struct {Name  stringAge   uintScore uint}func (stu Student) modifyName(newName string) {stu.Name = newName}func (stu *Student) modifyAge(newAge uint) {stu.Age = newAge}func main() {var stu Studentstu.Name = \"xiaoyao\"stu.Age = 25stu.Score = 60stu.modifyName(\"xiaoxiaoyao\")//(&stu).modifyAge(18)stu.modifyAge(18)/*最符合调用规则的写法其实是被注释的那种写法由于go的这种特性,跟其它语言相比有些反人类所以go做了一些优化,直接使用结构体变量而不是结构体变量的地址也可以调用该方法*/fmt.Println(stu)}/*output:API server listening at: 127.0.0.1:27542{xiaoyao 18 60}Process exiting with code: 0*/

    27.8 go里面通过匿名字段实现继承

    属性的继承上面已经写过,下面演示一下方法的继承。
    子类指针可以直接访问父类的方法。

    package mainimport (\"fmt\")type people struct {height uintweight uint}type man struct {peoplename string}func (p *people) growUp() {p.height++p.weight++}func main() {var xiaoyao man = man{people{175,81,},\"xiaoyao\",}fmt.Println(xiaoyao)xiaoyao.growUp()fmt.Println(xiaoyao)}/*output:API server listening at: 127.0.0.1:21367{{175 81} xiaoyao}{{176 82} xiaoyao}Process exiting with code: 0*/

    27.9 多重继承

    多个匿名字段构成多继承

    27.10 接口

      广义上讲,接口是抽象出来的某种规范。其最大的意义在于将算法和实现进行分离,使得算法设计者可以不必再关心操作对象的具体实现。
      在大多数编程语言中,接口是某个具体名称的函数。
      例如下面的代码,函数max返回 a 和 b 中的较大者,这个函数中用到了比较操作,将比较操作抽象出一个接口,那么所有实现了比较操作这个接口的类型都可以使用此函数,具体的实现,各种编程语言有所不同,C++中使用的是模板,而Go语言中有其它方法实现,在学习了Go语言的接口之后,读者自己就会明白了,此处不多做赘述。

    returnValue max(typeName a, typeName b) {if (a > b) {return a}return b}

      

    fmt.Printf(obj Object)

    在打印时,会自动调用obj.String()来填充到

    %s

    部分,因此只要我们实现了

    String()

    这个函数,我们就能使用 fmt.Printf() 这个函数来打印我们的自定义类型。
    下面是代码示范:

    package mainimport (\"fmt\")type people struct {height uintweight uint}type man struct {peoplename string}func (p *people) growUp() {p.height++p.weight++}func (p &man) String() string {str := fmt.Sprintf(\"name = %s,\\theight = %d, \\tweight = %d\", p.name, p.height, p.weight)return str}func main() {var xiaoyao man = man{people{175,81,},\"xiaoyao\",}fmt.Printf(\"%s\\n\", &xiaoyao)xiaoyao.growUp()fmt.Printf(\"%s\\n\", &xiaoyao)}/*output:API server listening at: 127.0.0.1:17255name = xiaoyao,	height = 175, 	weight = 81name = xiaoyao,	height = 176, 	weight = 82Process exiting with code: 0*/

    27.11 interface关键字

      关于

    interface

    关键字,这里有篇文章 理解go interface的5个关键点 讲得非常好,可以参考一下,这里我接着讲解go语言中的接口。

    终于到了正点了。

    1. 在go语言中,接口被定义为声明了一组方法的类型,暂且将这个接口类型称为
      InterfaceA

      ,这些方法暂且称为

      methmodA、methodB、methodC ...

    2. InterfaceA 并不需要去实现这些方法。
    3. 我们定义一个结构体
      B

      ,由结构体B来实现这些方法,如果B将这些方法全部都实现了,那么我们称B实现了接口

      InterfaceA

    4. 然后我们可以用
      InterfaceA

      定义一个接口变量

      a

      ,用 B 定义一个结构体变量

      b

    5. 因为 B 实现了接口
      InterfaceA

      ,因此,可以将 b 赋值给 a,然后通过 a 来调用这些方法。
      伪代码如下:

      type InterfaceA interface {methodA()methodB()methodC()...}type B struct {...}func (b B) methodA() {...}func (b B) methodB() {...}func (b B) methodC() {...}a InterfaceAb Ba = ba.methodA()a.methodB()a.methodC()

      现在看起来,很简单,也很枯燥,因为你不知道这么做的意义在哪里,下面干货来了,由于这种可以将 b 赋值给 a 的操作,我们可以将接口类型的变量当做函数的形参,从而实现

    函数的重载

    struct的多态


      我们知道,sort模块中有很多函数可以对很多类型进行排序。我们查看一下源码,可以发现它们的实现都调用了

    Sort()

    这个函数来实现。

      我们知道,go语言对于类型的检查极为严格,即使使用 type 对 int 取一个别名,都无法使用int类型的变量对新类型变量进行赋值,更何况是函数中的形参,而且 go 语言并不允许同一个包中存在同名的函数,即:同一个包中,函数无法利用形参列表进行重载,那么 go 语言是怎样实现使用一个 Sort() 函数对多种类型的数据进行排序的呢?
      答案就是接口,我们继续查看

    Sort()

    函数的源码:

       通过查看源码我们可以发现,

    Sort()

    函数接受的参数是一个

    Interface

    接口类型的变量,而该类型中声明了 3 个方法。按照前文中所述,因此只要我们自定义的类型实现了这三个方法,就等于我们实现了

    Interface

    这个接口,我们自定义的类型就可以使用Sort函数进行排序。
    我们来尝试一下:

    package mainimport (\"fmt\"\"sort\")type MyArr struct {arr *[10]int}func (v MyArr) Len() int {return 10}func (v MyArr) Less(i, j int) bool {if v.arr[i] <= v.arr[j] {return true}return false}func (v MyArr) Swap(i, j int) {v.arr[i], v.arr[j] = v.arr[j], v.arr[i]}func main() {var arr MyArrarr.arr = &[10]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}for i := 0; i < 10; i++ {fmt.Printf(\"%d \", arr.arr[i])}fmt.Println()sort.Sort(arr)for i := 0; i < 10; i++ {fmt.Printf(\"%d \", arr.arr[i])}fmt.Println()}/*output:API server listening at: 127.0.0.1:257489 8 7 6 5 4 3 2 1 00 1 2 3 4 5 6 7 8 9Process exiting with code: 0*/

    27.12 接口嵌套

    type Reader interface {Read()}type Writer interface {Write()}type ReadWriter interface {ReaderWriter}// ReadWriter 接口直接具有了 Read() 方法和 Write() 方法

    27.13 类型断言

    使用

    interfaceVar.(typeName)

    可以将 interfaceVar 转化成 typeName 类型的变量,但是如果 interfaceVar 不是由 typeName 类型实现,则会转换失败,引发panic。比较优雅的做法如接下来的代码所示。

    if t, ok := i.(typeName); ok {fmt.Println(\"s implements I\", t)}

    如果需要区分多种类型,可以使用 switch 断言,更简单直接,这种断言方式只能在 switch 语句中使用。

    switch val := i.(type) {case typeName1:fmt.Println(\"i store typeName1\", val)case typeName2:fmt.Println(\"i store typeName2\", val)...}

    27.14 反射

    运行时动态获取变量的相关信息。

    import \"reflect\"

    下面的代码演示了关于反射的四个相关的函数的使用,更多信息请自行查阅 golang 官方文档 。

    package mainimport (\"fmt\"\"reflect\")type Student struct {Name   stringAge    uintHeight uint}func test(a interface{}) {t := reflect.TypeOf(a) //get the type of the avriable that initializes afmt.Println(\"type is:\\t\", t)v := reflect.ValueOf(a)	// 返回一个 reflect.Value 结构体类型的变量fmt.Println(\"value is:\\t\", v)k := v.Kind()fmt.Println(\"kind is:\\t\", k)/*类型和类别的区别,类型是具体的,类别包含了一些类型例如自定义student结构体type就是packageName.student,但是类别是Struct*/iv := v.Interface()stu, ok := iv.(Student)if ok {fmt.Println(\"after Interface, then type assertion, stu is:\\t\", stu)}}func main() {var a Student = Student{\"xiaoyao\",25,175,}test(a)}/*output:API server listening at: 127.0.0.1:37068type is:	 main.Studentvalue is:	 {xiaoyao 25 175}kind is:	 structafter Interface, then type assertion, stu is:	 {xiaoyao 25 175}Process exiting with code: 0*/

    27.15 获取

    ValueOf

    返回值的具体值

    ValueOf() 的返回值是一个 reflect.Value 类型的结构体变量,并不是真正的源类型(比如说 int、string 之类的)。

    package mainimport (\"fmt\"\"reflect\")func testInt(a interface{}) {val := reflect.ValueOf(a)c := val.Int() //对于 int 类型的 value,使用 value.Int() 获取它的值fmt.Printf(\"get value interface{} %d\\n\", c)fmt.Printf(\"%s\\n\", val.String()) //对于 int 类型的 value,使用 value.String(),直接报错说是 int类型}func main() {testInt(1234)}/*output:API server listening at: 127.0.0.1:16195get value interface{} 1234<int Value>Process exiting with code: 0*/

    27.16 对 ValueOf() 返回值使用 SetXxx() 方法

    27.16.1 Example1:

    实验结果说明,在 testInt(a interface{}) 内部直接修改一个副本是无效的,并且go的运行时机制会检测到并且触发panic,正确的做法应该是传递指针进去。

    package mainimport (\"fmt\"\"reflect\")func testInt(a interface{}) {val := reflect.ValueOf(a)val.SetInt(200)c := val.Int() //对于 int 类型的 value,使用 value.Int() 获取它的值fmt.Printf(\"get value interface{} %d\\n\", c)// fmt.Printf(\"%s\\n\", val.String()) //对于 int 类型的 value,使用 value.String(),直接报错说是 int类型}func main() {var b int = 1testInt(b)}/*output:PS E:\\Code\\GoCode\\TestProject> go build .\\main.goPS E:\\Code\\GoCode\\TestProject> .\\main.exepanic: reflect: reflect.flag.mustBeAssignable using unaddressable valuegoroutine 1 [running]:reflect.flag.mustBeAssignableSlow(0x82)E:/Go/src/reflect/value.go:247 +0x13freflect.flag.mustBeAssignable(...)E:/Go/src/reflect/value.go:234reflect.Value.SetInt(0x4ad720, 0xc0000100b0, 0x82, 0xc8)E:/Go/src/reflect/value.go:1601 +0x42main.testInt(0x4ad720, 0xc0000100b0)E:/Code/GoCode/TestProject/main.go:10 +0xd0main.main()E:/Code/GoCode/TestProject/main.go:19 +0x41PS E:\\Code\\GoCode\\TestProject>*/

    27.16.2 Example2:

    指针版 SetXxx()

    package mainimport (\"fmt\"\"reflect\")func testInt(a interface{}) {val := reflect.ValueOf(a)val.Elem().SetInt(200)	//这里因为interface的缘故,不能直接使用指针,需要调用Elem()方法c := val.Elem().Int() //对于 int 类型的 value,使用 value.Int() 获取它的值fmt.Printf(\"get value interface{} %d\\n\", c)// fmt.Printf(\"%s\\n\", val.String()) //对于 int 类型的 value,使用 value.String(),直接报错说是 int类型}func main() {var b int = 1testInt(&b)}/*PS E:\\Code\\GoCode\\TestProject> go build .\\main.goPS E:\\Code\\GoCode\\TestProject> .\\main.exeget value interface{} 200*/

    27.17 反射操作结构体

    package mainimport (\"fmt\"\"reflect\")type Student struct {Name   stringAge    uintHeight uint}func (s *Student) SetName(newName string) {fmt.Println(\"newName is:\\t\", newName)s.Name = newName}func (s *Student) SetAge(newAge uint) {fmt.Println(\"newAge is:\\t\", newAge)s.Age = newAge}func (s *Student) SetHeight(newHeight uint) {fmt.Println(\"newHeight is:\\t\", newHeight)s.Height = newHeight}func TestStruct(a interface{}) {val := reflect.ValueOf(a)kd := val.Kind()if kd != reflect.Ptr {fmt.Println(\"Expect Ptr\")return}orignKd := val.Elem().Kind()if orignKd != reflect.Struct {fmt.Println(\"Expect struct\'s ptr\")return}keyNum := val.Elem().NumField() //获取结构体中的字段数量fmt.Printf(\"Struct has %d fileds\\n\", keyNum)methodNum := val.NumMethod() //获取结构体的方法的数量fmt.Println(val.Method(0)) //val.Method(0)可以获得方法的地址,然后通过地址调用方法,下面有示例fmt.Printf(\"Struct has %d methods\\n\", methodNum)newName := []reflect.Value{reflect.ValueOf(\"xiaojia\")}newAge := []reflect.Value{reflect.ValueOf(uint(30))}newHeight := []reflect.Value{reflect.ValueOf(uint(180))}/*下面三个方法的调用说明了方法定义的顺序和Val.Method(n)获取方法指针的顺序没有必然联系虽然多次调用,val.Method(n)搜索得到的方法的顺序是一样的但是这种和定义顺序不关联的不确定性还是很让人头疼通过这种方式调用方法时,Call方法的参数必须是 reflect.Value 类型的切片并且参数类型顺序必须严格一致,否则会panic由于前面说的这种不确定性,这种方式不推荐使用*/val.Method(2).Call(newName)val.Method(1).Call(newHeight)val.Method(0).Call(newAge)/*因此建议使用下面的两种方式调用方法:val.Interface().(*Student).SetName(\"xiaoxiaoyao\")val.MethodByName(\"SetName\").Call(newName)*/}func main() {stu := Student{\"xiaoyao\",25,175,}/*由于直接传递变量时,无法在函数内部完成对结构体变量的修改除非是类似切片这种,结构体变量中使用指针存储真正需要操作的数据的地址然后该结构体的方法都是用来操作这个指针的这样的结构体变量直接传递一个变量是没问题的此处我们定义的结构体较简单,因此需要传递一个指针*/TestStruct(&stu)}/*API server listening at: 127.0.0.1:25314Struct has 3 fileds0x4a6880Struct has 3 methodsnewName is:	 xiaojianewHeight is:	 180newAge is:	 30Process exiting with code: 0*/

    27.18 模仿

    encoding/json

    获取结构体tag

    27.18.1 下面是官方文档的示例:

    官方文档获取结构体Tag示例

    package mainimport (\"fmt\"\"reflect\")func main() {type S struct {F string `species:\"gopher\" color:\"blue\"`}s := S{}st := reflect.TypeOf(s)field := st.Field(0)fmt.Println(field.Tag.Get(\"color\"), field.Tag.Get(\"species\"))}/*output:blue gopher*/

    27.18.2 我们自己写一个

    package mainimport (\"fmt\"\"reflect\")type Student struct {Name   string `json:\"Student\'s name\"`Age    uintHeight uint}func TestStruct(a interface{}) {val := reflect.ValueOf(a)kd := val.Kind()if kd != reflect.Ptr {fmt.Println(\"Expect Ptr\")return}orignKd := val.Elem().Kind()if orignKd != reflect.Struct {fmt.Println(\"Expect struct\'s ptr\")return}/*由于这里的TypeOf接收的是由结构体变量赋值的interface变量参数而a是一个指针因此我们需要先由指针类型的 reflect.Value 还原成相应的结构体变量类型的 reflect.Value然后使用 Interface() 接口获得一个由结构体变量赋值得到的接口变量这里有点绕,注意理解*/t := reflect.TypeOf(val.Elem().Interface())fmt.Println(t.Field(0).Tag.Get(\"json\"))}func main() {stu := Student{\"xiaoyao\",25,175,}TestStruct(&stu)}/*output:API server listening at: 127.0.0.1:35031Student\'s nameProcess exiting with code: 0*/

    27.19 均衡负载案例

    均衡负载案例代码
    也可以直接在我的CSDN中下载资源:balance源码

    28 go的类型很严格

    即使是使用type起的别名,跟源类型也是属于两个类型,无法直接赋值。
    但是通过接口可以进行各种骚操作。

    29 读写

    29.1 终端读写

    package mainimport (\"fmt\"\"os\")func main() {fmt.Fprintln(os.Stdout, \"Print to stdout...\")file, err := os.OpenFile(\"./main.txt\", os.O_CREATE|os.O_WRONLY, 0664)if err != nil {fmt.Fprintln(os.Stderr, \"Open file ./main.txt error\", err)return}fmt.Fprintln(file, \"Print to file -- ./main.txt\")file.Close()}/*output:API server listening at: 127.0.0.1:12240Print to stdout...Process exiting with code: 0并且当前目录下,有一个man.txt文件生成,里面内容符合预期*/

    29.2 格式化读写

    package mainimport (\"fmt\")var (firstName, lastName, s stringi                      intf                      float32input                  = \"56.12 / 5212 / Go\"format                 = \"%f / %d / %s\")func main() {fmt.Println(\"Please enter your full name: \")fmt.Scanln(&firstName, &lastName)fmt.Printf(\"Hi %s %s!\\n\", firstName, lastName)fmt.Sscanf(input, format, &f, &i, &s)fmt.Println(\"From the string we read: \", f, i, s)}/*output:PS E:\\Code\\StudyCode\\GoCode\\src\\TestProject> go run .\\main\\main.goPlease enter your full name:xiao yaoHi xiao yao!From the string we read:  56.12 5212 Go*/

    29.3 带缓冲区的终端读写

    package mainimport (\"bufio\"\"fmt\"\"os\")var (inputReader *bufio.Readerinput       stringerr         error)func main() {inputReader = bufio.NewReader(os.Stdin)fmt.Println(\"Please enter some input:\")input, err = inputReader.ReadString(\'\\n\')if err == nil {fmt.Printf(\"The input was: %s\\n\", input)}}/*output:PS E:\\Code\\StudyCode\\GoCode\\src\\TestProject> go run .\\main\\main.goPlease enter some input:看我神威,无坚不摧The input was: 看我神威,无坚不摧*/

    29.4 带缓冲区的文件读写

    带缓冲区的文件读写要记得使用fflush刷新缓冲区

    package mainimport (\"bufio\"\"fmt\"\"os\")func main() {file, err := os.Open(\"./main.go\")if err != nil {fmt.Println(\"Open file err:\", err)return}defer file.Close()inputReader := bufio.NewReader(file)fmt.Println(\"Please enter some input:\")str, err := inputReader.ReadString(\'\\n\')if err == nil {fmt.Printf(\"The file context is: %s\\n\", str)}}/*output:PS E:\\Code\\StudyCode\\GoCode\\src\\TestProject\\main> go run .\\main.goPlease enter some input:The file context is: package main*/

    29.5 读取压缩文件

    package mainimport (\"bufio\"\"compress/gzip\"\"fmt\"\"os\")func main() {fileName := \"G:/BaiduNetdiskDownload/balance.go.gz\"var r *bufio.Readerfi, err := os.Open(fileName)if err != nil {fmt.Fprintf(os.Stderr, \"%v, Cann\'t open %s, error: %s\\n\", os.Args[0], fileName, err)os.Exit(-1)}defer fi.Close()fz, err := gzip.NewReader(fi)if err != nil {fmt.Fprintf(os.Stderr, \"open gzip failed, err: %v\\n\", err)return}r = bufio.NewReader(fz)for {line, err := r.ReadString(\'\\n\')if err != nil {fmt.Println(\"Done reading file\")os.Exit(0)}fmt.Printf(line)}}/*output:API server listening at: 127.0.0.1:15764package balancetype Balancer interface {DoBalance([]*Instance, ...string) (*Instance, error)}Done reading fileProcess exiting with code: 0*/

    29.6 文件拷贝

    核心函数

    io.Copy(dst, src)

    package mainimport (\"fmt\"\"io\"\"os\")func CopyFile(srcName, dstName string) (written int64, err error) {src, err := os.Open(srcName)if err != nil {return}defer src.Close()dst, err := os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE, 0644)if err != nil {return}defer dst.Close()return io.Copy(dst, src)}func main() {CopyFile(\"./main.go\", \"main.go.bak\")fmt.Println(\"Copy done!\")}/*output:API server listening at: 127.0.0.1:43392Copy done!Process exiting with code: 0并且main.go被拷贝成了man.go.bak*/

    30 命令行参数

    os.Args

    是一个string的切片,用来存储所有的命令行参数。

    30.1 原始用法

    package mainimport (\"fmt\"\"os\")func main() {fmt.Println(\"len of args:\\t\", len(os.Args))for i, v := range os.Args {fmt.Printf(\"args[%d] = %s\\n\", i, v)}}/*output:PS E:\\Code\\StudyCode\\GoCode\\src\\TestProject\\main> go run .\\main.go 123 456len of args:     3args[0] = C:\\Users\\16642\\AppData\\Local\\Temp\\go-build749284850\\b001\\exe\\main.exeargs[1] = 123args[2] = 456*/

    30.2 进阶用法

    使用

    flag

    包解析命令行参数。

    package mainimport (\"flag\"\"fmt\")func main() {var (confPath stringlogLevel int)flag.StringVar(&confPath, \"c\", \"\", \"Please input conf path\")flag.IntVar(&logLevel, \"d\", 0, \"Please input log level\")flag.Parse()fmt.Println(\"confPath:\\t\", confPath)fmt.Println(\"logLevel:\\t\", logLevel)}/*output:PS E:\\Code\\StudyCode\\GoCode\\src\\TestProject\\main> go run .\\main.goconfPath:logLevel:        0PS E:\\Code\\StudyCode\\GoCode\\src\\TestProject\\main> go run .\\main.go -c ./main.conf -d 100confPath:        ./main.conflogLevel:        100*/

    31 json协议

    • 导入包:
      import \"encoding/json\"
    • 序列化:
      json.Marshal(data interface{})
    • 反序列化:
      json.UnMarshal(data []type, v interface{})

    31.1 序列化

    package mainimport (\"encoding/json\"\"fmt\")type User struct {UserName    stringNickName    stringAge         uintBirthday    stringSex         stringEmail       stringPhoneNumber string}func testStruct() {user1 := &User{\"user1\",\"xiaoyao\",18,\"1995/08/10\",\"male\",\"[email protected]\",\"01234567890\",}data, err := json.Marshal(user1)if err != nil {fmt.Printf(\"json.Marshal failed, err:\", err)return}fmt.Println(string(data))}func testMap() {map1 := make(map[string]interface{})map1[\"username\"] = \"User1\"map1[\"age\"] = 18map1[\"sex\"] = \"male\"data, err := json.Marshal(map1)if err != nil {fmt.Printf(\"json.Marshal failed, err:\", err)return}fmt.Println(string(data))}func testSlice() {var slice []map[string]interface{}map1 := make(map[string]interface{})map1[\"username\"] = \"User1\"map1[\"age\"] = 18map1[\"sex\"] = \"male\"map2 := make(map[string]interface{})map2[\"username\"] = \"User2\"map2[\"age\"] = 20map2[\"sex\"] = \"female\"slice = append(slice, map1, map2)data, err := json.Marshal(slice)if err != nil {fmt.Printf(\"json.Marshal failed, err:\", err)return}fmt.Println(string(data))}func main() {testStruct()testMap()testSlice()}/*output:API server listening at: 127.0.0.1:44270{\"UserName\":\"user1\",\"NickName\":\"xiaoyao\",\"Age\":18,\"Birthday\":\"1995/08/10\",\"Sex\":\"male\",\"Email\":\"[email protected]\",\"PhoneNumber\":\"01234567890\"}{\"age\":18,\"sex\":\"male\",\"username\":\"User1\"}[{\"age\":18,\"sex\":\"male\",\"username\":\"User1\"},{\"age\":20,\"sex\":\"female\",\"username\":\"User2\"}]Process exiting with code: 0*/

    31.2 反序列化

    下面代码的执行结果说明在接口类型的切片中,结构体和map应该分开反序列化

    package mainimport (\"encoding/json\"\"fmt\")type User struct {UserName    stringNickName    stringAge         uintBirthday    stringSex         stringEmail       stringPhoneNumber string}func getSliceJsonMarshal() (str string, err error) {var slice []interface{}user1 := &User{\"user1\",\"xiaoyao\",18,\"1995/08/10\",\"male\",\"[email protected]\",\"01234567890\",}map1 := make(map[string]interface{})map1[\"username\"] = \"User1\"map1[\"age\"] = 18map1[\"sex\"] = \"male\"map2 := make(map[string]interface{})map2[\"username\"] = \"User2\"map2[\"age\"] = 20map2[\"sex\"] = \"female\"slice = append(slice, user1, map1, map2)data, err := json.Marshal(slice)if err != nil {fmt.Printf(\"json.Marshal failed, err:\", err)return}str = string(data)return}func testSliceJsonUnMarshal() {user1 := User{}map1 := make(map[string]interface{})map2 := make(map[string]interface{})var Slice []interface{}Slice = append(Slice, user1, map1, map2)data, err := getSliceJsonMarshal()if err == nil {err = json.Unmarshal([]byte(data), &Slice)if err == nil {fmt.Println(Slice)} else {fmt.Println(\"json.UnMarshal fail, err: \", err)}}}func main() {user1 := User{\"user1\",\"xiaoyao\",18,\"1995/08/10\",\"male\",\"[email protected]\",\"01234567890\",}fmt.Println(user1)userInfo := `{\"UserName\":\"user1\",\"NickName\":\"xiaoyao\",\"Age\":18,\"Birthday\":\"1995/08/10\",\"Sex\":\"male\",\"Email\":\"[email protected]\",\"PhoneNumber\":\"01234567890\"}`user2 := User{}err := json.Unmarshal([]byte(userInfo), &user2)if err != nil {fmt.Println(\"json.UnMarshal struct fail, err:\", err)} else {fmt.Println(user2)}testSliceJsonUnMarshal()}/*output:API server listening at: 127.0.0.1:5135{user1 xiaoyao 18 1995/08/10 male [email protected] 01234567890}{user1 xiaoyao 18 1995/08/10 male [email protected] 01234567890}[map[Age:18 Birthday:1995/08/10 Email:[email protected] NickName:xiaoyao PhoneNumber:01234567890 Sex:male UserName:user1] map[age:18 sex:male username:User1] map[age:20 sex:female username:User2]]Process exiting with code: 0*/

    32 错误处理

    32.1 自定义错误类型

    package mainimport (\"fmt\"\"os\"\"time\")type PathError struct {path      stringop        stringoccurTime stringmessage   string}func (p *PathError) Error() string {return fmt.Sprintf(\"path=%s\\nop=%s\\noccurTime=%s\\nmessage=%s\", p.path, p.op, p.occurTime, p.message)}func Open(fileName string) error {file, err := os.Open(fileName)if err != nil {return &PathError{path:      fileName,op:        \"open\",occurTime: fmt.Sprintf(\"%v\", time.Now()),message:   err.Error(),}}defer file.Close()return nil}func main() {err := Open(\"./main.txt\")if err != nil {fmt.Println(err)}}/*output:API server listening at: 127.0.0.1:38349path=./main.txtop=openoccurTime=2020-07-09 10:13:20.6787148 +0800 CST m=+0.010969801message=open ./main.txt: The system cannot find the file specified.Process exiting with code: 0*/

    32.2 panic&Recover

    panic信息可以使用Recover函数捕获到,这个在之前的案例中有演示到。
    详情见:从0开始学Go(一) 的 3.6 内置函数 第6部分代码。

    33 设置goroute运行在多少个核上

    go 1.5 以前,需要自己设置,1.5以后默认跑在所有核上。

    package mainimport (\"fmt\"\"runtime\")func main() {num := runtime.NumCPU()runtime.GOMAXPROCS(num - 1)fmt.Println(num)}

    33.1 goroute通信之全局变量与锁同步

    package mainimport (\"fmt\"\"sync\"\"time\")var m = make(map[int]uint64)var lock sync.Mutextype task struct {n int}func calc(t *task) {sum := uint64(1)for i := 1; i <= t.n; i++ {sum *= uint64(i)}lock.Lock()m[t.n] = sumlock.Unlock()}func main() {for i := 0; i < 10; i++ {t := &task{n: i}go calc(t)}time.Sleep(20 * time.Second)lock.Lock()for k, v := range m {fmt.Printf(\"%d! = %v\\n\", k, v)}lock.Unlock()}/*PS E:\\Code\\GoCode\\TestProject> go build --race .\\main.goPS E:\\Code\\GoCode\\TestProject> .\\main.exe4! = 245! = 1207! = 50400! = 12! = 23! = 66! = 7208! = 403209! = 3628801! = 1*/

    33.2 goroute通信之

    channel

    • 类似于 unix 中的管道,先进先出
    • 线程安全,多个 goroute 同时访问不需要加锁
    • channel 是有类型的,一个整数的 channel 只能存放整数
    • 声明语法
      var varName chan dataType

      ,例如:

      var intChan chan int
    • 上面的语法只是声明一个 channel,想要使用还需要用 make 给它分配内存。
      chanName = make(chan dataType, size)

    • 请务必注意: 虽然有些数据类型使用make分配空间时,可以不指定第二个参数size(例如map,slice),但是 channel 在分配时必须指定 size。
    • channel 的读写:
      package mainimport (\"fmt\")var intChan chan int = make(chan int, 1)func main() {//intChan = make(chan int, 10)intChan <- 10a := <-intChanfmt.Println(a)}/*output:API server listening at: 127.0.0.1:3293510Process exiting with code: 0*/

    33.2.1 读空channel会阻塞

    当所有goroutines均被阻塞时,gc会强制终止进程。

    package mainimport (\"fmt\")var intChan chan int = make(chan int, 2)func main() {<-intChanfmt.Println(\"xxxx\")}/*output:PS E:\\Code\\GoCode\\TestProject> go build .\\main.goPS E:\\Code\\GoCode\\TestProject> .\\main.exefatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]:main.main()E:/Code/GoCode/TestProject/main.go:36 +0x41*/

    33.2.2 读有数据的已关闭channel

    下面的代码说明了,当channel中有数据时,channel被关闭了,并不会立即销毁,反而还能继续读取数据。

    package mainimport (\"fmt\"\"time\")var intChan chan int = make(chan int, 1)func main() {//intChan = make(chan int, 10)intChan <- 10close(intChan)time.Sleep(time.Minute)a, ok := <-intChanif ok {fmt.Println(a)} else {fmt.Println(\"intChan has been closed.\")}}*output:API server listening at: 127.0.0.1:3540110Process exiting with code: 0*/

    33.2.3 读已关闭的空channel

    下面的代码说明了,当一个channel没有数据时,使用close关闭它,再读取时就会出现错误。

    package mainimport (\"fmt\")var intChan chan int = make(chan int, 1)func main() {//intChan = make(chan int, 10)intChan <- 10close(intChan)<-intChana, ok := <-intChanif ok {fmt.Println(a)} else {fmt.Println(\"intChan has been closed.\")}}/*output:API server listening at: 127.0.0.1:23498intChan has been closed.Process exiting with code: 0*/

    33.2.4 写一个满的channel

    下面的代码证明,当写一个满的channel时,会阻塞,当所有goroutines都被阻塞时,gc就会终止程序。

    package mainimport (\"fmt\")var intChan chan int = make(chan int, 1)func main() {intChan <- 10intChan <- 20fmt.Println(\"xxxx\")}/*output:API server listening at: 127.0.0.1:21370fatal error: all goroutines are asleep - deadlock!*/

    33.2.5 写一个尚有容量已关闭的channel会panic

    package mainimport (\"fmt\")var intChan chan int = make(chan int, 2)func main() {intChan <- 10close(intChan)intChan <- 20fmt.Println(\"xxxx\")}/*output:PS E:\\Code\\GoCode\\TestProject> go build .\\main.goPS E:\\Code\\GoCode\\TestProject> .\\main.exepanic: send on closed channelgoroutine 1 [running]:main.main()E:/Code/GoCode/TestProject/main.go:38 +0x70*/

    33.2.6 使用channel进行goroute间通信并保持同步

    1. 首先我们看下使用 range 读 channel 的表现
      代码执行结果表明,当channel为空且未关闭时,range 读不到数据会阻塞
      package mainimport (\"fmt\")var intChan chan int = make(chan int, 1)func main() {intChan <- 10for v := range intChan {fmt.Println(v)}fmt.Println(\"xxxx\")}/*output:API server listening at: 127.0.0.1:3444610fatal error: all goroutines are asleep - deadlock!Process exiting with code: 0*/
    2. 关闭channel,再使用range读取
      执行结果表明,当close被关闭后,range读完数据就会直接退出。
      package mainimport (\"fmt\")var intChan chan int = make(chan int, 1)func main() {intChan <- 10close(intChan)for v := range intChan {fmt.Println(v)}fmt.Println(\"xxxx\")}/*outputAPI server listening at: 127.0.0.1:2426610xxxxProcess exiting with code: 0*/
    3. 好了,开始一个小的找出100,000以内所有质数的程序
      上代码:
      package mainimport (\"fmt\"\"runtime\")func calc(dataChan chan int, resultChan chan int, exitChan chan bool) {var i int = 1var iMax intfor v := range dataChan {iMax = v/2 + 1for i = 2; i < iMax; i++ {if v%i == 0 {break}}if i == iMax {resultChan <- v}}exitChan <- true}func main() {dataChan := make(chan int, 20)resultChan := make(chan int, 20)exitChan := make(chan bool, 3)taskNum := runtime.NumCPU() - 2runtime.GOMAXPROCS(taskNum)go func() {for i := 2; i <= 100000; i++ {dataChan <- i}close(dataChan)}()for i := 0; i < taskNum; i++ {go calc(dataChan, resultChan, exitChan)}go func() {//等待taskNum个计算质数的goroute结束for i := 0; i < taskNum; i++ {<-exitChan}//所有计算质数的goroute结束后,关闭存储结果的resultChan//然后主goroute中就可以结束resultChan的读操作循环了close(resultChan)}()//主goroute中读 resultChan,在resultChan 关闭之前,这里会一直读或者阻塞住for v := range resultChan {fmt.Println(v)}}

    33.2.7 使用无缓冲的 channel 保持协程同步

    这种方法容易造成死锁,从而终结整个进程,详情见 34.4.2 的代码,如果将第 10 行代码

    stop := make(chan bool, 1)

    换成

    stop := make(chan bool)

    时,就会造成死锁。

    package mainimport (\"fmt\"\"time\")func main() {ch := make(chan string)defer fmt.Println(\"主协程结束\")go func() {defer fmt.Println(\"子协程结束\")for i := 0; i < 2; i++ {fmt.Println(\"子协程 i = \", i)time.Sleep(time.Second)}ch <- \"我是子协程,即将退出\"}()str := <-chfmt.Println(\"str = \", str)}/*ooutput:API server listening at: 127.0.0.1:13307子协程 i =  0子协程 i =  1str =  我是子协程,即将退出主协程结束子协程结束Process exiting with code: 0*/

    33.3 声明只能读/写的channel

    只读:

    chanName <-chan dataType

    只写:

    chanName chan<- dataType

    作用是用来声明形参,这样可以防止在函数内部误操作。

    33.4 对 channel 进行 select 操作

    • 对一组channel的执行控制。
    • select会监听case语句中channel的读写操作,当case中channel读写操作为非阻塞状态时,将会触发相应的动作。

    33.4.1 select 的分支执行机制

    • 除 default 外,如果只有一个 case 命中,那么就执行这个case里的语句;
    • 除 default 外,如果有多个 case 命中,那么通过伪随机的方式随机选一个;
    • 如果 default 外的 case 语句都没有命中,那么执行 default 里的语句;
    • 如果没有 default,那么 代码块会被阻塞,直到有一个 case 命中。
      关于 select,这里有篇博客,讲得很全面,可以参考下:go select 使用
      package mainimport (\"fmt\"\"time\")func main() {var ch chan intch = make(chan int, 10)for i := 0; i < 5; i++ {ch <- i}for {select {case <-ch:fmt.Println(1)case <-ch:fmt.Println(2)default:fmt.Println(\"get data timeout\")time.Sleep(time.Second)}}}/*outoput:API server listening at: 127.0.0.1:1583612212get data timeoutget data timeoutFailed to get threads - Process 416 has exited with status 0Process exiting with code: 0*/

    33.5 go中捕获panic

    如果某个goroutine panic了,而且这个goroutine 里面没有捕获(recover),那么整个进程就会挂掉。所以,好的习惯是每当go产生一个goroutine,就需要写下recover。

    package mainimport (\"fmt\"\"time\")func main() {defer func() {fmt.Printf(\"process exit at:%v\\n\", time.Now())}()go func() {time.Sleep(time.Second)defer func() {err := recover()if err != nil {fmt.Printf(\"This is goroutine, error has happened, time:%v\\n\", time.Now())fmt.Println(err)}}()b := 0_ = 1 / b}()time.Sleep(3 * time.Second)}/*output:API server listening at: 127.0.0.1:25053This is goroutine, error has happened, time:2020-07-10 10:26:14.6692426 +0800 CST m=+1.007317801runtime error: integer divide by zeroprocess exit at:2020-07-10 10:26:16.678158 +0800 CST m=+3.016233201Process exiting with code: 0*/

    34 定时器

    34.1 一次性定时器 time.NewTimer

    通过定时器可以实现sleep的定时功能。

    package mainimport (\"fmt\"\"time\")func main() {//后台的 goroutine 会在2S之后,向 timer.C 这个channel 中写一下当前时间。timer := time.NewTimer(2 * time.Second)fmt.Printf(\"%+v\\n\", time.Now())t := <-timer.Cfmt.Printf(\"%+v\\n\", t)}/*output:API server listening at: 127.0.0.1:116442020-07-12 10:26:09.2206481 +0800 CST m=+0.0050144012020-07-12 10:26:11.2215126 +0800 CST m=+2.005878901Process exiting with code: 0*/

    34.2 一次性定时器之 time.After

    查阅

    time.After()

    的源码我们发现,其底层居然就是简单地

    return NewTimer(d).C

    ,也就是说,使用 timer.After 时我们不需要使用

    <- timer.C

    的方式了,而是直接就可以

    <- C

    就可以了。

    // After waits for the duration to elapse and then sends the current time// on the returned channel.// It is equivalent to NewTimer(d).C.// The underlying Timer is not recovered by the garbage collector// until the timer fires. If efficiency is a concern, use NewTimer// instead and call Timer.Stop if the timer is no longer needed.func After(d Duration) <-chan Time {return NewTimer(d).C}

    下面是源码:

    package mainimport (\"fmt\"\"time\")func main() {fmt.Printf(\"%+v\\n\", time.Now())C := time.After(2 * time.Second)t := <-Cfmt.Printf(\"%+v\\n\", t)}/*output:API server listening at: 127.0.0.1:113892020-07-12 10:43:12.289121 +0800 CST m=+0.0049875012020-07-12 10:43:14.309193 +0800 CST m=+2.025059501Process exiting with code: 0*/

    34.3 多次定时器

    • 生成多次定时器:
      t := time.NewTicker(time.Second)
    • 停止多次定时器:
      t.Stop()
    • 官方文档描述,停止一个定时器并不会关闭相应的 channel,这是为了防止读取它的 goroutine 出问题。另外,这个 channel 是只读的,无法关闭,因此想要关闭这个 channel 只能等进程结束才可以了。
      // Stop turns off a ticker. After Stop, no more ticks will be sent.// Stop does not close the channel, to prevent a concurrent goroutine// reading from the channel from seeing an erroneous \"tick\".func (t *Ticker) Stop() {stopTimer(&t.r)}

    下面是 time.NewTicker 的用法案例:

    package mainimport (\"fmt\"\"time\")func main() {//每秒触发一次的定时器t := time.NewTicker(time.Second)go func() {for v := range t.C {fmt.Println(\"Hello, \", v)}}()time.Sleep(3 * time.Second)t.Stop()}/*output:API server listening at: 127.0.0.1:16545Hello,  2020-07-12 11:06:32.2162606 +0800 CST m=+1.004999301Hello,  2020-07-12 11:06:33.2232453 +0800 CST m=+2.011984001Process exiting with code: 0*/

    34.4 time.NewTicker 的用法探究

    34.4.1 time.NewTicker 所引起的阻塞

    ticker.Stop() 不关闭 channel 的做法使得我们不能用普通的循环方式来进行 ticker.C 的读取,因为这样势必会造成 goroutine 的永久性阻塞,直到go进程退出。
    从下面代码的执行结果可以看到,直到go进程退出,新创建的 goroutine 中匿名函数都一直在阻塞。

    package mainimport (\"fmt\"\"time\")func main() {ticker := time.NewTicker(time.Second)go func(ticker *time.Ticker) {for t := range ticker.C {fmt.Println(t)}}(ticker)ticker.Stop()fmt.Println(\"Start sleep 3s...\")time.Sleep(3 * time.Second)fmt.Println(\"Sleep 3s end...\")}/*output:API server listening at: 127.0.0.1:42735Start sleep 3s...Sleep 3s end...Process exiting with code: 0*/

    34.4.2 非阻塞版 time.NewTicker

    根据从 33.4.1 中学到的select同时监控多个channel的方法,我们可以向匿名函数中多传递一个channel,通过这个 channel 让 goroutine 退出或者从循环中退出。
    如下:

    package mainimport (\"fmt\"\"time\")func main() {ticker := time.NewTicker(time.Second)stop := make(chan bool, 1)go func(ticker *time.Ticker, stop chan bool) {defer fmt.Println(\"goroutine exit...\")for {select {case t := <-ticker.C:fmt.Println(t)case <-stop:break}}}(ticker, stop)fmt.Println(\"Start sleep 5s...\")time.Sleep(2 * time.Second)ticker.Stop()stop <- truetime.Sleep(3 * time.Second)fmt.Println(\"Sleep 5s end...\")}/*output:API server listening at: 127.0.0.1:18089Start sleep 5s...2020-07-12 13:23:17.5564097 +0800 CST m=+1.0050948012020-07-12 13:23:18.5707391 +0800 CST m=+2.019424201Sleep 5s end...Process exiting with code: 0*/

    35 单元测试

    • 文件名必须以
      _test.go

      结尾

    • 在用例所在目录,使用
      go test

      go test -v

      执行单元测试
      下面上代码:

    35.1 目录结构:

    PS E:\\Code\\GoCode> tree .\\TestProject\\ /fFolder PATH listing for volume ProgramVolume serial number is 9C8A-6920E:\\CODE\\GOCODE\\TESTPROJECTcalc.gocalc_test.gogo.modNo subfolders exist

    35. 2 go.mod:

    module TestProjectgo 1.14

    35.3 calc.go

    package test//待测试函数func add(a int, b int) int {return a - b//return a + b}func sub(a int, b int) int {return a - b}

    35.4 calc_test.go:

    package testimport (\"testing\")/*TestAdd必须大写的Test开头另外形参列表必须这样写,只接受一个参数,参数类型也必须是 *testing.T*/func TestAdd(t *testing.T) {result := add(2, 8) //测试add函数if result != 10 {t.Fatalf(\"add is not right\") //错误打印return}t.Logf(\"add is right\") //成功时的打印}func TestSub(t *testing.T) {result := sub(2, 8)if result != -6 {t.Fatalf(\"add is not right\")return}t.Logf(\"add is right\")}

    35.5 add函数有bug时的执行结果:

    可以看到,如果前面的测试函数出错了,整个测试过程会立即停止。

    PS E:\\Code\\GoCode\\TestProject> go test--- FAIL: TestAdd (0.00s)calc_test.go:10: add is not rightFAILexit status 1FAIL    TestProject     0.320s

    35.6 add函数OK时的执行结果:

    • 此时需要将add函数中的
      return a- b

      替换成

      return a + b
    • 可以看到,整个测试过程很顺利,你也可以看到相关的成功的打印,另外也能看到,
      go test -v

      会打印出 if 不匹配时的常规打印信息,

      go test

      则不会打印这些东西。

    PS E:\\Code\\GoCode\\TestProject> go testPASSok      TestProject     0.842sPS E:\\Code\\GoCode\\TestProject> go test -v=== RUN   TestAddTestAdd: calc_test.go:14: add is right--- PASS: TestAdd (0.00s)=== RUN   TestSubTestSub: calc_test.go:24: add is right--- PASS: TestSub (0.00s)PASSok      TestProject     1.474s
    赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » 从0开始学Go(二)