Golang-Day3
面向对象特征
封装
Java中的封装
Java语言中,封装是自然而来的,也是强制的。你所写的代码,都要属于某个类,某个class文件。类的属性封装了数据,方法则是对这些数据的操作。通过private和public来控制数据的可访问性
每个类(java文件),自然的就是一个对象的模板
Go中的封装
Go语言并不是完全面向对象的。其实Go语言中并没有类和对象的概念
首先,Go语言是完全可以写成面向过程风格的。Go语言中有很多的function是不属于任何对象的
然后,Go语言中,封装有包范围的封装和结构体范围的封装
Golang的封装特性体现在类名、属性名、方法名的首字母大写即表示对外(其他包)可以访问,否则只能在本包内访问
继承
Java中的继承
Java语言中,继承通过extends关键字实现。有非常清晰的父类和子类的概念以及继承关系。Java不支持多继承
Go中的继承
Go语言中其实并没有继承。但是Go中确实只是提供了一种伪继承,通过embedding实现的“伪”继承
父类的定义以及方法
子类的定义以及方法
子类的定义只需在结构体内加入父类名称即可认定为继承父类
如此看起来Go语言中的继承是不是更像一种提供了语法糖的has-a的关系,并不是is-a的关系
这里就引申出继承或是组合的问题
继承vs组合
继承 | 组合 | |
---|---|---|
优点 | 创建子类的对象时,无须创建父类的对象 | 不破坏封装,整体类与局部类之间松耦合,彼此相对独立 |
子类能自动继承父类的接口 | 具有较好的可扩展性 | |
支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 | ||
整体类可以对局部类进行包装,封装局部类的接口,提供新的接 | ||
缺点 | 子类不能改变父类的接口 | 整体类不能自动获得和局部类同样的接口 |
破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性 | 创建整体类的对象时,需要创建所有局部类的对象 | |
不支持动态继承。在运行时,子类无法选择不同的父类 | ||
支持扩展,但是往往以增加系统结构的复杂度为代价 |
那么合理选择继承和组合使用时机就很重要了
- 除非考虑使用多态,否则优先使用组合
- 要实现类似”多重继承“的设计的时候,使用组合
- 要考虑多态又要考虑实现“多重继承”的时候,使用组合+接口
多态
基本要素
方法重载也是多态的一种。但是Go语言中是不支持方法重载的
两种语言都支持方法重写(Go中的伪继承,son如果重写了father中的方法,默认是会使用son的方法的)
不过要注意的是,在Java中重写父类的非抽象方法,已经违背了里氏替换原则。而Go语言中是没有抽象方法一说的
Go中的多态采用和JavaScript一样的鸭式辩型:如果一只鸟走路像鸭子,叫起来像鸭子,那么它就是一只鸭子。
在Java中,我们要显式的使用implements关键字,声明一个类实现了某个接口,才能将这个类当做这个接口的一个实现来使用。在Go中,没有implements关键字。只要一个struct实现了某个接口规定的所有方法,就认为它实现了这个接口。
父类(接口)
子类(实现父类所有方法)
父类类型的变量(指针)指向(引用)子类的具体数据变量
理解了基本的接口的基本要素
而接下来,我们来讨论一个老生常谈的问题:接口和抽象类的区别
abstract class的核心在于,我知道一类物体的部分行为(和属性),但是不清楚另一部分的行为(和属性),所以我不能自己实例化(不知道的这部分)。如我们的例子,abstract class是Animal,那么我们可以定义他们胎生,恒定体温,run()等共同的行为,但是具体到“叫”这个行为时,得留着让非abstract的狗和猫等等子类具体实现。
interface的核心在于,我只知道这个物体能干什么,具体是什么不需要遵从类的继承关系。如果我们定一个Shouter interface,狗有狗的叫法,猫有猫的叫法,只要能叫的对象都可以有shout()方法,只要这个对象实现了Shouter接口,我们就能把它当shouter使用,让它叫。
所以abstract class和interface是不能互相替代的,interface不能定义(它只做了声明)共同的行为,事实上它也不能定义“非常量”的变量。而abstract class只是一种分类的抽象,它不能横跨类别来描述一类行为,它使得针对“别的分类方式”的抽象变得无法实现(所以需要接口来帮忙)。
interface
通用的万能类型
interface{}
- 空接口
- int 、string、float32、 float64、struct …. 都实现了interface{}
- 可以用interface{}类型引用任意的数据类型
类型断言
反射
什么是反射
在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。
实际上,它的本质是程序在运行期探知对象的类型信息和内存结构,不用反射能行吗?可以的!使用汇编语言,直接和内层打交道,什么信息不能获取?但是,当编程迁移到高级语言上来之后,就不行了!就只能通过反射
来达到此项技能。
不同语言的反射模型不尽相同,有些语言还不支持反射。《Go 语言圣经》中是这样定义反射的:
Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。
使用反射的理由
需要反射的 2 个常见场景:
- 有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。
- 有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。
在讲反射的原理以及如何用之前,还是说几点不使用反射的理由:
- 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
- Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
- 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。
反射的实现
Go 语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。
变量的结构
reflect 包里定义了一个接口和一个结构体,即 reflect.Type
和 reflect.Value
,它们提供很多函数来获取存储在接口里的类型信息。
reflect.Type
主要提供关于类型相关的信息,所以它和 _type
关联比较紧密;reflect.Value
则结合 _type
和 data
两者,因此程序员可以获取甚至改变类型的值。
Reflect包
ValueOf()方法与TypeOf()方法是Reflect包中最常用的方法
Reflect具体方法的实现
通过 Type()
方法和 Interface()
方法可以打通 interface
、Type
、Value
三者。Type() 方法也可以返回变量的类型信息,与 reflect.TypeOf() 函数等价
Interface() 方法可以将 Value 还原成原来的 interface
反射三大定律
根据 Go 官方关于反射的博客,反射有三大定律:
- Reflection goes from interface value to reflection object.
- Reflection goes from reflection object to interface value.
- To modify a reflection object, the value must be settable.
第一条是最基本的:反射是一种检测存储在 interface
中的类型和值机制。这可以通过 TypeOf
函数和 ValueOf
函数得到。
第二条实际上和第一条是相反的机制,它将 ValueOf
的返回值通过 Interface()
函数反向转变成 interface
变量。
前两条就是说 接口型变量
和 反射类型对象
可以相互转化,反射类型对象实际上就是指的前面说的 reflect.Type
和 reflect.Value
。
第三条不太好懂:如果需要操作一个反射变量,那么它必须是可设置的。反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反映到原变量本身;反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。
关于第三条,记住一句话:如果想要操作原变量,反射变量 Value
必须要 hold 住原变量的地址才行。
结构体标签
定义
通过 reflect.Type 获取结构体成员信息 reflect.StructField 结构中的 Tag 被称为结构体标签(Struct Tag)。结构体标签是对结构体字段的额外信息标签。
JSON、BSON 等格式进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。
提示
结构体标签(Struct Tag)类似于 C#中的特性(Attribute)。C# 允许在类、字段、方法等前面添加 Attribute,然后在反射系统中可以获取到这个属性系统。例如:
[Conditional("DEBUG")]
public static void Message(string msg)
{
Console.WriteLine(msg);
}
结构体标签的格式
应用
从结构体标签中获取值
StructTag 拥有一些方法,可以进行 Tag 信息的解析和提取,如下所示:
- func(tag StructTag)Get(key string)string
- 根据 Tag 中的键获取对应的值,例如
key1:"value1"key2:"value2"
的 Tag 中,可以传入“key1”获得“value1”。 - func(tag StructTag)Lookup(key string)(value string,ok bool)
- 根据 Tag 中的键,查询值是否存在。
主要的应用有JSON编解码以及ORM映射关系
这里举一个JSON编解码的例子
高阶语法
Goroutine
Goroutine的理解
Go语言最大的特色就是从语言层面支持并发(Goroutine),Goroutine是Go中最基本的执行单元。
事实上每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。
线程(Thread):有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。
协程(coroutine):又称微线程与子例程(或者称为函数)一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。
和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显式控制。它避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。
Goroutine和其他语言的协程(coroutine)在使用方式上类似,但从字面意义上来看不同(一个是Goroutine,一个是coroutine),再就是协程是一种协作任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。因此Goroutine可以理解为一种Go语言的协程。同时它可以运行在一个或多个线程上。
Go并发的实现原理
Go实现了两种并发形式。第一种是大家普遍认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。
CSP并发模型是在1970年左右提出的概念,属于比较新的概念,不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。
请记住下面这句话:
DO NOT COMMUNICATE BY SHARING MEMORY; INSTEAD, SHARE MEMORY BY COMMUNICATING.
“不要以共享内存的方式来通信,相反,要通过通信来共享内存。”
普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。例如Java提供的包”java.util.concurrent”中的数据结构。Go中也实现了传统的线程并发模型。
Go的CSP并发模型,是通过goroutine
和channel
来实现的。
goroutine
是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。channel
是Go语言中各个并发结构体(goroutine
)之前的通信机制。 通俗的讲,就是各个goroutine
之间通信的”管道“,有点类似于Linux中的管道。
创建Goroutine
通过简单的创建一个父子Goroutine来说明创建一个Goroutine的过程,而如果父Goroutine被终止那么子Goroutine也无法运行
Goexit
runtime.Goexit()方法提供了退出当前Goroutine的解决方案
Channel
Channel的定义
Channel 是 Golang中最核心的 feature 之一,因此理解 Channel 的原理对于学习和使用 Golang非常重要
Channel 是 Goroutine 之间通信的一种方式,可以类比成 Unix 中的进程的通信方式管道
Channel 介绍
channel 提供了一种通信机制,通过它,一个 goroutine 可以想另一 goroutine 发送消息。channel 本身还需关联了一个类型,也就是 channel 可以发送数据的类型
例如: 发送 int 类型消息的 channel 写作 chan int
Channel 创建
Channel的使用
无缓冲的Channel
- 在第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执⾏发送或者接收
- 在第 2 步,左侧的 goroutine 将它的⼿伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在通道中被锁住,直到交换完成
- 在第 3 步,右侧的 goroutine 将它的⼿放⼊通道,这模拟了从通道⾥接收数据。这个 goroutine ⼀样也会在通道中被锁住,直到交换完成
- 在第 4 步和第 5 步,进⾏交换,并最终,在第 6 步,两个 goroutine 都将它们的⼿从通道⾥拿出来,这模拟了被锁住的 goroutine 得到释放。两个 goroutine 现在都可以去做其他事情了
有缓冲的Channel
- 在第 1 步,右侧的 goroutine 正在从通道接收⼀个值
- 在第 2 步,右侧的这个 goroutine 独⽴完成了接收值的动作,⽽左侧的 goroutine 正在发送⼀个新值到通道
- 在第 3 步,左侧的goroutine 还在向通道发送新值,⽽右侧的 goroutine 正在从通道接收另外⼀个值。这个步骤⾥的两个操作既不是同步的,也不会互相阻塞
- 最后,在第 4 步,所有的发送和接收都完成,⽽通道⾥还有⼏个值,也有⼀些空间可以存更多的值
特点
- 当channel已经满,再向⾥⾯写数据,就会阻塞
- 当channel为空,从⾥⾯取数据也会阻塞
关闭Channel
- channel不像⽂件⼀样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel
- 关闭channel后,⽆法向channel再发送数据(引发 panic 错误后导致接收⽴即返回零值)
- 关闭channel后,可以继续从channel接收数据
- 对于nil channel,⽆论收发都会被阻塞
Channel与range
Channel与select
单流程下⼀个go只能监控⼀个channel的状态,select可以完成监控多个channel的状态
所以引入select来进行对多个channel的操作
下面用一个斐波那契的简单实例来展示select的功能
总结:select具备多路channel的监控状态功能
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!