Go核心36讲笔记-二

函数的正确姿势

  • 函数在go里面是数据类型,代表着函数不但可以用于封装代码、分割功能、解耦逻辑、还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等等,就想切片和字典的值那样。
    
    package main
    import "fmt"
    type Printer func(contents string) (n int, err error)

func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}

func main() {
var p Printer
p = printToStd
p("something")
}


- 函数的签名:函数的参数列表和结果列表的统称
- 两个函数的参数列表和结果列中的元素顺序及其类型是一致的,我们就可以说它们是一样的函数,或者说是实现了同意个函数类型的函数。
- 高阶函数
    - 接受其他函数作为参数传入
    - 把其他的函数作为结果返回
- 满足其中任意一个特点,就可以说这个函数是一个高阶函数
- 函数属于引用类型
- 卫述语句是指被用来检查关键的先决条件的合法性,并在检查未通过的情况下立即终止当前代码块的执行的语句。
- 闭包:在一个函数中存在对外来的标志符的引用。所谓的外来标识符,既不代表当前函数的任何参数或结果,也不是函数内部声明的,它是直接从外边拿过来的。标识符的专门术语也叫自由变量

func genCalculator(op operate) calculateFunc {
return func(x int, y int) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
}

genCalculator 函数只定义了一个匿名的calculateFunc类型的函数并把它作为结果值返回。
这个匿名函数就是一个闭包函数,它里面的op变量既不代表它的任何参数或结果也不是它自己声明的,而是定义它的genCalculator函数的参数,所以是一个自由变量;这个变量只有在genCalculator函数调用的时候才能确定

### 结构体
结构体类型表示的是实实在在的数据结构

type Animal struct {
scientificName string
AnimalCategory
}

type AnimalCategory struct {
kingdom string
species string
}


- 嵌入字段
第一个struct类型中的字段声明,AnimalCategory是一个struct,这里是一个嵌入字段。GO语言规范规定,如果以个字段的声明中只有字段的类型名而没有字段的名称,那么它就是一个嵌入字段,也可以被称为匿名字段。

- 值方法和指针方法
- 方法的接收者类型必须是某个自定义的数据类型,而且不能是接口类型或接口的指针类型。所谓的值方法,就是接收者类型是非指针的自定义数据类型的方法。
- 指针方法

func (cat *Cat) SetName(name string) {
cat.name = name
}


方法SetName的接收者类型是*Cat。Cat左边再加个*代表的就是Cat类型的指针类型。这时Cat可以被叫做*Cat的基本类型,可以认为这种指针类型的值表示的是指向某个基本类型值的指针。我们可以通过把取值操作符*放在这样一个指针值的左边来组成一个取值表达式,以获取该指针值指向的基本类型值,也可以通过把取址操作符&放在一个可寻址的基本类型值的左边来组成一个取值表达式,以获取该基本类型值的指针值。所谓的指针方法,就是接收者类型是上述指针类型的方法。

- 值方法与指针方法之间不同点
    - 值方法的接收者是该方法所属的那个类型值的一个副本,指针方法的接收者是该方法所属的那个基本类型值的指针值的一个副本

### 接口
接口说的是接口类型,它没法被值化,赋予它实际值之前,它的值是nil,没法被实例化

type Pet interface {
SetName(name string)
Name() string
Category() string
}
dog:= Dog("little pig")
var pet Pet = &dog

鸭子类型:只要一个数据类型的方法集合中有这3个方法,那么它就是一定Pet接口的实现类型,这是一种无侵入式地接口实现方式。
判定义个数据类型的某一个方法实现的就是某个接口类型的某个方法呢?
充分必要条件:2个方法的签名需要完全一致;2个方法名称要一模一样
实际值(动态值):接口类型的变量,赋给它的值可以被叫做它的实际值,而该值的类型可以被叫做这个变量的实际类型(动态类型)
静态类型:如上面的代码Pet 就是静态类型

- 接口的组合
接口类型的嵌入,接口之间有同名的方法就会产生冲突

### 指针的有限操作
指针是一个指向某个确切的内存地址的值。
unsafe.Pointer可以表示任何指向可寻址的值的指针

### GO的MPG
Go并发变成模型的中三个主要元素G(goroutine),P(processor 缩写)和M(Machine的缩写)
其中M指代的是系统级线程。而P指的是一种可以承载若干个G,且能够
是这些G适时地与M进行对接,并得到真正运行的中介。
从宏观上讲,G和M由于P的存在可以呈现出多对多的关系。当一个正在与某个M对接并运行着的G,需要印某个事件而暂停运行的时候,调度器总会及时地发现并把这个G和M分离,释放计算资源供那些等待运行的G使用。而当一个G需要恢复运行的时候,调度器又会尽快的为它寻找空闲的计算资源(包括M)并安排运行。另外当M不够用时,调度器会向系统申请新的系统级线程,当M无用时,调度器又会负责把它及时销毁掉。

- 主goroutine
    -  与一个进程总会有一个主线程类似,每一个独立的Go程序在运行时也总会有一个主goroutine
    -  go 函数真正被执行的时间总会与其他所属的go语句被执行的时间不同。当程序执行到一条go语句的时候,GO语言的运行时系统,会先试图从某个存放空闲的G队列中获取一个G(也就是goroutine),它只有在找不到空闲G的情况下才会去创建一个新的G;
    -  拿到G之后,go运行时系统会用着个G去包装当前那个go函数,然后再把这个G追加到某个存放可运行的G的队列中。队列中的G总是会按照陷入先出的顺序,很快的由运行时系统内的调度器安排运行。虽然很快,但是还是会耗时。
    -  go程序完全不会等待go函数的执行,它会立刻去执行后边的语句,这就是异步并发执行。
    -  一旦main中的代码执行完毕,当前的GO程序就会结束运行,这样goroutine 就可能得不到运行机会
 -  那问题就来了,怎么保证go func 这种goroutine运行完毕之后才让main goroutine 结束运行呢?
    -  time.Sleep(time.Millisecond * 500) 简单粗暴,不易控制时间
    -  通道法:创建一个通道,长度跟手动启用的goroutine的数量一致,每个手动启用的goroutine 即将运行完毕的时候,向着个通道发一个值
    -  sync.WaitGroup

### if、for、switch
- range 表达式只会在for语句开始执行时被求值一次,无论后面会有多少次迭代;
- range表达式的求值结果会被复制,也就是说,被迭代的对象是range表达式结果值的副本而不是原值。
numbers2 := [...]int{1,2,3,4,5,6}
maxIndex2 := len(numbers2)-1
for i, e:= range numbers2 {
    if i == maxIndex2 {
        numbers2[0] += e
    } else {
        numbers2[i+1] += e
    }
}

fmt.Println(numbers2) //7 3 5 7 9 11

 - switch 的表达式与case表达式
     - switch 1+3 {
         case value1,value2:
            fmt.Println("0 or 1")
     }

    - 1+3 是switch表达式,case 后面是case表达式,由case关键字和一个表达式列表组成,多个表达式之间需要有英文逗号,分割
    - 一但switch表达式的结果值与某个case表达式中的任意一个子表达式的结果值相等,该case表达式所属的case字句就会被选中。
    - 一旦某个case字句被选中,其中的福袋在case表达式后边的那些语句就会被执行,于此同时其他的所有case字句都被忽略
    - 如果被选中的acse子句附带的语句列表中包含了fallthrough语句,那么紧挨在它下边的那个case字句附带的语句也被执行
    - switch表达式的类型和case表达式地类型比较是 == ,这里的1+3是无类型常量4,默认是int,那么如果case是int型能比较否则编译无法通过
    - 反过来,如果case是无类型常量,那么它的结果值会被转化为跟switch表达式的结果类型一样的类型
    - switch 语句对它的case表达式约束: case表达式中的子表达式结果值,不能有相同的值
    - 如何绕过呢?
    - 利用数组的索引 case value5[0]: case value5[0],但是不能用于类型判断的switch,如 case unit8,unit16

### 错误处理
- errors 类型是一个接口类型
- errors.New("empty request")
- 具体错误的判断
    - 1 对于类型在已知范围内的一系列错误值,一般使用类型断言表达式或类型switch语句来判断;
    - 2 对于已有响应变量且类型相同的一系列错误值,一般直接使用判等操作来判断
    - 3 对于没有相应变量且类型未知的一系列错误值,只能使用期错误信息的字符串表示形式来做判断。
- panic 函數、recover函數以及defer語句

panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
/demo47.go:5 + 0x3d
exit statsu 2

第一行 runtime error的含义:这是一个runtime代码包中抛出的panic
panic 右边的内容,正是这个panic包含的runtime.Error类型值的字符串表示形式。
goroutine 1 [running] 达标ID为1的goroutine在此panic被引发的时候正在运行。
main.main() 表明了这个goroutine包装的go函数就是命令源码文件中那个main()函数,也就是说这里的goroutine 正是主goroutine。再下面的一行,指出的就是这个goroutine中哪一行代码在此panic被引发时正在执行;+0x3d 代表此行代码相对于其所属函数的入口程序计数偏移量。
exit status 2 表明这个程序是以退出状态码2结束运行的,一般来说正常情况下就是退出状态码0


- 有个疑问哦 从panic被引发到程序终止运行时的大致过程是什么?
- 某行代码引发了一个panic,这时初始的panic详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码,也就是调用栈中的上一级。
- 这也意味着 此行代码所属函数的执行随即终止。紧接着,控制权并不会再此有片刻停留,它又会立即转到再上一级的调用代码处。如此一级一级的沿着调用栈反方向传播至顶端,也就是我们编写的最外层函数那里。
- 这里最外层指的是go函数,对于主goroutine来说就是main函数。但是控制权也不会停留在那里,而是被GO语言运行时系统收回。
- 最后 程序崩溃并终止运行,承载程序这次运行的进程也随之死亡并消失。于此同时,在这个控制权传播的过程中,panic详情会被逐渐地积累和完善,并在程序终止之前被打印出来。
- revover和defer
- defer语句就是用被用来延迟执行代码的。延迟到该语句所在的函数即将执行结束的那一刻,无论结束执行的原因是什么。
- defer函数调用的执行顺序与他们分别所属的defer语句的出现顺序完全相反;defer语句每次执行的时候,Go语言会把它携带的defer函数以及其参数值另行存储到一个队列中。这个队列与该defer语句所属的函数是对应的,并且它是先进后出(FILO),相当于一个栈。需要执行defer的时候,从该队列中一个个取出defer函数和其参数值,并逐个执行调用。
- 
### 测试的基本规则和流程
- 人是否会进步以及进步得有多快,依赖的恰恰就是对自我的否定--不破不立
- 单元测试
    - 又称为程序员测试
    - 功能测试(test)
    - 基准测试(benchmark 也称性能测试)、
    - 以及示例测试(example)
    - 

- go 对测试函数的名称和签名有哪些规定
- 功能测试函数来说,其名称必须以Test为前缀,并且参数列表中只应有一个*testing.T 类型的参数声明。
- 对于性能测试函数来说,其名称必须以Benchmark为前缀,并且唯一参数的类型必须是*testing.B类型的
- 对于示例测试函数来说,其名称必须以Example为前缀,但是对函数的参数列表没有强制规定。
- go test命令开始运行时,会先做一些准备工作,比如确定内部需要用到的命令,检查我们制定的代码包或者源码文件的有效性,以及判断我们给予的标记是否合法,等等。
- 在准备工作顺利完成之后,go test命令就会针对每个被测试代码包,依次地进行构建,执行包中的符合要求的测试函数、清理临时文件、打印测试结果;
- $ go test /jack/article/go
- ok /jack/ariticle/go 0.008s
- ok 表示测试成功,0.008s 测试耗费的时间
- 手动删除所有的缓存数据:go clean -cache
- 失败的情况返回值不一样
- FAIL /jack/article/go 0.007s 并且会有具体的错误文件及具体错误行号
- 性能测试结果
  • go test -bench=. -run=^$ jack/article/go
  • goos: darwin
  • goarch: amd64
  • pkg: jack/article/go
  • BenchmarkGetPrimes-8 500000 2314 ns/op
  • PASS
  • ok jack/article/go 1.192s
  • -bench=. 有这个标记会进行性能测试.表示需要执行任意名称的性能测试函数。
  • -run=^$ 这个标记用于表明需要执行哪些功能测试函数,^$ 意味着只执行名称为空的功能测试函数,即不执行任何功能函数
  • BenchmarkGetPrimes-8 被称为单个性能测试名称,表示命令执行了性能测试函数BenchmarkGetPrimes,并且当时所用的最大的P数量为8
  • 最大P数量相当于可以同时运行goroutine的逻辑CPU的最大个数,逻辑CPU只是GO语言运行时系统内部的一个概念,代表着它同时运行goroutine的能力
  • 一台计算机的CPU核心的个数,以为它能在同一时刻执行多少条程序指令,代表着它并行处理程序指令的能力;
  • runtime.GOMAXPROCS 函数改变最大P数量,也可以在go test -cpu
  • 500000 代表被测试函数执行的次数
  • 2314 ns/op 表明单词执行GetPrimes函数平均耗时为2314纳秒,最后一次执行测试函数时的执行时间,除以被测函数的执行次数而得出的。

    更多的测试手法

MPG,这里的P是processor的缩写,每个processor都是一个可以承载若干个G,且能够使这些G适时地与M进行对接并得到真正运行的中介。

正是由于P的存在,G和M才可以呈现出多对多的关系,并能够及时、灵活地进行组合和分离。

这里的G就是goroutine的缩写,可以被理解为Go语言自己实现的用户级线程。
M即为
machine的缩写,代表着系统级线程,或者说操作系统内核级别的线程。

Go语言并发编程模型中的P,正是goroutine的数量能够数十万计的关键所在。P的数量意味着 Go程序背后的运行时系统中,会有多少个用于承载可运行的G的队列存在。每一个队列都相当于一条流水线,它会源源不断地把可运行的G输送给空闲的M,并使这两者对接。

一但对接完成,被对接的G就真正地运行在操作系统的内核级线程之上了,每条流水线之间虽然会
有联系,但都是独立运作的。
因此,最大P数量就代表着Go语言运行时系统同时运行goroutine的能力,也可以被视为其中逻辑CPU的最大个数。
而gotest命令的-cpu标记正是用于设置这个最大个数的。也许你已经知道,在默认情况下,最大P数量就等于当前计算机CPU核心的实际数量。

当然了,前者也可以大干或者小干后者,如此可以在一定程度上模拟拥有不同的CPU核心数的计
算机。
所以,也可以说,使用-cpu标记可以模拟被测程序在计算能力不同计算机中的表现。

  • 问题来了:怎么样设置-cpu标记的值,以及它会对测试流程产生什么样的影响
  • 如 go test -cpu 1,2,4 go test 以1,2,4为最大P数量分别去执行第一个测试函数,然后再同样的方式执行第二个测试函数,以此类推。
  • go test -cpu 1 -count 5 count标记专门用于重复执行测试函数的。
  • 单元测试:对单一功能的模块进行辩解清晰的测试,并不参杂任何对外部环境的检测。
  • parallel 标记作用是 设置同一个被测代码包中的功能测试函数的最大并发执行数。默认是运行时最大P的数量,可以通过runtime.GOMAXPROCS(0)获得;此标记对性能测试无效

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注