Go
函数
不支持:
嵌套定义命名函数
同名函数重载
默认参数
支持:
不定长参数
多返回值
一等公民 first object class
匿名函数
闭包
一等公民 first class object : 可以在运行期创建、可作为值(参数、返回值、变量)的实体。
作为一等公民的函数的局限性:只能与 nil 比较.即便有相同函数类型
fn1 == fn2
语句也是错误的,而其他的一等公民(基础类型等)就没这个限制
闭包 是引用了外部变量的函数.被引用的外部变量和函数一同存在,即使已经离开了外部环境(出栈),这些外部变量也不会被释放,在闭包中可以继续使用这个外部变量。
函数 + 引用到的外部变量 = 闭包
函数 是编译期静态的概念,一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,编译后它的一切就被确定了。
闭包 是运行期动态的概念,只有在运行时,有了外部环境变量,函数捕捉了这些变量后,才称为“闭包”。
闭包对环境中变量的引用过程也可以被称为 “捕获”。捕获有两种类型:
可以改变引用的原值叫做 “引用捕获”
捕获的过程值被复制到闭包中使用叫做 “复制捕获”
程序 = 数据 + 算法
算法可以表象为函数,将函数附着在数据上,就成了对象。
反之呢?
函数既然是一等公民,将数据附着在函数上,就成了闭包。
以前可以通过 “传递对象” 完成的功能,现在可以通过 “传递闭包” 完成。并且更加灵活,因为对象的数s据结构,往往提前设计好,而 “闭包” 却可以灵活的捕捉数据。
闭包设计的目的是:在函数间传递共享数据时,不想传参,不想依赖全局变量。是一种隐秘的共享数据的方式。
类型
slice map channel interface 也被设计成一种类型,但实际上它们拥有更复杂的存储结构,初始化需要设置一系列属性:指针、长度、hash分布、数据队列等。这些类型创建的变量本身只占少量字节,用于存放指针和长度等属性,真正存储数据的地方都在变量内部指针指向的内存空间。
同一类型的判定
基础类型、用户自定义类型,类型名称相同就是同一类型。
有相同声明的未命名类型也被视为同一类型,包括:
相同基础类型的 pointer、slice
相同基础类型、相同长度的 array
相同键值类型的 map
相同基础类型、相同方向的 channel
相同基础类型、相同字段顺序、相同 tag 的 匿名 struct
相同签名(参数、返回值列表相同)的 func
相同方法集(方法名、方法签名)的 interface
只有判定为同一类型,才可以进行赋值!!!
Goroutine
不足:没有将 Channel 扩展到多个进程,甚至是不同的机器进程之间,实现真正的分布式。
赋值退化
定义退化成赋值(垃圾特性),很容易带来隐蔽的逻辑错误。
在同一作用域,最少有一个新变量被定义,才会发生赋值退化
// eg. 1
x := 100
x, y := 200,"abc" // x 退化为赋值,y是变量定义
// eg. 2
x := 100
{
x,y := 200,"abc" // 不同作用域,x y 都是新变量定义
}
工具链
单元测试
性能测试
代码覆盖率
数据竞态条件检查
调优
pprof
通过环境变量输出运行时监控信息
垃圾回收和并发调度跟踪
包管理工具(go mod)一般
调度器(垃圾)
GC
传指针VS传值
对于分配在堆上的内存空间,当指向它的指针为 0 个时,GC 后会被回收。在传参时,指向该内存的指针被复制,相当于会延长该内存空间的存在时间。
传指:将对象分配在堆上的成本 + GC成本
传值:将值复制到函数栈的成本(指令少而快,未必会比指针慢)
面向对象
在经典的面向对象语言(像 C++,Java 和 C#)中数据和方法被封装为 类
的概念:类包含它们两者,并且不能剥离。
Go 没有类:数据(结构体或更一般的类型)和方法是一种松耦合的正交关系。
Go 中的接口跟 Java/C# 类似:都是必须提供一个指定方法集的实现。但是更加灵活通用:任何提供了接口方法实现代码的类型都隐式地实现了该接口,而不用显式地声明。
和其它语言相比,Go 是唯一结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。该特性允许我们在不改变已有的代码的情况下定义和使用新接口。
接收一个(或多个)接口类型作为参数的函数,其实参可以是任何实现了该接口的类型的变量。 实现了某个接口的类型可以被传给任何以此接口为参数的函数。
类似于 Python 和 Ruby 这类动态语言中的 动态类型(duck typing)
;这意味着对象可以根据提供的方法被处理(例如,作为参数传递给函数),而忽略它们的实际类型:它们能做什么比它们是什么更重要。
我们总结一下前面看到的:Go 没有类,而是松耦合的类型、方法对接口的实现。
OO 语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢?
封装(数据隐藏):和别的 OO 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层(参见 4.2 节的可见性规则):
1)包范围内的:通过标识符首字母小写,`对象` 只在它所在的包内可见 2)可导出的:通过标识符首字母大写,`对象` 对所在包以外也可见
类型只拥有自己所在包中定义的方法。
继承:用组合实现:内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现
多态:用接口实现:某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。
动态方法调用
像 Python,Ruby 这类语言,动态类型是延迟绑定的(在运行时进行):方法只是用参数和变量简单地调用,然后在运行时才解析(它们很可能有像 responds_to
这样的方法来检查对象是否可以响应某个方法,但是这也意味着更大的编码量和更多的测试工作)
Go 的实现与此相反,通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有函数。如果方法调用作用于像 interface{}
这样的“泛型”上,你可以通过类型断言来检查变量是否实现了相应接口。
例如,你用不同的类型表示 XML 输出流中的不同实体。然后我们为 XML 定义一个如下的“写”接口,甚至可以把它定义为私有接口。现在我们可以实现适用于该流类型的任何变量的 StreamXML
函数,并用类型断言检查传入的变量是否实现了该接口;如果没有,我们就调用内建的 encodeToXML
来完成相应工作:
type xmlWriter interface {
WriteXML(w io.Writer) error
}
func StreamXML(v interface{}, w io.Writer) error {
if xw, ok := v.(xmlWriter); ok {
// It’s an xmlWriter, use method of asserted type.
return xw.WriteXML(w)
}
// No implementation, so we have to use our own function (with perhaps reflection):
return encodeToXML(v, w)
}
// Internal XML encoding function.
func encodeToXML(v interface{}, w io.Writer) error {
// ...
}
因此 Go 提供了动态语言的优点,却没有其他动态语言在运行时可能发生错误的缺点。
对于动态语言非常重要的单元测试来说,这样即可以减少单元测试的部分需求,又可以发挥相当大的作用。
Go 的接口提高了代码的分离度,改善了代码的复用性,使得代码开发过程中的设计模式更容易实现。用 Go 接口还能实现 依赖注入模式
。