序:
在Java语言中,存在两套完全独立的类型系统:一套是值类型系统,主要是基本类型,如byte int boolean char double等,这些类型基于值语义;一套是以object类型为根的对象类型系统,这些类型可以定义成员变量和成员方法,可以有虚函数,基于引用语义,只允许在堆上创建(通过使用关键字new)。Java语言中的Any类型就是整个对象系统的根――java.lang.Object类型,只有对象类型系统中的实例才可以被Any类型引用。值类型想要被Any类型引用,需要装箱(boxing)过程,比如int类型需要装箱成为Integer类型。另外,只有对象类型系统中的类型才可以实现接口,具体方法是让该类型从要实现的接口继承。
相比之下,Go语言中的大多数类型都是值语义,并且都可以包含对应的操作方法。在需要的时候,你可以给任何类型(包括内置类型)"增加"新方法。而在实现某个接口时,无需从该接口集成(事实上,GO语言根本就不支持面向对象思想中的继承语法),只需要实现该接口要求的所有方法即可。任何类型都可以被Any类型引用。Any类型就是空接口,即interface()。
为类型添加方法:
在GO语言中,你可以给任意类型(包括内置类型,但不包括指针类型)添加相应的方法,例如:
type Integer int
func (a Integer) Less (b Integer) bool {
return a < b
}
在这个例子中,我们定义了一个新类型Integer,它和int没有本质不同,只是它为内置的int类型增加了个新方法Less()。
这样实现了Integer后,就可以让整型像一个普通类一样使用:
func main() {
var a Integer = 1
if a.Less(2) {
fmt.Println(a, "Less 2")
}
}
值语义和引用语义:
值语义和引用语义的差别在于赋值,比如下面的例子:
b = a
b.Modify()
如果b的修改不会影响a的值,那么此类型属于值类型。如果会影响a的值,那么此类型是引用类型。
GO语言中大多数类型都基于值语义,包括:
基本类型,如byte int bool float32 float64 和 string 等
复合类型,如数组(array) 结构体(struct) 和指针(pointer) 等
GO语言中类型的值语义表现的非常彻底。之所以这么说,是因为数组。
GO语言中的数组和基本类型没有区别,是很纯粹的值类型,例如:
var a = [3]int{1,2,3}
var b = a
b[1]++
fmt.Println(a, b)
该程序的运行结果如下:
[1 2 3] [1 3 3]
这表明b=a赋值语句是数组内容的完整复制。要想表达引用,需要用指针:
var a = [3]int{1, 2, 3}
var b = &a
b[1]++
fmt.Println(a, *b)
该程序的运行结果如下:
[1 3 3] [1 3 3]
这表明b=&a赋值语句是数组内容的引用。变量b的类型不是[3]int,而是*[3]int类型。
GO语言中4个类型比较特别,看起来像引用类型,如下所示。
数组切片:指向数组(array)的一个区间。
map:极其常见的数据结构,提供键值查询能力。
channel:执行体(goroutine)间的通信设施。
接口(interface):对一组满足某个契约的类型的抽象。
但是这并不影响我们将GO语言类型看做值语义。下面我们来看看这4个类型。
数组切片本质上是一个区间,你可以大致将[]T表示为:
type slice struct {
first *T
len int
cap int
}
因为数组切片是指向数组的指针,所以可以改变所指向的数组元素并不奇怪。数组切片类型本身的赋值仍然是值语义。
结构体:
GO语言的结构体(struct)和其他语言的类(class)有同等的地位,但Go语言放弃了包括集成在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。
上面我们说到,所有的Go语言类型(指针类型除外)都可以有自己的方法。在这个背景下,Go语言的结构体只是很普通的复合类型,平淡无奇。例如,我们要定义一个矩形类型:
type Rect struct {
x, y float64
width, height float64
}
然后我们定义成员方法Area()来计算矩形的面积:
func (r *Rect) Area() float64 {
return r.width * r.height
}