Go - Pattern
前置原则
在实际真正需要的时候才对程序进行抽象
不要为了抽象而抽象
不要为了使用接口而使用接口
解耦或者抽象是有成本的
造成运行效率的下降
影响代码的可读性
组合
如果 C++ 和 Java 是关于类型层次结构和类型分类的语言,那么 Go 是关于组合的语言
组合是 Go 的重要设计哲学之一,而正交性则为组合哲学的落地提供了方便的条件
正交
在计算机技术中,正交性用于表示某种不相依赖性或者解耦性
编程语言中的语法元素和语言特性也存在正交的情况,通过将这些正交的特性组合起来,可以实现更为高级的特性
Go 在语言设计层面提供的正交的语法元素
Key
Value
类型定义是正交的
无类型体系,没有父子类的概念
方法和类型是正交的
方法的本质只是将 receiver 参数作为第一个参数的函数而已
接口与其它语言元素是正交的
接口与它的实现者之间没有显式关联
方式
垂直组合
用在将多个类型通过类型嵌入的方式实现新类型的定义
传统的 OOP 编程语言大多是通过继承的方式构建出类型体系
Go 没有类型体系的概念,Go 通过类型的组合而不是继承让单一类型承载更多的功能 - ...
Go - nil
接口类型静态特性
接口类型变量具有静态类型
在编译阶段保证灵活性的安全
12345func main() { var err error = 1 // err 的静态类型为 error // cannot use 1 (type int) as type error in assignment: // int does not implement error (missing Error method)}
编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查
如果右值的类型没有实现接口方法集合中的所有方法,会编译报错
动态特性
接口类型变量在运行时存储了右值的真实类型信息(即接口类型变量的动态类型)
拥有与动态语言相近的灵活性
123var err errorerr = errors.New("this is an error")fmt.Printf("%T\n", err) // *errors.errorString
errors/errors.go123func New(text string) ...
Go - interface
概述
接口类型是由 type 和 interface 关键字定义的一组方法集合(唯一确定所代表的接口)称为方法,而非函数,更多是从这个接口的实现者的角度考虑的
1234type MyInterface interface { M1(int) error M2(io.Writer, ...string)}
方法的参数列表中的形参名和返回值列表中的具名返回值,都不作为区分两个方法的凭据
12345// 等价type MyInterface interface { M1(a int) error M2(w io.Writer, args ...string)}
在接口类型中声明的方法必须是具名的,且方法名在这个接口类型的方法集合中是唯一的
类型嵌入:在 Go 1.14 开始,接口类型允许嵌入不同接口类型的方法集合存在交集但需要方法名和函数签名也要保持一致,否则会编译报错
123456789101112131415type I1 interface { M1()}type I2 interface ...
Go - Type Embedding
类型嵌入
在一个类型的定义中嵌入其它类型
接口
语义:方法集合并入
接口类型声明了由一个方法集合代表的接口
12345678910type E interface { M1() M2()}type I interface { M1() M2() M3()}
完全等价:类型嵌入,新接口类型将嵌入的接口类型的方法集合,并入到自身的方法集合中 - 接口组合
123456789type E interface { M1() M2()}type I interface { E M3()}
Go 标准库
io/io.go1234567891011type Reader interface { Read(p []byte) (n int, err error)}type Writer interface { Write(p []byte) (n int, err error)}type Closer interfac ...
Go - receiver
影响12func (t T) M1() <=> F1(t T)func (t *T) M2() <=> F2(t *T)
Method
Desc
M1
基于值拷贝,传递的是 T 类型实例的副本
M2
基于值拷贝,传递的是 T 类型实例的指针
123456789101112131415161718192021222324252627282930type T struct { a int}func (t T) M1() { t.a = -1}func (t *T) M2() { t.a += 1}func main() { var t T println(t.a) // 0 t.M1() println(t.a) // 0 t.M2() println(t.a) // 1 p := &t p.M1() println(t.a) // 1 p.M2() println(t.a) // 2}
方法 ...
Go - Method
OOP
Go 并不支持 OOP 的语法元素(类、对象、继承等),但仍支持方法(Method)
Go 引入 Method 并非为了实现 OOP 编程范式,而是出自 Go 的组合设计哲学下类型系统实现层面的需要
Method 本质上是一个以 receiver 参数作为第 1 个参数的 Function(Go 编译器协助转换)
形式
receiver 参数是 Method 和 Type 之间的纽带
ListenAndServeTLS 归属于 *Server 类型,而非 Server 类型
receiver
Method 必须归属于某个 Type,即 receiver 参数的类型
123func (receiver *T或T) MethodName(参数列表) (返回值列表) { // 方法体}
无论 receiver 参数的类型是 *T 还是 T,receiver 参数的基类型都为 T
如果 receiver 的类型为 T,则这个 Method 是类型 T 的一个方法
如果 receiver 的类型为 *T,则这个 Method 是类型 *T 的一个方法
每个 Me ...
Go - defer
defer
defer 是 Go 提供的一种延迟调用机制,defer 的运作离不开函数
只有函数或者方法内部才能使用 defer
defer 关键字后只能接受函数或者方法,被称为 deferred 函数
defer 将 deferred 函数注册到其所在的 goroutine 中,用于存放 deferred 函数的栈数据结构
这些 deferred 函数将在执行 defer 的函数退出前,按 LIFO 的顺序被调度
无论执行到函数体尾部成功返回,还是在某个错误处理分支显式 return,或者出现 panic
已经存储到 deferred 函数栈中的函数,都会被调度执行 - 收尾
1234567891011121314151617181920212223242526272829303132package mainimport "sync"type Closable interface { Close()}type Resource struct {}func (r *Resource) Close() {} ...
Go - panic
原则
不要相信任何外部输入的参数
函数需要对所有输入的参数进行合法性的检查
一旦发现问题,立即终止函数的执行,返回预设的错误值
不要忽略任何一个错误
显式检查这些函数调用返回的错误值
一旦发现错误,要及时终止函数执行,防止错误继续传播
不要假定异常不会发生
异常不是错误
错误是可预期的,也会经常发生,有对应的公开错误码和错误处理方案
异常是不可预期的,通常指的是硬件异常、操作系统异常、语言运行时异常、代码 Bug(数组越界访问)等
异常是小概率事件,但不能假定异常不会发生
根据函数的角色和使用场景,考虑是否要在函数内设置异常捕获和恢复的环节
panic
在 Go 中,由 panic 来表达异常的概念
panic 指的是 Go 程序在运行时出现的一个异常情况
如果异常出现了,但没有被捕获并恢复,则 Go 程序的执行会被终止
即便出现异常的位置不在主 goroutine
panic 来源:Go 运行时 / 开发者通过 panic 函数主动触发
当 panic 被触发,后续的执行过程称为 panicking
手动调用 panic 函数,主动触发 panicking
...
Go - error
C vs Go
Go 语言的错误处理机制是在 C 语言错误处理机制基础上的再创新
通常使用类型为整型的函数返回值作为错误状态标识,函数调用者会基于值比较的方式来处理错误
返回值为 0,代表函数调用成功,否则函数调用出现错误
优点
要求开发者必须显式地关注和处理每个错误
错误一般为普通值,不需要额外的语言机制来处理,让代码更容易调试
C 语言错误处理机制具有简单和显式结合的特征,非常符合 Go 的设计哲学
因此 Go 继承了 C 的错误处理机制
缺点
C 语言中的函数最多仅支持一个返回值
一值多用
承载函数要返回给调用者的信息
承载函数调用的最终错误状态
当返回值为其它类型,如字符串,很难将数据与错误融合在一起
做法不一,很难形成统一的错误处理策略
因此 Go 函数新增了多返回值机制,用于支持数据与错误(返回值列表的末尾)的分离
fmt/print.go123456789// Fprintf formats according to a format specifier and writes to w.// It returns the number of bytes writ ...
Go - Function
声明
关键字 func
Go 函数声明必须以关键字 func 开始
函数名
在同一个 Go 包中,函数名应该是唯一的
遵循 Go 标识符的导出规则
参数列表
Go 函数支持变长参数,即一个形式参数对应多个实际参数
返回值列表
支持具名返回值,比较少用
函数体
可选,如果没有函数体,说明该函数可能在 Go 语言之外实现的
可能使用汇编语言,然后通过链接器将实现与声明中的函数名链接在一起
类比
将函数声明等价转换为变量声明的形式 - 声明一个类型为函数类型的变量
Key
Value
变量名
函数名
函数签名
参数列表 + 返回值列表
函数类型
func + 参数列表 + 返回值列表func + 函数签名
函数签名:决定两个函数类型是否相同
在表述函数类型时,通常会省略函数签名参数列表中的参数名,以及返回值列表中的返回值变量名
12// 函数类型,只关注入参和出参func (io.Writer, string, ...interface{}) (int, error)
相同的函数签名,相同的函数类型
12345678func (a ...