Go 学习笔记

  1. 1. 语言特性
    1. 1.1. 主要文件夹
    2. 1.2. 工程结构
      1. 1.2.1. 1、工作区
      2. 1.2.2. 2、GOPATH
      3. 1.2.3. 3、源码文件
      4. 1.2.4. 4、库源码文件
      5. 1.2.5. 5、测试源码文件
    3. 1.3. 代码包
      1. 1.3.1. 1、包声明
      2. 1.3.2. 2、包导入
      3. 1.3.3. 3、包初始化
    4. 1.4. 编译顺序
    5. 1.5. 程序实体
  2. 2. CH1 命令
    1. 2.0.1. go run
    2. 2.0.2. go build
    3. 2.0.3. go install
    4. 2.0.4. go test
    5. 2.0.5. go clean
    6. 2.0.6. go doc
    7. 2.0.7. go fix
    8. 2.0.8. go fmt
    9. 2.0.9. go generate
    10. 2.0.10. go get
    11. 2.0.11. go list
    12. 2.0.12. go tool
    13. 2.0.13. go version
    14. 2.0.14. -a
    15. 2.0.15. -n
    16. 2.0.16. -race
    17. 2.0.17. -v
    18. 2.0.18. -work
    19. 2.0.19. -x
  • 3. CH2 语法概览
    1. 3.1. 一、基本构成要素
      1. 3.1.1. 1、标识符
      2. 3.1.2. 2、关键字
      3. 3.1.3. 3、字面量——值
      4. 3.1.4. 4、操作符
      5. 3.1.5. 5、表达式
        1. 3.1.5.1. 变量声明
    2. 3.2. 二、 基本数据类型
      1. 3.2.1. 常量
      2. 3.2.2. rune
        1. 3.2.2.1. 转义符
      3. 3.2.3. String
        1. 3.2.3.1. 声明
        2. 3.2.3.2. 初始化
        3. 3.2.3.3. 字面量表现形式
        4. 3.2.3.4. 拼接
    3. 3.3. 三、高级类型
      1. 3.3.1. 引用类型
      2. 3.3.2. 数组
        1. 3.3.2.1. 声明与赋值
      3. 3.3.3. 切片
        1. 3.3.3.1. 内部结构
        2. 3.3.3.2. 定义与赋值
        3. 3.3.3.3. append
        4. 3.3.3.4. copy()
      4. 3.3.4. 字典
        1. 3.3.4.1. 声明
      5. 3.3.5. 切片与数组的区别
      6. 3.3.6. 函数与方法
      7. 3.3.7. 闭包
      8. 3.3.8. 函数作为实参
      9. 3.3.9. 匿名函数立即执行
      10. 3.3.10. 方法
      11. 3.3.11. 接口
      12. 3.3.12. 结构体
    4. 3.4. 四、流程控制
      1. 3.4.1. 1、代码块与作用域
        1. 3.4.1.1. 代码块
        2. 3.4.1.2. 作用域
      2. 3.4.2. 2、if语句
      3. 3.4.3. 3、switch语句
        1. 3.4.3.1. 1、表达式switch
        2. 3.4.3.2. 2、类型switch
      4. 3.4.4. 4、select
      5. 3.4.5. 5、for语句
        1. 3.4.5.1. 1、for子句
        2. 3.4.5.2. 2、range子句
      6. 3.4.6. 6、defer语句
        1. 3.4.6.1. 使用规则
        2. 3.4.6.2. 优势
        3. 3.4.6.3. 注意
      7. 3.4.7. 7、panic和recover
        1. 3.4.7.1. panic
        2. 3.4.7.2. recover
  • 4. CH3 并发
    1. 4.1. 多进程编程
      1. 4.1.1. 一、进程
        1. 4.1.1.1. 1、进程衍生
        2. 4.1.1.2. 2、进程的标识
        3. 4.1.1.3. 3、进程状态
        4. 4.1.1.4. 4、进程空间
        5. 4.1.1.5. 5、系统调用
      2. 4.1.2. 二、同步
      3. 4.1.3. 三、管道
      4. 4.1.4. 四、信号 os/signal
        1. 4.1.4.1. Example
      5. 4.1.5. 五、socket
        1. 4.1.5.1. 系统调用
        2. 4.1.5.2. 基于TCP/IP协议栈的socket通信
          1. 4.1.5.2.1. Read方法:
          2. 4.1.5.2.2. Write方法:
          3. 4.1.5.2.3. Close方法
          4. 4.1.5.2.4. LocalAddr和RemoteAddr方法
          5. 4.1.5.2.5. SetDeadLine、SetReadDeadLine、SetWriteDeadLine方法
    2. 4.2. 多线程编程
      1. 4.2.1. 线程
      2. 4.2.2. 线程同步
        1. 4.2.2.1. 死锁的解决
        2. 4.2.2.2. 线程安全性
        3. 4.2.2.3. 多线程与多进程
        4. 4.2.2.4. 多核时代并发编程
  • 5. Go的并发机制
    1. 5.1. 一、原理
      1. 5.1.1. 1、线程实现模型
        1. 5.1.1.1. 1.1、 M
        2. 5.1.1.2. 1.2、P
        3. 5.1.1.3. 1.3、G
        4. 5.1.1.4. 1.4、核心元素的容器
      2. 5.1.2. 2、调度器
        1. 5.1.2.1. 2.1、基本结构
        2. 5.1.2.2. 2.2、一轮调度
        3. 5.1.2.3. 2.3、全力查找可运行的G
        4. 5.1.2.4. 2.4、启用或停止M
        5. 5.1.2.5. 2.5、系统检测任务
        6. 5.1.2.6. 2.6、变更P的最大数量
      3. 5.1.3. 3、细节
        1. 5.1.3.1. 1、g0和m0
        2. 5.1.3.2. 2、调度器锁和原子操作
        3. 5.1.3.3. 3、调整GC
    2. 5.2. 二、goroutine(G)
      1. 5.2.1. 1、go语句和goroutine
      2. 5.2.2. 2、主goroutine的运作
      3. 5.2.3. 3、runtime包以及goroutine
        1. 5.2.3.1. 3.1、runtime.GOMAXPROCS
        2. 5.2.3.2. 3.2、runtime.Goexit
        3. 5.2.3.3. 3.3、runtime.Gosched
        4. 5.2.3.4. 3.4、runtime.NumGoroutine
        5. 5.2.3.5. 3.5、runtime.LockOSThread和runtime.UnlockOSThread
        6. 5.2.3.6. 3.6、runtime/debug.SetMaxStack
        7. 5.2.3.7. 3.7、runtime/debug.SetMaxThreads
        8. 5.2.3.8. 3.8、与垃圾回收相关的一些函数
    3. 5.3. 三、channel
      1. 5.3.1. 1、基本概念
        1. 5.3.1.1. 1.1、类型表示法
        2. 5.3.1.2. 1.2、值表示法
        3. 5.3.1.3. 1.3、操作特性
        4. 5.3.1.4. 1.4、初始化通道
        5. 5.3.1.5. 1.5、接收元素值
        6. 5.3.1.6. 1.6、Happens before
        7. 5.3.1.7. 1.7、发送元素值
          1. 5.3.1.7.1. 有时候被传递的值的类型不能简单地判定为值类型或引用类型
        8. 5.3.1.8. 1.8、关闭通道
        9. 5.3.1.9. 1.9、长度与容量
      2. 5.3.2. 2、单向channel
      3. 5.3.3. 3、for语句和channel
      4. 5.3.4. 4、select语句
        1. 5.3.4.1. 4.1、组成与编写方法
        2. 5.3.4.2. 4.2、分支选择规则
        3. 5.3.4.3. 4.3、与for语句连用
      5. 5.3.5. 5、非缓冲的channel
        1. 5.3.5.1. 5.1、happens before
        2. 5.3.5.2. 5.2、同步的特性
      6. 5.3.6. 6、time包与channel
        1. 5.3.6.1. 6.1、定时器Timer
        2. 5.3.6.2. 6.2、断续器

  • 这是我在实习期间学习《Go并发编程实战》第2版时的笔记,本人水平有限,错误难以避免,欢迎发邮件与我交流。

    《Go并发编程实战》第2版的作者郝林的GitHub仓库为:https://github.com/GoHackers/go_command_tutorial

    《Go并发编程实战》第2版书中的代码git仓库为:https://github.com/gopcp/example.v2

    《Go并发编程实战》第2版书中操作系统为linux,而我使用的操作系统为windows,部分内容会有所差异。

    感谢《Go并发编程实战》第2版这本书在我学习go语言时给我的帮助。

    语言特性

    • 静态类型:在Go中,每个变量或常量都必须在申明时指定类型,且不可改变。另外,程序必须通过编译生成归档文件或可执行文件,而后才能被使用或执行。
    • 跨平台
    • 自动垃圾回收
    • 原生的并发编程:拥有自己的并发编程模型:goroutine(Go例程)和channel(通道)以及go。
    • 完善的构建工具。
    • 多编程范式:支持函数式编程。支持面向对象编程,有接口类型与实现类型的概念,但用嵌入替代了继承
    • 代码风格强制统一。
    • 高效编程与运行
    • 丰富的标准库

    主要文件夹

    • api文件夹:存放依照版本顺序的API增量列表文件。API包含公开的变量、常量、函数,用于Go语言API检查
    • bin文件夹:存放主要标准命令文件,包括go、godoc和gofmt。
    • blog文件夹:存放官方博客中所有文章(markdown)
    • doc:存放标准库HTML格式的程序文档,用godoc命令启动一个web
    • lib:存放特殊库文件
    • misc:存放辅助类的说明和工具。
    • pkg:存放安装Go标准库的所有归档文件
    • src:存放Go自身、Go标准工具以及标准库的所有源码文件。
    • test

    工程结构

    1、工作区

    • src目录

    组织并保存Go源码文件,与src下的子目录一一对应。

    • pkg目录

    存放通过go install命令安装后的代码包(其中必须包含Go库源码文件)的归档文件。该目录与GOROOT目录下的pkg目录功能类似。区别时,该目录专门存用户代码的归档文件。编译和安装用户代码的过程一般会以代码包为单位进行

    归档文件是指名称以“.a”结尾的文件。

    • bin目录

    与pkg类似,再通过go install 指令完成安装后,保存由Go命令源代码文件生成的可执行文件,该文件名是主文件名后加.exe后缀。

    命令源码文件与库源码文件的区别

    命令源码文件:申明main代码包并且包含无参数申明和结果申明的main函数的源码文件。是程序的入口,可以独立运行(使用go run命令),也可以通过go build 或 go install命令得到相应的可执行文件

    库源码文件:指存在于某个代码包中的普通源码文件。

    2、GOPATH

    需要将工作区中的目录路径添加到环境变量GOPATH中。否则即使处于同一工作区(事实上,未被加入GOPATH中的目录不应该称之为工作区),代码之间也无法通过绝对代码包路径调用。实际开发中,工作区可以有一个也可以有多个,但它们的目录路径都需要添加到GOPATH中与GOROOT一样,我们要确保GOPATH一直有效。需要注意的是:

    GOPATH中不要包含Go的根目录

    通过go get命令,可将指定项目的源码下载到我们在GOPATH中的第一个工作区中。

    3、源码文件

    • 命令源码文件

    如果一个源码文件被声明属于

    1
    package main

    ,且该文件代码无参数声明和结果声明的main函数即:

    1
    2
    3
    4
    func main()
    {
    //函数
    }

    那么他就是命令源码文件,可通过go run命令直接启动,例如

    1
    go run ./hellow.go

    包名一致性:同一代码包中所有源码文件,其所属包的名称必须一致。如果命令源码文件和库源码文件处于同一个代码包中,那么在该包中就无法正确执行go build和go install命令,即无法通过常规方法编译和安装。因此,命令源码文件常被单独放在一个代码包中。

    单一命令源码文件:同一个代码包可以有多个命令源码文件,可通过go run命令分别运行,但这无法正确执行go build和go install命令。所以,我们不应该把多个命令源码文件放在同一个包中。

    只有一个命令源码文件时,go build即可在该目录下生成一个与目录同名的可执行文件;而若使用go install,则可在当前工作区的bin目录下生成相应的可执行文件。

    4、库源码文件

    库源码文件声明的包名与它直接所属的代码包(目录)名一致,且库源码文件中不包含无参数声明和结果声明的main函数。

    • 所产生的归档文件会被存放到当前工作区的pkg目录中
    • 根据被编译时的目标计算环境,归档文件会被放在gaipkg目录下的平台相关目录中。
    • 存放归档文件的目录的相对路径与被安装的代码包的上一级代码包的相对路径一致。

    5、测试源码文件

    是一种特殊的库文件,通过go test运行包中的所有测试源码文件。成为测试源码的充分条件有两个:

    • 文件名需要以“_test.go”结尾
    • 文件中至少包含一个名称以Test开头或Benchmark开头,且拥有一个类型为*testing.T或*testing.B的参数的函数(分别是功能测试和基准测试所需的)

    代码包

    1、包声明

    任意名称的源码文件都必须以包声明作为文件的第一行,(表从属,感觉类似于java)

    不论命令源文件放在哪个包,它都必须声明属于main包

    2、包导入

    导入的是代码包在工作区的src目录下的相对路径,当导入多个代码包时,用()括起来,在使用被导入代码包中公开的程序实体时,需用包路径最后一个元素加“.”的方式指定代码所在的包如导入gopcp.v2/helper/log中的logger.go需要依赖base子包和logrus子包:

    1
    2
    3
    4
    import (
    "gopcp.v2/helper/log/base"
    "gopcp.v2/helper/log/logrus"
    )

    同一个源码文件中导入的多个代码包的最后一个元素不能重复,否则会引起编译错误。

    解决方案是起别名:

    1
    2
    3
    4
    import (
    "github.com/helper/log/logrus"
    mylogrus "gopcp.v2/helper/log/logrus"
    )

    如果不想加前缀直接使用某个依赖包中的程序实体,可以用“.”来代替别名,如

    1
    2
    3
    4
    5
    6
    7
    import (
    "github.com/helper/log/logrus"
    . "gopcp.v2/helper/log/logrus"
    )

    //在当前源码文件中
    var logger = NewLogger("gopcp")//NewLogger是gopcp.v2/helper/log/logrus包中函数

    只初始化某个包而不使用,用“_”来代替别名:

    1
    2
    3
    import(
    _ "github.com/helper/log/logrus"
    )

    3、包初始化

    init函数专门负责包初始化,该函数无参数声明和结果声明,类似于main。

    该函数在程序真正执行前执行,在main执行前执行完毕,只执行一次

    所有全局变量初始化都在init执行前完成

    1
    2
    3
    func init(){
    fmt.Println("INitialize....")
    }

    编译顺序

    • 全局变量初始化
    • init函数执行
      • 被导入的代码包的init函数
      • 当前文件的init函数(若有多个,无法保证顺序)
    • main函数执行

    程序实体

    变量、常量、函数和类型声明,它们名称统称为标识符。

    标识符可以是Unicode字符集中任意能表示自然语言文字的字符、数字、以及下划线。标识符不能以数字或下划线开头

    标识符首字符大小写控制着对应程序实体的访问权限。“大写”表明该实体可被本代码包外的代码访问到(即可导出的或公开的);否则,只能被本包内的代码访问到(不可导出或私有的)

    要想导出,需要满足:

    • 程序实体非局部。即定义在函数或结构体外
    • 代码包所属目录必须包含在GOPATH中定义的工作区目录内

    CH1 命令

    go run

    可直接启动运行命令源码文件

    go build

    编译代码包,在代码包目录下生成一个与目录同名的可执行文件

    go install

    安装代码包,在当前工作区的bin目录下生成相应的可执行文件

    只有在GOPATH中只含一个工作区的目录路径时,go install命令才会把命令源码文件安装到当前工作区的bin迷路下;否则执行失败,此时必须设置环境变量COBIN,该环境变量的值是一个目录的路径,该目录用于存放所有因安装Go命令源码文件而生成的可执行文件。

    go test

    运行代码包中的所有测试源码文件

    go clean

    清除其他go命令执行遗留下的临时目录和文件

    go doc

    显示Go语言代码包及程序实体的文档

    go fix

    修正指定源码文件中过时语法和代码调用,方便同步升级

    go fmt

    格式化指定源码文件

    go generate

    识别指定源码文件中的go:generate注释,并执行其携带的任意命令(独立于Go语言标准的编译和安装体系)。常用于自动生成或改动Go源码文件

    go get

    下载、编译并安装指定的代码包及依赖包。

    go list

    显式指定代码包信息,利用text/template中规定的模板语法,灵活控制输出信息

    go tool

    运行Go语言的一些特殊工具,直接执行go tool可以看到这些工具

    pprof. 用于以交互的方式访问一些性能概要文件。将会分析给定的概要文件(CPU、内存、程序阻塞),并根据要求提供高可读性的输出信息。这些概要文件可通过标准代码包runtime/pprof中的程序来生成。

    trace 用于读取Go程序踪迹文件,并以图形化的方式展现出来。让我们更深入的了解Go程序在运行过程中的内部情况。如堆的大小及使用情况,多个goroutine是怎样被调度的,及被调度的原因。Go程序踪迹文件可通过标准库 包runtime/trace和net/http/pprof中的程序来生成。

    go version

    -a

    强行重新编译所有涉及的Go语言代码包

    -n

    仅打印命令执行过程中用到的所有命令,而不真正执行

    -race

    检测并报告指定Go语言程序中存在的数据竞争问题

    -v

    打印命令执行过程中所涉及的代码包(无论直接或间接)

    -work

    打印命令执行和使用的临时工作目录的名字,且命令执行完成后不删除它。

    -x

    打印命令执行过程中用到的所有命令,同时执行它们

    CH2 语法概览

    一、基本构成要素

    1、标识符

    ​ 可以表示程序实体(变量、常量、函数和类型声明),前者为后者的名称,同一代码块不允许出现同名的程序实体。

    包括:

    • 所有基本数据类型的名称
    • 接口类型error
    • 常量true、false、iota
    • 所有内建函数名称,即append、cap、close、complex、copy、delete、imag、len、make、panic、print、println、real和recover

    2、关键字

    ​ 也就是保留字

    • 程序声明

    import和package

    • 程序实体声明和定义

    chan、const、func、interface、map、struct、type、var

    • 程序流程控制

    go,select,break,case,continue,default,defer,else,fallthrough,for,goto,if,range,return,switch

    type——类型声明

    1
    2
    3
    type myString string  //myString为String的一个别名类型
    //两者可以进行类型转换
    string(myString("ABC"))//不会产生新值,几乎没什么代价

    空接口——特殊类型,任何类型都是空接口的实现类型,字面量为interface{}

    3、字面量——值

    的一种标记法,有以下三类:

    • 表示基础类型值的各种字面量,例如,表示浮点数类型值的12E-3

    • 构造各种自定义的复合数据类型的类型字面量,例如:

      1
      2
      3
      4
      5
      //定义一个名为Name的结构体
      type Name struct{
      Forename string
      Surname string
      }
    • 表示数据类型的值的符合字面量,可以用来构造struct、array、slice和map类型的

      1
      Name{Forename: "Robert", Surname: "Hao"}

    4、操作符

    1
    2
    3
    4
    ^		按位异或				5^11=14
    一元为按位补码 ^5=-6
    &^ 按位清除 5 &^11=4
    <- 接收 <- ch //若ch代表元素类型为byte的通道类型值,则此表达式为从ch中接受一个byte类型值的操作

    5、表达式

    • 选择表达式:选择一个值中的字段或方法

      1
      cotext.Speaker		//context是变量名
    • 索引表达式:选取数组、切片、字符串或字典中某个元素

      1
      array1[1]
    • 切片表达式:选取数组、切片、字符串或字典中某个范围的元素

      1
      2
      3
      slice1[0:m]//0~m-1
      slice1[:m]//start~m-1
      slice1][m:]//m~end
    • :判断一个接口值是否为某类型,或非接口值的类型是否实现了某个接口类型

      1
      v1.(I1)//v1表示一个接口值,I1表示与该值关联的一个接口类型
      • 若v1是非接口值,则必须再断言前转换为接口值。方法:interface{}(v1).(I1)

      • 若断言判断结果为否,则失败,会引发一个运行时异常(恐慌),解决方法为:

        1
        var i1,okc //ok是布尔类型的变量,它的值体现了类型断言的成败,若成功i1就是经过类型转换的I1类									型的值,否则为I1的零值(默认值)
    • 调用表达式:调用一个函数或一个值的方法

      1
      v1.M1()//v1表示一个值,M1表示与该值关联的一个方法
    变量声明
    1
    2
    3
    4
    5
    6
    //平行赋值
    var i1 I1 // var 变量名 类型
    var ok bool
    i1,ok = interface{}(v1).(I1)
    //短变量声明
    i1, ok := interface{}(v1).(I1) //可以省略var

    二、 基本数据类型

    • 整数类型: uint/int、uint8/int8、uint16.int16、uint32/int32、uint64/int64
    • 浮点类型:float32、float64
    • 布尔类型:bool
    • 复数类型: complex64、complex128
    • 字符串类型: string
    • 字符类型: byte、rune

    byte 和 rune可以分别看作uint8和uint32的别名类型。

    常量

    只有基本类型及其别名才能作为常量类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //声明1:const 常量名 常量类型 = 常量值
    const DEFAULT_IP string = "192.168.0.1"
    const DEFAULT_PORT int = 9001
    //声明2:简写
    const(
    DEFAULT_IP string = "192.168.0.1"
    DEFAULT_PORT int = 9001
    )
    //官方规定用驼峰法命名,书中为全大写加_

    rune

    专用于存储Unicode编码的单个字符,表示rune字面量的5种方式

    • 该字面量所对应的字符,比如’a’、’-‘,这个字符必须Unicode编码规范所支持的
    • 使用”\x”为前导并后跟两位十六进制数,可以表示宽度1字节的值,即一个ASCII编码值
    • 使用”\“为前导并后跟3位八进制数,只能表示有限宽度的值,即只能用于表示再0和255之间的值,与上一个表示范围一致
    • 使用”\u”为前导后跟4位十六进制数,只用于表示2字节宽度的值
    • 使用”\u”为前导后跟8位十六进制数,只用于表示4字节宽度的值
    转义符
    转义符 Unicode代码点 说明
    \a U+0007 告警铃声或蜂鸣声
    \b U+0008 退格符
    \f U+000C 换页
    \n U+000A
    \r U+000D
    \t U+0009
    \v U+000b 垂直制表符
    \\ U+005c
    \‘ U+0027
    \" U+0022

    除制表符外,rune字面量种以”\“为前导的字符序列都是不合法的

    String

    如C++,表示字符值的集合。但是”Go中字符串值是不可变的“,不可能改变一个字符串的内容。对字符串的操作只会返回一个新字符串,而不会改变原字符串并返回。

    声明

    var 变量名 string

    初始化

    默认与C++类似,默认空值为”“

    字面量表现形式
    • 原生字符串字面量:由反引号`包裹
    • 解释型字符串字面量:由双引号””包裹。

    前者所见即所得,而后者可以解析转义字符。

    拼接
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    s1 := "字符串"
    s2 := "拼接"
    //fmt提供的拼接函数
    //虽然不会像直接使用 + 那样产生临时字符串。但是效率也不高
    str := fmt.Sprintf("%s %s %s", "format", "string", "by fmt.Sprintf")

    //+连接
    //由于golang中的字符串是不可变的类型,因此用 + 连接会产生一个新的字符串对效率有影响。
    s3="format"+"string"+"by fmt.Sprintf"

    //用strings包提供的Join函数
    //in函数会先根据字符串数组的内容,计算出一个拼接之后的长度,然后申请对应大小的内存,一个一个字符串填入,在已有一个数组的情况下,这种效率会很高,如果没有的话效率也不高。

    //定义一个字符串数组包含上述的字符串
    var str []string = []string{s1, s2}
    //调用Join函数
    s3 := strings.Join(str, "")
    fmt.Print(s3)

    //调用buffer.WriteString函数,这种方法的性能就要大大优于上面的了。
    var bt bytes.Buffer
    向bt中写入字符串
    bt.WriteString(s1)
    bt.WriteString(s2)
    //获得拼接后的字符串
    s3 := bt.String()

    //用buffer.Builder,这个方法和上面的差不多,不过官方建议用这个,使用方法和上面基本一样
    var build strings.Builder
    build.WriteString(s1)
    build.WriteString(s2)
    s3 := build.String()

    三、高级类型

    为用户定义自己的类型而服务的

    引用类型

    ​ slice、map、channel

    数组

    • 与C++一样,一旦数组长度在声明中确定就无法改变。
    • 只要类型声明中数组长度不同,即使两个数组元素类型相同,它们也是不同的类型
    • 索引表达式和切片表达式都可以用于数组,前者得到某个元素,后者得到一个子数组
    • Go的内建函数 len和cap也都可以应用于数组,并都可以得到它的长度
    • 数组可以避免耗时费力的二次分配操作,因为它长度不可变
    • 多维数组用法不变
    • go中数组传递给函数的是一个副本(值),所以函数操作数组不会改变源数组
    声明与赋值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    //声明:
    var variable_name [SIZE]variable_type
    //例如
    var ipv4 [4]uint8

    //赋值
    //var 变量名 数组类型([size]T) =数组类型{}
    var ipv4 [4]uint8 =[4]uint8{192,168,0,1} //数组类型为[4]uint8,该类型空值为[4]uint8{}
    //由于ipv4同时赋值和定义,所以变量名右边的类型字面量可以省略:var 变量名 =数组类型{}
    var ipv4 =[4]uint8{192,168,0,1}
    //如果在函数中,也可以省略var,但=必须改为:= 变量名 :=数组类型{}
    ipv4 :=[4]uint8{192,168,0,1}

    //如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度:
    ipv4 :=[...]uint8{192,168,0,1}

    //数组传递给函数的是一个副本(值)
    x := [3]int{1,2,3}
    func(arr [3]int) {
    arr[0] = 7
    fmt.Println(arr) //prints [7 2 3]
    }(x)
    fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3])

    //如果你需要更新原始数组数据,请使用数组指针类型。
    func main() {
    x := [3]int{1,2,3}

    func(arr *[3]int) {
    (*arr)[0] = 7
    fmt.Println(arr) //prints &[7 2 3]
    }(&x)

    fmt.Println(x) //prints [7 2 3]
    }

    切片

    • 一种对数组的包装形式,它包装的数组称为该切片的底层数组。
    • 切片是对其底层数组中某个连续片段的描述。
    • 切片类型字面量并不携带长度信息
    • 切片长度是可变的,并不是类型的一部分
    • 只要元素类型相同,两个切片的类型就相同
    • 切片类型的零值总是null
    • 切片相当于对某个底层数组的引用
    • 切片是指针类型,切片传递给函数的是一个副本(指针),所以函数操作切片中元素会改变源切片中的元素。
    内部结构

    包含三个元素:指向底层数组中某个元素的指针、切片长度以及切片容量(不换底层数组前提下,长度的最大值)

    定义与赋值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    //与数组类似,但不需要说明长度: var 变量名 []type 
    var ips []string
    //用make()创建切片,可以用很短的代码初始化一个长度很大的值
    var ips []string =make([]string, len)
    //也可简写为
    ips :=make([]string, len)//由len个元素,都为string的空值

    //直接定义并赋值
    var ips =[]string{"192.168.0.1","192.168.0.2","192.168.0.3"}

    //切片初始化
    s :=[] int {1,2,3 }
    s := arr[:] //是数组arr的引用
    s := arr[startIndex:endIndex] //从startIndex到endIndex-1
    s := arr[startIndex:]
    s := arr[:endIndex]
    s1 := s[startIndex:endIndex] //通过切片s初始化切片s1
    s :=make([]int,len,cap)

    //切片是指针类型
    func main() {
    x := []int{1,2,3}

    func(arr []int) {
    arr[0] = 7
    fmt.Println(arr) //prints [7 2 3]
    }(x)

    fmt.Println(x) //prints [7 2 3]
    }
    append

    与len和cap一样,append也可以用于切片值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ips = append(ips,"192.168.0.4")
    //流程大致如下(根据文字描述推测的)
    //1,新建一个slice
    //2,把ips传给temp
    //3,把新增值赋给temp返回temp
    temp := make([]string, 4)
    for i := 0; i < 3; i++ {
    temp[i] = ips[i]
    }
    temp[3] = "192.168.0.4"
    ips = temp
    copy()
    1
    2
    /* 拷贝 numbers 的内容到 numbers1 */
    copy(numbers1,numbers)

    字典

    • 散列表的一个实现,官方称谓为map
    • 与切片一样,字典类型是一个应用类型,正因此,零值是nil
    声明
    1
    2
    3
    4
    5
    6
    //声明变量,默认map是nil:var map_variable map[key_data_type]value_data_type
    var ipSwitches map[string] bool //key为string,值为bool
    //使用make函数:map_variable := make(map[key_data_type]value_data_type)
    ipSwitches :=make(map[string]bool)

    var ipSwitches :=map[string]bool{}

    添加与修改

    1
    2
    //使用索引表达式
    ipSwitches["192.168.0.1"]=true

    delete

    1
    delete(ipSwitches,"192.168.0.1") 	//无论192.168.0.1是否存在都可以完成

    切片与数组的区别

    • 数组定长(静态分配空间),不含元素下长度也是确定的,切片变长(动态分配空间),不含元素下长度为0

      1
      2
      3
      4
      5
      6
      7
      8
      func main() {
      var testArray [4]int
      fmt.Printf("the length of empty Array is %d \n", len(testArray))
      fmt.Printf("the capacity of Array is %d \n", cap(testArray))
      var testSlice []int
      fmt.Printf("the length of empty slice is %d \n", len(testSlice))
      fmt.Printf("the capacity of Array is %d \n", cap(testSlice))
      }

      结果为

      切片与数组区别

    函数与方法

    • 函数类型的零值是nil,检查外来函数值是否非nil总是有必要的
    • 默认按值传递,也有按引用传递

    函数定义格式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func function_name( [parameter list] ) [return_types] {
    函数体
    }
    //例如
    func divide( divided int, divisor int ) (int,error) {
    函数体
    }//参数列表中参数必须有名称,而结果列表中结果的名称可有可无。不过结果要么都省略名称,要么都要有名称。
    //也可以
    func printTab(){
    //函数体
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //函数体每个条件分支后面一般有return,如果函数声明的结果是有名称的,那么return后不用加东西,否则需要加结果
    //以结果有名称为例
    func divide( divided int, divisor int ) (result int,err error) {
    if divisor == 0{
    err = errors.New("division by zero")
    return
    }
    result = dividend/divisor
    reutrn
    }

    闭包

    Go 语言支持匿名函数,可作为闭包。匿名函数是一个”内联”语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func getSequence() func() int {
    i:=0
    return func() int {
    i+=1
    return i
    }
    }

    func main(){
    /* nextNumber 为一个函数,函数 i 为 0 */
    nextNumber := getSequence()

    /* 调用 nextNumber 函数,i 变量自增 1 并返回 */
    fmt.Println(nextNumber())
    fmt.Println(nextNumber())
    fmt.Println(nextNumber())

    /* 创建新的函数 nextNumber1,并查看结果 */
    nextNumber1 := getSequence()
    fmt.Println(nextNumber1())
    fmt.Println(nextNumber1())
    }

    函数作为实参

    Go 语言可以很灵活的创建函数,并作为另外一个函数的实参。以下实例中我们在定义的函数中初始化一个变量,该函数仅仅是为了使用内置函数 **math.sqrt()**,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package main

    import (
    "fmt"
    "math"
    )

    func main(){
    /* 声明函数变量 */
    getSquareRoot := func(x float64) float64 {
    return math.Sqrt(x)
    }

    /* 使用函数 */
    fmt.Println(getSquareRoot(9))

    }

    匿名函数立即执行

    1
    2
    3
    4
    5
    //自执行匿名函数:匿名函数定义完加()直接执行
    func(x, y int) {
    fmt.Println(x + y)
    }(10, 20)

    方法

    • 实际上是与某个数据类型关联一起的函数

    • 一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。

    1
    2
    3
    func (variable_name variable_data_type) function_name() [return_type]{
    /* 函数体*/
    }

    example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package main

    import (
    "fmt"
    )

    /* 定义结构体 */
    type Circle struct {
    radius float64
    }

    func main() {
    var c1 Circle
    c1.radius = 10.00
    fmt.Println("圆的面积 = ", c1.getArea())
    }

    //该 method 属于 Circle 类型对象中的方法
    func (c Circle) getArea() float64 {
    //c.radius 即为 Circle 类型对象中的属性
    return 3.14 * c.radius * c.radius
    }//c为接收者变量,在值方法中,对接收者变量的赋值不会影响到原值

    在值方法中,对接收者变量的赋值不会影响到原值,按引用传递就可能会了

    • 当接收这类型是引用类型或它的别名类型时,即使是值方法,也会改变源值
    • 对于非指针的数据类型,与它关联的方法集合只包含它的值方法。而对它的指针类型,其方法既包括指针方法也包含值方法
    • 在非指针数据类型的值上,也是能够调用其指针方法的。因为Go在内部做了自动转换,例如下面的代码中,i3.addByPtr(2)会被自动转换为(&i3).addByPtr(2)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func main() {
    i1 := myInt(1)
    i2 := i1.add(2)
    fmt.Printf("按值传递中 i1=%d, i2=%d \n", i1, i2)
    i3 := myInt(1)
    i4 := i3.addByPtr(2)
    fmt.Printf("按引用传递中 i3=%d, i4=%d \n", i3, i4)
    }

    //send param by value 值方法
    func (i myInt) add(another int) myInt {
    i = i + myInt(another)
    return i
    }

    //send param by ptr 引用方法
    func (i *myInt) addByPtr(another int) myInt {
    *i = *i + myInt(another)
    return *i
    }

    按值传递与按引用传递区别

    接口

    • 接口把所有的具有共性的方法定义在一起
    • 接口定义了一组行为,每个行为都由一个方法声明表示
    • 与C++类似,实现与接口分离,接口有方法声明而无实现
    • Go的数据类型之间和接口之间不存在继承关系,但一个接口类型的声明可以嵌入任意其他接口类型中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /* 定义接口 */
    type talk interface {
    Hello(userName string) string
    Talk(heard string) (saying string, end bool, err error)
    }

    //接口实现
    type mytalk string

    func (talk *mytalk) Hello(userName string) string {
    fmt.Printf("userName:%s\t", userName)
    return userName
    }
    func (talk *mytalk) Talk(heard string) (saying string, end bool, err error) {
    //函数体
    }

    在上面的代码中*mytalk才是talk的实现类,而mytalk则不是。

    在Chatbot接口中嵌入talk,*mytalk只要再添四个方法就是Chatbot的实现类了

    1
    2
    3
    4
    5
    6
    7
    type Chatbot interface{
    Name () string
    Begin ()(string,error)
    Talk
    ReportError(err error) string
    End() error
    }

    结构体

    • 结构体不仅可以关联方法,而且可以有内置元素(字段)
    • 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
    • 每个字段声明独占一行
    • 嵌入字段:只有类型字面量的无名称字段,不建议使用
    • 结构体类型的值由符合字面量来表达
    • 结构体类型属于值类型,零值为simpleCN{},不是nil
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //格式如下
    type struct_variable_type struct {
    member definition
    member definition
    ...
    member definition
    }
    //example
    type simpleCN struct{
    name string
    talk Talk
    }

    结构体类型的值,表示结构体类型的值的字面量称为结构体字面量

    1
    2
    3
    4
    5
    //1、符合字面量可以由类型字面量和由花括号包裹的键值对列表组成
    //2、键是结构体中某字段名称,值是赋给该字段的值
    //3、同一结构体中,一个字段名称只能出现一次
    simpleCN{name:"simple.cn",name:"simple.cn"}//由3知这是不合法的

    在编写结构体字面量时可以忽略字段名称,需满足以下两个条件

    • 一视同仁:要么都忽略,要么都不忽略
    • 顺序一致,每个都要赋值
    1
    2
    3
    4
    5
    6
    simpleCN{"simple.cn",nil}//合法
    simpleCN{name:"simple.cn",talk:nil}//合法
    simpleCN{name:"simple.cn"}//合法
    simpleCN{name:"simple.cn",nil}//不合法
    simpleCN{name:"simple.cn"}//不合法
    simpleCN{"simple.cn"}//不合法

    四、流程控制

    1、代码块与作用域

    代码块

    花括号包裹的表达式和语句的序列,可以为空

    • 全域代码块:所有Go代码,最大
    • 代码包代码块:每个代码包中的代码共同组成
    • 源码文件代码块:每个源码文件
    • 每个if、for、switch和select
    • 每个case分支
    作用域

    大体没变化

    • 预定义标识符:全域代码块
    • 表示常量、变量、类型或函数,且声明在函数之外的标识符的作用域是当前的代码包代码块
    • 被导入的代码包的名称:当前源码文件代码块
    • 表示方法接收者、方法参数或方法结果的标识符:当前的方法代码块
    • 表示常量、变量、类型或函数,且声明在函数内部的标识符:包含其声明的最内层代码块

    2、if语句

    • 条件不需要括号!
    • 可以包含一句初始化子语句,初始化局部变量,用;隔开

    3、switch语句

    1、表达式switch
    • switch和所有case携带的表达式都会被求值,顺序是自左向右,自上向下
    • 可以包含一句初始化子语句,初始化局部变量,用;隔开
    • default最多只有一个
    • 可以在switch中使用fallthrough,来向下一个case转移流程控制权
    • 也可以用break退出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var content string
    switch content{
    default:
    语句
    case 情况1:
    语句
    case 情况2:
    语句
    }

    在switch中使用fallthrough,来向下一个case转移流程控制权(实现或)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    switch lang:=string.TrimSpace(content);lang{
    case "Ruby":
    fallthrough
    case "Python":
    代码段1
    case "C","java","Go"://这也是实现或
    代码段2
    default:
    代码段3
    }// 只要lang等于"Ruby"或"Python",代码段2就会被执行
    2、类型switch
    • 对类型进行判定而不是值。
    • 不允许使用fallthrough
    • v.(type)可以写成i:=v.(type),那么i的值一定会是v的值的实际类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var interface{}
    ...//省略部分代码
    switch v.(type){
    case string:
    代码段1
    case int,uint,int8,...://整数型
    代码段2
    default:
    代码段3
    }

    4、select

    • 类似于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。

    • 每个 case 都必须是一个通信

    • 所有 channel 表达式都会被求值

    • 所有被发送的表达式都会被求值

    • 如果任意某个通信可以进行,它就执行,其他被忽略。

    • 如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。

      否则:

      • 如果有 default 子句,则执行该语句。

      • 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    select {
    case communication clause :
    statement(s);
    case communication clause :
    statement(s);
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
    statement(s);
    }

    5、for语句

    1、for子句
    • 条件判断不需要括号
    • 可以用:=初始化子句,其它类似C++
    2、range子句
    • 类似于foreach
    • 一条for语句可以携带一条range子句
    • range 关键字右边的是range表达式,range表达式一般只会在迭代开始前被求值一次

    使用range需注意

    • 若对数组、切片、字符串值进行迭代,且:=左边只有一个迭代变量时,只会得到元素的索引
    • 迭代没有任何元素的数组值、为nil的切片值、为nil的字典值或为“”的字符串值,for在一开始就会执行结束,不会执行其中的代码
    • 迭代为nil的通道值会让当前流程永远阻塞在for语句上
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //这是我们使用range去求一个slice的和。使用数组跟这个很类似
    nums := []int{2, 3, 4}
    sum := 0
    for _, num := range nums {
    sum += num
    }
    fmt.Println("sum:", sum)
    //在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
    for i, num := range nums {
    if num == 3 {
    fmt.Println("index:", i)
    }
    }
    //range也可以用在map的键值对上。
    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
    fmt.Printf("%s -> %s\n", k, v)
    }
    //range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
    for i, c := range "go" {
    fmt.Println(i, c)
    }

    运行结果

    rang表达式类型 第一个值产出 第二个值产出 备注
    a: [n]E、*[n]E或[]E i: int类型 索引值 类型为E的a[i] a为range的结果值,n为数组长度、E为类型
    s: string类型 i: int 索引值 s[i] 类型为rune
    m: map[k]V k 与k对应的v
    c: chan E或<-chan E e:元素的值类型为E E为通道类型的元素类型

    6、defer语句

    • 用于延迟调用指定的函数

    • 只能出现在函数内部

    • defer关键字以及针对某个函数的调用表达式组成

    • 是执行释放资源或异常处理等收尾任务的首选

      1
      2
      3
      4
      func outerFunc(){
      defer fmt.Println("函数执行结束前一刻才会被打印")
      fmt.Println("第一个被打印")
      }//outFunc为外围函数
    使用规则
    • 当外围函数中的语句正常执行完毕时,只有其中所有延迟函数都执行完毕,外围函数才会真正结束执行
    • 当执行外围函数中的return语句时,只有其中所有的延迟函数都执行完毕,外围函数才会真正返回,但是return后面的延迟函数不会被执行
    • 外围函数中的代码引发恐慌(异常)时,只有其中所有的延迟函数都执行完毕后,该恐慌才会被真正扩散至调用函数
    优势
    • 对延迟函数的调用总会在外围函数执行结束前执行
    • defer语句在外围函数体中的位置不限,并且数量不限
    注意
    • 在延迟函数中使用外部变量,应该通过参数传入

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      func printNumbers(){
      for i:=0;i<5;i++{
      defer func(){
      fmt.Printf("%d",i)
      }()
      }
      }
      //执行结果为55555,因为当延迟函数执行时,它们使用的i值已经是5了

      //正确的做法应该是:
      for i := 0; i < 5; i++ {
      defer func(i int) {
      fmt.Printf("%d", i)
      }(i)
      }
      //执行结果是43210,因为同一个外围函数中的延迟函数会被压入同一个栈中
    • 执行顺序为先到后服务,因为同一个外围函数中的延迟函数会被压入同一个栈中

    • 延迟函数调用若有参数传入,那么那些参数的值会在当前defer语句执行时求出

      1
      2
      3
      4
      5
      6
      7
      8
      	for i := 0; i < 5; i++ {
      fmt.Printf("没被延迟的i:%d", i)
      defer func(i int) {
      fmt.Printf("%d", i)
      }(i * 2)
      }
      //其运行结果为:
      没被延迟的i:0没被延迟的i:1没被延迟的i:2没被延迟的i:3没被延迟的i:486420

    7、panic和recover

    一般函数报错是返回一个error类型的值,但是致命错误很可能会使程序无法继续运行。

    panic
    • Go专用内建函数,用于停止当前的控制流程,并引发一个运行时的恐慌。

    • 可以接受任意类型的参数值(通常时string或error,易于描述恐慌的详细信息)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      func main(){
      outerFunc()
      }

      func outerFunc(){
      innerFunc()
      }
      func innerFunc(){
      panic(errors.New("An intended fatal error!"))
      }
      //运行结果为:
      /*panic: An intended fatal error!

      goroutine 1 [running]:
      main.innerFunc(...)
      main.outerFunc2(...)uanyi/zhuanyi.go:27
      */
      //恐慌会沿着调用栈传播,直到最顶层
      //一到最顶层,调用栈中所有函数的执行都被停止,程序已经崩溃
    • 恐慌也可能由逻辑错误导致系统报告(比如数组越界),相当于显示调用panic函数并传入一个runtime.Error类型(这是一个接口类型,内嵌了Go内置的error接口类型)的数值。

    recover
    • 专用于”拦截“运行时恐慌的内建函数recover

    • 使当前程序从恐慌中恢复并重新获得流程控制权

    • 被调用后,会返回一个interface{}类型的结果,如果程序当时处于恐慌的状态,那么该结果非nil

    • 应与defer配合使用

    • 放在函数体开始处(注意defer,所以得在开始的位置)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      func outerFunc2() {
      defer func() {
      if p := recover(); p != nil {
      fmt.Printf("Recovered panic: %s\n", p)
      }
      }()
      innerFunc()
      fmt.Printf("outerFunc没被堵住\n")
      }
      func innerFunc() {
      panic(errors.New("An intended fatal error!"))
      fmt.Printf("innerFunc没被堵住\n")
      }
      func main() {
      //printNumbers()
      outerFunc2()
      fmt.Printf("main没被堵住\n")
      }
      //结果为:
      Recovered panic: An intended fatal error!
      main没被堵住
      //显然outerFunc2被堵住了

    下面代码值得借鉴:

    • 当恐慌携带值类型是scanError时,这个值会被赋给代表结果值的err,否则该恐慌会继续发生

    • 如果重新引发的恐慌传递到最顶层,那么标准输出上就会打印

      panic:<>[recovered]

      ​ panic:<>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    defer func(){
    if e:=recover(); e != nil{
    if se, ok:=e.(scanError);ok{
    err =se.err
    }else{
    panic(e)
    }
    }
    }()

    惯用法:

    • 把恐慌的携带值转换为error类型值,并当作常规结果返回。

      既组织恐慌的扩散,又传递了引起恐慌的原因

    • 检查运行时恐慌携带值的类型,并根据类型做不同的后续动作,这样可以精确的控制程序的错误处理行为

    CH3 并发

    串行程序:只能被顺序执行的指令列表

    并发程序:可以被并发执行的两个及以上的串行程序综合体

    多元程序:允许串行程序运行在一个或多个可共享的CPU上,由操作系统内核支持并提供多个串行程序复用多个CPU的方法

    多元处理:计算机中多个CPU共用一个内存,并且同一时刻可能会有数个串行程序分别运行在不同的CPU上

    并发:可以被同时发起执行的程序,包含了并行

    并行:可以在并行硬件上执行的并发程序

    并发系统更有可能时并行的(分布式系统与之同义)。

    并发程序的执行顺序具有不确定性

    多进程编程

    进程间的通信称为IPC(Inter-Process Communication),讨论IPC时,只针对Linux操作系统。

    在Linux操作系统中,从处理机制的角度来看,IPC可以分为三大类:

    • 基于通信的IPC方法
      • 以数据传送为手段的IPC方法:包括管道(pipe)用于传送字节流和消息队列(message queue)用于传送结构化的消息对象
      • 以共享内容为手段的IPC方法:主要以共享内存区(shared memory)为代表,时最快的一种IPC方法。
    • 基于信号的IPC方法:操作系统中的信号(signal)机制,唯一的异步方法
    • 基于同步的IPC方法:最重要的是信号量(semaphore)

    go支持的方法有管道、信号、socket

    一、进程

    是Unix及其衍生系统的根本,所有代码都是在进程中执行。

    1、进程衍生

    fork()创建子进程,子进程是父进程的副本,它会获得父进程的数据段、堆和栈的副本,每一份副本独立,子进程对副本的修改对于兄弟和父进程来说是透明的

    内核使用写时复制(Copy on Write,简称COW)等技术来提高进程创建的效率。

    新建的子进程可以通过系统调用exec把一个新的程序加载到自己的内存中,原先在其内存中的数据段、堆、栈以及代码段就会被替换掉。之后,子进程调用的就是刚刚加载进来的新程序。

    所有进程共同组成一个树状结构,内核启动进程作为进程树的根,负责系统的初始化操作,它是所有进程的祖先,它的父进程就是它自己。

    2、进程的标识

    如果一个进程先于它子进程结束,那么这些子进程将会被内核启动进程”收养“,成为它的直接子进程

    内核启动进程的PID为1。新建进程ID总是前一个进程ID递增的结果。

    PID可以重复使用

    当PID达到最大值时,内核会从头开始查找闲置的进程ID并使最先找到的哪一个作为新进程ID。

    描述符还会包含当前进程的父进程ID(称为PPID)

    通过Go的标准库代码包os可以查看当前进程的PID和PPID

    1
    2
    pid := os.Getpid()
    ppid :=os.Getppid()
    3、进程状态

    进程状态

    • 可运行状态(TASK_RUNNING, 简称R)

      进程将要或正在运行,但是运行时机不确定,由进程调度器来决定

    • 可中断的睡眠状态(TASK_INTERRUPTIBLE,简称S)

      等待某个事件到来,该状态下的进程会被放在等待队列中

    • 不可中断的睡眠状态(TASK_UNINTERRUPTIBLE,简称D)

      不可被打断,发送给此状态的进程的信号知道它从该状态转出才会被传递过去。此状态下的进程通常是在等待一个特殊的事件,比如等待同步的IO操作完成

    • 暂停状态或跟踪状态(TASK_STOPPED或TASK_TRACED,简称T)

      向进程发送SIGSTOP信号,就会使该进程转入暂停状态,除非该进程正处于不可中断的睡眠状态。

      向正处于暂停状态下的进程发送SIGCONT信号,会使该进程转向可运行状态

      处于该状态的进程会暂停,并等待另一个进程(跟踪它的那个进程)对它进行操作。例如:断点

      向处于跟踪状态的进程发送SIGCONT,并不能使它恢复,只有调试进程进行了相应的系统调用或者退出后,它才能恢复。

    • 僵尸状态(TASK_DEAD-EXIT_ZOMBIE, 简称Z)

      处于此状态的进程即将结束运行,该进程占用的绝大多数资源也都已经被回收,不过还有一些信息未删除,比如退出码以及一些统计信息。

      此时进程主体已经被删除而只留下一个空壳

    • 退出状态(TASK_DEAD-EXIT_DEAD,简称X)

      该状态下,可能连退出码以及统计信息都不需要保留,造成的原因可能是显示地让该进程的父进程忽略掉SIGCHLD信号(当一个进程消亡的时候,内核会给其父进程发送SIGCHLD信号以告知此情况),也可能是该进程已经被分离(即子进程和父进程分别独立运行)。分离后的子进程将不会再使用和执行与父进程共享的代码段中的指令,而是加载并运行一个全新的程序。在这些情况下,该进程在退出的时候就不会转入僵尸状态,而会直接转入退出状态。

      该状态下的进程会立即被干净利落的结束掉,它所占用的系统资源也会被操作系统自动回收。

    4、进程空间
    • 用户进程不能与计算机硬件交互
    • 用户空间虚拟地址是从0到TASK_SIZE,内核占据了剩余的空间。
    • 内核为每个用户分配的是虚拟内存而不是物理内存。
    • 虚拟内存彼此独立、互不干扰、
    5、系统调用
    • 用户态的封装例程(内核暴露给用户的接口,位于用户态)与内核态的系统调用处理程序是一一对应的。
    • 系统调用服务程序和系统调用服务例程可以看作是内核为了相应用户进程的系统调用而执行的一系列函数,统称为内核函数。

    二、同步

    竞态条件(race condition):多个进程同时对同一个资源进行访问时,可能产生的互相干扰。可能发生不频繁,但是一旦发生,就绝对会造成运行结果的错误,并且排查这种错误也很困难。

    原子操作必须由一个单一的汇编指令表示,并且得到芯片级别的支持,内核只提供二进制位和整数的原子操作。只适合细粒度的操作。go中标准库sync/atomic中的一些函数提供了对原子操作的支持。

    sync也包含了对互斥的支持。

    三、管道

    管道(pipe)时一种半双工(或者说单向)的通信方式,只能用于父子进程以及同祖先的子进程之间的通信。例如在使用shell命令时,常常会用到管道(下面这是匿名管道):

    1
    ps aux | grep go	#产看进程状态

    shell为每个命令都创建一个进程,然后把左边命令的标准输出用管道与右边命令的标准输入连接起来。

    优点是简单,缺点是只能单向通信,并且对通信双方的关系做了严格限制

    Go的os/exec提供了支持管道的API,我们可以执行操作系统命令,并在此基础上建立管道。如下,创建一个exec.Cmd类型的值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    cmd0 := exec.Command("echo","-n","My first command comes from golang")
    //对应的shell的命令为:
    //echo -n "My first command comes from golang"

    //创建一个能获得cmd0的输出通道,获取输出,需要在if前加入如下语句
    //
    stdout0,err:=cmd0.StdoutPipe()//StdoutPipe()会返回一个输出管道, 这里把代表该通道的值赋给stdout0
    //stdout0的类型是io.ReadCloser
    if err!=nil{
    fmt.Printf("Error: Couldn't obtain the stdout pipe for command No.0:%s\n",err)
    }
    //

    //在exec.Cmd类型上有一个名为Start的方法,可以用来启动命令
    if err :=cmd0.Start(); err!=nil{
    fmt.Printf("Error: The Command No.0 can not be startup: %s\n",err)
    return
    }

    //启动cmd0之后,可以通过调用stdout0来获取命令的输出
    output0 :=make([]byte,30)
    n,err:=stdout0.Read(output0)//Read把读出数据存入调用方法传递给他的字节切片中,并返回一个int类型值和一个error值
    //若命令输出小于output0的长度,那么变量n的值会是命令实际输出的字节数量,否则n的值就等于output的长度,这往往意味着没有完全读出输出管道中的数据,需要再次读
    //如果管道没有可以读区的数据,那么err就会是io.EOF的值,由此判断是否读完
    if err != nil {
    fmt.printf("Error: Couldn't read data from the pipe:%s\n",err)
    return
    }
    fmt.Printf("%s\n",output[:n])

    多次读,并存放到缓冲区

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var outputBuf0 byte.Buffer
    for{
    tempOutput := make([]byte,5)
    n,err:=stdout0.Read(tempOutput)
    if err !=nil{
    if err==io.EOF{
    break
    }else{
    fmt.printf("Error: Couldn't read data from the pipe:%s\n",err)
    return
    }
    }
    if n>0 {
    outputBuf0.Write(tempOutput[:n])//此处看String中的拼接,作用是将每次读的输出拼接起来
    }
    }
    fmt.Printf("%s\n",outputBuf0.String())

    更便捷的方法是,一开始就用缓冲读取器,此时需要用到bufio库

    1
    2
    3
    4
    5
    6
    7
    outputBuf0 :=bufio.NewReader(stdout0)//NewReader会返回一个bufio.Reader类型的值,也就是一个缓冲读取器(默认缓冲区长度4096)
    output0,_,err :=outputBuf0.ReadLine()//第二个返回结果为bool型表明是否读完。
    if err !=nil{
    fmt.Printf("Error:Couldn't read data from pipe: %s\n",err)
    return
    }
    fmt.printf("%s\n",string(output0))

    管道可以把一个命令的输出作为另一个命令的输入,Go也可以做到:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    cmd1 := exec.Command("ps","aux")
    cmd2 := exec.Command("grep","apipe")
    //首先,设置cmd1的Stdout字段,然后启动cmd1,并等待它运行完毕:
    var outputBuf1 bytes.Buffer // *bytes.Buffer实现了io.Writer的接口,所以才能把&outputBuf1赋值给cmd1.Stdout
    cmd1.Stdout = &outputBuf1
    if err :=cmd1.Start(); err != nil{
    fmt.Printf("Error: The first Command can not be startup: %s\n",err)
    return
    }//这样cmd1启动后的所有内容都被写到outputBuf1.
    if err :=cmd1.Wait(); err != nil{
    fmt.Printf("Error: Couldn't wait for the first Command: %s\n",err)
    return
    }//对cmd1.Wait的调用会一直阻塞到cmd1完全运行结束。

    //接下来,设置cmd2的Stdin和Stdout字段,然后启动cmd2,并等待它运行完毕:
    cmd2.Stdin = &outputBuf1// // *bytes.Buffer也实现了io.Reader的接口,所以才能把&outputBuf1赋值给cmd2.Stdin
    var outputBuf2 bytes.Buffer
    cmd2.Stdout =&outputBuf2
    if err := cmd2.Start(); err != nil{
    fmt.Printf("Error: The second Command can not be startup: %s\n",err)
    return
    }
    if err :=cmd2.Wait(); err != nil{
    fmt.Printf("Error: Couldn't wait for the second Command: %s\n",err)
    return
    }
    fmt.Printf("%s\n",outputBuf2.Bytes())//等cmd2运行结束后再区查看缓冲区outputBuff2中的内容。

    上述代码模拟了操作系统命令(匿名管道):

    1
    ps aux | grep apipe #ps 查看状态,结合grep,查看进程apipe状态

    不过cmd2的输出与操作系统命令输出的结果不同,因为上述程序相当于再运行过程中又运行了一次这个操作系统命令。

    命名管道

    • 任何进程都可以通过命名进程交换数据
    • 命名管道以文件的形式存在于文件系统中
    • 使用方法与文件类似
    • 默认是阻塞式的,只有在其读操作与写操作就绪后,数据才能流转
    • 单向
    • 可以被多路复用,但多个输入端同时写入数据时,需要考虑原子性问题
    1
    2
    3
    mkfifo -m 644 myfifo1	#在当前目录下创建命名管道myfifo1
    tee dst.log < myfifo1 & #用该管道和命令tee把src.log文件中的内容写到 了dst.log文件中
    cat src.log >myfifo1

    在Go的os中,包含了创建这种管道的API

    1
    2
    3
    4
    reader, writer, err := os.Pipe()
    //reader代表了该管道输出端的*os.File类型的值
    //writer代表了输入端的*os.File类型的值
    //err代表了可能的错误,若无错误发生,值为nil
    1
    2
    3
    4
    5
    n,err :=writer.Write(input)
    if err != nil{
    fmt.Printf("Error: Couldn't write data to the named pip: %s\n",err)
    }
    fmt.Printf("written %d byte(s).[file-based pipe]",n)
    1
    2
    3
    4
    5
    6
    output :=make([]byte,100)
    n,err :=reader.Read(output)
    if err != nil{
    fmt.Printf("Error: Couldn't read data from the named pip: %s\n",err)
    }
    fmt.Printf("read %d byte(s).[file-based pipe]",n)

    ​ 若上面两段代码并发运行,那么在reader之上调用Read方法就可以按照顺序获取到通过调用writer的Writer写入的数据。

    exec.Cmd类型的值之上调用StdinPipe或StdoutPipe方法后,得到的输入管道或输出管道也是通过os.Pipe函数生成的,只不过这两个方法内部又对生成的管道做了少许的附加处理。

    • os.Pipe生成的通道在底层是由系统级别的管道来支持的
    • 仍需并发执行分别对内存管道两端进行操作的那两块代码
    • 需要考虑操作原子性的问题 。
    • 操作系统提供的通道是不提供原子操作支持的

    因此Go在io包中提供了基于原子性操作保证的管道。

    四、信号 os/signal

    ​ 操作系统信号(signal)时IPC中唯一一种异步的通信方法,本质是用软件模拟硬件的中断机制。

    • 每个信号都有一个以“SIG”为前缀的名字,例如SIGINT、SIGQUIT、SIGKILL

    • 这些信号都由正整数表示,这些正整数称为信号编号

    • 对同一个进程来说,每种标准信号只会被记录并处理一次。

    • 如果发送给某一进程的信号种类有很多,那么信号处理顺序不确定

    实时信号 解决了上述问题,多个同种类的实时信号可以被记录在案,并且可以按照发送顺序被处理。

    信号来源:

    • 键盘输入:ctrl+c
    • 硬件故障
    • 系统函数调用
    • 软件中的非法运算

    响应信号方式:

    • 忽略
    • 捕捉
    • 执行默认操作

    操作方式:

    • 中止进程
    • 忽略该信号
    • 中止进程并保存内存信息
    • 停止进程
    • 恢复进程(若进程已停止)

    Go命令会对其中一些以键盘输入为来源的标准信号做出响应,通过os/signal中的一些API实现。

    1
    2
    3
    4
    5
    //接口类型os.Signal
    type Signal interface{
    String() string
    Signal() //to distinguish other Stringers,只是用作标识,无实际意义,所有实现它的方法都是空方法
    }

    标准代码库syscall中有与不同操作系统所支持的每一个标准信号对应的同名变量。这些信号变量的类型都是syscall.Signal的。syscall.Signal是os.Signal接口的一个实现类型,也是int类型的别名类型。

    os/signal中的Notify函数用来当操作系统向当前进程发出指定信号时发出通知

    1
    2
    func Notify(c chan<-os.Signal, sig ...os.Signal)//c是一个发送通道,在Notify中只能向它发送一个os.Signal的值,而不能从中接收值,该函数会把当前进程接收到的指定信号放入c代表的通道中,该函数的调用方就可以从中按顺序获取操作系统发来的信号并进行相应的处理。
    //sig是一个可变长的参数,调用signal.Notify时可以在c后面加任意个os.Signal类型的参数值。sig代表的参数值包含我们希望处理的所有信号,接收到需要自行处理的信号后,os/signal包中的signal处理程序会把它封装成syscall.Signal类型的值并放入到signal接收通道中。

    可以把第一个参数绑定实际值,signal处理程序会把我们的意图理解为想要自行处理所有的信号,并把接收的所有信号都逐一进行封装并放入到signal接收通道中。

    1
    2
    3
    4
    5
    6
    sigRecv :=make(chan os.Signal, 1)
    sigs :=[]os.Signal{syscall.SIGINT,syscall.SIGQUIT}
    signal.Notify(sigRecv,sigs...)
    for sig:=range SigRecv{
    fmt.printf("Receive a signal %s\n",sig)
    }//只要sigRecv存在元素,for就会把它们按顺序接收并赋给迭代变量sig,否则,for会被阻塞,并等待新的元素值发送到sigRecv。sigRecv代表的通道关闭后,for会立即退出

    signal处理程序在向signal接收通道发送值时,不会因为通道已满而产生阻塞,因此signal.Notify函数的调用方法应该确保signal接收通道有足够的空间缓存并传递到来的信号。更好的方法是只创建长度为1的signal接收通道,并时刻准备从该通道中接收数据。

    上述代码中,若当前进程接收到为自定义处理的信号,(本该处理)就会执行由操作系统指定的默认操作。

    面以SIGINT为例:Ctrl+c可以停止一个已经失去控制的程序,然而如果上面的程序运行过程中无论按下多少次Ctrl+c,都不能让它停下来,而仅会使标准输出上多出几行信息

    如果代码改成下面这样:

    1
    2
    3
    4
    5
    sigRecv :=make(chan os.Signal, 1)
    signal.Notify(sigRecv)
    for sig : range sigRecv{
    fmt.Printf("Received a signal: %s\n", sig)
    }

    那么发给改进程的所有信号几乎都会被忽略掉。

    在类Unix操作系统下,SIGKILL和SIGSTOP这两种信号既不能自行处理,也不会被忽略,对它们的响应只能使执行系统的默认操作,即使这样调用signal.Notigy函数:

    1
    signal.Notify(sigRecv, syscall.SIGKILL,syscall.SIGSTOP)

    也不会改变当前进程对SIGKILL和SIGSTOP的处理动作。

    对于其它信号,不仅能自行处理它们,还可以在之后的任意时刻恢复对它们的系统默认操作,这需要用到os/signal包中的Stop函数

    1
    func Stop(c chan<-os.Signal)

    该函数会取消掉在之前调用signal.Notify函数使告知signal处理程序需要自行处理若干信号的行为。只有把当初传递给signal.Notify函数的那个signal接收通道作为调用signal.Stop函数时的参数值,才能取消掉之前的行为,否则调用signal.Stop函数不会起到任何作用。调用完signal.Stop函数后,作为参数的signal接收通道将不会再被发送任何信号。副作用是前面的那个for将会一直被阻塞,因此再调用signal.Stop后,使用Close关闭该signal接收通道

    1
    2
    signal.Stop(sigRecv)
    Close(sigRecv)

    只需再次调用signal.Notify,并重新设定与其参数sig绑定的参数值就可以只取消一部分信号的自定义处理,当然要保证信号接收通道相同。

    ​ 如果signal通道不同,那么signal处理程序会视其为不同的调用分别处理。

    1
    sigRecv
    Example
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    func examOfSig() {
    //如果收到SIGQUIT信号,则signal处理程序将他封装后,先后发给sigRecv1,sigRecv2
    //如果收到SIFINT信号,signal处理程序将他封装后,只会发给sigRecv1
    sigRecv1 := make(chan os.Signal)
    sigs1 := []os.Signal{syscall.SIGINT, syscall.SIGQUIT}
    fmt.Printf("Set notification for %s... [sigRecv1]\n", sigs1)
    signal.Notify(sigRecv1, sigs1...)

    sigRecv2 := make(chan os.Signal)
    sigs2 := []os.Signal{syscall.SIGINT, syscall.SIGQUIT}
    fmt.Printf("Set notification for %s... [sigRecv2]\n", sigs2)
    signal.Notify(sigRecv2, sigs2...)

    //用两条for从signal接收通道sigRecv1和sigRecv2中接收信号值。
    //由于它们都会被阻塞,所以必须冰凡执行。
    //需要用到sync中类型WaitGroup让函数再这两段被并发执行的程序片段都执行完毕后再退出执行。
    var wg sync.WaitGroup
    wg.Add(2)//添加一个值为2的差量,咋感觉像同步的信号量
    go func() {
    for sig :=range sigRecv1{
    fmt.Printf("Received a signal from sigRecv1: %s\n",sig)
    }
    fmt.Printf("End. [sigRecv1]\n")
    wg.Done()//使差量减一
    }()
    go func() {
    for sig :=range sigRecv2{
    fmt.Printf("Received a signal from sigRecv2: %s\n",sig)
    }
    fmt.Printf("End. [sigRecv2]\n")
    wg.Done()
    }()

    //停止与signal接收通道sigRecv1对应的信号自定义处理。
    fmt.Println("wait for two seconds... ")
    time.Sleep(2 * time.Second )//等待2秒让我们有时间进行测试
    fmt.Printf("Stop notification... ")
    signal.Stop(sigRecv1)
    close(sigRecv1)
    fmt.Printf("done. [sigRecv1]\n")
    //
    wg.Wait()//与前面的Add、Done组合使用,实现同步。避免示例函数提前退出
    }

    可以使用os.StartProcess启动进程,或者使用os.FindProcess查找进程。这两个函数都会放回一个*os.Process类型的值(进程值)和一个error类型的值

    调用进程值的Signal方法,可以向该进程发送一个信号,该方法可以接受一个os.Signal类型的参数值并返回一个error类型的值。

    在sendSignal函数中实现:

    1、 执行一系列命令并获得演示进程的进程ID。前提使演示进程已经生成。

    2、根据演示进程的ID初始化一个进程值

    3、 使用该进程值上的方法向对应进程发送一个SIGQUIT信号

    4、在标准输出上打印出演示进程已接收到信号的凭证

    1、获取当前进程ID:

    1
    ps aux | grep "signal" | grep -v "grep" |grep -v "go run" |awk '{print $2}'

    也可以创建一个*exec.Cmd类型值:

    1
    2
    3
    4
    5
    6
    7
    cmd := []*exec.Cmd{
    exec.Command("ps", "aux"),
    exec.Command("grep", "signal"),
    exec.Command("grep","-v","grep"),
    exec.Command("grep","-v","go run"),
    exec.Command("awk","{print $2}"),
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //自己实现的不知道正确与否,没跑,返回代表进程ID列表的[]string类型值和一个error类型值。
    func runCmds(cmds []*exec.Cmd) ([]string, error) {
    var IDs []string = make([]string, len(cmds))

    for i, cmd := range cmds {
    stdouti, err := cmd.StdoutPipe()
    if err != nil {
    fmt.Printf("Error: Couldn't obtain the stdout pip for commamd No.%d:%s\n", i, err)
    return IDs, err
    }
    if err := cmd.Start(); err != nil {
    fmt.Printf("Error:the command No %d can not be startup:%s\n", err)
    return IDs, err
    }
    outputBuf := bufio.NewReader(stdouti)
    outputi, _, err := outputBuf.ReadLine()
    if err != nil {
    fmt.Printf("Error: Couldn't read data from the pipe:%s\n", err)
    return IDs, err
    }
    IDs[i]=string(outputi)
    }
    return IDs, nil
    }

    调用runCmds获得进程列表:

    1
    2
    3
    4
    5
    6
    output,err:=runCmds(cmds)
    if err!=nil{
    fmt.Printf("Command Execution Error:%s\n",err)
    return
    }

    1
    pid, err := strconv.Atoi(output) //把output变量中的元素值全部变为int类型

    对于pids中的每一个进程ID:

    1
    proc,err :=os.FindProcess(pid)

    得到进程值,通过它的Signal方法向该值对应的进程发送信号:

    1
    err = proc.Signal(syscall.SIGINT)

    在signal处理程序内部存在一个包级私有的字典(信号集合字典),用于存放以signal接收通道为键并以信号集合的变体为元素的键-元素对。调用signal.Notify时,signal处理程序就会在信号集合字典中找该对,若不存在,就会添加一个,否则就更新它,当调用signal.Stop时,会删除它。

    利用go的信号处理接口,我们可以做很多事情,比如在进程终止之前进行一些善后处理。

    五、socket

    系统调用

    在linux系统中存在一个名为socket的系统调用:

    1
    int socket(int domain, int type, int protocol)

    其功能为创建一个socket实例,三个参数分别代表socket的通信域、类型和所用协议。

    • 每个socket都必将存在于一个通信域当中,而通信域决定了该socket的地址格式和通信范围:

      通信域 含义 地址形式 通信范围
      AF_INET IPv4域 IPv4地址(4个字节)端口号(2个字节) 在基于IPv4协议的网络中任意两台计算机上的两个应用程序
      AF_INET6 IPv6域 IPv6地址(16个字节)端口号(2个字节) 在基于IPv6协议的网络中任意两台计算机上的两个应用程序
      AF_UNIX Unix域 路径名称 在同一台计算机上的两个应用程序

    soket的类型:

    socket类型
    特性 SOCK_DGRAM SOCK_RAW SOCK_SEQPACKET SOCK_STREAM
    数据形式 数据报 数据报 字节流 字节流
    数据边界 没有
    逻辑连接 没有 没有
    数据有序性 不能保证 不能保证 能够保证 能够保证
    传输可靠性 不具备 不具备 具备 具备
    • 以数据报为数据形式意味着数据接收方的socket接口程序可以意识到数据的边界会对它们进行切分,省去了接收方的应用程序寻找数据边界和切分数据的工作量
    • 字节流实际上传输的是一个字节接着一个字节的串,我们可以把它想象成一个很长的字节数组
      • 一般情况下,字节流不能体现出哪些字节属于哪个数据包
      • 只能由应用程序去分离处独立的数据包
      • SOCK_SEQPACKET是例外,发送方的socket接口程序可以忠实地记录数据边界。接收方的socket接口程序会根据数据边界把字节流切分成(还原成)若干个字节流片段并按照需要一次传递给应用程序。

    在面向有连接的socket之间传输数据之前,必须先建立逻辑连接。

    • 由于连接已暗含双方地址,所以在传输数据的时候不必再指定目标地址。

    面向无连接的socket传输的每一个数据包都是独立的,并且会直接发送到网络上。

    • 这些数据包都含有目标地址
    • 数据流只能是单向的。

    SOCK_RAW提供了一个可以直接通过底层(TCP/IP中的网络互联层)传送数据的代码,为了保证安全性,应用程序必须具有操作系统的超级用户权限才能使用这种方法。使用成本也相对较高,应用程序需要自己构建数据传输格式。因此,应用程序极少使用这种类型的socket。

    系统调用socket时,一般把0作为它第三个参数值,让操作系统根据第一个参数和第二个参数的值自行绝对socket所使用的协议

    通信域和类型与所用协议之间的对应关系如下:

    决定因素 SOCK_DGRAM SOCK_RAW SOCK_SEQPACKET SOCK_STREAM
    AF_INET UDP IPv4 SCTP TCP或SCTP
    AF_INET6 UDP IPv6 SCTP TCP或SCTP
    AF_UNIX 有效 无效 有效 有效

    “有效”表示该通信域和类型的组合会使内核选择某个内部的socket协议。“无效”表示通信域和类型的组合不合法。

    当没任何错误时,返回一个int类型的值,其为socket实例唯一标识符的文件描述符,可以通过它调用其他系统调用来进行各种相关操作,比如绑定和监听端口、发送和接收数据以及关闭socket实例。

    基于TCP/IP协议栈的socket通信

    socket接口与TCP/IP协议栈、操作系统内核的关系

    实现客户端与服务端程序需要使用标准代码包net中的API,

    实现服务端需要用到Listen:

    1
    func Listen(net, laddr string)(Listener,error)

    net是指以何种协议监听给定的地址,

    • 这个参数所代表的必须是面向流的协议

    • TCP和STCP都属于面向流的传输层协议,但TCP无法记录和感知任何消息边界,也无法从字节流分离出消息,而SCTP可以。

    • 消息是数据包在TCP/IP协议的应用层中的称谓。

    • 消息边界与数据边界的含义基本相同,但消息边界仅针对消息,而数据边界的对象范围更广

    • 数据段是TCP为了使数据流满足网络传输的要求而做的分段,与用于区分独立消息的消息边界毫不相关。

    综上,net必须使tcp(/4/6)、unix和unixpacket中的一个。

    laddr表示当前程序在网络中的标识,格式是host:port,为服务端地址

    • 若host是主机名,该API会先通过DNS找到与该主机名对应的IP地址。若主机名未在DNS中注册,那么会出错

    代表协议的字符串字面量如下:

    字面量 socket协议 备注
    “tcp” TCP 兼容tcp4和tcp6两个版本。
    “tcp4” TCP 网络互连层协议仅支持IPv4
    “tcp6” TCP 。。。IPv6
    “udp” UDP
    “udp4” UDP 网络互连层协议仅支持IPv4
    “udp6” UDP 。。。IPv6
    “unix” 有效 可看作通信为AF_UNIX,类型为SOCK_STREAM时内核采用的默认协议
    “unixgram” 有效 可看作通信为AF_UNIX,类型为SOCK_DGRAM时内核采用的默认协议
    “unixpacket” 有效 可看作通信为AF_UNIX,类型为SOCK_SEQPACKET时内核采用的默认协议

    实现客户端需要用到Dial:

    1
    func Dial(network,address string)(conn,error)

    network与net非常类似,但它有更多的可选值,因为发送数据前不一定要先建立连接,因此udp(/4/6)、ip(\4\6)、unixgram都可以作为参数network的值。

    address与laddr完全一致。如果想与服务端相连,则其值为服务端地址,也可由raddr代替。

    laddr与raddr是相对的,前者指当前程序所使用地址(本地地址),后者指参与通信的另一端所使用的地址(远程地址)

    设定net.Dial的超时时间(Linux默认75s):

    1
    2
    3
    4
    5
    func DialTimeout(network,address string,timeout time.Duration)(conn,error)
    //最后一个参数专用于设定超时时间,单位是纳秒
    //有与常用时间单位对应的time.Duration类型的常量,可以用常量拼凑时间
    //例如:time.Nanosecond代表1纳秒,它的值就是1
    //而常量time.Microsecond代表1微秒,其值为1000*Nanosecond以此类推

    go的socket编程APi程序在底层获取的是一个非阻塞式的socket实例,这意味着在该实例上的数据读取操作也是非阻塞式的。也就是说当系统调用read从socket的接收缓冲区中读取数据时,即使接收缓冲区没有数据,操作系统内核也不会使系统调用read进入阻塞状态,而是直接返回一个错误码EAGAIN的错误,但是程序不应该将其视为错误,而应该忽略它,过一段时间后再继续尝试读取。如果在读取数据时,接收缓冲区有数据,read就会携带这些数据立刻返回,即使只有一个字节的数据也会这样。这被称为“部分读

    另一方面,write也与之类似,缓冲区满了,write不会被阻塞,而是直接返回一个错误码EAGAIN的错误,程序应忽略错误并过一段时间后再次尝试写。即使缓冲区空间不能完全写下,也会尽可能的写入一部分数据,然后返回已写入字节的数据量,这被称为“部分写”,程序每次调用write后都应检查该结果值,并在数据未完全写完时,继续调用write写完数据。

    accept也会呈现出一致的非阻塞风格。不会被阻塞以等待新连接到来,而是直接返回一个错误码EAGAIN的错误。socket编程API程序屏蔽了相关系统调用的EAGAIN错误,它同样屏蔽了部分写的特性,相关API指导把所有数据全部写入到socket中才返回。但是它保留了部分读特性,并呈现给了它的调用方程序,所以部分读需要我们在程序中做额外的处理

    Read方法:

    从socket缓冲区中接收数据:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    Read(b []Byte)(n int,err error)
    //b相当于存放从连接上接收到的数据的容器,长度完全由应用程序决定
    //Read会把它当作空容器并试图填满
    //相应位置上的原元素值会被替换
    //因此,应保证其在填充前保持绝对干净。应该是一个不包含任何非零值元素的切片值
    //一般情况下参数值填满之后才返回
    //但是也可能未满前就返回,这可能由相关的网络缓存机制导致的

    //n代表本次操作实际读取到的字节的个数
    //可以这样使用n:
    b:=make([]byte,10)
    n,err:=conn.Read(b)
    content:=String(b[:n])

    //对于err
    //若读取数据时发现TCP连接被另一端关闭,err=io.EOF,象征文件内容完结,也意味着TCP连接上无可读数据,或者TCP已无用
    //用法如下:
    var dataBuffer bytes.Buffer
    b:=make([]byte, 10)
    for{
    n,err:=conn.Read(b)
    if err != nil {
    if err==io.EOF {
    fmt.Println("The connection is closed.")
    }else{
    fmt.Printf("Read error:%s\n", err)
    }
    break
    }
    dataBuffer.Write(b[:n])
    }

    net.Conn是io.Reader接口的实现类型,所以可以使用bufio.NewReader函数来包装变量conn:

    1
    2
    3
    4
    5
    reader :=bufio.NewReader(conn)
    //之后通过ReadBytes方法来依次获取经过切分之后的数据
    //ReadBytes接收一个byte类型的消息边界
    line,err:=ReadBytes('\n')
    //调用ReadBytes后,获得一段以'\n'为结尾的数据
    Write方法:

    向缓冲区写入数据

    1
    Write(b []byte)(n int,err error)

    同样可以使用bufio包使其更灵活,net.conn是io.Writer接口的实现类型。所以可以使用bufio.NewWriter函数来包装变量conn

    1
    writer :=bufio.NewWriter(conn)
    • 可以通过writer上的以Write为前缀的方法来分批次向其中的缓冲区中写入数据。

    • 也可以通过ReadFrom从其他io.Reader中读出并写入数据

    • 还可以通过Reset达到重置和复用它的目的。

    • 在向其写入全部数据后,应调用它的Flush方法,包装所有数据全部写入其代理的对象中。

    Close方法

    关闭当前连接,不接受任何参数,并返回一个error类型的值。调用Close后,对其连接值(conn)上的Read、Write、Close的调用都会立即返回一个error值,提示信息为:

    1
    use of closed network connection

    若调用Close时,Read或Write正被调用还未执行结束(或者处于阻塞态),那么它们会立即结束执行并返回非nil的error类型

    LocalAddr和RemoteAddr方法

    不接受参数,返回net.Addr结果。

    net.Addr有两个方法:

    • Network:返回当前连接所使用协议名称

      1
      conn.LocalAddr().Network
    • String:返回相应地址

      1
      conn.RemoteAddr().String()//获取另一端网络地址

      对于客户端,若在与服务端程序通信时未指定本地地址,那么

      1
      conn.LocalAddr().String()

      返回的时操作系统内核为该客户端程序分配的网络地址。

    SetDeadLine、SetReadDeadLine、SetWriteDeadLine方法

    都只接受一个time.Time类型值作为参数,并返回一个error类型值作为结果。

    SetDeadLine会设定在当前连接上的I/O(包括但不限于读写)操作的超时时间,

    • 该超时时间为“绝对时间”,若I/O操作在此超时时间内未完成,会被立即结束执行,并返回一个非nil的error值。它提示信息未“i/o timeout”

    • 以循环方式不断尝试从一个连接上读取数据时,若要设置超时时间,则必须在每次i/o前都设定一次。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      //错误写法
      b:=make([]byte,10)
      conn.SetDeadLine(time.Now().Add(2*time.Second))
      for{
      n,err:=conn.Read(b)
      //省略若干语句
      }
      //上面代码明显不正确
      //其意思是所有操作的总用时得在2s以内。

      //正确写法
      b:=make([]byte,10)
      for{
      conn.SetDeadLine(time.Now().Add(2*time.Second))
      n,err:=conn.Read(b)
      //省略若干语句
      }
      //上述代码中,只有某次I/O用时超过2s才会出错
    • 当不再需要设定超时时间了,就应该及时取消它,SetDeadLine的参数值为time.Time的零值时,就可以取消超时时间:

      1
      conn.SetDeadLine(time.Time{})

    SetReadDeadLine、SetWriteDeadLine与之类似,但是即使一个写操作超时了,也不一定表示写操作没有完全成功。因为超时前,Write方法背后的程序可能已经将一部分数据写到socket的发送缓冲区了,也就是说即使Write因超时而被迫结束,它的第一个结果值也可能大于0。其表示真正写入的数据的字节量。

    它们仅针对当前连接值上的I/O

    多线程编程

    线程

    Unix中,POSIX标准定义的线程及操作方法已经被广泛认可

    Linux中,最贴近POSIX线程标准的线程实现为NPTL(Native POSIX Threads Library)

    线程可以视为进程的控制流

    线程共享:虚拟内存地址中的代码段、数据段、堆、信号处理函数,以及当前进程所持有的文件描述符。

    系统调用:

    1
    pthread_create
    • 线程标识:TID,进程内唯一,系统范围内不唯一,可复用,由内核分配和维护。

    • 线程间控制:线程之间平等,任何线程都可以堆同一进程中的其他线程进行有限的管理:

      • 创建线程:主线程在其所属进程启动时创建。此外,任何线程(称为调用线程)都可以通过系统调用pthread_create创建新的线程。调用线程需要给定新线程将要执行的函数以及传入该函数的参数值(参数被命名为start,所以常称为start函数)。若新线程创建成功,调用线程会得到新线程的ID
      • 终止线程:线程可以通过多种方式终止同一进程中的其他线程,其中的方式之一就是调用pthread_cancel,其作用是取消掉给定线程ID代表的那个线程。准确的说它只是向目标线程发送一个请求(并立即返回),要求它立即终止执行。不会等待目标线程对该请求做出响应,而是立即返回。至于目标线程什么时候响应该请求,做出怎样的响应,则取决于其它因素。(比如目标线程的取消状态及类型)在默认情况下,目标线程总是会接收线程取消请求,不过等到时机成熟的时候,才会去响应线程取消请求。
      • 连接已终止的线程。由系统调用pthread_join来执行,该函数会一直等待与给定的线程ID对应的那个线程终止,并把该线程执行的start函数的返回值告知调用线程。若目标线程已经处于终止状态,则该函数会立即返回。这就像把调用线程放在目标线程后面,当目标线程交出流程控制权后,调用线程会接过流程控制权并继续执行pthread_join函数调用之后的代码。若一个线程可被连接,那么他在终止之时就必须连接,否则就会变成一个僵尸线程
      • 分离线程。由系统调用pthread_detach来执行,接受一个代表了线程ID的参数值。将一个线程分离意味着它不再是一个可连接的线程。默认情况下,一个线程总可以被其他线程连接。分离的另一个作用是让内核在目标线程终止时自动进行清理和销毁工作。分离是不可逆的,但是对于一个处于分离状态的线程 ,执行终止操作仍然会起作用

      线程滋生也可以进行两种控制:终止和分离。在线程执行的start函数中执行return语句,会使该线程随着start函数的结束而终止。若在主线程中执行了return语句,那么当前进程中所有线程都会终止。任意线程中调用exit也会达到这种效果。显式地调用pthread_exit也可以终止自身。若在主线程中调用pthread_exit,那么只有主线程自己会被终止,其他线程仍然会正常运行

    • 线程状态:

      线程状态

      处于终止状态地线程才会被系统内核回收。

    • 线程调度:

      • 线程地执行总是倾向于CPU受限或者I/O受限。
      • I/O受限的线程具有更高的动态优先级以优先使用CPU,因为调度器认为I/O操作往往会花费更多的时间。
      • 如果应用程序没有显示指定一个线程的静态优先级,那么它将被设定为0.
      • 调动器不会更改静态优先级,而是更改动态优先级
      • 动态优先级决定线程运行顺序,静态优先级决定了线程单次在CPU上运行的最长时间。
      • 所有等待使用CPU的线程按照动态优先级从高到低排列,存放到与该CPU对应的运行队列上。
      • CPU包含两个优先级队列:存放正在等待运行的的线程的激活的优先级队列。存放已经运行过但还未完成的线程的过期的优先级队列。当激活的优先级队列没有等待运行的线程时,两个队列身份互换。
      • 抢占式,同等优先级等待的高
      • 调度器往往会稍稍调高被唤醒的线程。
    • 线程实现模型:总共由3个,它们之间的差异在于线程与内核调度实体(内核级线程,Kernel Scheduling Entity,简称KSE)之间的对应关系

      • 用户级线程(m:1,多对一)多个用户线程一个KSE。用户线程的各种管理与协调是用户级程序的自主行为,与内核无关。应用程序在对线程进行创建、终止、切换或者同步等操作时,不需要切换到内核态速度较快,可移植性较强,但不是真正并发。一个阻塞全阻塞,粒度太粗,现在都不使用
      • 内核级线程(1:1,1对1)内核负责管理线程,应用程序在对线程进行创建、终止、切换或者同步等操作时,必须切换到内核态。真正实现并发,管理成本较高资源消耗较大,速度较慢。很多系统do真以内核级线程模型实现线程的。
      • 两级线程(m:n)更加灵活,资源消耗大大减少,效能提高。管理复杂。Go与之相似。

    线程同步

    共享数据的一致性

    多线程程序多以共享数据作为线程之间传递数据的手段。

    共享数据大多以内存为载体。

    应尽量少的使用互斥量,每个临界区应在合理的范围内尽可能地大。

    死锁的解决

    共有两种方法:

    试锁定-回退:需要用到操作系统提供的线程库地功能,核心思想是:若在执行一个代码块时,需要先后(顺序不定)锁定两个互斥量,那么在成功锁定其中一个互斥量之后应该使用试锁定地方法来锁定另一个互斥量。若试锁定失败,则解锁第一个互斥量,重新进行锁定和试锁定。

    固定顺序锁定:更廉价,不需要更多地线程库函数,对程序复杂度没有太大影响,但是降低了程序的灵活性。总以固定不变地顺序锁定多个互斥量

    保持共享数据的独立性是预防死锁的最佳方法。

    条件变量的3中操作:wait,signal,broadcast(广播,给正在等待它通知的所有线程发送通知,表示某个共享数据的状态已经改变。)

    等待通知的操作并不是简单地阻塞当前线程。在执行该操作时,会先解锁与该条件变量绑定在一起的那个互斥量,然后再使当前线程阻塞,其中隐藏两个细节:

    • 细节一:只有在当前的共享数据状态不满足条件时,才执行等待通知操作,而检查共享数据的状态也需要收到互斥量的保护。也就是说,检查共享数据状态和等待通知的操作都需要在相应的临界区中进行。
    • 细节二:若等待通知操作在阻塞当前线程之前不对互斥量进行解锁,那么其他线程也无法进入相应的临界区。若当前线程因等待共享数据状态的改变而阻塞,而其他线程也因互斥量的阻挡而无法改变共享数据的状态,那么会形成死锁。

    当等待通知操作因收到条件变量发送的通知而唤醒当前线程后,会首先重新锁定与该条件变量绑定在一起的那个互斥量,若该互斥量被其他线程抢先锁定,那么当前线程会再次陷入睡眠状态。

    线程安全性

    若一个代码块可以被多个线程并发执行,且总能够产生预期结果,那么该代码块就是线程安全的(thread-safe)。

    让函数具有线性安全性的最有效的方式使其可重入(reentrant)

    若某个进程中的所有线程都可以并发地对一个函数进行调用,并且无论它们调用该函数的实际执行情况怎么样,该函数都可以产生于一个可以预期的结果,那么就说这个函数是可重入的。

    若一个函数把共享数据作为它返回的结果或者包含在它返回的结果中,那么该函数肯定不是一个可重入的函数。

    一般来说使用局部变量的函数也是安全的,但是:

    • 若当前进程还并发执行了更新共享数据的代码的化,这些函数的执行效果可能受到更新操作的影响,不管函数是否被并发执行。写前读,写时读,写后读的局部变量可能不一样。
    • 某个局部变量包含了对共享数据的应用,就不能说该局部变量所属的函数是可重入的
    多线程与多进程

    多线程在系统资源的利用和程序性能的提高方面都更有优势。但是,某些情况下,如对信号的处理、同时运行多套不同的程序以及包含多个需要超大内存支持的任务等,多进程可能会更合适。

    Go两者兼顾,但更倾向于多线程。它对多线程的使用比其他语言更加先进和充分。

    多核时代并发编程

    在保证程序正确性和可伸缩性的前提下提升程序的响应时间和吞吐量。伸缩性之指再增加CPU核心数量的情况下,运行速度不会受到负面影响。

    同步方法越多,程序的执行耗时越长。

    • 控制临界区的纯度。尽量不要把无关代码囊括其中尤其是各种相对耗时的I/O操作
    • 控制临界区的粒度。粒度过细的临界区会增加底层协调工作的发生次数。若存在相邻的多个临界区,并且它们内部都是操作同一个共享数据的代码,那么就合并它们并调整它们之间的无关代码的位置。
    • 减少临界区中代码的执行耗时。提高临界区的纯度可以减少临界区中代码的执行耗时。
      • 临界区包含了对几个共享数据的操作代码。考虑将它们拆分到不同的临界区中,并使用不同的同步方法加以保护。
      • 临界区包含了操作同一个共享数据的代码。应检查其中的业务逻辑和算法并加以改进。
    • 避免长时间持有互斥量。线程长时间持有互斥量所带来的危害是明显的,同样明显的是,在减少临界区中代码的执行耗时可以减少线程持有相应互斥量的时间。使用条件变量可以减少持有互斥量的时间。
    • 优先使用原子操作而不是互斥量。使用互斥量一般会比使用原子操作造成大得多的性能损耗。

    Go的并发机制

    一、原理

    在内核线程上,Go搭建了一个特有的两级线程模型(m:n)。goroutine是Go语言独创,代表可以并发执行的Go代码片段。其代表的正确含义是指用通信手段来共享内存而不是用内存的方式来通信

    ​ go推荐使用channel来在多个goroutine之间传递数据,channel会保证整个过程的并发安全性。

    1、线程实现模型

    • M: machine的缩写。一个M代表一个内核线程,或称“工作线程”
    • P: processor的缩写。一个P代表一个Go代码片段所必须的资源(或称”上下文环境“)
    • G: goroutine的缩写。一个G代表一个Go代码片段。前者是对后者的一种封装。

    一个G的执行需要P和M的支持。一个M在与一个P关联之后,就形成了一个有效的G运行环境(内核线程+上下文环境)。每一个P都会包含一个可运行的G的队列(runq)。该队列中的G会被依次传递给与本地P关联的M,并获得运行时机。

    一个M(用户态)能且仅能代表一个内核调度实体(KSE)。M与P,P与G之间的关联都是易变的。M与P之间总是一对一的,P与G之间是一对多的。M与G也会建立关联,一个G最终会由一个M负责运行。

    1.1、 M

    一个M代表了一个内核线程。大多数情况下,创建一个M,都是由于没有足够的M来关联P并运行其中可运行的G。系统执行系统监控或垃圾回收等任务时,也会导致新M的创建。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //M
    type m struct{
    go *g //特殊的goroutine,Go运行时系统在启动之初创建的,用于执行一些运行时任务。
    mstartfn //func,M的起始函数,在编写go语句时携带的函数
    curg *g //当前M正在运行的那个G指针
    p puintptr //指向当前M相关联的P
    nextp puintptr //暂存与当前M有潜在关联的P,M和P的预联
    spinning bool //当前M是否在寻找可运行的G,M处于自旋状态。
    lockedg *g //与当前M锁定的那个G(如果有)
    }

    M在创建之初,会被加入全局的M列表中(runtime.allm)。这时,它的起始函数和预联的P也会被设置。最后,运行时系统会为这个M专门创建一个新的内核线程并与之相关联。起始函数仅当运行时系统要用此M执行系统监控或垃圾回收等任务的时候才会被设置。全局M列表可以防止M被当作垃圾回收掉。

    初始化工作完成之后,该M的起始函数会执行(若存在),若起始函数代表的时系统监控任务的话,那么该M会一直执行它,否则当前M在其执行完毕后会与预联的p完成关联。

    M被停止后,会被放入空闲M列表(runtime.sched.midle)

    使用runtime/debug中的setMaxThread函数对单个Go程序所使用的M最大数量进行设置(默认为1000个),其返回值为旧的最大数量。

    1.2、P

    P是G能够在M中运行的关键。Go的运行时系统会适时的让P与不同的M建立或断开关联,与进程或线程的切换类似。

    改变Go程序间接拥有的P的最大数量方法:

    • 调用runtime.GOMAXPROCS并把想要设定的数量作为参数传入。该函数的调用会让所有P都脱离运行状态,并试图阻止任何用户级别的G的运行,设定完成后,才会陆续恢复它。
    • 在Go运行前设置GOMAXPROCS的值。默认值与CPU总核心数相同。硬性上限值(256)

    P的最大数量是对程序中并发运行的G的规模的限制。

    M数量很多时候比P多

    在确定P最大数量后,运行时系统会根据该值重整全局P列表(runtime.allp)。运行时系统会把这些P中的可运行G全部取出并放入调度器的可运行G队列中,以后再经由调度再次放入某个P的可运行G队列中。

    同样存在空闲P列表(runtime.sched.pidle)

    与M不同,p本身有状态:

    • Pidle: 当前P未与任何M存在关联
    • Prunning:当前P正与某个M关联
    • Psyscall:当前P中运行的那个G正在进行系统调用
    • Pgcstop: 运行时系统需要停止调度。如运行时系统在开始垃圾回收的某些步骤前,就会试图把全局P列表中的所有P都置于此状态。
    • Pdead: 当前P已经不会再被使用。

    P在创建之初状态是Pgcstop.该状态很短暂,在紧接着的初始化后,运行时系统会将其状态设置未Pidle,并放入调度器的空闲P列表。

    1.3、G

    一个G就代表一个goroutine(go例程),也与go函数相对应。

    Go的编译器会把go语句变成对内部函数newproc的调用,并把go函数及其参数都作为参数传递给这个函数。

    运行时系统也持有一个G的全局列表(runtime.allgs),其作用是:集中存放当前运行时系统中的所有G的指针。

    无论用于封装当前这个go函数的G是否是新的,运行时系统都会对它进行依次初始化,包括关联go函数以及设置该G的状态和ID等步骤。初始化完成后,这个G会立即被存储到本地P的runnext字段中。若runnext字段中已存有一个G,那么已有的G会被”踢到“该P的可运行G队列的队尾,若该队列已满,就会被追加到调度器的可运行G队列中。

    G的状态:

    • Gidle:当前G刚被新分配,还未初始化
    • Grunnable:当前G正在可运行队列中等待运行
    • Grunning:当前G正在运行
    • Gsyscall:当前G正在执行某个系统调用
    • Gwaiting:当前G正在阻塞
    • Gdead:当前G正在闲置,该状态下的G可以被重新初始化使用
    • Gcopystack:当前G的栈正被移动,原因可能是栈的扩展或收缩。

    还有一个状态Gscan,其不能独立存在,而是组合使用,如Gscan和Grunnable组合成Gscanrunnable状态,代表当前G正等待运行,同时它的栈正被扫描,扫描的原因一般是GC(垃圾回收)任务的执行。

    1.4、核心元素的容器
    中文名 源码名 作用域 说明
    全局M列表 runtime.allm 运行时系统 存放所有M的一个单向链表
    全局P列表 runtime.allp 运行时系统 存放所有P的一个数组
    全局G列表 runtime.allgs 运行时系统 存放所有G的一个切片
    调度器的空闲M列表 runtime.sched.midle 调度器 存放空闲M的一个单链表
    调度器的空闲P列表 runtime.sched.pidle 调度器 存放空闲P的一个单链表
    调度器的自由G列表 runtime.sched.gfreeStack或runtime.sched.gfreeNoStack 调度器 存放自由G的两个单链表
    调度器的可运行G队列 runtime.sched.runqhea和runtime.sched.runqtail 调度器 存放可运行G的一个队列
    P的可运行G队列 runtime.p.runq 本地P 存放当前P中的可运行G的一个队列
    P的自由G列表 runtime.p.gfree 本地P 存放当前P中自由G的一个单向链表

    三个全局容器时为了罗列某个核心元素的全部。

    把G放入自由G列表之前,运行时系统会检查该G的栈空间是否为初始大小,不是的话会释放掉它。

    2、调度器

    2.1、基本结构

    为了更方便地管理和调度各个核心元素的实例,调度器有自己的数据结构。除上面出现过的空闲M/P列表,可运行G队列和自由G列表。还有:

    字段名称 数据类型 用途简述
    gcwaiting uint32 表示是否需要因一些任务而停止调度
    stopwait int32 需要停止但未停止的P的数量
    stopnote note 用于实现与stopwait相关的事件通知机制
    sysmonwait uint32 表示在停止调度期间系统监控任务是否在等待
    sysmonnote note 用于实现与sysmonwait相关的事件通知机制

    在Go运行实时系统中,一些任务在执行前需要暂停调度,如垃圾回收任务中的某些子任务,如发起运行时恐慌(panic)的任务。将它们暂且称为串行运行时任务。

    gcwaiting、stopwait和stopnote都是串行运行时任务执行前后的辅助协助手段

    2.2、一轮调度

    一轮调度

    引导程序为Go程序的运行建立必要的环境。在引导程序完成一系列初始化工作后,Go程序的main函数才会真正执行。引导程序会在最后让调度器进行一轮调度,这样才能让封装了main函数的G马上有机会运行。封装main函数的G总是Go运行时系统的第一个用户G。

    • 用户G:封装用户级别的程序片段(需并发执行的函数)
    • 运行时G:封装运行时任务的G

    一轮调度由Go标准库代码包runtime中的schedule函数代表。

    CGO是Go和C之间的一座桥梁,使它们之间的相互调用成为可能。锁定M和G的操作是为它准备的。可以通过调用runtime.LockOSThread把当前的G与当时运行它的M锁定在一起,也可以用runtime.UnLockOSThread解锁。多次调用runtime.LockOSThread,只有最后一次有效,即使G没有与任何M锁定,runtime.UnLockOSThread也不会产生任何副作用,它会直接返回。

    Go调度器并不是运行在某个专用内核线程中的程序,调度程序会运行在若干已存在的M(或者说内核线程)之中。几乎所有的M都会参与调度任务的执行,它们共同实现了Go调度器的调度功能。

    2.3、全力查找可运行的G

    多次尝试从各处搜索可运行的G,甚至还会从别的P(非本地P)偷取可运行的G。它由runtime.findrunnable函数代表,该函数会返回一个处于Grunnable状态的G。其步骤如下:

    • 获取执行终结器的G

      一个终结器(终结函数)可以与一个对象关联,通过调用runtime.SetFinalizer产生这种关联。当一个对象不可达时,垃圾回收期在回收该对象前,就会执行与之关联的终结器。所有终结函数都会由一个专用的G负责。调度器会在判定这个专用G完成任务之后尝试获取它,然后将它置为Grunnable状态并放入本地P的可运行G队列

    • 从本地P的可运行G队列获取G

    • 从调度器的可运行G队列获取G

    • 从网络I/O轮询器(或称netpoller)处获取G

      若netpoller已被初始化且有过网络I/O操作,那么调度器会试着从netpoller获取一个G列表,并把表头那个G作为结果返回,同时把其余G放入调度器的可运行G队列。即使未成功也不会阻塞。

    • 从其他P的可运行队列获取G

      在条件允许的情况下,调度器会以一种伪随机算法在全局P列表中选取P,然后试着从它们的可运行G队列中盗取一半的G到本地P的可运行G队列。选取P和盗取G的过程会重复多次,成功即停止,会把盗取的第一个G返回。否则搜索第一阶段结束

    • 获取执行GC标记任务的G

      搜索第二阶段,调度器会先判断是否正处在GC的标记阶段,以及本地P是否可用于GC的标记任务。若都是true,就会把本地P持有的GC标记专用G置为Grunnable状态并作为结果返回。

    • 从调度器的可运行G队列获取G

      若依然找不到可运行的G,就会解除本地P与当前M的关联,并把该P放入调度器的空闲P列表。

    • 从全局P列表中每个P的可运行G队列获取G

      遍历全局P列表中的P,并检查它们的可运行G队列。只要发现某个P的可运行G队列不是空的,就从调度器的空闲P列表中取出一个P,并在判定其可用后与当前M关联在一起,然后返回第一阶段重新搜索可运行G。否则,继续下一步

    • 获取执行GC标记任务的G

      判断是否正处于GC的标记阶段,以及与GC标记任务相关的全局资源是否可用。若都是true,调度器就会从其空闲P列表中拿出一个P,若该P持有一个GC标记专用G,就关联该G与当前M,然后再次执行第二个阶段。

    • 从netpoller中获取G

      此处获取时阻塞的,只有当netpoller那里有可用的G时,阻塞才会解除。若netpoller还未初始化或还未有过网络I/O这一步就会跳过

    若上述流程都没找到可运行的G,那么就会停止当前M

    2.4、启用或停止M

    Go标准库代码包中负责启用或停止M的函数:

    • **stopm()**。停止当前M的执行,直到因有新的G变得可运行而被唤醒。
    • **gcstopm()**。为串行运行时任务的执行让路,停止当前M的执行。串行运行时任务执行完毕后被唤醒。
    • **stoplockedm()**。停止已与某个G锁定的当前M的执行,直到因这个G变得可运行而被唤醒。
    • **startlockedm()**。唤醒与gp锁定的那个M,并让该M去执行gp。
    • **startm(_p_ *p, spinning bool)**。唤醒或创建一个M去关联_p_并开始执行。

    启用和停止M

    1)调度器在执行调度流程时,先检查当前M是否已与某个G锁定。若锁定则会调用stoplockedm()停止当前M。该函数会先解除当前M与本地P之间的关联,并通过调用handoffp的函数把这个P转手给其他M,在转手过程中间接调用startm函数。一经转手,stoplockedm函数就会停止当前M的执行并等待唤醒。

    2)若调度程序为当前M找到了一个可运行的G,却发现该G已与某个M锁定了,那么就会调用startlockedm函数并把这个G作为参数传入。startlockedm会通过参数gp的lockedm字段找到与之锁定的那个M(”已锁M“),并把当前M的本地P转手给它。startlockedm会先解除当前M与本地P之间的关联,然后把这个P赋给已锁M的nextp字段。

    3)startlockedm会使与其参数锁定的M(即已锁M)被唤醒,一旦已锁M被唤醒,就会与和它预联的P产生正式关联,并去执行与之关联的G。

    4)startlockedm函数最后会调用stopm函数。stopm函数会先把当前M放入调度器的空闲M列表,然后停止当前M,可能会在之后因有P需要转手,或有G需要执行而被唤醒。

    由上述流程可知,P总是会被高效利用。若handoffp函数无法把作为其参数的P转给一个M,那么就会把这个P放入调度器的空闲P列表。该列表中的P会在需要时被取用。

    5)调度器在执行调度流程的时候,也会检查是否有串行运行时任务正在等待被执行。若有就会调用gcstopm停止当前M。gcstopm会先通过当前M的spinning字段检查它的自旋状态,若值为true,则置为false,然后把调度器中用于记录自旋M数量的nmspinning字段的值自减1.一个将要停止的M理应脱离自旋状态。之后,gcstopm会释放本地P,并将其状态置为Pgcstop。然后再去自减并检查调度器的stopwait字段,并在发现stopwait字段的值为0时,通过调度器的stopnote字段唤醒等待执行的串运行时任务。

    6)gcstopm函数会在最后调用stopm。

    ​ 只要有串行运行时任务准备执行,”Stop the world“(简称STW,执行运行时串行任务时需要停止Go调度器的操作)就会开始,所有在调度过程中的M就会执行步骤5)和6)。其中5)决定了串行运行时任务是否能够被尽早地执行。

    7)调度不成功的时候,即一轮调度后,人找不到一个可运行的G给当前M执行,那么掉读程序就会通过调用stopm函数停止当前的M。也就是说,这时已经没有多余的工作可以做了,为了节省资源就要停掉一些M。一旦停掉的M被唤醒,stopm就会负责关联它和已与它预联的P若stopm发现当前M是因有可并发执行的GC任务而被唤醒,那么就在执行完该任务之后再次停止当前M

    8)所有经由stopm停止的M,都可以通过调用startm唤醒一个M被唤醒的原因总是有新工作要做。比如有了新的自由的P,或者有了新的可运行的G。传入startm函数的参数_p_为nil,说明在唤醒一个M的同时需要从调度器的空闲P列表获取一个P作为M运行G的上下文环境。若这个列表已经空了,start函数也就无能为力了。这时startm会直接返回。一旦有了一个P,start函数就会再从调度器的空闲M列表获取一个M,若列表空了,就会创建一个新的M。

    2.5、系统检测任务

    由sysmon函数实现。

    概括来说系统监测任务做了:

    • 在需要时抢夺符合条件的P和G
    • 在需要时进行强制GC
    • 在需要时清扫堆
    • 在需要时打印调度器跟踪信息

    检测任务在执行之初,会根据既定条件睡眠并可能暂停一小段时间,然后真正开始。

    变量idle和delay的值决定了每次检测任务执行之初的睡眠时间。

    idle代表最近已连续多少次检测任务执行但未能成功夺取P,一旦某次执行过程中成功夺取了P,其值就会被清零。

    delay代表了睡眠的具体时间,单位是微秒,最大值是10000(即10ms)。

    此外在睡醒过后,监测任务还会因GC的执行或所有P的空闲暂停一段时间,这段时间的长短取决于局部变量forcegcperiod和scavengelimit

    抢夺P和G的途径有两个,首先是通过网络I/O轮询器获取可运行的G,其次是从调度器那里抢夺符合条件的G和P。对于第一个途径有一个前提条件,即:自上次通过该途径获取G是否已超过10ms。若已超过,则记录当前时间以供下次判断,然后再次获取,否则跳过。对于第二个途径,涉及子流程”抢夺符合条件的P和G“,该子流程由runtime包中的retake函数实现。具体功能如下图

    image-20220112140506602

    若P的状态为Psyscall,程序就会检查它的系统调用计数是否同步,一个P所经历的系统调用次数被记录在它的syscalltick中。系统监测程序也会持有一个备份,它存在于用于描述该P的结构体对象(简称描述对象)的syscalltick中。这里的同步是指两个数是否相同:若不同,程序就更新该备份,然后忽略堆该P的进一步检查;若相同,就判断后续三个条件,其目的是确定是否真的有必要抢夺该P。

    ​ syscallwhen会在系统调用计数不同步时被更新为now(now代表当次监测任务真正开始执行的时间)。

    若P状态为prunning,就会检查其调度计数(由P的schedtick存储)是否同步,只要它的可运行G队列中某个G被取出运行了,schedtick就会递增。系统监测程序也会持有一个调度计数的备份,由该P的描述对象的schedtick字段值代表。若两者不同,就更新该备份,同时把P的描述对象的schedwhen更新为now,然后忽略该p做进一步检查。若相同,就判断上次同步该P的调度时间是否不足10ms,若为false,就说明该P的G已经运行太长时间,需要停止并把运行机会让给其他G。不过,即使一个G运行过长时间,系统监测也因此告知它需要停止,它也不一定会停止。因此系统监测程序仅会也仅能旅行告知义务,而既不保证告知的正确达到,也不保证那个G会做出响应

    专用于强制GC的G,其实在调度器初始化时就已经开始运行了,zhi不过它一般会处于暂停状态,只有系统监测程序可以恢复它。一旦判定GC当前未执行,且距离上一次执行已超过GC最大间隔时间,系统检测程序就会恢复这个专用G,把它放入调度器的可运行G队列。GC最大间隔时间由forcegcperiod变量代表,初始值为2min。

    清扫堆的工作仅在距上次执行已超过清扫堆间隔时间时才会执行,而清扫堆的任务是把一段时间内未使用的堆内存还给操作系统。清扫堆的间隔时间与scavengelimit变量有关,为它所代表时间的一般。scavengelimit的初始值为5min。

    是否打印调度器跟踪信息,是受当前操作系统的环境变量GODEBUG控制的。若在Go程序运行前,设置该环境变量的值并使其包含schedtrace=X,那么系统检测程序就会适时地向标准输出打印调度器跟踪信息。这里地X需要替换,其含义时多少毫秒打印一次信息,系统检测程序也会依据此值控制打印频率。

    GODEBUG还可以强制缩短GC和清扫堆地间隔时间。只要其值包含scavenge=1,就可使scavengelimit的值变为20ms,且forcegcperiod变为10ms。但是这相当于开启了GC的调试模式,仅应在调试时使用。若需要在GODEBUG的值中放置多个形如”Y=X“的名称-值对,就需要在它们之间插入英文半角逗号以示分隔。

    2.6、变更P的最大数量

    image-20220112151027498

    ​ P最大数量的变更就意味着改变G运行的上下文环境,也直接影响着Go程序的并发性能。

    ​ 默认情况下,P的 最大数量等于正在运行当前Go程序的逻辑CPU(CPU核心)的数量。一台计算机的逻辑CPU数量,说明了它能够在同一时刻执行多少程序指令。我们通过调用runtime.COMAXPUOCS函数改变这个最大数量,但是这样做有时有较大损耗。

    ​ 当我们调用runtime.G时,会先进行下面两项检查,以确保变更合法和有效。

    • 若传入的参数值(新值)比运行时系统为此设定的硬性上限值(256)大,那么会变为256.
    • 若新值不是正整数,或与存储在运行时系统中的P最大数量值相同,那么忽略此变更而直接返回旧值。

    ​ 一旦通过这两项检查,该函数就会先通过调度器停止一切调度工作,即STW。然后,它会调度新值、重启调度工作(或称”Start the world“),最后将旧值作为结果返回。在调度工作真正被重启之前,调度器若发现有新值暂存,那么就会进入P最大数量的变更流程。P最大数量的变更流程由runtime包中的procresize函数实现。

    在此变更流程中,旧值也会被获取。若发现旧值或新值不合法,程序就会发起一个运行恐慌,流程和程序也会随机终止。不过由于runtime.GOMAXPUOCS已做过检查,此流程这个分支在此永远不会被执行。再通过对旧值和新值的检查后,程序会对全局P列表中的前I(代表新值)个P进行检查和必要的初始化,若P不够,还会新建相应数量的P,并把它们追加到全局P列表中。新P的状态为Pgcstop。**全局P列表中所有P的可运行G队列的固定长度都会是256.**若这个队列满了,程序就会把其中半数的可运行G转移到调度器的可运行G队列中。之后,程序会清理第I+1至第J(代表旧值,若有的话)个P。把这些P可运行G队列中的G及其runnext字段中的G全部取出,并依次放入调度器的可运行G队列中。程序也会试图获取这些P持有的GC标记专用G,若取到,就放入调度器的可运行G队列。此外,程序还会把这些P的自由列表中。最后这些P都会被设置为Pdead状态,以便之后进行销毁。

    至此全局列表中的所有P都已经被重新设置,也包括了与执行procresize函数的当前M关联的那个P。当前M不能没有P,所以程序会试图把该M之前的P还给它,若发现那个P已经被清理,就把全局列表中第一个P给它。

    最后程序会再检查一遍前N个P。若它们可运行G队列为空,就把它放入调度器的空闲P列表,否则就试图拿一个M与之绑定,之后把它放入本地的可运行P列表。这样筛选出一个拥有可运行G的P列表,procresize会把这个列表作为结果值返回。负责重启调度工作的程序会检查这个列表中的P,以保证它们一定能与一个M产生关联。随后程序会让与这些P关联的M都运作起来。

    再次强调,runtime.GOMAXPROCS改变运行时系统中P的最大数量时会引起调度工作的暂停

    3、细节

    1、g0和m0

    ​ 运行时系统中的每个M都会拥有一个特殊的G,一般称为M的g0。g0的管辖内存称为M的调度栈。g0对应于操作系统为相应线程创建的栈。因此M的调度栈也可以称为OS线程栈或系统栈。

    M的g0不是由go语句间接生成的,而是由Go运行时系统在初始化M时创建并分配给M的。g0一般用于执行调度、垃圾回收、栈管理等方面的任务。g0不会被阻塞,也不会被包含在任何G队列或列表中。它的栈也不会在垃圾回收期间被扫描

    ​ M还拥有一个专用于处理信号的G,称为gsignal。它的栈可称为信号栈。

    信号栈和系统栈不会自动增长,但一定会有足够的空间执行代码

    ​ 除g0之外,其他由M运行的G都可以视作用户级别的G,简称用户G,g0和gsignal都可以称为系统G。Go运行时系统会进行切换,以使每个M都可以交替运行用户G和它的go。

    ​ 在Go程序拥有的第一个内核线程中,还存在一个runtime.g0用于执行引导程序,该内核线程也被称为runtime.m0。runtime.m0和runtime.g0都是静态分配的,引导程序无需为它们分配内存。

    2、调度器锁和原子操作

    ​ 调度器会在读写一些全局变量以及它的字段的时候动用调度器锁进行保护。

    ​ Go运行时系统在一些需要保证并发安全的变量的存取上使用了原子操作。

    3、调整GC

    ​ Go的GC是基于CMS算法的(Concurrent Mark-Sweep,并发的标记-清扫)。Go的GC也是非分代和非压缩的。

    GC三种执行模式:

    • gcBackgroundMode。并发执行垃圾收集(标记)与清扫
    • gcForceMode。串行地执行垃圾收集(执行时停止调度),但并发地执行清扫。
    • gcForceBlockMode。串行地执行垃圾收集和清扫。

    调度器驱使的自动GC和系统监测任务中的强制GC,都会以gcBackgroundMode模式执行。但是前者会检查当前内存使用量,仅当使用增量过大时才真正执行GC,后者无视这个前提条件。

    可以通过GODEBUG控制自动GC的并发性。只要使其值包含gcstoptheworld=1或gcstoptheworld=2。就可以让GC执行模式变为gcForceMode或gcForceBlockMode。

    ​ GC会在为Go分配的内存翻倍增长时被触发。Go运行时系统会在分配新内存时检查Go程序的内存使用增量。可以通过调用runtime/debug.SetGCPercent来改变这个增量的阈值。该函数接受一个int参数,其含义是新分配的内存是上次记录的已分配内存的百分之几时出发GC。其预设值为100。设置环境变量GOGC也可以达到相同效果。其值的含义和设置规则也与SetGCPercent相同。另外将其值设为off会关闭自动GC。对GOGC设置需要在Go程序启动之前进行,否则不会生效。

    关闭自动GC意味着要手动GC,不然程序占用的内存即使不再使用也不会被回收。调用runtime.GC可以手动触发一次GC,但它不会阻塞调用方法直到GC完成,且其会以gcBackgroundMode模式执行。此外runtime/debug包中的FreeOSMemory也可以手动触发一次完全串行的GC,并且在GC完成后还会做一次清扫堆的操作。两者在执行时都不会检查Go程序的内存使用增量。

    二、goroutine(G)

    1、go语句和goroutine

    • 一条go语句意味着一个函数或方法的并发执行。
    • Go运行时系统对go函数的执行是并发的
    • go函数会被单独放入一个goroutine中
    • 之后go函数执行独立于当前goroutine
    • go函数并发执行,谁先谁后并不确定。
    • go函数是可以有结果声明的,但在其执行完时被丢弃

    针对如下函数的调用表达式不能用表达式语句:

    append、cap、complex、imag、len、make、new、real、unsafe.Alignof、unsafe.Offsetof和unsafe.Sizeof

    前8个是内建函数,最后3个是标准代码包unsafe中的函数

    go后面是针对匿名函数的调用表达式:

    1
    2
    3
    go func(){
    println("GO!Gorountine!")
    }()

    go函数并发执行,谁先谁后并不确定

    下方代码并不会输出结果。因为G封装go函数并把它放到可运行G队列中,但是G何时运行要看调度器的实时调度情况了,而go语句之后没有语句。一旦main执行结束,就意味着该Go程序运行的结束,可是此时go函数还未来得及执行。所以不要对并发执行的先后顺序做任何假设,也不要指望main所在的G会最后一个执行完毕

    1
    2
    3
    4
    5
    6
    7
    func main() {
    go gobase1()
    }

    func gobase1() {
    println("Go! Goroutine!")
    }

    针对上述现象,最简陋的方法是用time包中的sleep让调用它的goroutine暂停(进入Gwaiting状态)一段时间。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func main() {
    go gobase1()
    time.Sleep(time.Millisecond)
    }

    func gobase1() {
    println("Go! Goroutine!")
    }
    //输出为:Go! Goroutine!

    可以使用runtime.Gosched暂停当前G,但无法解决复杂的情况

    但是实时调度我们无法控制,所以上述方法并不保险,如下例:

    1
    2
    3
    4
    5
    6
    7
    name :="Eric"
    go func() {
    fmt.Printf("Hello,%s\n", name)
    }()
    name ="Harry"
    time.Sleep(time.Millisecond)
    //输出结果为:Hello,Harry

    在赋值name=”Harry“后go函数才得以执行。

    1
    2
    //将最后两条语句互换一下位置
    //输出结果为:Hello,Eric

    因为在改变name值之前,就给了go函数执行的机会。

    当要问候多个人时:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func gobase3()  {
    names:=[]string{"Eric","Harry","Robert","Jim","Mark"}
    for _,name:=range names{
    go func() {
    fmt.Printf("Hello,%s\n", name)
    }()
    }
    time.Sleep(time.Millisecond)
    }

    其输出结果为:

    1
    2
    3
    4
    5
    Hello,Mark
    Hello,Mark
    Hello,Mark
    Hello,Jim
    Hello,Mark

    其原因在于每次获取的迭代值都会被赋给相应的迭代变量(name),这里并发执行的五个函数中有4个是在for执行完毕才执行的,一个是在第四次迭代执行后执行的。所以不要对并发执行的先后顺序做任何假设

    可以将sleep放在循环内解决上述问题,但是当go函数比较复杂时,问题仍然无法解决。

    1
    2
    3
    4
    5
    6
    for _, name := range names {
    go func() {
    fmt.Printf("Hello,%s\n", name)
    }()
    time.Sleep(time.Millisecond)
    }

    若name的值不受到外部变量变化的影响,就可以既保证go函数的独立执行,又不用担心它们的正确性被破坏(即可重入函数)。这一思想可通过将name作为参数传给go函数来解决:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func gobase4() {
    names := []string{"Eric", "Harry", "Robert", "Jim", "Mark"}
    for _, name := range names {
    go func(who string) {
    fmt.Printf("Hello,%s\n", who)
    }(name)
    }
    time.Sleep(time.Millisecond)
    }

    上述方案最多只能保证go函数执行的正确性,而无法保证这些函数总要先于main函数执行完毕,即使用time.Sleep和runtime.Gosched也是一样。

    2、主goroutine的运作

    下面是封装main函数的G(称为主goroutine)从”诞生“到”死亡”的过程。

    • 主goroutine由runtine.m0负责运行。

    • 主goroutine并不是仅仅执行main。

    主goroutine首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸(32位系统中为250MB,64位系统中为1GB)。若某个G的栈空间大于这个尺寸,会发生栈溢出,从而产生运行时恐慌,会终止该程序。

    之后,主goroutine会在当前M的g0上执行系统监测任务

    此后,会进行一系列初始化:

    • 检查当前M是否是m0,若不是,说明前面出现问题,主goroutine立即抛出异常,程序启动失败。
    • 创建一个特殊的defer语句,用于主goroutine退出时做出必要的善后处理。因为主goroutine也可能非正常的结束
    • 启用专用于后台清扫内存垃圾的goroutine,并设置GC可用的标识。
    • 执行main包中的init函数

    然后,执行main函数。之后,还会检查主goroutine是否引发运行时恐慌,并进行必要的处理。最后,主goroutine会结束自己以及当前进程的运行。

    3、runtime包以及goroutine

    runtime中的程序实体提供了各种可以使用户程序与Go运行时系统交互的功能。下面是一些能够直接或间接控制goroutine运行的函数:

    3.1、runtime.GOMAXPROCS

    ​ 用户在运行期间设置常规运行时系统中P的最大数量。但会引起“Stop the world”。应尽量早地设置,最好是设置环境变量GOMAXPROCS

    P最大数量范围是1~256,传参超过256会变为256

    3.2、runtime.Goexit
    • 会立即使当前goroutine地运行终止,不会影响其他goroutine。

    • 在终止当前goroutine前,会执行该goroutine中还未执行地defer语句

    • 被终止的goroutine置于Gdead状态,并放入本地P的自由G列表

    • 千万不要在主goroutine调用该函数

    3.3、runtime.Gosched
    • 暂停当前goroutine的运行。
    • 当前goroutine会被置为Grunnable状态,并放入调度器的可运行G队列
    • 经过调度器的调度,该goroutine马上就会再次运行
    3.4、runtime.NumGoroutine
    • 返回当前Go运行时系统中处于非Gdead状态的用户G数量
    • 这些goroutine被视为“活跃的”或者“可被调度运行的”
    • 返回值总是大于等于1
    3.5、runtime.LockOSThread和runtime.UnlockOSThread
    • 前者使当前goroutine与当前M锁定在一起
    • 后者解锁
    • 多次调用前者不会造成任何问题,但只有最后一次调用会生效,类似对同一个变量的多次赋值
    • 即使没有调用前者,调用后者也不会产生任何副作用。
    3.6、runtime/debug.SetMaxStack
    • 约束单个goroutine所能申请的最大尺寸
    • 32位默认250MB,64位默认1GB
    • 接收一个int类型参数,指欲设定栈空间最大字节数。
    • 返回值为之前设定值。
    • 若增加栈空间时,发现实际尺寸超过设定值,就会引起运行恐慌并终止程序的运行。
    • 不会像runtime.GOMAXPROCS那样对传参进行检查和纠正
    • 当我们设置过小的值时,相关问题也一般不会再程序运行初期就显现出来,因为运行时系统仅再增长goroutine的栈空间时,才会对他实际尺寸进行检查
    3.7、runtime/debug.SetMaxThreads
    • 对Go运行时系统所使用的内核线程(也可以认为是M)数量进行设置。在引导程序中其被设置为10000.
    • 接受一个int类型的参数。代表欲设定的新值
    • 返回一个int结果,代表先前设定的旧值。
    • 新值比实际数量小的话,会立即引起运行时恐慌
    • 函数调用完成后,新值会立即发挥作用。
    • 每当新建一个M,都会检查M数量,若大于最大数量,就会引起运行时恐慌。
    3.8、与垃圾回收相关的一些函数

    runtime/debug.SetGCPercent、runtime.GC和runtime/debug.FreeOSMemory。

    • 前者用于设定触发自动GC的条件。后两者用于手动GC。
    • 自动GC默认并发运行。手动GC总是串行的。这意味着在后两个函数执行期间调度时停止的。
    • runtime/debug.FreeOSMemory比runtime.GC多做了一件事,那就是GC之后还要清扫一次内存。

    三、channel

    channel是以通信作为手段来共享内存最直接和最重要的体现。

    它提供了一种机制。既可以同步两个并发执行的函数,又可以让这两个函数通过相互传递特定类型的值来通信。

    1、基本概念

    ​ 在Go中,channel指通道类型,也指代可以传递某种类型的值的通道。通道类型即某一个通道类型的值。

    1.1、类型表示法

    ​ 与切片类型和字典类型相同,也属于引用类型。泛化的通道类型声明如下:

    1
    2
    3
    4
    5
    chan T//T为元素类型
    //例如,声明这样一个别名类型
    type IntChan chan int
    //声明一个chan int变量
    var intChan chan int

    上面通道类型是双向的,既可以接收元素值,又可以发送元素值。

    也可声明单向的通道类型,简称发送通道类型:

    1
    2
    //只能用于发送的通道类型泛化:
    chan<- T //只能向此类型发送元素值,不能从此类型接收元素值

    接收发送类型:

    1
    <-chan T
    1.2、值表示法
    • 因为其是引用类型,所以在变量初始化前,其为nil
    • 它是用来传递值的,而不是存储值,所以它没有对应的值表示方法。
    • 值具有及时性,无法用字面量准确表达
    1.3、操作特性
    • 通道是多个goroutine之间传递数据和同步的重要手段,对通道本身的操作也是同步的
    • 同一时刻仅有一个goroutine能向一个通道发送元素值,同时也仅有一个goroutine能从它那接收元素值。
    • 各元素值严格按照发送到通道的顺序先后排列,想到于一个FIFO消息队列。
    • 通道中元素值都具有原子性,不可分割
    • 每个元素值只可能被某一个goroutine接收,以一被接收,会立刻从通道中删除
    1.4、初始化通道

    一个通道的缓冲容量总是固定不变的

    引用元素的值都需要使用make来初始化:

    1
    2
    make(chan int, 10)//同一时刻最多缓冲10个元素值
    make(chan int)//此为非缓冲通道。这意味着通道永远无法缓冲任何元素值。发送给它的元素值应该被立刻取走,否则发送方的goroutine会被暂停,直到它被接收
    1.5、接收元素值

    接收操作符<-不但可以作为通道声明的一部分,也可以用于通道操作(发送或接收元素值)

    试图从一个未被初始化的通道中接收元素值,会造成当前goroutine的永久阻塞

    1
    2
    3
    4
    5
    strChan :=make(chan string,3)
    //接收语句
    elem :=<-strChan //从通道接收元素值,把strChan第一个元素值赋给elem,此时通道中无元素值,会使当前goroutine阻塞(Gwaiting),直到有新的元素值可取时才会被唤醒。
    elem,ok:=<-strChan//与上面相同,无元素值,会使当前goroutine阻塞,若在接收之前或过程中该通道关闭(通道中无元素值),会立即结束该操作,elem会被赋予该通道中元素类型的零值,ok为false,否则为true。
    //<-右边还可以是任意的结果类型为通道类型的表达式(通道表达式)
    1.6、Happens before

    对于一个缓冲通道,有如下规则:

    • 发送操作会使通道复制被发送的元素。若缓冲空间已满而无法立即复制,则阻塞进行发送操作的goroutine。复制的目的地址有两种:
      • 当通道已空且有接收方在等待元素值时,目的地址是最早等待的那个接收方持有的内存地址。
      • 否则会是通道持有的缓冲中的内存地址
    • 接收操作会使通道给出一个已发给它的元素值的副本,若因通道的缓冲空间已空而无法立即给出,则阻塞进行接收操作的goroutine。一般情况下,接受方会从通道的缓冲中得到元素值。
    • 对于同一个元素值来说,把它发送给某个通道的操作,一定会在从通过到接收它的操作完成之前完成。在通道完全复制一个元素值之前,任何goroutine都不可能从它那里接收到这个元素值的副本。
    1.7、发送元素值
    • 发送语句由通道表达式、接收操作符<-和代表元素值的表达式(元素表达式)组成。

    • 元素表达式的结果类型必须与通道表达式的结果类型必须与通道表达式的结果类型中元素类型之间存在可赋予关系。

    • 对<-两边表达式的求值会先于发送操作执行。在求值完成前,发送操作一定会被阻塞。

    1
    strChan<-"a"//向strChan发送一个“a”
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    func chanbase() {
    syncChan1 := make(chan struct{}, 1)
    syncChan2 := make(chan struct{}, 2)
    go func() { //用于演示接收操作
    <-syncChan1//信号
    fmt.Println("Received: a sync signal and wait a second...[receiver]")
    time.Sleep(time.Second)
    for {
    if elem, ok := <-strChan; ok {
    fmt.Println("Received", elem, "[receiver]")
    } else {
    break
    }
    }
    fmt.Println("Stopped. [receiver]")
    syncChan2 <- struct{}{}
    }()
    go func() { //用于演示发送操作
    for _, elem := range []string{"a", "b", "c", "d"} {
    strChan <- elem
    fmt.Println("Sent:", elem, "[sender]")
    if elem == "c" {
    syncChan1 <- struct{}{}
    fmt.Println("Sent a sync signal.[sender]")
    }
    }
    fmt.Println("wait 2 seconds...[sender]")
    time.Sleep(time.Second * 2)
    close(strChan)
    syncChan2 <- struct{}{}
    }()
    <-syncChan2
    <-syncChan2
    }

    //输出结果
    Sent: a [sender]
    Sent: b [sender]
    Sent: c [sender]
    Sent a sync signal.[sender]
    Received: a sync signal and wait a second...[receiver]
    Sent: d [sender]
    wait 2 seconds...[sender]
    Received a [receiver]
    Received b [receiver]
    Received c [receiver]
    Received d [receiver]
    Stopped. [receiver]

    syncChan1 <- struct{}{},向syncChan1 发送一个信号,会使接收方恢复工作

    由于在针对通道的发送/接收语句和打印语句之间的执行间隙很可能会插入Go运行时系统的调度,每次打印出的内容可能都不同

    • struct{}时空结构体,Go中空结构体的变量不占用内存空间。所有该类型的变量都拥有相同的内存地址。
    • 若多个goroutine因向同一个已满的通道发送元素值而被阻塞,那么但通道有多余的空间时,最早被阻塞的那个goroutine会最先被唤醒。接收也与之类似。
    • 发送方向通道发送的值会被复制,所以接收方接收的总是该值的副本。
    • 经由通道传递的值至少会被复制一次,至多会被复制两次。
    • 像一个已空的通道发送值,且已有至少一个接收方因此等待,该通道会绕过本身的缓冲队列,直接把这个值赋值给最早等待的那个接收方。
    • 接受方接收到一个值类型的值时,对该值的修改不会影响到发送方持有的源值。但是对于引用类型的值来说,这种修改会同时影响收发双方持有的值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    func chanvall() {
    var mapChan = make(chan map[string]int, 1)
    syncChan := make(chan struct{}, 2)
    go func() { //用于演示接收操作
    for {
    if elem, ok := <-mapChan; ok {
    elem["count"]++
    } else {
    break
    }
    }
    fmt.Println("Stopped. [receiver]")
    syncChan <- struct{}{}
    }()
    go func() { //用于演示发送操作
    countMap := make(map[string]int)
    for i := 0; i < 5; i++ {
    mapChan <- countMap
    time.Sleep(time.Millisecond)
    fmt.Printf("The count map:%v.[sender]\n", countMap)
    }
    close(mapChan)
    syncChan <- struct{}{}
    }()
    <-syncChan
    <-syncChan
    }
    //输出结果为:
    The count map:map[count:1].[sender]
    The count map:map[count:2].[sender]
    The count map:map[count:3].[sender]
    The count map:map[count:4].[sender]
    The count map:map[count:5].[sender]
    Stopped. [receiver]

    //发送方源值应为:
    The count map:map[].[sender]
    The count map:map[].[sender]
    The count map:map[].[sender]
    The count map:map[].[sender]
    The count map:map[].[sender]

    上述代码中mapChan的元素类型属于引用类型。因此,接收方对元素值的修改会影响到发送方的源值。

    有时候被传递的值的类型不能简单地判定为值类型或引用类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    func chanvall2() {
    //计数器类型
    type Counter struct {
    count int
    }
    var mapChan = make(chan map[string]Counter, 1)

    syncChan := make(chan struct{}, 2)
    go func() { //接收
    for {
    if elem, ok := <-mapChan; ok {
    counter := elem["count"]
    counter.count++
    } else {
    break
    }
    }
    fmt.Println("Stopped. [receiver]")
    syncChan <- struct{}{}
    }()
    go func() { //发送操作
    countMap := map[string]Counter{
    "count": Counter{},
    }
    for i := 0; i < 5; i++ {
    mapChan <- countMap
    time.Sleep(time.Millisecond)
    fmt.Printf("The count map:%v.[sender]\n", countMap)
    }
    close(mapChan)
    syncChan <- struct{}{}
    }()
    <-syncChan
    <-syncChan
    }
    //输出结果为:
    The count map:map[count:{0}].[sender]
    The count map:map[count:{0}].[sender]
    The count map:map[count:{0}].[sender]
    The count map:map[count:{0}].[sender]
    The count map:map[count:{0}].[sender]
    Stopped. [receiver]

    上述代码中,接受方对counter的修改未导致源值修改。

    1
    2
    3
    4
    //我们在接收方counter.count++后面加上
    fmt.Printf("The counter:%v.[receiver]\n", counter)
    //在输出中,我们可以看到
    The counter:{1}.[receiver]

    这是因为虽然mapChan的元素类型是引用类型,但是counter:=elem[“count”]不是按引用传递,而仅仅是把elem[“count”]的值(Counter类型,这不是一个引用类型)传递给了counter。

    若将上述代码做如下更改:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var mapChan = make(chan map[string]*Counter, 1)
    countMap := map[string]*Counter{
    "count": &Counter{},
    }
    fmt.Printf("The count map:%v.[sender]\n", countMap["count"])
    //其输出结果为:
    The count map:&{1}.[sender]
    The count map:&{2}.[sender]
    The count map:&{3}.[sender]
    The count map:&{4}.[sender]
    The count map:&{5}.[sender]
    Stopped. [receiver]

    显然源值也随之发生更改,因为此时的counter是一个引用类型

    1.8、关闭通道
    • 试图向一个已关闭的通道发送元素值,会让发送操作引起运行时恐慌。
    • 应该在发送端关闭通道。
    • 若通道在被关闭时其值仍有元素值,依然可以用接收表达式将之取出,并且根据第二个结果值判断是否已关闭且已无元素可取
    • 同一个通道仅允许关闭一次,重复关闭会引发运行时恐慌
    • 调用close时,需把要关闭的通道作为参数传入,若此时变量值为nil,也会引发恐慌
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    func chanclose() {
    dataChan := make(chan int, 5)
    syncChan1 := make(chan struct{}, 1)
    syncChan2 := make(chan struct{}, 2)
    go func() { //用于接收操作
    <-syncChan1
    for {
    if elem, ok := <-dataChan; ok {
    fmt.Printf("Received:%d [receiver]\n", elem)
    } else {
    break
    }
    }
    fmt.Println("Done. [receiver]")
    syncChan2 <- struct{}{}
    }()
    go func() { //用于发送
    for i := 0; i < 5; i++ {
    dataChan <- i
    fmt.Printf("Sent:%d [sender]\n", i)
    }
    close(dataChan)
    syncChan1 <- struct{}{}
    fmt.Println("Done.[sender]")
    syncChan2 <- struct{}{}
    }()
    <-syncChan2
    <-syncChan2
    }

    1.9、长度与容量

    len和cap也可以作用在通道上。len获取元素数量,len获取通道可容纳元素值的最大数量(及容量)

    容量初始化时已经确定,之后不能改变。

    2、单向channel

    • 无论哪一种单向通道都不应该出现在变量声明中。
    • 单向通道应由双向通道变换而来,可以用这种变换来约束程序对通道的使用方式。”代码即注释“编程风格

    基于此,可以对chanbase1进行改造:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    func chanbase2() {
    var strChan = make(chan string, 3)
    syncChan1 := make(chan struct{}, 1)
    syncChan2 := make(chan struct{}, 2)
    go receiveCB2(strChan, syncChan1, syncChan2)
    go sendCB2(strChan, syncChan1, syncChan2)
    <-syncChan2
    <-syncChan2
    }
    func receiveCB2(strChan <-chan string,
    syncChan1 <-chan struct{},
    syncChan2 chan<- struct{}) {
    <-syncChan1
    fmt.Println("Received: a sync signal and wait a second...[receiver]")
    time.Sleep(time.Second)
    for {
    if elem, ok := <-strChan; ok {
    fmt.Println("Received", elem, "[receiver]")
    } else {
    break
    }
    }
    fmt.Println("Stopped. [receiver]")
    syncChan2 <- struct{}{}
    }
    func sendCB2(strChan chan<- string,
    syncChan1 chan<- struct{},
    syncChan2 chan<- struct{}) {
    for _, elem := range []string{"a", "b", "c", "d"} {
    strChan <- elem
    fmt.Println("Sent:", elem, "[sender]")
    if elem == "c" {
    syncChan1 <- struct{}{} //向syncChan1 发送一个信号,会使其恢复工作
    fmt.Println("Sent a sync signal.[sender]")
    }
    }
    fmt.Println("wait 2 seconds...[sender]")
    time.Sleep(time.Second * 2)
    close(strChan)
    syncChan2 <- struct{}{}
    }

    • 单向通道不能转为双向通道
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func chanconv() {
    var ok bool
    ch := make(chan int, 1)
    _, ok = interface{}(ch).(<-chan int)
    fmt.Println("chan int=> <-chan int:", ok)
    _, ok = interface{}(ch).(chan<- int)
    fmt.Println("chan int=> chan<- int:", ok)

    sch := make(chan<- int, 1)
    _, ok = interface{}(sch).(chan int)
    fmt.Println("chan<- int=> chan int:", ok)

    rch := make(<-chan int, 1)
    _, ok = interface{}(rch).(chan int)
    fmt.Println("<-chan int=> chan int:", ok)
    }
    //结果为:
    chan int=> <-chan int: false
    chan int=> chan<- int: false
    chan<- int=> chan int: false
    <-chan int=> chan int: false

    3、for语句和channel

    • range子句的迭代目标不能是一个发送通道,这会造成一个编译错误
    • for会不断地尝试从通道中接收元素值,直到通道关闭。
    • 通道关闭时,若通道无元素值,则for会立即结束,若还有遗留的元素值,则for仍可以继续把它们取完。
    1
    2
    3
    for elem:=range strChan{
    fmt.Println("Received:",elem."[receiver]")
    }

    4、select语句

    4.1、组成与编写方法
    • select语句中,每个分支以关键字case开始。
    • 但与switch语句不同,跟在case后面的只能是针对某个通道的发送语句或接受语句
    • select右边直接后跟花括号
    1
    2
    3
    4
    5
    6
    7
    8
    9
       var intChan =make(chan int,10)
    var strChan =make(chan string,10)
    select {
    case e1 :=<-intChan:
    fmt.Printf("The 1th case was selected. e1=%v.\n",e1)
    case e2 :=<-strChan:
    fmt.Printf("The 1th case was selected. e1=%v.\n",e2)
    default:
    fmt.Printf("Default!")
    4.2、分支选择规则

    开始执行select语句的时候,所有跟在case关键字右边的发送语句或接受语句中的通道表达式和元素表达式都会被求值(求值顺序是从左到右,自上而下的)无论它们所在case是否被选择都会求值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    var intChan1 chan int
    var intChan2 chan int
    var channels = []chan int{intChan1, intChan2}

    var numbers = []int{1, 2, 3, 4, 5}

    func selecteval() {

    select {
    case getChan(0) <- getNumber(0):
    fmt.Println("1th case is selected")
    case getChan(1) <- getNumber(1):
    fmt.Println("2th case is selected")
    default:
    fmt.Printf("Default!")
    }
    }

    func getChan(i int) chan int {
    fmt.Printf("channels[%d]", i)
    return channels[i]
    }
    func getNumber(i int) int {
    fmt.Printf("numbers[%d]", i)
    return numbers[i]
    }
    //输出结果为:
    channels[0]numbers[0]channels[1]numbers[1]Default!

    在上面的代码中,由于intChan1和intChan2都未被初始化,向它们发送元素值的操作会被永久阻塞。

    • 执行select语句时,运行时系统会自上而下地判断每个case中地发送或接收操作是否可以立即执行(指当前goroutine不会因此操作被阻塞)

    • 只要发现有一个case地判断是肯定地,该case就会被选中

    • 当有只一个case被选中时,运行时系统就会执行该case及其包含的语句,其他case会被忽略

    • 同时有多个case满足条件,那么运行时系统会通过一个伪随机算法选中一个case。下面的代码会向一个通道随机发送5个范围为[1,3]的整数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      func selectrandom() {
      chanCap := 5
      intChan := make(chan int, chanCap)
      for i := 0; i < chanCap; i++ {
      select {
      case intChan <- 1:
      case intChan <- 2:
      case intChan <- 3:
      }
      }
      for i := 0; i < 5; i++ {
      fmt.Printf("%d\n", <-intChan)
      }
      }
      //其输出结果为
      1
      2
      3
      3
      3
      //其输出结果为
      1
      3
      2
      2
      2
    • 若所有case都不满足条件,且没有default case,那么当前goroutine就会一直被阻塞于此,直到至少有一个case满足条件为止。

    • default只能有一个且可以任意位置

    4.3、与for语句连用
    • case中的接收语句也支持:=

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      var strChan = make(chan string,10)
      ......
      select{
      case e,ok:= <-strChan:
      if !ok{
      fmt.Println("End.")
      break//立即结束当前select语句的执行
      }
      fmt.Printlf("Received: %v\n", e)
      }
    • 在实际场景中,常常需要把select放到一个单独的goroutine中去执行。这样即使select阻塞了,也不会造成死锁

    • select也常常和for联用,以便持续操作其中的通道。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      func selectFor() {
      intChan := make(chan int, 10)
      for i := 0; i < 10; i++ {
      intChan <- i
      }
      close(intChan)
      syncChan := make(chan struct{}, 1)
      go func() {
      Loop:
      for {
      select {
      case e, ok := <-intChan:
      if !ok {
      fmt.Println("End.")
      break Loop //只有这样才能正确地结束外层for循环,否则结束的时select
      //Loop:和break Loop是遥相呼应的
      }
      fmt.Printf("Received:%v\n", e)
      }
      }
      syncChan <- struct{}{}
      }()
      <-syncChan
      }
      //输出结果为:
      Received:0
      Received:1
      Received:2
      Received:3
      Received:4
      Received:5
      Received:6
      Received:7
      Received:8
      Received:9
      End.

    5、非缓冲的channel

    若初始化一个通道时将其容量设置为0,或者直接忽略对容量的设置,就会使该通道成为一个非缓冲通道

    与以异步的方式传递元素值的缓冲通道不同,非缓冲通道只能同步传递元素值。

    5.1、happens before

    与缓冲通道相比,针对非缓冲通道的happens before原则有两个特别之处:

    • 向此类通道发送元素值的操作会被阻塞,直到至少有一个针对该通道的接收操作进行为止。该接收操作会先得到元素值的副本,然后在唤醒发送方所在的goroutine之后返回。这时接收操作会在对应的发送操作完成之前完成。
    • 从此类通道接收元素值的操作会被阻塞,直到至少有一个针对该通道的发送操作进行为止。该发送操作会直接把元素值复制给发送发,然后在唤醒发送方所在的goroutine之后返回。这时发送操作会在对应的接收操作完成之前完成

    这两条原则都是源码级的。由于Go运行时系统的实时调度,不一定能从程序的表象(如输出)验证它们。

    5.2、同步的特性
    • 单向非缓冲通道与单向缓冲通道在使用上没有什么特别之处。

    • 在与for、select连用时也与缓冲通道一般无二

    • 非缓冲通道会以同步的方式传递元素值,在其上收发值的速度总是与慢的那一方持平。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    func chan0cap() {
    sendingInterval := time.Second
    receptionInterval := time.Second * 2
    intChan := make(chan int, 0)
    go func() {
    var ts0, ts1 int64
    for i := 1; i <= 5; i++ {
    intChan <- i
    ts1 = time.Now().Unix()
    if ts0 == 0 {
    fmt.Println("Sent:", i)
    } else {
    fmt.Printf("Sent: %d [interval: %d s]\n", i, ts1-ts0)
    }
    ts0 = time.Now().Unix()
    time.Sleep(sendingInterval)
    }
    close(intChan)
    }()
    var ts0, ts1 int64
    Loop:
    for {
    select {
    case v, ok := <-intChan:
    if !ok {
    break Loop
    }
    ts1 = time.Now().Unix()
    if ts0 == 0 {
    fmt.Println("Received:", v)
    } else {
    fmt.Printf("Received: %d [interval: %d s]\n", v, ts1-ts0)
    }
    }
    ts0 = time.Now().Unix()
    time.Sleep(receptionInterval)
    }
    fmt.Println("End.")
    }
    Received: 1
    Sent: 1
    Sent: 2 [interval: 2 s]
    Received: 2 [interval: 2 s]
    Received: 3 [interval: 2 s]
    Sent: 3 [interval: 2 s]
    Received: 4 [interval: 2 s]
    Sent: 4 [interval: 2 s]
    Received: 5 [interval: 2 s]
    Sent: 5 [interval: 2 s]
    End.

    可以看到发送操作和接收操作的时间间隔都与receptionInterval变量的值相一致。

    6、time包与channel

    time包中的一些API是用通道辅助实现的,这些API可以帮助我们对通道的手法操作进行更有效的控制。

    6.1、定时器Timer

    Timer是time包中的结构体类型,包含了一个包级私有字段,所以不能直接使用复合字面量来初始化该类型的变量,且不能忽略对它的初始化。

    可以使用time.NewTimer函数和time.AfterFunc函数构建time.Timer:

    time.NewTimer(d time.Duration):d(相对到期时间)的含义是,从定时器被初始化的那一刻起,距到期时间需要多少纳秒(ns)。

    1
    timer :=time.NewTimer(3*time.Hour + 36*time.Minute)//据此时间间隔为3小时36分钟的定时器

    这里的timer是*time.Timer类型的,包含连个函数Reset和Stop:

    • Reset:重置定时器,会返回一个bool类型的值,无论bool结果如何,一旦调用完成,该定时器就会被重置
    • Stop:停止定时器,返回一个bool类型的值

    两个函数返回的结果含义相同:若为false,表明定时器早已到期或者被停止

    time.Timer中对外通知定时器到期的途径就是通道,由字段C代表。C代表的是一个chan time.Time类型的带缓冲的接收通道。C原先的值为双向通道,但是在赋给C时自动转换为了接收通道。定时器内部仍持有该通道,且未被转换,因此可以向它发送元素值。一旦触及到期时间,定时器就会向C的通知通道发送一个元素值。该元素值代表了该定时器到期的绝对到期时间

    <初始化的绝对时间>+<相对到期时间>==<绝对到期时间>

    通过C我们可以及时得到定时器到期的通知,并对此做出响应:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func timerbase1() {
    timer := time.NewTimer(2 * time.Second)
    fmt.Printf("Present time: %v.\n", time.Now())
    expirationTime := <-timer.C
    fmt.Printf("expiration time: %v.\n", expirationTime)
    fmt.Printf("Stop timer: %v.\n", timer.Stop())
    }
    //输出结果为:
    Present time: 2022-01-13 16:33:44.0548497 +0800 CST m=+0.003217101.
    expiration time: 2022-01-13 16:33:46.0635276 +0800 CST m=+2.011895001.
    Stop timer: false.

    接收操作会<-timer.C一直阻塞到定时器到期。中间存在的误差是由于计算机精确度导致的。

    使用定时器,我们可以便捷地实现对接收操作地超时设定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    func chantimeout1() {
    intChan := make(chan int, 1)
    go func() {
    time.Sleep(time.Second)
    intChan <- 1
    }()
    select {
    case e := <-intChan:
    fmt.Printf("Received:%v.\n", e)
    case <-time.NewTimer(time.Millisecond * 500).C:
    fmt.Println("Timeout!")

    }
    }

    可以用time.After(time.Millisecond*500)替换time.NewTimer(time.Millisecond * 500).C

    time.After会新建一个定时器,并把它地字段作为结果返回。

    定时器是可以复用的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    func chantimeout2() {
    intChan := make(chan int, 1)
    go func() {
    for i := 0; i < 5; i++ {
    time.Sleep(time.Second)
    intChan <- i
    }
    close(intChan)
    }()
    timeout := time.Millisecond * 500
    var timer *time.Timer
    for {
    if timer == nil {
    timer = time.NewTimer(timeout)
    } else {
    timer.Reset(timeout)
    }
    select {
    case e, ok := <-intChan:
    if !ok {
    fmt.Println("End.")
    return
    }
    fmt.Printf("Received:%v.\n", e)
    case <-timer.C:
    fmt.Println("Timeout!")

    }
    }

    }
    //输出为:
    Timeout!
    Received:0.
    Timeout!
    Timeout!
    Received:1.
    Timeout!
    Timeout!
    Received:2.
    Timeout!
    Received:3.
    Timeout!
    Timeout!
    Received:4.
    End.
    • 若在定时器到期前停止它,即调用定时器的方法结果为true,那么它的字段C没有机会缓冲任何元素值了,这样会使<-timer.C所在goroutine永远阻塞。因此,重置定时器之前一定不要再次对它的C字段执行接收操作
    • 若定时器到期了,为能及时地从它的C字段中接收元素值,那么该字段会一直缓冲着这个元素值,即使该定时器重置了之后也是如此,由于C的容量是1,这会影响重置后的定时器再次发送到期通知。虽然这不会造成阻塞,但是后面的通知会被直接丢掉,因此若想要复用定时器,应确保旧的通知被接收
    • 传入的代表相对到期时间的值应该为正整数否则定时器在被初始化或者重置会立即到期,之后从它的字段C接收元素值时就会立即成功,而不会有任何延时,这样定时器就失去了意义。

    time.AfterFunc是另一种新建定时器的方法,接受两个参数,第一个代表相对到期时间,第二个指定到期时需要执行的函数。它同样会返回新建的定时器。但是在定时器到期时,并不会向它的通知通道发送元素值,而是新启用一个goroutine执行调用方传入的函数。无论它是否被重置以及被重置多少次。

    6.2、断续器

    time包中另一个重要的结构体是time.Ticker。表示了断续器的静态结构。所含字段与time.Timer一致,但行为大不相同。

    • 定时器在重置之前只会到期一次,而断续器则会在到期后立即进入下一周期并等待再次到期周而复始,直到被停止。
    • 断续器传达到期通知默认途径也是它的字段C。传送当次到期绝对时间。容量依然是1。
    • 若向C发送新值时,发现C中旧值还未被接收,就会取消当次发送操作
    • time.NewTicker接受了一个time.Duration类型的参数(相对到期时间,单位纳秒)
    • *time.Ticker只有Stop方法,功能是停止断续器。一经停止,就不再向C中发送任何元素值了。但若C中有值,那么会一直在那,等待被接收。
    • 断续器适合做定时任务触发器,一旦被初始化,所有的绝对到期时间就已确定了。

    初始化如下:

    1
    var ticker *time.Ticker = time.NewTicker(time.Second)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    func tickercase() {
    intChan := make(chan int, 1)
    ticker := time.NewTicker(time.Second)
    go func() {//每隔1s发送一个1~3的随机数,不会主动停止。
    for _ = range ticker.C {
    select {
    case intChan <- 1:
    case intChan <- 2:
    case intChan <- 3:
    }
    }
    fmt.Println("End. [sender]")
    }()
    var sum int
    for e := range intChan {
    fmt.Printf("Received:%v\n", e)
    sum += e
    if sum > 10 {//sum大于10时停止接收
    fmt.Printf("Got:%v.\n", sum)
    break
    }
    }
    fmt.Println("End. [receiver]")
    }
    //运行一次结果为:
    Received:1
    Received:1
    Received:1
    Received:1
    Received:3
    Received:2
    Received:1
    Received:2
    Got:12.

    定时器设定超时时间,控制完成时间点

    断续器设定开始时间点