C++ 回调函数示例
简单示例
1 |
|
输出:
Enter: callback()
print()
Leave: callback()
接下来我们把这两个函数放入类中实现,在调用的时候绑定函数名和其对应实例就可以按以上例子方法调用。
1 |
|
现在我们把绑定函数对象的过程封装起来
1 |
|
简单示例
1 | #include <functional> |
输出:
Enter: callback()
print()
Leave: callback()
接下来我们把这两个函数放入类中实现,在调用的时候绑定函数名和其对应实例就可以按以上例子方法调用。
1 | #include <functional> |
现在我们把绑定函数对象的过程封装起来
1 | #include <functional> |
字符串是不可变字节(byte
)序列,其本身是一个符合结构.
1 | type stringStruct struct { |
头部指针指向字节数组,但没有NULL
结尾。默认以UTF-8
编码存储Unicode
字符,字面量里允许使用十六进制、八进制和UTF
编码格式。
内置函数
len
返回字节数组长度,cap
不接受字符串类型参数。
字符串默认值不是nil
, 而是""
.
使用for
遍历字符串是,分byte
和rune
两种方式。
1 | func main () { // byte |
要修改字符串,须将其转换为可变类型([]rune
或[]byte
), 待完成后再转换回来。但不管怎么转换,都须重新分配内存,并复制数据。
1 | func pp(format string, ptr interface{}) { |
编译器会为了某些场合进行专门优化,避免额外分配和复制操作:
[]byte
转换为string key
, 去map[string]
查询的时候。string
转换为[]byte
, 进行for range
迭代时,直接取字节赋值给局部变量。除了类型转换外,动态构建字符串也容易造成性能问题。
用加法操作符拼接字符串时,每次都须重新分配内存。如此,在构建超大字符串时,性能就显得极差。
改进思路时预分配i足够大的空间。常用方法是用string.Join
函数,他会统计所有参数长度,并一次性完成内存分配操作。
另外
utf8.ValidString(s) 返回s是不是一个有效的字符串
utf8.RuneCountInString(s) 替代len
返回unicode
的字符数量
方法是与对象实例绑定的特殊函数。
方法是面向对象编程的基本概念,用于维护和展示对象的自身状态。对象是内敛的,每个实例都有各自不同的独立特征,以属性和方法来暴露对外通信接口。普通函数则专注于算法流程,通过接收参数来完成特定逻辑运算,并返回最终结果。换句话说,方法是有关联的而函数通常没有。
方法和函数定义语法区别,在于前者有前置实例接收参数,编译器以此确定方法所数类型。
可以为当前包,以及除接口和指针以外的任何类型定义方法。
1 | type N int |
方法同样不支持重载(overload
)。receiver
参数名没有限制,按惯例会选用简短有意义的名称。如方法内部并不引用实例,可省略参数名,仅保留类型。
1 | type N int |
方法可看作特殊的函数,那么receiver
的类型自然可以是基础类型或指针类型。这会关系到调用时对象实例是否被赋值。
1 | type N int |
可使用实例值或指针调用方法,编译器会根据方法receiver
类型自动在基础类型和指针类型间转换。
1 | func main() { |
指针类型的receiver
必须时合法指针(包括nil
), 或能获取实例地址。
1 | type X struct {} |
如何选择方法的receiver
类型:
*T
T
*T
, 以减少复制成本。Mutex
等同步字段,用*T
,避免因复制造成锁操作无效*T
可以访问匿名字段成员那样调用其方法,有编译器负责查找。
1 | type data struct { |
方法也会有同名遮蔽问题。但利用这一特性可实现类似(override
)操作。
1 | type user struct {} |
尽可能直接访问匿名字段的成员和方法,但他们依然不属于继承关系。
类型有一个与之相关的方法集,这决定了它是否实现某个接口。
T
方法集包含所有receiver T
方法。*T
方法集包含所有recever T
+ *T
方法。S
, T
方法集包含所有receiver S
方法。*S
, T
方法集包含所有receiver S
+*S
方法。S
或*S
, *T
方法集包含所有receiver S
+*S
方法。可利用反射测试这些规则。
1 | type S struct{} |
方法集影响接口实现和方法表达式转换,于通过实例或实例指针调用方法无关。实例并不使用方法集,而是直接调用(或通过隐式字段名).
很显然,匿名字段就是为方法准备的。否则,完全没必要为少写个字段名而大费周折。
面向对象的三大特征"封装",“继承"和"多态”, Go仅仅实现了部分特征,它更倾向于”组合优于继承“这种思想。将模块分解成相互独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入方式组合到一起,共同实现对外接口。而且其简短一致的调用方式,更是隐藏了内部实现细节。
组合没有父子依赖,不会破坏封装。且整体和局部松耦合,可任意增加来实现实现扩展。各单元持有单一职责,互无关联,实现和维护更加简单。
方法和函数一样,除直接调用外还可以赋值给变量,或作为参数传递。依照具体引用方式不同,可分为expression
和value
两种状态。
通过类型引用Method expression
会被还原为普通函数央视,receiver是第一参数,调用时须显示传参。至于类型,可以是T
或*T
, 只要目标方法集中即可。
1 | type N int |
当然,也可直接以表达式方式调用。
1 | func main(){ |
基于实例或指针引用的method value
, 参数签名不会改变,依旧按正常方式调用。
但当method value
被赋值给变量或作为参数传递时,会立即计算并复制该方法执行所需的receiver
对象,与其绑定,以便在稍后执行时,能隐式传入receiver
参数。
1 | type N int |
编译器会为method value生成一个包装函数,实现间接调用。至于
receiver
复制,和闭包的实现方法基本相同,打包成funcval
, 经由DX
寄存器传递。
当method value
作为参数是,会复制含receiver
在内的整个method value
。
1 | func call(m func()) { |
当然,如果目标方法的receiver
是指针类型,那么被复制的仅是指针。
1 | type N int |
只要receiver参数类型正确,使用nil
同样可以执行。
1 | type N int |
前序遍历:
遍历的顺序是:根节点-左节点-右节点
递归代码:
1 | class Solution { |
迭代代码:
1 | class Solution { |
中序遍历:
遍历的顺序是:左节点-根节点-右节点
递归代码
1 | class Solution { |
迭代代码
1 | class Solution { |
后序遍历:
遍历的顺序是:左节点-右节点-根节点
递归代码
1 | class Solution { |
迭代代码
1 | class Solution { |
题目描述:
有一种排序算法定义如下,该排序算法每次把一个元素提到序列的开头,例如2, 1, 3, 4,只需要一次操作把1提到序列起始位置就可以使得原序列从小到大有序。现在给你个乱序的1-n的排列,请你计算最少需要多少次操作才可以使得原序列从小到大有序。
输入描述
输入第一行包含两个正整数n,表示序列的长度。(1 <= n <= 100000)
接下来一行有n个正整数,表示序列中的n个元素,中间用空格隔开。(1 <= a_i <= n)
输出描述
输出仅包含一个整数,表示最少的操作次数。
样例输入
4
2 1 3 4
样例输出
1
1 | #include <algorithm> |
接口代表一种调用契约,是多个方法声明的集合。接口最常见的使用场景,是对包外提供访问,或预留扩展空间。
Go
接口的实现机制很简洁,只要目标类型方法集内包含接口声明的全部方法,就被视为实现了该接口,无须做显式声明。当然,目标类型可实现多个接口。
接口:
接口通常以er
作为名称后缀,方法名是声明组成部分,但参数名可不同或省略。
1 | type tester interface { |
如果接口没有任何声明方法声明,那么就是一个空接口, 他的用途类似面向对象的根类型Object
, 可被赋值为任何类型的对象。
接口变量默认值是nil
。如果实现接口的类型支持,可做相等运算。
1 | func main() { |
可以像匿名字段一样,嵌入其他接口。目标类型方法集中必须拥有包含嵌入接口方法在内的全部方法才算实现了该接口。
前提是,不能有同名方法, 不能嵌入自身或循环嵌入,那会导致递归错误。
1 | type stringer interface { |
超集接口变量可隐式转换为子集,反过来不行。
1 | func pp(a stringer) { |
支持匿名接口类型,可直接用于变量定义,或作为结构字段类型。
1 | type data struct{} |
接口执行一个名为itab
的结构存储运行期所需的相关类型信息。
1 | type iface struct { |
相关类型信息里保存了接口和实际对象的元数据。同时itab
还用fun
数组(不定长结构)保存了实际方法地址,从而实现在运行期对目标方法的动态调用。
除此之外,接口还有一个重要特征:将对象赋值给接口变量时,会复制该对象。我们甚至无法修改结构存储的复制品,因为它也是unaddressable
的。
1 | func main() { |
即便将其复制出来,用本地变量修改后,依然无法对iface.data
赋值。解决方法就是将对象指针赋值给接口,那么接口内存存储的就是指针的复制品。
只有当接口变量内部的两个指针(itab
, data
)都为nil
时, 接口才等于nil
.
类型推断可将接口变量还原为原始类型,或用来判断是否实现了某个更具体地接口类型。
1 | type data int |
使用ok-idiom
模式,即便转换失败也不会引发panic
。还可用switch
语句在多种类型间做出推断匹配,这样空接口就有更多发挥空间。
1 | func main() { |
提示:
type switch
不支持fallthrought
让编译器检查,确保类型实现了指定接口
1 | type x int |
定义函数类型,让相同签名地函数自动实现某个接口
1 | type FuncString func() string |
简单将goroutine
归纳为协程并不合适。运行时创建多个线程来执行并发任务,且任务单元可被调度到其他线程并行执行。这更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力。
只须在函数调用前添加go
关键字即可创建并发任务。
1 | go println("hello, world!") |
关键字go
并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列中,等待调度器安排合适系统线程去获取执行权。当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行次序。
每个任务单元除保存函数指针、调用参数外,还会分配执行所需的栈内存空间。相比系统默认MB
级别的线程栈,goroutinue
自定义栈初始仅须2 KB
,所以才能创建成千上万的并发任务。自定义栈采取按需分配策略,在需要时进行扩容,最大能到GB
规模。
与defer
一样,gorountine
也会因"延迟执行"而立即计算并复制执行参数。
1 | package main |
进程退出时不会等待并发任务结束,可用管道(channel
)阻塞,然后发出退出信号。
1 | func main() { |
除关闭通道外,写入数据也可解除阻塞。
如要等待多个任务结束,推荐使用sync.WaitGroup
。通过设定计数器,让每个goroutine
在退出前递减,直至归零时接触阻塞。
1 | func main() { |
尽管WaitGroup.Add
实现了原子操作,但建议在goroutine
外累加计数器,以免Add
尚未执行,Wait
已经退出。
可在多处使用Wait
阻塞,他们都能接收到通知
1 | func main() { |
运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该数量默认与处理器核数相等,可用runtime.GOMAXPROCS
函数(或环境变量)修改。
与线程不同,goroutine
任务无法设置优先级,无法获取编号,没有局部存储(TLS), 甚至连返回值都会被抛弃。但除优先级外,其他功能都很容易实现。
1 | func main() { |
如使用
map
作为局部存储容器,建议做同步处理,因为运行时会对其做并发读写检查。
暂停,释放线程去执行其他任务。当前任务被放回队列,等待下次调度时恢复执行。
1 | func main() { |
Goexit
立即终止当前任务,运行时确保所有已注册延迟调用被执行。该函数不会影响其他并发任务,不会引发panic
, 自然也就无法捕获。
1 | func main() { |
如果在main.main
里调用Goexit
, 它会等待其他任务结束,然后让进程直接崩溃。
无论身处哪一层,Goexit
都能立即终止整个调用堆栈,这与return
仅退出当前函数不同。标准库函数os.Exit
可终止进程,但不会执行延迟调用。
Go
语言并未实现严格的并发安全。
允许全局变量、指针、引用类型这些非安全内存共享操作,就需要开发人员自行维护数据一致和完整性。Go
鼓励使用CSP
通道,以通信来代替内存共享,实现并发安全。
CSP: Communicating Sequential Process
通过消息来避免竞态的模型除了CSP
, 还有Actor
。但两者由较大区别
作为CSP
核心,通道(channel)是显式的,要求操作双方必须知道数据类型和具体通道,并不关心另一端操作者身份和数量。可如果另一端未准备妥当,或消息未能及时处理时,会阻塞当前端。
相比起来,Actor
是透明的,它不在乎数据类型及通道,只要知道接收者信箱即可。默认就是异步方式,发送方对消息是否被接收和处理并不关心。
从底层实现上来说,通道知识一个队列。同步模式下,发送和接受双方配对,然后直接赋值数据给对方。如配对失败,则置入等待队列,直到另一方出现后才被唤醒。异步模式抢夺的则是数据缓冲槽。发送方要求有空槽可供写入,而接收方则要求有缓冲数据可读。需求不符时,同样加入缓冲队列,直到有另一方写入数据或腾出空槽后被唤醒。
除传递消息(数据)外,通道还常被用做事件通知。
1 | func main() { |
同步模式必须有配对操作的goroutine
出现,否则会一直阻塞。而异步模式在缓冲区未满或数据未读完前,不会阻塞。
1 | func main() { |
多数时候,异步通道有助于提升性能,减少排队阻塞。
缓冲去大小仅是内部属性,不属于类型组成部分。另外通道变量本身就是指针,可用相等操作符判断是否为同一对象或nil
。
内置函数cap
和len
返回缓冲区大小和当前已缓冲数量;而对于同步通道则都返回0;据此可判断通道时同步还是异步
1 | func main () { |
除使用简单的发送和接受操作符外,还可用ok-idom
或range
模式处理数据
1 | func main() { |
一次性事件用
close
效率更好,没有多余开销。连续或多样性事件,可传递不同数据标志实现。还可使用sync.Cond
实现单播或广播事件。
对于closed
或nil
通道,发送和接收操作都有相应规则:
panci
nil
通道都会阻塞。1 | func main() { |
重复关闭或关闭nil
通道都会引发panic
错误。
通道默认时双向的,并不区分发送和接收端。
尽管可用make
创建单向通道,但那没有任何意义。通常使用类型装欢来获取单向通道,并分别赋予操作双方。
1 | func main() { |
不能在单向通道上做逆向操作。
1 | func main(){ |
同样,close不能用于接收端
1 | func main() { |
无法将单向通道重新转换回去。
1 | func main() { |
如要同时处理多个通道,可选用select
语句。它会随机选择一个可用通道做收发操作。
1 | func main() { |
如要等全部通道消息处理结束(closed),可将已完成通道设置为nil
。这样它就会被阻塞,不再被select
选中。
即使是同一通道,也会随机选择case
执行。
当所有通道都不可用时,select
会执行default
语句。如此可避开select
阻塞,但须注意处理外层循环,以免陷入空耗。
通常使用工厂方法将goroutine
和通道绑定。
1 | type receiver struct { |
鉴于通道本身就是一个并发安全的队列,可用作ID generator
、Pool
等用途。
1 | type pool chan []byte |
用通道实现信号量(semaphore)
1 | func main() { |
标准库time
提供了timeout
和tick channel
实现。
1 | func main() { |
捕获INT
、TERM
信号,顺便实现一个简易的atexit
函数。
1 | import ( |
通道可能会引发goroutine leak
, 确切的说,是指goroutine
处于发送或接收阻塞状态,但一直未被唤醒。垃圾回收器并不收集此类资源,导致他们会在等待队列里长久休眠形成资源泄露。
标准库sync
提供了互斥和读写锁,另有原子操作等,可基本满足日常开发需要。Mutex
、RWMutex
的使用并不复杂,只有几个地方需要注意。
将Mutex
作为匿名字段时,相关方法必须实现为pointer-receiver
, 否则会因赋值导致锁机制失效。
1 | type data struct { |
defer Unlock
RWMutex
性能会更好一些依照规范,工作空间由src
、bin
、pkg
三个目录组成。通常需要将空间路径添加到GOPATH
环境变量列表中, 以便相关工具能正常工作。
1 | workspace/ |
在工作空间里,包括子包在内的所有源码文件都保存在src
目录下。至于bin
、pkg
两个目录, 其主要影响 go install/get
命令,他们会将编译结果(可执行文件或静态库)安装到这两个目录下,以实现增量编译。
编译器等相关工具按GOPATH
设置的路径搜索目标。也就是说在导入目标库时,排在列表前面的路径比当前工作空间优先级更高。另外,go get
默认将下载的第三方包保存到列表中第一个工作空间内。
环境变量GOPATH
用于指示工具链和标准库的存放位置。在生成工具链时,相关路径就已经嵌入到可执行文件内,故无需额外设置。
除通过设置GOROOT
环境变量覆盖内部路径外,还可移动目录(改名、符号链接等), 或重新编译工具链来解决。
至于GOBIN
, 则是强制替代工作空间的bin
目录,作为go install
目标保存路径。这可避免将所有工作空间的bin
路径添加到PATH
环境变量当中。
使用标准库或第三方包前,须用import
导入,参数是工作空间中以src
为起始的绝对路径。编译器从标准库开始搜索,然后依次搜索GOPATH
列表中的各个工作空间。
1 | import "net/http" // 实际路径: /usr/local/go/src/net/http |
除使用默认包名外,还可使用别名,以解决同名冲突问题。
1 | import osx "github.com/apple/osx/lib" |
注意:
import
导入参数是路径,而非包名。尽管习惯将包和目录名保持一致,但这不是强制规定。在代码中引用包成员时,使用包名而非目录名。
有四种不同的导入方式。
1 | import "github.com/Mercy1101/test" // 默认方式: test.A |
不能直接或间接导入自己,不支持任何形式的循环导入。
未使用的导入(不包括初始化方式)会被编译器视为错误。
除工作空间和绝对路径外,部分工具还支持相对路径。可在非工作空间目录下,直接运行、编译一些测试代码。
但在设置了GOPATH
的工作空间后相对路径会导致编译失败。go run
不受影响。
包内每个源码文件都可定义一到多个初始化函数,但编译器不保证执行次序。
实际上,所有这些初始化函数(包括标准库和导入的第三方包)都由编译器自动生成的一个包装函数进行调用,因此可保证在单一线程上执行,且仅执行一次。
编译器首先确保完成所有全局变量初始化,然后才开始执行初始化函数。直到这些全部结束后,运行时才正式进入main.main
入口函数。
可在初始化函数中创建goroutine
,或等到它结束执行。
1 | func init(){ |
如果在多个初始化函数中引用全局变量,那么最好在变量定义处直接赋值。因无法保证执行次序,所以任何初始化函数中的赋值都有可能"延迟无效"。
内部包机制相当于增加了新的访问权限控制:所有保存在internal
目录下的包(包括自身)仅能被其父目录下的包(包含所有子目录) 访问。
1 | workspace/ |
在lib
目录外(比如main.go
)导入内部包会引发编译错误。
导入内部包必须使用完整路径, 例如: import “lib/internal/a”
如何使用vendor
,专门存放第三方包,实现将源码和依赖完整打包分发。
1 | workspace/ |
1 | package main |
在
main.go
中导入github.com/mercy1101/test
时,优先使用vendor/github.com/mercy1101/test
导入vendor
中的第三方包,参数是以vendor/
为起点的绝对路径。这避免了vendor
目录位置带来的麻烦,让导入无论使用vendor
,还是GOPATH
都能保持一致。
注意:
vendor
优先级比标准库高
当多个vendor
目录嵌套时,匹配规则如下:
从当前源文件所在目录开始,逐级向上构造vendor
全路径,直到发现路径匹配的目标为止。匹配失败,则依旧搜索GOPATH
要使用vendor
机制,须开启GO15VENDOREXPERIMENT=1
环境变量开关(Go 1.6默认开启),且必须设置了GOPATH
的工作空间。
使用
go get
下载第三方包时,依旧使用GOPATH
第一个工作空间,而非vendor
目录。当前工具链中并没有真正意义上的包依赖管理,好在由不少第三放工具可选。
反射能让我们能在运行期探知对象的类型信息和内存结构,同时反射还是实现元编程的重要手段。
Go对象头部并没有类型指针,通过自身是无法在运行期获知任何类型相关信息的。反射操作所需的全部信息都源自接口变量。接口变量除自身存储自身类型外,还会保存实际对象的类型数据。
1 | func TypeOf(i interface{}) Type |
这两个反射入口函数,会将任何传入的对象转换为接口类型。
在面对类型是,需要区分Type
和Kind
。前者表示真实类型(静态类型), 后者表示器接触接口(底层类型)类别。
1 | type X int |
1 | func main() { |
方法Elem
返回指针、数组、切片、字典值或通道的基类型。
1 | func main() { |
只有在获取结构体指针的基类型后,才能遍历它的字段。
1 | package main |
对于匿名字段,可用多级索引(按定义顺序)直接访问
1 | func main() { |
FieldByName
不支持多级名称,如有同名遮蔽,须通过匿名字段二次获取
反射能探知当前包或外包的非导出结构成员
1 | package main |
相对
reflect
而言,当前包和外包都是"外包"
可用反射提取struct tag
, 还能自动分解。其常用于ORM映射, 或数据格式验证。
1 | type user struct { |
辅助判断方法Implements
、ConvertibleTo
、AssignableTo
都是运行期进行动态调用和赋值所必需的。
1 | type X int |
和Type
获取类型信息不同, Value
专注于对象实例数据读写
接口变量会赋值对象,且时unaddressable
的,所以要修改对象就必须使用指针。
1 | func main() { |
就算传入指针,一样需要通过
Elem
获取目标对象。因为被接口存储的指针本身时不能寻址和进行设置操作的。
注意:不能对非导出字段进行设置操作,无论是当前包还是外包。
1 | type User struct { |
Value.Pointer
和Value.Int
等方法类似,将Value.data
存储的数据转换为指针,目标必须是指针类型。
而UnsafeAddr
返回任何CanAddr Value.data
地址(相当于&取地址操作),比如Elem
后的Value
, 以及字段成员地址。
以结构体里的指针类型字段为例,Pointer
返回该字段所保存的地址,而UnsafeAddr
返回该字段本身的地址(结构对象地址+偏移量)
可通过Interface
方法进行类型推断
1 | func main() { |
也可以直接使用
Value.Int
、Bool
等方法进行类型转换,但失败时会引发panic
, 且不支持ok-idiom
复合类型对象设置示例:
1 | func main() { |
接口有两种nil
状态,这一致是个潜在麻烦。解决方法是用IsNil
判断值是否为nil
1 | func main() { |
也可用unsafe
转换后直接判断iface.data
是否是零值
1 | func main() { |
让人很无奈的是, Value
里的某些方法并未实现ok-idom
或返回error
, 所以得自行判断返回的是否为Zero Value
1 | func main(){ |
动态调用方法,谈不上有多麻烦。只须按In
列表准备好所需参数即可。
1 | type X struct{} |
对于变参来书,用CallSlice
要更方便一些
1 | type X struct {} |
反射库提供了内置函数make
和new
的对应操作,其中最有意思的就是MakeFunc
。可用它实现通用模板,使用不同数据类型。
1 | // 通用算法函数 |
标准库自带单元测试框架
1 | package main |
参数 | 说明 | 示例 |
---|---|---|
-arg | 命令行参数 | |
-v | 输出详细信息 | |
-parallel | 并发执行, 默认执行GOMAXPROCS | -parallel 2 |
-run | 指定测试函数,正则表达式 | -run “Add” |
-timeout | 全部测试累计时间超时将引发panic, 默认值为10ms | -timeout 1m30s |
-count | 重复测试次数,默认次数为1 |
1 | func TestMain(m * testing.M){ |
多测试用例
1 | func TestMain(m * testing.M) { |
1 | func ExampleAdd() { |
注意:如果没有output注释,该示例就不会被执行。另外,不能使用内置函数print/printIn, 因为他们输出到stderr
1 | func BenchmarkAdd(b *testing.B) { |
go test -bench .
如果希望仅执行性能测试,那么可以用run=NONE
忽略所有测试用例。
性能测试默认以并发方式进行测试,但可用cpu参数设定多个并发限制来观察结果。
go test -bench . -cpu 1,2,4
某些耗时的目标,默认循环测试过少,取平均值不足以准确计量性能。可用benchtime
设定最小测试时间来增加循环次数,以便返回更准确的结果。
go test -bench . -benchtime 5s
如果在测试函数中要执行一些额外的操作,那么应该临时i组织计时器工作。
1 | func BenchmarkAdd(b *testing.B) { |
性能测试查看内存情况
1 | func heap() []byte { |
go test -bench . -benchmem -gcflags “-N -l” # 禁止内联和优化, 以便观察结果
也可将测试函数设置为总是输出内存分配信息,无论使用benchmem参数与否
1 | func BenchmarkHeap(b *testing.B) { |
go test -cover
为获取更详细信息,可指定covermode 和coverprofile 参数
go test -cover -covermode count -coverprofile cover.out
还可以在浏览器中查看包括具体的执行次数等信息
go tool cover -html=cover.out
引发性能问题的原因无外乎执行时间过长、内存占用过多,以及意外阻塞。通过捕获或监控相关执行状态数据,就可定位引发问题的原因,从而针对性改进算法。
go test -run NONE -bench . -memprofile mem.out -cpuprofile cpu.out net/http
参数 | 说明 | 示例 |
---|---|---|
-cpuprofile | 保存执行时间采样到指定文件 | -cpuprofile cpu.out |
-memprofile | 保存内存分配采样到指定文件 | -memprofile mem.out |
-memprofilerate | 内存分配采样起始值,默认为512KB | -memprofilerate 1 |
-blockprofile | 保存阻塞时间采样到指定文件 | -blockprofile block.out |
-blockprofilerate | 阻塞时间采样起始值,单位为:ns |
如果执行性能测试,可能需要设置benchtime
参数,以确保有足够的采样时间
可使用交互模式查看,或用命令行直接输出单向结果。
go tool pprof http.test mem.out
(pprof) top5
top命令可指定排序字段,比如top5 -cum
找出需要进一步查看的目标,使用peek
命令列出调用来源
也可用list命令输出源码统计样式,以便更直观的定位
除文字模式以外,还可输出svg图形,将其保存或用浏览器查看
在线采集数据须诸如 http/pprof
包
1 | import ( |
用浏览器访问指定路径,就可看到不同的检测项。
go tool pprof http://localhost:8080/debug/pprof/heap?debug=1
必要时还可抓取数据,进行离线分析。
curl http://localhost:8080/debug/pprof/heap?debug=1 > mem.out
go tool pprof test mem.out