Golang 入门(七) —— 接口

Go语言的主要设计者之一罗布派克曾经说过,如果只能选择一个Go语言的特性移植到其他语言上去,他会选择接口。
Go语言中的接口有着至关重要的地位,如果说goroutinechannel是支撑起Go语言并发模型的基石,那么接口Go语言整个类型系统的基石。

Go语言中一个类只需要实现了接口需要的所有方法,那么我们可以说这个类实现了这个接口,例如:

共有4个接口,IFileIReadIWriteIClose

File类分别实现了以上4个接口所需要的方法,那么File同时实现了IFileIReadIWriteIClose这4个接口。

Go语言的接口是非侵入式的,看似制作了很小的语法调整,实则影响深远。

接口赋值

接口赋值分为两种情况:

  • 将对象实例赋值给接口
  • 将一个接口赋值给另一个接口

我们先讨论将对象实例赋值给接口,例如我们之前做过的Integer类型,如下:

相应的我们定义接口LessAdder如下,

现在有个问题,稼穑我们定义了一个Integer类型对象的实力,怎么将其复制给LessAdder接口呢?

以上应该使用表达式1呢还是使用表达式2呢?

正确答案应该是表达式2,为什么呢?

因为Go语言会根据下面的函数,

自动生成一个新的函数:

这样,类型*Integer就既存在Less方法,又存在Add方法,满足LessAdder接口。而另一方面来说,根据方法Add无法生成一下方法:

因此,类型Integer只存在Less方法,缺少Add方法,无法满足LessAdder接口,所以表达式1是不能被赋值的。

我们再来讨论另外一个赋值,将一个接口赋值给另一个接口

在Go语言中,只要两个接口拥有相同的方法列表,那么他们就是等同的,可以相互赋值。

在Go语言中,这两个接口并没有什么区别,因为:

  • 任何实现了IReadWrite的接口类,均实现了IStream
  • 任何IReadWrite接口对象可以赋值给IStream,反之亦然
  • 在任何地方使用IReadWrite接口与使用IStream并无差异

接口查询

我们可以把IStream的对象赋值给IWrite,但是反过来确认报错。

这个时候就需要用到接口的查询功能,

类型查询

在Go语言中,还可以更加直接了当的询问接口指向对象实例的类型,例如:

接口组合

我们可以将上面的IFile接口写成这个样子

Any类型

由于Go语言中任何对象实例都满足空接口interface{},所以interface{}看起来像是可以指向任何对象的Any类型,如下:

Golang 入门(六) —— 面向对象编程

Go语言的面向对象设计的非常简洁。简洁在于Go语言并没有沿袭传统面向对象编程中的诸多概念,比如继承、虚函数、构造函数、析构函数、隐藏this指针等。

1、类型系统

类型系统是指一个语言的类型体系结构。一个典型的类型系统通常包含如下基本内容:

  • 基础类型:如byteintboolfloat
  • 复合类型:如数组、结构体、指针等
  • 可以指向任意对象的类型Any类型
  • 值语义和引用语义
  • 面向对象,即所有具备面向对象特征的类型
  • 接口

在Go语言中,可以为任意类型(包括内置类型、但不包括指针类型)添加相应的方法。例如:

在这个例子中,我们定义了一个类型Integer,他和int没有本质的区别,只是它为内置的int类型增加了一个新的方法Less()

在Go语言中没有隐藏的this指针:

  • 方法施加的目标(也就是对象)显示传递,没有被隐藏起来
  • 方法施加的目标(也就是对象)不需要非得是指针,也不用非得是this

php中,使用写一段差不多意思的例子。

初学者可能会比较难离理解其背后的机制,一直this到底从何而来。其实能是php自动隐藏了this指针,如果不隐藏的话,代码可能会这个样子:

Go语言就非常直观的表现出来了,我们重写一下上面的例子:

就会发现,原来this是这么过来的。

如果要求对象必须以指针形式传递,这有时会是个额外的成本,因为对象有时很小(比如4字节),用指针传递并不划算。

只有在你需要修改对象的时候,才必须使用指针。它不是Go语言的约束,而是一种自然的约束。
举个例子:

2、值语义和引用语义

值语义和引用语义的差别在于赋值,比如下面的例子:

如果b的修改没有影响到a,那么此类型属于值类型,反之为引用类型。

Go语言中的大多数类型都是值语义,包括:

  • 基本类型:byteintfloat32float64string
  • 符合类型:arraystructpointer

Go语言中的数组和基本类型没有什么区别,是很纯粹的值类型。

如果希望完全复制,需要用到指针

Go语言中有4个类型比较特别,看起来像引用类型,

  • 切片: 指向数组的一个区间
  • map: 极其常见的数据结构,提供键值查询能力
  • channel: 执行体goroutine间的通信设施
  • 结构: 对一组满足某个且越的类型的抽象

但是这并不影响我们将Go语言看作值语义。

切片本事上是一个区间,大致可以将[]T表示为:

因为数组切片内部是指针,所以可以改变所指向的数组元素并不奇怪。切片类型本身的复制仍然是值语义。

map本质上是一个字典指针,你可以大致将map[K]V表示为:

基于指针,我们完全可以创建一个引用类型,如:

channelmap类似,本质上是一个指针。将他们设计为引用类型的原因是,完整复制一个channelmap并不是常规需求。

同样,接口具备引用语义,是因为维持了两个指针,示意为:

3、结构体

Go语言的结构体和其他语言的类有同等的地位,但是Go语言放弃了包括继承在内的大量面向对象特性,只保留了组合这个最基础的特新。

上面我们说道,所有的Go语言类型(指针除外)都可以有自己的方法。在这个背景下,Go语言的结构体只是很普通的复合类型,平淡无奇。

定义了Rect类型后,该如何创建初始化对象实例呢?

Go语言中未进行显示初始化的变量都为被初始化为该类型的零值。

Go语言虽然不支持继承,但是我们可以采用组合的方式,来完成继承所做的事情,我们将其称之为匿名组合:

以上代码实现了Foo继承了Base,并且Foo重写BaseBar方法。

在Go语言官方网站提供了Effective Go中提到一个匿名组合的例子:

在适合的赋值后,我们在Job类型的所有成员方法中可以很舒适的借用log.Logger提供的方法。

对Job的实现者来说,根本就不需要意识到log.Logger的存在,这既是匿名组合的魅力所在。

Libevent(一)

一、什么是libevent

libevent是一个事件触发的网络库,适用于windows、linux、bsd等多种平台,内部使用select、epoll、kqueue、IOCP等系统调用管理事件机制。著名分布式缓存软件memcached也是基于libevent,而且libevent在使用上可以做到跨平台,而且根据libevent官方网站上公布的数据统计,似乎也有着非凡的性能。——引用自百度百科

需要注意以下几点:

  1. libevent是一个事件触发的网络库
  2. 可以跨平台(windows、linux、bsd等)
  3. 使用select、epoll(linux)、kqueue(freebsd)、IOCP(windows)等系统调用管理事件机制(IO复用)
  4. libevent在Linux环境下默认采用epoll作为IO多路复用方法

用户线程使用libevent则通常按以下步骤:

  1. 用户线程通过event_init()函数创建一个event_base对象。event_base对象管理所有注册到自己内部的IO事件。多线程环境下,event_base对象不能被多个线程共享,即一个event_base对象只能对应一个线程。
  2. 然后该线程通过event_add函数,将与自己感兴趣的文件描述符相关的IO事件,注册到event_base对象,同时指定事件发生时所要调用的事件处理函数(event handler)。服务器程序通常监听套接字(socket)的可读事件。比如,服务器线程注册套接字sock1的EV_READ事件,并指定event_handler1()为该事件的回调函数。libevent将IO事件封装成struct event类型对象,事件类型用EV_READ/EV_WRITE等常量标志。
  3. 注册完事件之后,线程调用event_base_loop进入循环监听(monitor)状态。该循环内部会调用epoll等IO复用函数进入阻塞状态,直到描述符上发生自己感兴趣的事件。此时,线程会调用事先指定的回调函数处理该事件。例如,当套接字sock1发生可读事件,即sock1的内核buff中已有可读数据时,被阻塞的线程立即返回(wake up)并调用event_handler1()函数来处理该次事件。
  4. 处理完这次监听获得的事件后,线程再次进入阻塞状态并监听,直到下次事件发生。

—— 引用自Memcached网络模型

二、php使用libevent扩展实现高性能服务器

此处不再赘述php-libevent扩展的安装。
上述在描述libevent步骤时候,用的并非php函数,php实现是略微有区别,大致流程不变。

在之前的文章『php-socket 实现简单服务器』中,我们使用stream+select的方式来实现一个简单的服务器。

现在我们就使用libevnet来实现更高性能的服务器。

附上libevent的常量

Golang 入门(五) —— 错误处理

1、error接口

Go语言引入了一个关于错误处理的标准模式,即error接口,该接口的定义如下:

对于大多数函数,如果需要返回错误,大致可以定义如下模式,将error作为多返回值中的最后一个即可,但是这并非是强制要求:

需要时,可以如下调用:

下面使用Go库中的实际代码来示范如何自定义error类型。

调用可以如下:

2、defer

关键字defer允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return 语句同样可以包含一些操作,而不是单纯地返回某个值)。

关键字 defer 的用法类似于面向对象编程语言 JavaC#finally 语句块,它一般用于释放某些已分配的资源。

下面的例子可以简单的解释defer的用法,

如果一句话干不完清理的工作,可以使用在defer后加一个匿名函数的做法:

另外,一个函数里可以存在多个defer,因此需要注意的是,defer语句遵循先进后出的原则,即最后一个defer第一个执行,第一个defer最后一个执行。

3、panicrecover

Go语言引入了两个内置函数panic()recover(),用以报告和处理运行时错误和程序中的错误场景:

使用方法如下:

Golang 入门(四) —— 函数

在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。

1、函数定义

ab为参数
reserr为返回值
当然返回值也可以不写变量名称。

2、函数调用

函数调用非常方便,只要事先导入该函数的包,就可以直接调用函数了:

需要牢记规则:小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。这个规则也是用于变量的可见性。

3、不定参数

  • 不定参数类型

这个方法我们可以个通过如下方式来调用:

形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。它是一个语法糖syntactic sugar,即这种语法对语言的功能没有影响,但是更方便程序员使用。

用内部机制上来说,类型...type本质上是一个切片,也就是[]type,这也就是为什么上面的参数args可以用for循环来获取每个传入的参数。

调用方式:

  • 不定参数的传递

我们可以使用如下方式调用myFunc

  • 任意类型的不定参数

在之前例子中将不定参数的类型约束为int,如果你希望传任意类型,可以指定类型为interface{}
下面是Go语言标准库中的fmt.Printf()函数原型:

4、多返回值

Go语言的函数或者成员的方法可以有多个返回值,这个特性能够使我们写出比其他语言更优雅,更简洁的代码。比如,File.read()函数就可以同时返回读取的字节数和错误信息。如果文件读取成功者返回值中的n为读取字节数,errnil,否则err为具体的出错信息。

5、匿名函数与闭包

  • 匿名函数

匿名函数可以赋值给一个变量,也可以直接执行:

  • 闭包

基本概念:闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不再这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块为自由变量提供绑定的计算环境(作用域)。

闭包的价值:闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数。

Go语言中的闭包:如下,

Golang 入门(三) —— 流程控制

从根本上讲,流程控制只是为了控制程序语句的执行顺序,一般要与各种条件配合,因此,在各种流程中,会加入条件判断语句。流程控制语句一般起以下3个作用:

  • 选择,即根据条件跳转到不同的执行序列
  • 循环,即根据条件反复执行某个序列
  • 跳转,即根据条件返回到某个执行序列

Go语言支持一下几种流程控制语句:

  • 条件语句,对应关键字为ifelseelse if
  • 选择语句,对应关键字为switchcaseselect
  • 循环语句,对应关键字为forrange
  • 跳转语句,对应关键字为goto

在具体的应用场景中,为了满足更丰富的控制需求,Go语言还添加了如下关键字:breakcontinuefallthrough

1、条件语句

需要注意以下几点:

  • 条件语句不需要使用括号将条件包含起来()
  • 无论语句体内有几条语句都需要使用花括号{}括起来
  • 左花括号{必须与 if 或者 else 处于同一行
  • if之后,条件语句之前,可以添加变量初始化语句,使用;间隔
  • 在有返回值的函数中,不允许将最终的return语句包含在if...else...结构中

2、选择语句

需要注意以下几点:

  • 左花括号{必须与 switch 处于同一行
  • 条件表达式不限制为常量或者整数
  • 单个case可以出现多个结果选项
  • 不需要使用break来跳出case语句,只用在case中添加关键字fallthrough才会继续执行紧跟下个case
  • 可以在switch之后不写条件表达式,这种情况下,整个switch结构与多个if...else...的逻辑作用类似

3、循环语句

Go语言还进一步考虑到了无限循环的场景,让开发者不用写无聊的for(;;) {}do {} while(1),而直接简化为如下的写法:

在脚尖表达式中也可以多重赋值:

需要注意以下几点:

  • 左花括号{必须与 for 处于同一行
  • Go语言的for循环同样支持continuebreak来控制循环,还提供了一个更高级的break

更高级的break如下:

4、跳转语句

Golang 入门(二) —— 类型

Go语言内置以下这些基础类型:

  • 布尔类型:bool
  • 整型:int8byteint16intuintuintptr
  • 浮点类型:float32float64
  • 复数类型:complex64complex128
  • 字符串类型:string
  • 字符类型:rune
  • 错误类型:error

此外,Go语言也支持一下这些符合类型

  • 指针(pointer)
  • 数组(array)
  • 切片(slice)
  • 字典(map)
  • 通道(chan)
  • 结构体(struct)
  • 接口(interface)

1、布尔类型

2、整型

在Go语言中intint32为不同的类型,以下为错误示例:

同时2个不同类型的变量不能直接进行比较,但是可以直接和字面常量进行比较

3、浮点类型

因为浮点数不是一种精确的表达方式,所以不能像整型那样直接用==来判断两个浮点数是否相等,这可能会导致不稳定的结果。
下面是一种推荐的替代方案:

4、复数类型
复数实际上由两个实数(在计算机中用浮点数表示)构成,一个表示实部real,一个表示虚部imag

5、字符串

Go支持两种方式遍历字符串,一种是以字节数组的方式遍历:

执行此程序后发现,字符串长度为13,是因为中个字符在UTF-8中占了3个字节。

一种是以Unicode字符遍历:

Unicode方式遍历时,每个字符的类型是rune

6、字符类型
Go语言中支持两个字符类型,一个是byte(实际上是uint8的别名),代表UTF-8字符串的单个字节的值;另个是rune,代表单个Unicode字符。

关于rune相关操作, 可查阅Go标准库的unicode包。另外unicode/utf8包也提供了utf8unicode之间的转换。

7、数组
以下为一些常规声明数组的方式:

元素遍历

需要注意的是,在Go语言中数组是一个值类型value type。所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。

8、切片
数组的长度定义后就无法修改,每次赋值都将产生一份副本。为了弥补数组的不足Go语言提供了切片这种数据类型。

初看起来,切片就像指向数组的指针,实际上他拥有自己的数据结构,二不仅仅是一个指针。切片的数据结构可以抽象成以下3个变量:

  • 一个指向原生数组的指针
  • 数组切片中的元素个数
  • 切片已分配的存储空间

创建切片的方法主要有两种——基于数组和直接创建。

  • 基于数组

  • 直接创建

元素遍历同数组一样

appendcopy

9、map

Golang 入门(一) —— 变量

一、声明变量

二、变量初始化

方式1为标准初始化变量方式;
方式2编译器可以自动推导出v2的类型;
方式3编译器可以自动推导出v3的类型,此种方式不能用于声明全局变量;

三、变量赋值

方式1为标准复制
方式2Go语言可以多重赋值,方式2的意思是变量i变量j互换值

四、匿名变量

符号_结合多重返回值可以将不需要的返回值丢弃掉。

五、常量定义

Go常量定义可以限定常量类型,但不是必需的。
常量定义的右值可以是一个在编译期运算的常量表达式。
由于常量的赋值是一个编译器行为,所以右值不能出现任何运行期才能得到结果的表达式。如:const Home = os.GetEnv("HOME")

六、预定义常量

Go的预定义了这些常量: truefalseiota
iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后再下一个const出现前,没出现一次iota,其所代表的数字会自动增1。

枚举指一系列相关的常量,比如一个星期中的每天的定义。
同Go语言的其他符号(symbol)一样,以大写字母卡头的常量在包外可见。上面例子中numberOfDay为包内私有。