安装
1 | brew install go@1.17 |
go mod
命令 | 解释 |
---|---|
go mod download | 下载依赖包到本地(默认为 GOPATH//pkg//mod 目录) |
go mod edit | 编辑 go.mod 文件 |
go mod graph | 打印模块依赖图 |
go mod init | 初始化当前文件夹,创建 go.mod 文件 |
go mod tidy | 增加缺少的包,删除无用的包 |
go mod vendor | 将依赖复制到 vendor 目录下 |
go mod verify | 校验依赖 |
go mod why | 解释为什么需要依赖 |
go mod build | 将当前目录打包成package |
go的一个目录对应一个包,包应当与目录名相同;
xxxx_test.go
这类的命名存在bug,不要使用- Go的测试工具只会任务xxx_test.go的文件是测试文件,否则
go test
的时候可能会报没有测试文件的错误。
go plugin
init函数
init函数的主要作用:
- 初始化不能采用初始化表达式初始化的变量。
- 程序运行前的注册。
- 实现sync.Once功能。
- 其他
init函数的主要特点:
- init函数先于main函数自动执行,不能被其他函数调用;
- init函数没有输入参数、返回值;
- 每个包可以有多个init函数;
- 包的每个源文件也可以有多个init函数,这点比较特殊;
- 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。
- 不同包的init函数按照包导入的依赖关系决定执行顺序。
defer/recover/panic联系
- recover 只有在发生 panic之后调用才会生效;
- panic 只会触发当前 Goroutine 的 defer;
- recover 只有在 defer 中调用才会生效;
- panic 允许在 defer 中嵌套多次调用;
Go协程与线程的区别
简介
- 协程,英文名Coroutine。但在 Go语言中,协程的英文名是:gorutine。它常常被用于进行多任务,即并发作业。没错,就是多线程作业的那个作业。
- 虽然在 Go 中,我们不用直接编写线程之类的代码来进行并发,但是 Go 的协程却依赖于线程来进行。
特点
- 多个协程可由一个或多个线程管理,协程的调度发生在其所在的线程中;
- 可以被调度,调度策略由应用层代码定义,即可被高度自定义实现;
- 执行效率高;
- 占用内存少。
- 因为协程的调度切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
- 调度发生在应用态而非内核态。内存的花销,使用其所在的线程的内存,意味着线程的内存可以供多个协程使用。
- 其次协程的调度不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,所以执行效率比多线程高很多。
Example 1:在main或其他函数中开协程,主程序结束了,协程还继续执行吗?
- main函数中的协程,如果main结束了,协程也会结束
- 其他函数里的协程,函数结束了,只要main没结束,协程就会执行。
Example 2:并发非重Practice
1 | package mutexSyntex |
闭包
闭包的本质源自两点,词法作用域和函数当作值传递。
词法作用域,就是,按照代码书写时的样子,内部函数可以访问函数外面的变量。引擎通过数据结构和算法表示一个函数,使得在代码解释执行时按照词法作用域的规则,可以访问外围的变量,这些变量就登记在相应的数据结构中。
函数当作值传递,即所谓的first class对象。就是可以把函数当作一个值来赋值,当作参数传给别的函数,也可以把函数当作一个值return。一个函数被当作值返回时,也就相当于返回了一个通道,这个通道可以访问这个函数词法作用域中的变量,即函数所需要的数据结构保存了下来,数据结构中的值在外层函数执行时创建,外层函数执行完毕时理因销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数。这也就是私有性。本来执行过程和词法作用域是封闭的,这种返回的函数就好比是一个虫洞,开了挂。显然,闭包的形成很简单,在执行过程完毕后,返回函数,或者将函数得以保留下来,即形成闭包。
Example
1 | package main |
1 | package main |
每一个接口都包含两个属性,一个是值,一个是类型。
而对于空接口来说,这两者都是 nil,可以使用 fmt 来验证一下
1 | package main |
输出如下
1 | type: <nil>, value: <nil> |
2. 如何使用空接口?
第一,通常我们会直接使用interface{}作为类型声明一个实例,而这个实例可以承载任意类型的值。
1 | package main |
第二,如果想让你的函数可以接收任意类型的值,也可以使用空接口接收一个任意类型的值
1 | package main |
接收任意个任意类型的值
1 | package main |
** 第三,你也定义一个可以接收任意类型的array、slice、map、strcut,例如这边定义一个切片
1 | package main |
3. 空接口几个要注意的坑
坑1:空接口可以承载任意值,但不代表任意类型就可以承接空接口类型的值
从实现的角度看,任何类型的值都满足空接口。因此空接口类型可以保存任何值,也可以从空接口中取出原值。但要是你把一个空接口类型的对象,再赋值给一个固定类型(比如 int, string等类型)的对象赋值,是会报错的。
1 | package main |
这个报错,它就好比可以放进行礼箱的东西,肯定能放到集装箱里,但是反过来,能放到集装箱的东西就不一定能放到行礼箱了,在 Go 里就直接禁止了这种反向操作。(声明:底层原理肯定还另有其因,但对于新手来说,这样解释也许会容易理解一些。)
1 | .\main.go:11:6: cannot use i (type interface {}) as type int in assignment: need type assertion |
坑2::当空接口承载数组和切片后,该对象无法再进行切片
1 | package main |
执行会报错。
1 | .\main.go:11:8: cannot slice i (type interface {}) |
坑3:当你使用空接口来接收任意类型的参数时,它的静态类型是 interface{},但动态类型(是int,string还是其他类型)我们并不知道,因此需要使用类型断言。
1 | package main |
输出如下
1 | 参数的类型是 int |
Go的内存管理
Go 实现的垃圾回收器是无分代(对象没有代际之分)、 不整理(回收过程中不对对象进行移动与整理)、并发(与用户代码并发执行)的三色标记清扫算法。 从宏观的角度来看,Go 运行时的垃圾回收器主要包含五个阶段:
阶段 | 说明 | 赋值器状态 |
---|---|---|
清扫终止 | 为下一个阶段的并发标记做准备工作,启动写屏障 | STW |
标记 | 与赋值器并发执行,写屏障处于开启状态 | 并发 |
标记终止 | 保证一个周期内标记任务完成,停止写屏障 | STW |
内存清扫 | 将需要回收的内存归还到堆中,写屏障处于关闭状态 | 并发 |
内存归还 | 将过多的内存归还给操作系统,写屏障处于关闭状态 | 并发 |
- 对象整理的优势是解决内存碎片问题以及“允许”使用顺序内存分配器。
- 但Go 运行时的分配算法基于 tcmalloc,基本上没有碎片问题。并且顺序内存分配器在多线程的场景下并不适用。
- Go 使用的是基于tcmalloc的现代内存分配算法,对对象进行整理不会带来实质性的性能提升。
三色标记
三色抽象只是一种描述追踪式回收器的方法,在实践中并没有实际含义, 它的重要作用在于从逻辑上严密推导标记清理这种垃圾回收方法的正确性。 也就是说,当我们谈及三色标记法时,通常指标记清扫的垃圾回收。
从垃圾回收器的视角来看,三色抽象规定了三种不同类型的对象,并用不同的颜色相称:
- 白色对象(可能死亡):未被回收器访问到的对象。在回收开始阶段,所有对象均为白色,当回收结束后,白色对象均不可达。
- 灰色对象(波面):已被回收器访问到的对象,但回收器需要对其中的一个或多个指针进行扫描,因为他们可能还指向白色对象。
- 黑色对象(确定存活):已被回收器访问到的对象,其中所有字段都已被扫描,黑色对象中任何一个指针都不可能直接指向白色对象。
波面
这样三种不变性所定义的回收过程其实是一个波面不断前进的过程,这个波面同时也是黑色对象和白色对象的边界,灰色对象就是这个波面。
当垃圾回收开始时,只有白色对象。随着标记过程开始进行时,灰色对象开始出现(着色),这时候波面便开始扩大。当一个对象的所有子节点均完成扫描时,会被着色为黑色。当整个堆遍历完成时,只剩下黑色和白色对象,这时的黑色对象为可达对象,即存活;而白色对象为不可达对象,即死亡。这个过程可以视为以灰色对象为波面,将黑色对象和白色对象分离,使波面不断向前推进,直到所有可达的灰色对象都变为黑色对象为止的过程。
根对象
根对象在垃圾回收的术语中又叫做根集合,它是垃圾回收器在标记过程时最先检查的对象,包括:
- 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
- 执行栈:每个 goroutine都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
- 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。
Go面向对象(封装/多态/继承)
- 封装:首字母大小写为区分,大写的暴露出来,小写的只在package内部用;
- 继承:一个struct可以包含另外一个struct,同时拥有了它的methods以及属性;
- 多态:即接口的使用,注意不是struct,struct必须是相等的,与Java类似。
Example
1.多态
1 | /* 多态行为的例子 */ |
2.继承
1 | package main |
select-case语法
select 语句的语法:
- 每个 case 都必须是一个通信
- 所有 channel 表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意某个通信可以进行,它就执行,其他被忽略。
- 如果有多个 case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
- 否则:
- 如果有 default 子句,则执行该语句。
- 如果没有 default子句,select将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
time.After(dutation)
- 调用time.After(duration),此函数马上返回,返回一个time.Time类型的Chan(type: <- chan time.Time只读),不阻塞。
- 后面你该做什么做什么,不影响。到了duration时间后,自动塞一个当前时间进去。
channel通道
- 有/无缓冲通道,根据缓冲区大小进行阻塞;
- 默认是阻塞的;
- 可以是只读、只写、同时可读写的;
- 常用作goroutine的通信/同步。
1 | //定义只读的channel |
定义只读和只写的channel意义不大,一般用于在参数传递中,见代码:
1 | package main |
iota关键字
- 关键字 iota 在常量声明区里有特殊的作用。
- 这个关键字让编译器为每个常量复制相同的表达式,直到声明区结束,或者遇到一个新的赋值语句。
- 关键字 iota 的另一个功能是,iota 的 初始值为 0,之后 iota 的值在每次处理为常量后,都会自增 1。
log包
- log 包有一个很方便的地方就是,这些日志记录器是多 goroutine 安全的;
- 这意味着在多个 goroutine可以同时调用来自同一个日志记录器的这些函数,而不会有彼此间的写冲突;
- 标准日志 记录器具有这一性质,用户定制的日志记录器也应该满足这一性质。
单元测试
单元测试时用来测试包或者程序的一部分代码或者一组代码的函数。测试的目的是确认目标代码在给定的场景下,有没有按照期望工作。
- 正向路径测试:就是在正常执行的情况下,保证代码不产生错误的测试;
- 负向路径测试:保证代码不仅会产生错误,而且是预期的错误。
Go mod 测试命令
go test -v [xxx_test.go]
-v
表示输出详细内容
基础测试(basic test)
- 测试函数必须以Test单词开头,而且必须接受一个指向testing.T类型的指针,并且不返回任何值;
- 否则,测试框架就不会认为这个函数是一个测试函数。
1 | package unitTestSyntax |
1 | === RUN TestDownloadByUnitTest |
表组测试(table test)
1 | package unitTestSyntax |
1 | === RUN TestDownloadByTableTest |
模拟调用(Mock)
1 | package unitTestSyntax |
1 | === RUN TestDownloadByMock |
服务端点测试
- 服务端点(endpoint)是指与服务宿主信息无关,用来分辨某个服务的地址,一般是不包含 宿主的一个路径。
- 如果在构造网络API,你会希望直接测试自己的服务的所有服务端点,而不用 启动整个网络服务。
- 包 httptest 正好提供了做到这一点的机制。
1 | package handlers |
1 | package unitTestSyntax |
1 | === RUN TestSendJSON |
Doc示例
1 | package handlers_test |
godoc -http=:6060
可以看到示例Examplego test -v -run="ExampleSendJSON
基准测试
- 基准测试是一种测试代码性能的方法。想要测试解决同一问题的不同方案的性能,以及查看哪种解决方案的性能更好时,基准测试就会很有用。
- 基准测试也可以用来识别某段代码的CPU或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。
- 许多开发人员会用基准测试来测试不同的并发模式,或者用基准测试来辅助配置工作池的数量,以保证能最大化系 统的吞吐量。
单组测试
1 | package unitTestSyntax |
go test -v -run="none" -bench="BenchmarkSprintf"
默认最少持续测试1秒go test -v -run="none" -bench="BenchmarkSprintf" -benchtime="3s"
持续测试3秒-run="none"
用于匹配弄开头的单元测试,这里是为了过滤所有的单元测试
多组测试
1 | package unitTestSyntax |
go test -v -run="none" -bench=. -benchtime="3s"
:
1 | goos: darwin |
go test -v -run="none" -bench=. -benchtime="3s" -benchmem
:
1 | goos: darwin |
Go与C++/Java/Python的区别
- Go没有类,可以给结构体定义方法;
- Go没有继承,转而通过结构体的组合方式实现相应逻辑,也就是has-a的关系,而不是继承的is-a的关系;
- Go的多态通过接口实现,实现接口只要结构体定义了接口对应的方法就行;
- 重写覆盖也类似于其他面向对象的语言;
- Go有自动垃圾回收方式,不需要C++那种手动释放,文件/tcp连接/数据库句柄等除外;
- case语句自带break;