目录

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 接口还能实现 依赖注入模式